Azure pipeline series

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.