Skip to main content
Gorilla Tactics

Part II - Creating a multi-stage pipeline to build and deploy to a Linux Web App

·3 mins

This article is part of a series for setting up the single pipeline}

With the PR build set up, we can now test it by adding a YAML file for our build & deploy pipeline, and creating a PR to merge it in.

We’re going to eventually create a pipeline with three stages, but let’s add the build stage first. This is mostly the same as the earlier pipeline we created, with a some changes and additional steps:

trigger:
- main

pr: none

This sets up a trigger to start the build, namely when the main branch is updated, and not to run it for PRs.

Now let’s set up our first stage. which starts with the build file we created in the previous post, only nested inside a stage:

stages:
- stage: Build
  jobs:
  - job: Build
    pool:
      vmImage: 'windows-2022'

    continueOnError: false

    variables:
      solution: '$(Build.SourcesDirectory)/<solution file name>.sln'
      buildPlatform: 'Any CPU'
      buildConfiguration: 'Release'

    - task: NuGetToolInstaller@1
      displayName: Install Nuget

    - task: NuGetCommand@2
      displayName: Install Packages
      inputs:
        restoreSolution: '$(solution)'

We now need to add the build task, however, because I want to install this to a Linux App Service, this requires different parameters for MS Build, as I discovered from a very helpful Hanselman article.

Scott explains that the default parameters for building:

/p:WebPublishMethod=Package
/p:PackageAsSingleFile=true

create a zip file with a ridiculously nested folder structure. And whilst the deployment process to a Windows App Service copes with this structure, the Linux deploy very much does not. His solution is to take that zip file, extract everything from it, then create a new zip without the horrendous nesting. However, a commentor suggested using these parameters instead:

/p:DeployDefaultTarget=WebPublish
/p:WebPublishMethod=FileSystem
/p:publishUrl="$(build.stagingDirectory)\$(BuildConfiguration)\web_package"

which doesn’t create a zip file, instead outputting all the files to the publish url, in this case a folder called web_package. So, the build task should look like this:

    - task: VSBuild@1
      displayName: Build Solution
      inputs:
        solution: '$(solution)'
        msbuildArgs: '/p:DeployOnBuild=true /p:WebPublishMethod=FileSystem /p:PublishUrl="$(build.artifactStagingDirectory)/web_package"'
        platform: '$(buildPlatform)'
        configuration: '$(buildConfiguration)'

The unit test and migration script generation are unchanged from the PR build:

    - task: DotNetCoreCLI@2
      displayName: Run Tests
      inputs:
        command: 'test'
        projects: '**/*.Tests/*.csproj'

    - task: PowerShell@2
      displayName: Create EF Migration Script
      inputs:
        targetType: 'inline'
        script: |
          dotnet tool install --global dotnet-ef
          dotnet ef migrations script --idempotent --project $(Build.SourcesDirectory)\<Data Layer folder>\<Data Layer project>.csproj --output $(Build.ArtifactStagingDirectory)/migration.sql

Creating a deployment package #

We now need a task to create something that can be deployed to the app service. Frustratingly, whilst I found a post that said you can deploy a folder of files to a web service, that does not appear to work with a Linux app service. I’m guessing that it only works for Windows web apps.

It’s not a big deal though, we’ll just have to manually create a zip file for deployment then, which is actually easy enough to do:

    - task: ArchiveFiles@2
      displayName: Create Deployment Package
      inputs:
        rootFolderOrFile: '$(Build.ArtifactStagingDirectory)/web_package'
        includeRootFolder: false
        archiveType: 'zip'
        archiveFile: '$(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip'
        replaceExistingArchive: true

Cleaning up #

With a deployable zip file created, we can now delete the web_package folder to be deleted:

    - task: PowerShell@2
      displayName: Delete Build Files
      inputs:
        targetType: 'inline'
        script: |
          Remove-Item -Path $(Build.ArtifactStagingDirectory)\web_package -Recurse

Saving everything for later stages #

The final step is to publish the migration script and deployment package as an artifact, we’ll call it ‘drop’, for use later in this pipeline. This is the reason for deleting the web_package folder in the previous task, to significantly reduce the amount of data that needs to be saved.

    - publish: '$(Build.ArtifactStagingDirectory)'
      artifact: 'drop'

With that merged in, it’s time to add the next stage, deploying to the Test environment.