Making a cleaner and more intentional azure-pipelines.yml for an ASP.NET Core Web App
A few months back I moved my CI/CD (Continuous Integration/Continuous Development) to Azure DevOps for free. You get 1800 build minutes a month FREE and I'm not even close to using it with three occasionally-updated sites building on it.
It wasn't too hard, but as with all build pipelines you'll end up with a bunch of trial and error builds until you really get it dialed in.
I was working/pairing with Damian today because I wanted to get my git commit hashes and build ids embedded into the actual website so I could see exactly what commit is in production. How to do that will be the next post!
However, while tidying up we noticed some possible speed up and potential issues with my original azurepipeslines.yml file, so here's my new one!
NOTE: There's MANY ways to write one of these. For example, note that I'm allowing the "dotnet restore" to happen automatically as a sign effect of the call to dotnet build. Damian prefers to make that more explicit as its own task so he can see timing info for it. It's up to you, just know the side effects and measure!
Let's read the YAML and see what's up here.
- My primary Git branch is called "main" so my Pipeline triggers on commits to main.
- I'm using a VM from the pool that's the latest Ubuntu.
- I'm doing a Release (not Debug) build and putting that value in a variable that I can use later in the pipeline.
- I'm using a "runtime id" of linux-x64 and I'm storing that value also for use later. That's the .NET Core runtime I'm interested in.
- I'm passing in the -r $(rid) to be absolutely clear about my intent at every step.
- I want to build ONCE so I'm using --no-build on the publish command. It's likely not needed, but because I was using a rid on the build and then not using it later, my publish was wasting time by building again.
- The dotnet test command uses -r for results (dumb) so I have to pass in --runtime if I want to pass in a rid. Again, likely not needed, but it's explicit.
- I publish and name the artifact (fancy word for the resulting ZIP file) so it can be used later in the Deployment pipeline.
Here's the YAML
# https://docs.microsoft.com/azure/devops/pipelines/languages/dotnet-core
trigger:
- main
pool:
vmImage: 'ubuntu-latest'
variables:
buildConfiguration: 'Release'
rid: 'linux-x64'
steps:
- task: UseDotNet@2
inputs:
version: '3.1.x'
packageType: sdk
- task: DotNetCoreCLI@2
displayName: 'dotnet build $(buildConfiguration)'
inputs:
command: 'build'
arguments: '-r $(rid) --configuration $(buildConfiguration) /p:SourceRevisionId=$(Build.SourceVersion)'
- task: DotNetCoreCLI@2
displayName: "Test"
inputs:
command: test
projects: '**/*tests/*.csproj'
arguments: '--runtime $(rid) --configuration $(buildConfiguration)'
- task: DotNetCoreCLI@2
displayName: "Publish"
inputs:
command: 'publish'
publishWebProjects: true
arguments: '-r $(rid) --no-build --configuration $(BuildConfiguration) --output $(Build.ArtifactStagingDirectory)'
zipAfterPublish: true
- task: PublishBuildArtifacts@1
displayName: "Upload Artifacts"
inputs:
pathtoPublish: '$(Build.ArtifactStagingDirectory)'
artifactName: 'hanselminutes'
Did I miss anything? What are your best tips for a clean YAML file that you can use to build and deploy a .NET Web app?
Sponsor: This week's sponsor is...me! This blog and my podcast has been a labor of love for over 18 years. Your sponsorship pays my hosting bills for both AND allows me to buy gadgets to review AND the occasional taco. Join me!
About Scott
Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.
About Newsletter
They are unnecessary and distracting :P
- task: DotNetCoreCLI@2
inputs:
command: 'publish'
publishWebProjects: true
arguments: '--configuration $(BuildConfiguration) --output $(Build.ArtifactStagingDirectory)'
zipAfterPublish: false
- task: DeleteFiles@1
inputs:
SourceFolder: '$(Build.ArtifactStagingDirectory)/NowWebsite.Blazor/NowWebsite.Blazor/dist'
Contents: |
assets/css/bootstrap
assets/devicon/!(fonts|icons|devicon.min.css)
assets/fontawesome/css/!(*.min.css|*.css.map)
assets/fontawesome/js/!(*.min.js|*.js.map)
assets/fontawesome/less
assets/fontawesome/metadata
assets/fontawesome/scss
assets/plugins/owlcarousel/!(assets|*.min.js|*.js.map)
assets/plugins/owlcarousel/assets/!(*.min.css|*.css.map|*.png|*.gif)
- task: ArchiveFiles@2
inputs:
rootFolderOrFile: '$(Build.ArtifactStagingDirectory)/NowWebsite.Blazor/NowWebsite.Blazor/dist'
includeRootFolder: false
archiveType: 'zip'
archiveFile: '$(Build.ArtifactStagingDirectory)/nowwebsite.blazor.dist/nowwebsite.blazor.dist-$(Build.BuildId).zip'
replaceExistingArchive: true
- task: PublishBuildArtifacts@1
inputs:
PathtoPublish: '$(Build.ArtifactStagingDirectory)\nowwebsite.blazor.dist'
ArtifactName: 'nowwebsite.blazor.dist'
publishLocation: 'Container'
And Most of the time I run test with docker-compose because I only mock dependencies that I can't run locally.
For a system where you want to then deploy your application then you'll have actual defined environments or at least prod / nonprod. This pushes you into multistage land where you're going to be adding Azure Pipeline Environments, stages to your yml file and then have the meat of your deployment in a template yml file.
The Pipelines UI seems to work much better with them left out and put into releases, but I can't see how to commit releases to git. The documentation seems to suggest that we should be using Deployment Jobs in the azure-pipelines.yaml file but then what is the whole releases thing for?
Comments are closed.
When I'm building up my pipelines I prefer not to have the sdk version baked in. The global.json is the source of truth and I can reference it in the build pipeline. This way I don't have to change it in multiple places :)
So I have a global.json file which specifies the sdk version like so:
Then in the UseDotNet step in the pipeline looks like this:
--matt