There are countless articles on what is Continuous Integration and Deployment/Delivery (CI/CD) and how it helps application development teams to deliver code changes more frequently and reliably.
CI/CD is a devops best practice because it addresses the misalignment between developers who want to push changes frequently, with operations that want stable applications. With automation in place, developers can push changes more frequently and operations teams see greater stability.
In this post I'd like to provide a simple, no-nonsense recipe for building Continuous Integration pipeline for ASP.NET Core application using Azure DevOps.
There might be many variations of how to structure a build pipeline depending on project dependencies, environment and other factors, but essential steps will be the same for any pipeline.
- Getting a code from source control.
- Building projects.
- Executing unit tests
- Performing code quality analysis
- Producing a deployable artifact
Let's see how we can accomplish that in Azure DevOps.
In Azure DevOps it is possible now to create a one multi-stage pipeline to combine build and release stages, as well as deploy to multiple environments. I still like the idea of separation between build and release pipelines, as it gives more flexibility in how they can be scheduled and executed. Therefore I will create a YAML CI pipeline. later it can be augmented with additional steps to convert it to full CI/CD cycle.
In Azure Devops there is also an option to create a "classic" pipeline. The difference is that YAML pipeline follows "infrastructure as code" principle, as it is stored in the source control, and can be tracked and versioned along with the application's code. This is a best practice now.
.NET Core application can be built either on Windows or Linux machines. To build on Linux machine we need to install a .NET Core version that your application targets. If you have some libraries or dependencies that uses different .NET version then it also should be installed. Also, at the time of writing, to build Azure Function, even if was developed with 3.x, you still need to have 2.1 on a build machine. I am sure that will be corrected in the future, but that is something to keep in mind if your build is failing. Anyway, here are the tasks to install necessary .NET Core versions:
- task: UseDotNet@2 displayName: 'Use .NET SDK 2.1' inputs: packageType: 'sdk' version: '2.1.x' - task: UseDotNet@2 displayName: 'Use .NET SDK 3.0' inputs: packageType: 'sdk' version: '3.0.x'
A step to restore NuGet packages:
- task: DotNetCoreCLI@2 displayName: 'Restore project dependencies' inputs: command: 'restore' projects: '**/*.csproj'
The essential step of building the project:
- task: DotNetCoreCLI@2 displayName: 'Build the project - $(buildConfiguration)' inputs: command: 'build' arguments: '--no-restore --configuration $(buildConfiguration)' projects: '*/.csproj' '''
When building on Windows we can use built-in data collector for code coverage metrics. However, as we are trying to create a universal pipeline that can be run both on Windows and Linux, it is better to use Coverlet. For this to work, your unit test projects must include two NuGet packages:
Let's install report generator on a build machine too.
- task: DotNetCoreCLI@2 displayName: 'Install ReportGenerator' inputs: command: custom custom: tool arguments: 'install --global dotnet-reportgenerator-globaltool'
Now we are ready to run unit tests with options to produce code coverage data
- task: DotNetCoreCLI@2 displayName: 'Run unit tests - $(buildConfiguration)' inputs: command: 'test' arguments: '--no-build --configuration $(buildConfiguration) /p:CollectCoverage=true /p:CoverletOutputFormat=cobertura /p:CoverletOutput=$(Build.SourcesDirectory)/TestResults/Coverage/' publishTestResults: true projects: '**/Api.UnitTests.csproj'
- script: | reportgenerator -reports:$(Build.SourcesDirectory)/**/coverage.cobertura.xml -targetdir:$(Build.SourcesDirectory)/CodeCoverage -reporttypes:HtmlInline_AzurePipelines displayName: 'Create code coverage report' - task: PublishCodeCoverageResults@1 displayName: 'Publish code coverage report' inputs: codeCoverageTool: 'cobertura' summaryFileLocation: '$(Build.SourcesDirectory)/**/coverage.cobertura.xml'
Finally we are ready to produce an artifact that can later be used in Release pipeline to deploy to development, staging, and production environment. Notice that the artifact does not change between those environments. We should deploy the same build to ensure stability and predictability of deployments. All environment configurations should be kept separately.
- task: DotNetCoreCLI@2 displayName: 'Publish the project - $(buildConfiguration)' inputs: command: 'publish' projects: '**/MyProject.Api.csproj' publishWebProjects: false arguments: '--no-build --configuration $(buildConfiguration) --output $(Build.ArtifactStagingDirectory)/$(buildConfiguration)' zipAfterPublish: true - task: PublishBuildArtifacts@1 displayName: 'Publish Artifact: MyProjectApi' condition: succeeded() inputs: PathtoPublish: '$(Build.ArtifactStagingDirectory)' ArtifactName: 'MyProjectApi' publishLocation: 'Container'
Now, as we have our automated build pipeline, the question is how often we want to execute it? There are multiple options for that. It can be triggered by every change made to the master branch. But if you have a big team with frequent changes it might not be practical as it will consume a lot of resources. In this case the changes could be batched, or the build can be done on schedule, for example daily. Ultimately, it is for your development team to decide.
Don't forget that this pipeline is only a "CI" part in CI/CD, so you need to create a release pipeline as well. That is a part of another topic.
In this post we went through the specific tasks to create a complete build pipeline for ASP.NET Core or Azure Function application in Azure DevOps.
Here is a full YAML Gist for your reference:
Thank you for reading, and here is a cat (and a pipeline?) for you: