Azure pipeline series
- Part 1 - Efficiently manage your YAML pipelines in Azure DevOps with reusable templates
- Part 2 - This Post
In the first part of this series, I shared with you my base recipe, which is an excellent starting point but far from enough if you want to build more complex applications. Most of what I make these days are in two parts; an ASP.Net web API and a SPA application for the frontend, usually in Angular.
Evolution
Here's the evolution of my base template that supports a default ASP.Net application that host a Web API backend and a SPA application.
templates/job.template.build.yml
parameters:
- name: buildPlatform
type: string
default: 'x64'
- name: buildConfiguration
type: string
default: 'Release'
- name: solutionPath
type: string
- name: artifactName
type: string
default: 'backend-package'
- name: clientAppPath
type: string
default: ''
jobs:
- job: 'Build'
displayName: 'Build'
pool:
vmImage: 'windows-latest'
steps:
- checkout: self
fetchDepth: 100
lfs: false
# FrontEnd
- task: Npm@1
displayName: 'Npm Install'
condition: ne('${{ parameters.clientAppPath }}', '')
inputs:
workingDir: '${{ parameters.clientAppPath }}'
verbose: false
- task: Npm@1
displayName: 'npm run build'
condition: ne('${{ parameters.clientAppPath }}', '')
inputs:
command: 'custom'
workingDir: '${{ parameters.clientAppPath }}'
customCommand: 'run-script build-prod'
# Backend
- task: DotNetCoreCLI@2
displayName: Restore
inputs:
command: restore
projects: '${{ parameters.solutionPath }}*.sln'
noCache: true
- task: DotNetCoreCLI@2
displayName: 'Build'
inputs:
command: 'build'
projects: '${{ parameters.solutionPath }}*.sln'
arguments: '--configuration ${{ parameters.buildConfiguration }}'
- task: DotNetCoreCLI@2
displayName: 'Unit tests'
inputs:
command: 'test'
projects: '${{ parameters.solutionPath }}**/*.Tests.Unit.csproj'
arguments: '--configuration ${{ parameters.buildConfiguration }} --collect "Code Coverage"'
verbosityRestore: Minimal
- task: DotNetCoreCLI@2
displayName: 'Integration tests'
inputs:
command: 'test'
projects: '${{ parameters.solutionPath }}**/*.Tests.Integration.csproj'
arguments: '--configuration ${{ parameters.buildConfiguration }} --collect "Code Coverage"'
verbosityRestore: Minimal
- task: DotNetCoreCLI@2
displayName: 'Package'
inputs:
command: publish
publishWebProjects: false
projects: ${{ parameters.solutionPath }}*.sln
arguments: '--configuration ${{ parameters.buildConfiguration }} --output $(Build.ArtifactStagingDirectory) --no-restore'
zipAfterPublish: True
- task: PublishBuildArtifacts@1
displayName: 'Publish Artifacts'
inputs:
ArtifactName: '${{ parameters.artifactName }}'
Notice that all frontend related tasks have a condition condition: ne('${{ parameters.clientAppPath }}', '')
. Doing so will make sure to skip those tasks if the template is used for a backend only application. It allows us to maintain a single template that works for both scenarios; backend only or backend + frontend.
Build
Using that template, you can now create a new build pipeline in only a few lines of YAML like this.
trigger:
batch: true
paths:
include:
- Sources/PathToSln/*
stages:
- stage: BuildAndPackage
displayName: 'Build and package'
jobs:
- template: 'templates/be.job.template.build.yml'
parameters:
solutionPath: Sources/PathToSln/
clientAppPath: Sources/PathToSln/WebApp/ClientApp/
Of course, depending on your needs and practices, you can add pretty much anything to this template. Good examples are advanced analyzers like SonarQube, WhiteSource, Snyk, or some linters for the frontend. With the rich plugin model of Azure DevOps, Sky is the limit! Stay tuned for the next part, where we'll explore how I use stages in my pipelines.