Following up on this call to action from Jonas Rapp:
I decided to finally automate the build & release process for SQL 4 CDS. I mostly followed the process in Jonas’s blog, but I set up a slightly different build process to automatically validate PRs and get versioning working how I wanted it.
Rather than a date-based versioning scheme, I prefer to use semantic versioning. This means you have some idea what you’re getting in each release just by looking at the version number:
- 1.x.x to 2.0.0 – a major release, likely including breaking changes from the previous version
- 1.0.x to 1.1.0 – a minor release including new features but no breaking changes
- 1.0.0 to 1.0.1 – a patch release including fixes but no new features
Of course, it takes a little work to classify each change between releases to work out what the next version number should be. There are tools to help though, which I’ll look at shortly.
Configuring the build process as a YAML file rather than graphically is the way that Azure DevOps encourages things these days, and I like being able to edit, branch & merge the build definition in the same way as other code. It is yet another language to have to learn though, but it doesn’t have to be very complicated.
I now have two similar build scripts. One runs an automated build to check each PR before it can be merged, and another I run manually when I want to produce a new release.
The one that runs the automated CI builds for each pull request currently looks like:
trigger: none pr: - master pool: vmImage: 'windows-latest' variables: solution: '**/*.sln' buildPlatform: 'Any CPU' buildConfiguration: 'Release' steps: - checkout: self persistCredentials: true - task: NuGetCommand@2 displayName: Install GitVersion inputs: command: custom arguments: install GitVersion.CommandLine -Version 4.0.0 -OutputDirectory $(Build.BinariesDirectory)/tools -ExcludeVersion - script: $(Build.BinariesDirectory)/tools/GitVersion.CommandLine/tools/GitVersion.exe /output buildserver /nofetch /updateassemblyinfo displayName: Determine Version - task: NuGetCommand@2 displayName: NuGet Restore inputs: command: restore restoreSolution: '**/*.sln' - task: VSBuild@1 displayName: Build inputs: solution: '$(solution)' platform: '$(buildPlatform)' configuration: '$(buildConfiguration)' - task: VSTest@2 inputs: platform: '$(buildPlatform)' configuration: '$(buildConfiguration)'
Taking this a section at a time:
trigger: none pr: - master
This tells Azure DevOps not to run the build on each commit (the
trigger: none line), but do run it on each PR that targets the
pool: vmImage: 'windows-latest'
XrmToolBox tools run under .NET Framework rather than .NET Core, so they need to be built on a Windows build server. Here I’m selecting to use whatever latest version of Windows is available.
variables: solution: '**/*.sln' buildPlatform: 'Any CPU' buildConfiguration: 'Release'
Here I’m setting up a few variables I can refer to later in the script, defining exactly what & how it’s going to build.
- checkout: self persistCredentials: true
The details of what code the build is going to check out from GitHub is defined as you create the pipeline. The build agent will check out that code without an explicit step like this, but this also preserves the Git login credentials for other steps to use later.
- task: NuGetCommand@2 displayName: Install GitVersion inputs: command: custom arguments: install GitVersion.CommandLine -Version 4.0.0 -OutputDirectory $(Build.BinariesDirectory)/tools -ExcludeVersion
Now it starts to get interesting. As I mentioned earlier I want to use semantic versioning. The GitVersion tool automatically works out the version based on comments in the commits, saving me work on each build having to manually add a version. This tool isn’t installed by default though, so I’m using NuGet here to install it.
- script: $(Build.BinariesDirectory)/tools/GitVersion.CommandLine/tools/GitVersion.exe /output buildserver /nofetch /updateassemblyinfo displayName: Determine Version
Now GitVersion is installed I need to run it. Note the
/updateassemblyinfo flag – this automatically modifies the local copy of all AssemblyInfo.cs files and puts in the new version number it’s just worked out. This will make the right version number appear in the list of tools in XrmToolBox.
- task: NuGetCommand@2 displayName: NuGet Restore inputs: command: restore restoreSolution: '$(solution)' - task: VSBuild@1 displayName: Build inputs: solution: '$(solution)' platform: '$(buildPlatform)' configuration: '$(buildConfiguration)'
Now I can build the solution. The build will fail if the required NuGet packages aren’t present though, so I need to restore them first. Note I’m using the variables here that I set up earlier using the
- task: VSTest@2 inputs: platform: '$(buildPlatform)' configuration: '$(buildConfiguration)'
Finally, now the tool is built I can run the tests. This will automatically discover and run all the tests in the solution, and if any fail the errors will be reported back and the build will show as being in error.
I want all changes to go through a pull request, and this build should run to validate the changes. If the build or any of the tests fail, I shouldn’t be able to merge the PR.
I can set this up in the repo settings in GitHub under Settings > Branches > Branch protection rules:
Now when I create a new PR I immediately see that it’s waiting for validation.
Until the build passes the merge button is disabled. When it does pass I get a little tick icon next to the PR and I know it’s safe to merge.
As I merge a PR I need to consider whether this represents a major, minor or patch change, as this will feed into the automated versioning on the next release.
If it’s a patch, there’s nothing more I need to do other than merge the PR as normal. Otherwise I just need to add a comment to the merge commit including the text
+semver:minor. GitVersion will take this into account when working out the next version number.
You can also add the same version information into the original commits. If you do this you can see what the next version number will be by looking at the name of the automated build that’s triggered.
I could go another step and automatically push out a new release for every change, but I don’t want to swamp users with notifications of new releases every day. So after accumulating a number of changes I now need to force a build manually that will form my new release.
This uses another yaml script similar to the one above. This one doesn’t have the PR trigger at the start and has a few extra steps at the end:
- task: NuGetCommand@2 displayName: Pack inputs: command: pack packagesToPack: '**/*.nuspec' packDestination: '$(Build.ArtifactStagingDirectory)' versioningScheme: byEnvVar versionEnvVar: GITVERSION_NUGETVERSION - task: PublishPipelineArtifact@1 displayName: Publish NuGet packages to pipeline inputs: targetPath: '$(Build.ArtifactStagingDirectory)' artifact: 'NuGetPackages' publishLocation: 'pipeline' - task: GitTag@5 displayName: Label Source inputs: workingdir: '$(Build.SourcesDirectory)' tag: 'v$(GitVersion.NuGetVersion)' env: SYSTEM_ACCESSTOKEN: '$(System.AccessToken)'
The first step generates the NuGet packages, and applies the version number generated by GitVersion to those packages. The second step then publishes the generated packages to the pipeline where we can access them from later.
Finally, the last step adds a tag to the source to indicate the version number. This lets me quickly go back to the source that was used to publish any given version in case there’s a new bug introduced at any point. Again, this uses the GitVersion-generated version number for the tag.
Once the build is complete I need to push it to NuGet so that it’s available to download in XrmToolBox. Jonas has already covered this in his blog so I’m not going to repeat that here.
Another step I want to do though is publish the release to GitHub. This lets users easily see a history of releases and download the source and binaries of any earlier release.
Thankfully Microsoft have already thought of this and have a great GitHubRelease task that I can drop into the same release pipeline. As well as storing the same .nupkg files as it’s pushed to NuGet, GitHub also creates a zip file of the source code at that point. The task also pulls together all the commits or issues since the previous release to form the basis of the release notes, which is really helpful in letting users know what’s changed.