Back to Blog

CI/CD in Azure DevOps

How to structure a continuous delivery pipeline in Azure DevOps with build, tests, quality gate and multi-environment deploy.

2 min read
Azure DevOpsCI/CDDevOps

A well-designed CI/CD pipeline is the difference between deploying with fear and deploying with confidence. In this post I show the structure I use in .NET projects.

Basic structure

The pipeline is split into stages: build, test, quality gate and deploy. Each stage runs only if the previous one passes.

trigger:
  branches:
    include:
      - main
      - develop
 
pool:
  vmImage: ubuntu-latest
 
variables:
  buildConfiguration: Release
  projectPath: src/MyApp/MyApp.csproj
 
stages:
  - stage: Build
    jobs:
      - job: BuildJob
        steps:
          - task: UseDotNet@2
            inputs:
              version: 8.x
          - task: DotNetCoreCLI@2
            displayName: Restore
            inputs:
              command: restore
              projects: $(projectPath)
          - task: DotNetCoreCLI@2
            displayName: Build
            inputs:
              command: build
              projects: $(projectPath)
              arguments: --configuration $(buildConfiguration)
          - task: DotNetCoreCLI@2
            displayName: Publish
            inputs:
              command: publish
              projects: $(projectPath)
              publishWebProjects: true
              arguments: --configuration $(buildConfiguration) --output $(Build.ArtifactStagingDirectory)
          - task: PublishBuildArtifacts@1
            inputs:
              pathtoPublish: $(Build.ArtifactStagingDirectory)
              artifactName: drop

Tests and quality

After the build, I run tests with coverage and publish the result:

  - stage: Test
    dependsOn: Build
    jobs:
      - job: TestJob
        steps:
          - task: DotNetCoreCLI@2
            displayName: Test
            inputs:
              command: test
              arguments: --configuration $(buildConfiguration) /p:CollectCoverage=true /p:CoverletOutputFormat=cobertura
          - task: PublishCodeCoverageResults@2
            inputs:
              summaryFileLocation: $(Agent.TempDirectory)/**/coverage.cobertura.xml

Multi-environment deploy

For each environment (dev, staging, prod) I use a separate stage with approvals:

  - stage: DeployProd
    dependsOn: Test
    condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
    jobs:
      - deployment: DeployProd
        environment: production
        strategy:
          runOnce:
            deploy:
              steps:
                - task: AzureWebApp@1
                  inputs:
                    azureSubscription: my-service-connection
                    appName: myapp-prod
                    package: $(Pipeline.Workspace)/drop

environment: production enables manual approvals in Azure DevOps — essential for any deploy that touches production.

Conclusion

The secret of a good pipeline is small, fast and reliable. Every second saved in the build is a second multiplied by every dev on the team. Every well-placed gate is an incident avoided.