I recently started creating Azure DevOps templates to create more extensible pipelines. I figured setting tags would be a good place to start with templates.

Giving your pipeline permissions to tag

Your pipeline by default does not have permissions to:

  1. Use the git credentials it initially used to checkout your code
  2. Cannot by default push the tags

If you try to just straight up run a git tag you’ll get an error along the lines of:

connection-error

Set the build service permissions on the project

By setting the create tag and contribute permissions on the project as a whole, each repo will have access to push tags. If you want to limit it by repo, you can do that as well. For our needs, we are going to set it at the project level. Again, since we are setting a project level setting, select project settings then repositories. Notice we select the [project-name] Build Service under Users.

set-proj-perms-tags

Allow the pipeline to use the git credential

We need to explicitly use the checkout task to allow the pipeline to persistCredentials. This will most likely be the first step in the steps section of the template.

- checkout: self
  clean: true
  persistCredentials: true

Once we’ve set both of these permissions. We are ready to code away!

Writing Templates

For my needs, I want to tag to be the latest version from the CHANGELOG.md. I decided to use the pwsh task because I am using Microsoft hosted agents and PowerShell Core is available by default on both Linux and Windows agents.

I am making the assumption for my code that the CHANGELOG.md is at the root of the repo.

My get-changelog.template.yml looks like:

steps:
- pwsh: |
    $cl = Get-Content 'CHANGELOG.md'
    $version = ($cl | Sort-Object { [version] ($_ -replace '^.*?(\d+(\.\d+){1,3})$', '$1') } -Descending -ErrorAction SilentlyContinue)[0]
    $version = $version | Select-String -Pattern "\d.\d.\d" | foreach {$_.Matches.Value}
    echo "##vso[task.setvariable variable=cl_version]$version"
    write-output "latest version is: $version"
  workingDirectory:  $(Build.SourcesDirectory)
  displayName:  get-changelog-version

I am using the logging command syntax to set a variable called cl_version to equal the $version returned from the CHANGELOG.md. This syntax shouldn’t be utilized in all scenarios.

When writing templates it is important to understand how Azure DevOps pipelines run and execute. I am not going to cover this here, it is a completely different topic and there are other blogs that cover this, as well as Microsoft.

Notice, I didn’t specify setting tags in my get-changelog.template.yml because I want it to be a separate template. I can just call my get-changelog.template.yml from my set-tag.template.yml.

I’ve also accounted for if the user wants to overwrite an existing tag. In my case, I don’t want this to occur by default so i’ll set overwrite_existing_tag to false. I am also able to access the cl_version variable in my set-tag.template.yml.

My set-tag.template.yml looks like:

parameters:
- name: overwrite_existing_tag
  type: boolean
  default: false

steps:
- checkout: self
  clean: true
  persistCredentials: true

- template: get-changelog-version.template.yml

- ${{ if eq(parameters.overwrite_existing_tag, true) }}:
  - pwsh: |
      git config --global user.name "AzureDevOps Agent"
      git tag "$(cl_version)" --force
      git push origin "$(cl_version)" --force
      Write-Output "Tagging $(Build.Repository.Name) with $(cl_version)"
    displayName:  set-tag


- ${{ if eq(parameters.overwrite_existing_tag, false) }}:
  - pwsh: |
      git config --global user.name "AzureDevOps Agent"
      if (git tag | select-string "$(cl_version)") {
        Write-Output "tag already exists for $(cl_version).  Set overwrite_existing_tag to true if you want to override it"
        exit 1
      }
      else {
        git tag "$(cl_version)"
        git push origin "$(cl_version)"
        Write-Output "Tagging $(Build.Repository.Name) with $(cl_version)"
      }
    displayName:  set-tag

The azure-pipelines.yml file

Now in my regular pipelines file, I only want to set a tag if we’re running on the master branch, so it looks like:

trigger:
  branches:
    include:
      - '*'

name: $(BuildID)

pool:
  vmImage: 'ubuntu-latest'

steps:
- ${{ if eq(variables['Build.SourceBranchName'], 'master') }}:
  - template: set-tag.template.yml

Where do I store my templates?

For testing and noodling around, you can put your templates next to the regular azure-pipelines.yml but this isn’t scalable.

For a more scalable approach, put all templates in their own repo and publish them as an artifact. As long as the projects are located in the same organization, it is very easy to pull artifacts from other projects and repos.