Lesson 1.9: Azure Build Pipeline

Now that we have a Blazor app and test projects, we will create an automated build that will build our code whenever changes are committed into Azure DevOps. Automated builds ensure that any code check-ins always build on clean systems and all of our tests continue to run and pass continuously. Automated builds are an important part of a continuous integration process.

Continuous integration is the practice of frequently integrating one’s new or changed code with the existing code repository – should occur frequently enough that no intervening window remains between commit and build, and such that no errors can arise without developers noticing them and correcting them immediately. Normal practice is to trigger these builds by every commit to a repository, rather than a periodically scheduled build.

wikipedia – Continuous Integration

The principles of CI (Continuous Integration) are:

  • Maintain a code repository – we have Azure DevOps
  • Automate the build – we’ll use Azure Pipelines
  • Make the build self-testing – test automation is included in the automated build, and we have a test project already.
  • Commit to baseline daily (at least — but usually many times per day)
  • Every push is built – with the same automation and in all of our branches
  • Keep the build fast – it happens with every push, so it must be rapid to keep our coding flow and identify breaks immediately
  • Everyone has access to build results – in the Azure DevOps pipeline dashboard
  • Automate the deployment – this will be covered by Continuous Delivery in a future lesson of this series

We’re going to use Azure Pipelines, which are integrated with Azure DevOps, to perform our automated build. Azure Pipelines use YAML files (.yml) to define the stages and steps of a build and deployment. To familiarize yourself with the details of Pipelines YAML files and tasks, read the documentation.

Our Blazor app build pipeline needs to do multiple steps:

  1. Install .NET Core – ensure the correct version of .NET core is installed on the build agent.
  2. Restore NuGet Packages – get the referenced NuGet packages onto the build agent.
  3. Build Projects – build all of the projects in the repository.
  4. Run Tests – run all test projects and report the results.
  5. Publish Artifacts – prepare source projects for deployment.

Creating Azure-Pipelines.yml

Let’s create a new azure-pipelines.yml file in our solution as a simple text file (I like to include these in my VS solution folders too). You can find the file for the full build in our repository, but we’ll go over each section here.

Pipeline Setup

The start of the file includes the setup and variable definition for our build. We use variables to share the same version and build configuration across all of the steps. This way if we need to change them, it is easy to do.

trigger:
- '*'

# all of the variables used by this pipeline and dependent templates.
variables:
  buildConfiguration: 'Release'
  dotnetSdkVersion: '3.1.200'
  releaseBranchName: 'main'
  localPackageFeed: 'd20Tek'

# define the image to use for the whole pipeline... can be overridden by specific jobs.
pool:
    vmImage: 'ubuntu-16.04'

Let’s see each of these:

  • trigger: defines when the build should run. We’ve made it the broadest… execute on any commit into any branch. We’re using one build script with conditions for when certain steps are on or off, rather than individuals scripts per branch (for example).
  • variables: defines the variables used by this pipeline with the values we use in the build steps. We have project specific variables here so that our scripts don’t assume particular configurations or versions or branches and can be reused.
  • pool: defines the VM image to use for the pipeline. We’re using a Linux build agent. Defining it at the root makes it the default build image, but each stage or job can define a more specialized or different image, if needed.

Then, we define the stages of the pipeline… we’re only going to have the Build stage at this point. As we will see, it’s pretty simple. It’s sets a friend display names, defines the jobs tag, and then includes a list of steps/tasks.

Install .NET Core

This code installs a particular version of the .NET Core SDK. If a newer version is already installed on the build agent, then this step does nothing. Note that it uses the UseDotNet task and the donetSdkVersion variable that we defined at the top of the file.

# ensure the right version of .NET Core is installed -- defaults to 3.1.
- task: UseDotNet@2
  displayName: 'Use .NET Core SDK $(dotnetSdkVersion)'
  inputs:
    version: '$(dotnetSdkVersion)'

Restore NuGet Packages

Most of the following steps use the DotNetCoreCLI task to perform various operations. If we want to learn more about the DotNetCoreCLI build task, review the documentation. The restore step ensures the correct NuGet packages used by our application/game are installed on the build agent.

    # restore NuGet packages used by the projects.
    - task: DotNetCoreCLI@2
      displayName: 'Restore project dependencies'
      inputs:
        command: 'restore'
        projects: '**/*.csproj'
        feedsToUse: 'select'
        vstsFeed: '$(localPackageFeed)'

Build Projects

The next steps build and publish all of the projects in the repository. It does this by setting the projects parameter to ‘**/*.csproj’. This notation means run the build command on all of the .csproj files found in this repository. The steps use the buildConfiguration variable to build the “Release” version of our projects.

    # build all projects in this repo... defined by folders with .csproj files.
    - task: DotNetCoreCLI@2
      displayName: 'Build the project - $(buildConfiguration)'
      inputs:
        command: 'build'
        arguments: '--no-restore --configuration $(buildConfiguration)'
        projects: '**/*.csproj'
        versioningScheme: byBuildNumber
    
    # publish all artifacts from the builds.
    - task: DotNetCoreCLI@2
      displayName: 'Publish the project - $(buildConfiguration)'
      inputs:
        command: 'publish'
        projects: '**/*.csproj'
        publishWebProjects: false
        arguments: '--no-build --configuration $(buildConfiguration) --output $(Build.ArtifactStagingDirectory)/$(buildConfiguration)'
        zipAfterPublish: true

Run Tests

After all of the projects are built, we run test projects to get the results for our latest build. Note that the build step above already built the test projects too, so this step doesn’t need to rebuild – thus the --no-build flag. And we only run tests on Test projects which we define by convention to follow the appended “.Tests” project name.

    # runs tests for all projects in this repo... defined by folders with .Test.csproj files.
    - task: DotNetCoreCLI@2
      displayName: 'Run unit tests - $(buildConfiguration)'
      inputs:
        command: 'test'
        arguments: '--no-build --no-restore --configuration $(buildConfiguration)'
        # publish the test pass/fail results to the pipeline, so that they are available in the Azure DevOps pipeline dashboard.
        publishTestResults: true
        projects: '**/*.Tests.csproj'

Also the publishTestResults parameter ensures that the test results are published to the DevOps build page, so you can see the results within that build UI.

Publish Artifacts

The final step simply copies all build artifacts to the drop folder. This gives us access to the output of the build on the DevOps portal and also provides the drop to additional steps when we automate the deployment.

    # publish the artifacts created by this build in the drop location.
    - task: PublishBuildArtifacts@1
      displayName: 'Publish Artifact: drop'
      condition: succeeded()

Writing build scripts can be a time-consuming process because there aren’t great tools for validating the YAML file without running it. I’ve gone through dozens of iterations to get at a stable, working build pipeline. But this script is very general and can be copied and reused in many ASP.NET application projects. When you’re ready to dive deeper into Azure Pipelines, check out the documentation site.

The full code listing for the azure-pipelines.yml file looks like the following. Please keep in mind that the spacing and tabs in YAML files has meaning. So if tasks and properties are set at the wrong space, the YAML interpreter may fail to understand them and show an error when we try to run an automated build.

# Starter pipeline
# Start with a minimal pipeline that you can customize to build and deploy your code.
# Add steps that build, run tests, deploy, and more:
# https://aka.ms/yaml

trigger:
- '*'

# all of the variables used by this pipeline and dependent templates.
variables:
  buildConfiguration: 'Release'
  dotnetSdkVersion: '3.1.200'
  releaseBranchName: 'main'
  localPackageFeed: 'd20Tek'

# define the image to use for the whole pipline... can be overridden by specific jobs.
pool:
    vmImage: 'ubuntu-16.04'

stages:
# Build
- stage: 'Build'
  displayName: 'Build app'
  jobs:
  - job: 'Build'
    displayName: 'Build job'

    steps:
    # ensure the right version of .NET Core is installed -- defaults to 3.1.
    - task: UseDotNet@2
      displayName: 'Use .NET Core SDK $(dotnetSdkVersion)'
      inputs:
        version: '$(dotnetSdkVersion)'

    # restore NuGet packages used by the projects.
    - task: DotNetCoreCLI@2
      displayName: 'Restore project dependencies'
      inputs:
        command: 'restore'
        projects: '**/*.csproj'
        feedsToUse: 'select'
        vstsFeed: '$(localPackageFeed)'

    # build all projects in this repo... defined by folders with .csproj files.
    - task: DotNetCoreCLI@2
      displayName: 'Build the project - $(buildConfiguration)'
      inputs:
        command: 'build'
        arguments: '--no-restore --configuration $(buildConfiguration)'
        projects: '**/*.csproj'
        versioningScheme: byBuildNumber
    
    # publish all artifacts from the builds.
    - task: DotNetCoreCLI@2
      displayName: 'Publish the project - $(buildConfiguration)'
      inputs:
        command: 'publish'
        projects: '**/*.csproj'
        publishWebProjects: false
        arguments: '--no-build --configuration $(buildConfiguration) --output $(Build.ArtifactStagingDirectory)/$(buildConfiguration)'
        zipAfterPublish: true

    # runs tests for all projects in this repo... defined by folders with .Test.csproj files.
    - task: DotNetCoreCLI@2
      displayName: 'Run unit tests - $(buildConfiguration)'
      inputs:
        command: 'test'
        arguments: '--no-build --no-restore --configuration $(buildConfiguration)'
        # publish the test pass/fail results to the pipeline, so that they are available in the Azure DevOps pipeline dashboard.
        publishTestResults: true
        projects: '**/*.Tests.csproj'

    # publish the artifacts created by this build in the drop location.
    - task: PublishBuildArtifacts@1
      displayName: 'Publish Artifact: drop'
      condition: succeeded()

Configuring Pipeline in Azure DevOps

Let’s check in our build script into the ‘chapter-1’ feature branch, and sync it to our repo. This gets the script into our repository. Now, we can configure an Azure Pipeline to use for our automated build.

  1. Go to the Azure DevOps website, and click the Pipelines tab in the left side navigation panel.
  2. Then, click the New Pipeline button on the top right of the page.
  3. Select your repo from the Azure Repos list — simple-rpg-game.
Fig 1 – Create pipeline – Select repository
  1. Configure the pipeline by selecting – Existing Azure Pipeline YAML file.
Fig 2 – Create pipeline – Configure pipeline
  1. In the dialog, select our current feature branch – chapter-1.
  2. In the path, enter /build/azure-pipelines.yml (this is the file we created earlier).
Fig 3 – Create pipeline – Select existing YAML file
  1. This will load the file into the pipeline editor for review, but we do not need to change anything.
  2. So, click the Run button to launch a build — make sure to run the build in the feature branch (since our code hasn’t made it to the main branch just yet).
Fig 4 – Resulting feature branch build

Once the build completes, let’s take a look at the test results page by selecting the Tests tab. We should have 3 passing tests at this point.

Fig 5 – Test results page

In conclusion, we now have a continuous integration script that builds, tests, and publishes our Blazor application in any branch. We are ready to start making changes secure in the fact that we can build and test our code. The quality of our build will be verified with every push to feature branches and pull requests to the main branch. That is all very cool.

9 thoughts on “Lesson 1.9: Azure Build Pipeline

  1. When trying to run the build, I get:
    /nsc-rpg-game/NSCRPG.Game/azure-pipelines.yml: (Line: 16, Col: 1, Idx: 415) – (Line: 16, Col: 2, Isx: 416): While parsing a block mapping, did not find expected key.

    Like

      1. Throws an error: “{“$id”:”1″,”innerException”:null,”message”:”TF400813: The user ‘Windows Live ID\\xxxx@xxxx.com’ is not authorized to access this resource.”,”typeName”:”Microsoft.TeamFoundation.Framework.Server.UnauthorizedRequestException, Microsoft.TeamFoundation.Framework.Server”,”typeKey”:”UnauthorizedRequestException”,”errorCode”:0,”eventId”:3000}”

        Like

      2. That is very strange. Other people have been able to access the project, and I can get in anonymously as well.
        In any case, I posted the full azure-pipelines.yml file in this article, so check that your file looks exactly like my posted full file.

        Like

      3. Did getting the full source file for azure-pipelines.yml help?
        You should also try to go to the repo with your browser in Incognito mode… that also worked for me to see the project and repository.

        Like

Leave a comment