Azure Pipelines for Legacy .NET Projects

2 minute read

If, like me, you’ve had to go through the process of migrating build pipelines for legacy .NET Framework to Azure DevOps, you know it can be a struggle without a template to work from.

To that end, here is a very quick post you can use as the basis for your shiny new YAML pipeline.

NB: Sending your artifacts to your deployment system and pushing packages to NuGet are left as an exercise to the reader

name: 1.0.0.$(Rev:r) # Ensures build name follows the version number

trigger:
- '*' # Build on changes to any branch

pool:
  name: 'MyPool' # Ensures Pipeline runs on a certain pool that may have specially configured agents
  demands:
  - agent.os -equals Windows_NT

variables:
  - name: applicationName
    value: 'MyApplication'
  - name: solution
    value: '$(applicationName).sln'
  - name: 'buildPlatform'
    value: 'Any CPU'
  - name: 'buildConfiguration'
    value: 'Release'
  - name: hostOutputPath # Build output location for the main application binaries
    value: '$(applicationName)/bin/$(buildConfiguration)'
  - name: nugetSource
    value: 'https://api.nuget.org/v3/index.json' # Public NuGet feed, but replace with a custom one if necessary
  - name: signCommand
    value: '"C:\Program Files (x86)\Windows Kits\10\App Certification Kit\signtool.exe" sign /a /fd SHA256' # Automatically locate signing certificate

workspace:
  clean: all # Ensure agent build directories start empty

steps:
- script: |
    echo Restoring packages for ${{ solution }} using source ${{ nugetSource }}
    nuget.exe restore ${{ solution }} -Source ${{ nugetSource }} -Verbosity Detailed -NonInteractive -NoCache
  displayName: "Restore NuGet packages with CLI"

- task: VSBuild@1
  displayName: 'Build Solution'
  inputs:
    solution: '$(solution)'
    platform: '$(buildPlatform)'
    configuration: '$(buildConfiguration)'

- task: VSTest@2
  displayName: 'Run Tests'
  inputs:
    platform: '$(buildPlatform)'
    configuration: '$(buildConfiguration)'
    otherConsoleOptions: '/platform:x64' # Only needed to use the 64-bit test runner
    # Ignore integration tests on non-master branches
    ${{ if ne(variables['Build.SourceBranch'], 'refs/heads/master') }}:
        testFiltercriteria: 'Category!=Integration'
        runInParallel: True

- script: |
    echo Using '${{ signCommand }}' to sign executables in '${{ hostOutputPath }}'
    ${{ signCommand }} ${{ hostOutputPath }}/*.exe
  displayName: 'Sign Executables'

# Add steps to send artifacts to your deployment system e.g. Octopus

# Create and push NuGet packages with NuGet CLI

Simply create a new file in the root of your repository (e.g. azure-pipelines.yaml) and push. You can then configure the pipeline through the Azure UI.

Step Templates and Shared Variables

As you build out more pipelines, you may wish to create standardised step templates. To do so, create a new repository to hold your YAML files.

As an example, the Sign Executables task in the pipeline above could be extracted out to its own template called sign-executables.yaml:

parameters:
  - name: hostOutputPath
    type: string
  - name: signCommand
    type: string

steps:
  - script: |
    echo Using '${{ signCommand }}' to sign executables in '${{ hostOutputPath }}'
    ${{ signCommand }} ${{ hostOutputPath }}/*.exe
  displayName: 'Sign Executables'

The template repository can then be referred to you in your pipeline with a resources section as follows:

resources:
  repositories:
  - repository: taskTemplatesAndVariables # This is a label used to refer to tasks in the steps section
    type: git
    name: TeamProject/TemplatesAndVariables # Path to your template repo in Azure DevOps
    ref: refs/heads/v1 # Branch name

The task template can then be added as a step in your pipeline:

- template: sign-executables.yaml@taskTemplatesAndVariables
  parameters:
    hostOutputPath: $(hostOutputPath)
    signCommand: $(signCommand)

Variable values can also be configured in a yaml file in your new repository (e.g. variables.yaml):

variables:
  buildConfiguration: 'Release'
  buildPlatform: 'Any CPU'

These can then be referenced in a similar way as part of the variables section of your pipeline:

variables:
  - template: variables.yaml@taskTemplatesAndVariables
  - name: applicationName
    value: 'MyApplication'
  - name: solution
    value: '$(applicationName).sln'

Image credits:

Updated: