Skip to content

Automating release notes in Azure DevOps

Updated:
Automating release notes in Azure DevOps

Having release notes for software might seem like a thing from the past for many teams. A lot of software is web-based and released continuously, making release notes less relevant. However, there are still many projects that are released less frequently and require tracking of what changes are part of a release.

The problem

On a recent project I worked on we needed to provide an overview of changes to a solution that is released to remote customers. This information was relevant to the DevOps team as well as the internal first line support team.
So, release notes it is, but obviously we were not overly excited by the idea of writing these by hand.

The solution in question is build and released using Azure YAML (multi-stage) pipelines. YAML pipelines are a great improvement over Classic Pipelines but are still lacking in some areas, especially when it comes to having a good overview of changes per release.

The multi-stage pipeline is responsible for a build and releases to test, acceptance and production environments. Not every build will make it to production, so we only want release notes for production releases and it should contain all changes since the previous release to production.

The release notes should contain the following information:

Possible solutions

It makes sense to solve the problem using the pipeline we already have because all information should be available from there. We can gather all required information from the DevOps REST API but that’s not trivial. Luckily, there is an awesome DevOps extension called Generate Release Notes available that will do all the hard work for us.

The extension supports both Classic Build and Release Pipelines as well as single and multi-stage YAML Pipelines on both Windows and Linux.
It uses Handlebars templates to generate the release notes and can access all fields from the build and release REST API.

Generating a release notes file

The first step is to generate a release notes file using the extension. It’s very simple to format it using markdown so we’ll use that, but we could just as easily have generated HTML.
For now, we’ll just generate a markdown file and publish it as an artifact of the pipeline.

In our existing pipeline, directly in the product release stage, we’re going to add the following task and define an inline template for create the release notes.

- task: XplatGenerateReleaseNotes@4
  inputs:
    outputfile: '$(Build.ArtifactStagingDirectory)/releasenotes.md'
    templateLocation: 'InLine'
    inlinetemplate: |
    # {{buildDetails.buildNumber}}
    {{buildDetails.startTime}}

    **Work Items**
    |Id|Type|Title|
    |-|-|-|
    {{#forEach this.workItems}}
    |{{this.id}}|{{lookup this.fields 'System.WorkItemType'}}|[{{lookup this.fields 'System.Title'}}]({{replace this.url "_apis/wit/workItems" "_workitems/edit"}})|
    {{/forEach}}

    **Pull Requests**
    {{#forEach pullRequests}}
    * [{{this.title}}]({{replace (replace this.url "_apis/git/repositories" "_git") "pullRequests" "pullRequest"}})
    {{/forEach}}
    
    checkStage: true
    replaceFile: false
    getParentsAndChildren: false
    getAllParents: false
    getIndirectPullRequests: false
    stopOnError: false
    considerPartiallySuccessfulReleases: false
    checkForManuallyLinkedWI: false
    wiqlFromTarget: 'WorkItems'

- publish: $(Build.ArtifactStagingDirectory)
  artifact: ReleaseNotes

Note the checkStage: true parameter, which ensures that only the changes since the last successful release to the production deployment stage are included.
The template is fairly straightforward but does contain some string replacement to make the links to work items and pull requests work in the generated markdown file.

There is a lot more possible in the template than what we’re using here, but this simple setup should suit our needs.

The generated release notes artifact that is published looks something like this: Markdown artifact

Distributing the release notes

So now we have a markdown file as an artifact in the pipeline, but we still need to distribute it somehow.

There are several options:

For this example, we’ll publish it to a DevOps (Services) wiki since that’s the quickest to setup, distribute and maintain.

Since DevOps wiki’s are just git repositories, we could push to it from a script task. However, there’s a nice extension for that as well: Wiki Updater Task (from the same author as the Generate Release Notes extension).

We can supply the markdown file we generated in the previous step and feed it the the Wiki Updater Task. The configuration looks like this:

- task: WikiUpdaterTask@2
  inputs:
    repo: 'https://dev.azure.com/m-w/MyProduct/_git/ExampleWiki'
    branch: 'main'
    filename: 'Root-page/MyProduct-Versions.md'
    replaceFile: false
    dataIsFile: true
    sourceFile: '$(Build.ArtifactStagingDirectory)/releasenotes.md'
    message: 'Automatic commit from pipeline'
    gitname: 'pipeline'
    gitemail: '[email protected]'
    useAgentToken: true
    localpath: '$(System.DefaultWorkingDirectory)\repo'

In order to use the file we generated in the previous step, we need to set dataIsFile: true and sourceFile: '$(Build.ArtifactStagingDirectory)/releasenotes.md'.

To keep adding release notes to the same page, set dataIsFile: true. This way every release is added to the bottom of the same wiki page.

Finding the URL of the wiki repo is a bit tricky. It’s not directly available in the DevOps UI for Project wiki’s but can be found by selecting ‘clone wiki’ and copying the HTTPS URL: Clone Wiki

Authorizing the pipeline to push to the wiki

By default, the pipeline will not be authorized to push to the wiki. There are 3 things we need to fix for that:

Authentication can be done in several ways but the most maintainable way is to use the Build Agents OAUTH token. For YAML pipelines this token is always available and can be accessed using the $(System.AccessToken) variable.
Luckily, the Wiki Updater Task supports this out of the box and we do not need to do anything for it.

Give the buid agent user access to the Wiki repo from the security tab of the repo in Project settings -> Repositories -> Your-Wiki-Repo -> Security. Add the user Your-Project-Name Build Service and allow ‘Contribute’ access.

By default build agents are not authorized to push to other repos other than the one they are building. This is controlled by the organization and project setting Protect access to repositories in YAML pipelines, which is enabled by default for all organisations and projects created after May 2020. For more information, see the official Microsoft Docs.
We could disable this setting, but there’s a better way. Especially in larger organizations a developer will probably not be allowed to modify organizational settings.
Fortunately the work-around is simple: we can reference the wiki repo in the pipeline as a resource and the build agent will be authorized to push to it. Add the following piece of YAML to the top of the pipeline to give the build agent access to the wiki repo:

resources:
  repositories:
    - repository: wiki
      type: git
      name: MyProduct/ExampleWiki
      ref: main

Since March 2023 it’s required to reference or checkout the repo otherwise the access token is not scoped to it. Since the extension will clone the repo we just need to add a uses statement to the job:

- job:
  uses: 
    repositories: [ wiki ]

If for some reason the authorization fails the following error will be shown:
The Git repository with name or identifier ExampleWiki does not exist or you do not have permissions for the operation you are attempting.

Putting everything together

Now we have all pieces in place we can gather the changes, build a markdown file and push it to our wiki. The final pipeline looks like this:

trigger:
- main

pool:
  vmImage: ubuntu-latest

resources:
  repositories:
    - repository: wiki
      type: git
      name: MyProduct/ExampleWiki
      ref: main

stages:
  - stage: build
    jobs:
      - job: 
        displayName: build website
        steps:
          - bash: echo "Dummy build step"

  # other stages not included in example

  - stage:
    displayName: deploy prod
    jobs:
    - deployment: 
      displayName: deploy website
      environment: my-prod-environment
      strategy:
          runOnce:
            deploy:
              steps:
                - bash: echo "Dummy deployment step"
    - job:
      uses: 
        repositories: [ wiki ] # reference to the git repo  
      steps:
      - task: XplatGenerateReleaseNotes@4
        inputs:
          outputfile: '$(Build.ArtifactStagingDirectory)/releasenotes.md'
          templateLocation: 'InLine'
          inlinetemplate: |
            # {{buildDetails.buildNumber}}
            {{buildDetails.startTime}}

            **Work Items**
            |Id|Type|Title|
            |-|-|-|
            {{#forEach this.workItems}}
            |{{this.id}}|{{lookup this.fields 'System.WorkItemType'}}|[{{lookup this.fields 'System.Title'}}]({{replace this.url "_apis/wit/workItems" "_workitems/edit"}})|
            {{/forEach}}

            **Pull Requests**
            {{#forEach pullRequests}}
            * [{{this.title}}]({{replace (replace this.url "_apis/git/repositories" "_git") "pullRequests" "pullRequest"}})
            {{/forEach}}
            
          checkStage: true
          replaceFile: false
          getParentsAndChildren: false
          getAllParents: false
          getIndirectPullRequests: false
          stopOnError: false
          considerPartiallySuccessfulReleases: false
          checkForManuallyLinkedWI: false
          wiqlFromTarget: 'WorkItems'
        
      - task: WikiUpdaterTask@2
        inputs:
          repo: 'https://dev.azure.com/m-w/MyProduct/_git/ExampleWiki'
          branch: 'main'
          filename: 'Root-page/MyProduct-Versions.md'
          replaceFile: false
          dataIsFile: true
          sourceFile: '$(Build.ArtifactStagingDirectory)/releasenotes.md'
          message: 'Automatic commit from pipeline'
          gitname: 'pipeline'
          gitemail: '[email protected]'
          useAgentToken: true
          localpath: '$(System.DefaultWorkingDirectory)\repo'

If the specified wiki page does not exist, it will be created. If the page already exists, the new content will be appended to the bottom of the page.
To have a nice overview of all releases, we can add a table of contents to the top of the page:

[[_TOC_]]

The wiki page will look like this: Wiki page

Conclusion

With a few simple steps we now automatically add release notes to our wiki with every release to production, giving us a nice overview of all releases.

There are a few things that could be improved, such as the formatting of the release date, which does not seem to be possible at the moment.
Another thing which I did not include in this post is using Gitversion to automatically generate the semantic version number. It’s relatively easy to add it to our build pipeline, but I will leave that for another time.

To conclude, a big thank you to Richard Fennell for creating these great free DevOps extensions.

Update March 07, 2023: Added uses statement to ensure access token is still scoped to wiki repo.