Integrations: Azure Pipelines

The Azure Pipelines CI/CD service is a great match for Cranko because its ability to divide builds into stages that exchange artifacts works very nicely with Cranko’s model for CI/CD processing. This section will go over ways that you can use Cranko in the Azure Pipelines framework.

Examples

Here are some projects that use Cranko in Azure Pipelines:

General structure

For many projects, it works well to adopt an overall pipeline structure with two stages:

trigger:
  branches:
    include:
    - master
    - rc

stages:
- stage: BuildAndTest
  jobs:
  - template: azure-build-and-test.yml

- stage: Deploy
  condition: and(succeeded('BuildAndTest'), ne(variables['build.reason'], 'PullRequest'))
  jobs:
  - template: azure-deployment.yml

The BuildAndTest stage can contain many parallel jobs that might build your project on, say, Linux, MacOS, and Windows platforms. If all of those jobs succeed, and the build is not a pull request (so, it was triggered in an update to the master or rc branch), the deployment stage will run.

Here, we use templates to group the jobs for the two stages into their own files. Templates are generally helpful for breaking CI/CD tasks into more manageable chunks. However, they can be a bit tricky to get the hang of; a key restriction is that templates are processed at “compile time”, and some variables or other build settings are not known until “run time”.

Installing Cranko

To install the latest version of Cranko into your build workers, we recommend the following pair of tasks. By using a condition here, these tasks can be run on agents running any operating system, and the right thing will happen. This is useful if this setup step goes into a template.

- bash: |
    set -euo pipefail  # note: `set -x` breaks ##vso echoes
    d="$(mktemp -d /tmp/cranko.XXXXXX)"
    cd "$d"
    curl --proto '=https' --tlsv1.2 -sSf https://pkgw.github.io/cranko/fetch-latest.sh | sh
    echo "##vso[task.prependpath]$d"
  displayName: Install latest Cranko (not Windows)
  condition: and(succeeded(), ne(variables['Agent.OS'], 'Windows_NT'))

- pwsh: |
    $d = Join-Path $Env:Temp cranko-$(New-Guid)
    [void][System.IO.Directory]::CreateDirectory($d)
    cd $d
    [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
    iex ((New-Object System.Net.WebClient).DownloadString('https://pkgw.github.io/cranko/fetch-latest.ps1'))
    echo "##vso[task.prependpath]$d"
  displayName: Install latest Cranko (Windows)
  condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'))

If all of your agents will be running on the same operating system, you can choose the appopriate task and remove the condition.

Creating and transferring the release commit

If you use a multi-stage build process, a wrinkle emerges. You need to create a single “release commit” to be published if the CI/CD succeeds. But if publication happens in your Deploy stage, those jobs are separate from the build jobs that actually ran the cranko release-workflow apply-versions and cranko release-workflow commit commands.

We recommend publishing the release commit as an Azure Pipelines artifact. This can be accomplished pretty conveniently with the Git bundle functionality. All of your main build jobs should apply version numbers:

- bash: |
    set -xeuo pipefail
    git status # [see below]
    cranko release-workflow apply-versions
  displayName: Apply versions with Cranko

(The git status helps on Windows, where it seems that sometimes libgit2 thinks that the working tree is dirty even though it’s not. There’s some issue about updating the Git index file.)

One of your build jobs should also commit the version numbers into a release commit, and publish the resulting commit as a Git bundle artifact:

- bash: |
    set -xeuo pipefail
    git add .
    cranko release-workflow commit
    git show  # useful diagnostic
  displayName: Generate release commit

- bash: |
    set -xeuo pipefail
    mkdir $(Build.ArtifactStagingDirectory)/git-release
    git bundle create $(Build.ArtifactStagingDirectory)/git-release/release.bundle origin/master..HEAD
  displayName: Bundle release commit

- task: PublishPipelineArtifact@1
  displayName: Publish git bundle artifact
  inputs:
    targetPath: '$(Build.ArtifactStagingDirectory)/git-release'
    artifactName: git-release

(As a side note, if you run bash tasks on Windows, there is currently a bug where variables such as $(Build.ArtifactStagingDirectory) are expanded as Windows-style paths, e.g. C:\foo\bar, rather than Unix-style paths, /c/foo/bar. You will either need to transform these variables, or not use bash in Windows.)

Your deployment stages should then retrieve this artifact and apply the release commit:

# Fetch artifacts from previous stages
- download: current

# Check out source repo again
- checkout: self
  submodules: recursive

- bash: |
    set -xeuo pipefail
    git switch -c release
    git pull --ff-only $(Pipeline.Workspace)/git-release/release.bundle
  displayName: Restore release commit

Standard deployment jobs

If your pipeline is running in response to an update to the rc branch, and your CI tests succeeded, there are several common deployment steps that you can invoke as (more or less) independent jobs. We recommend using a template with standard setup steps to install Cranko and recover the release commit, as shown above. Here we’ll assume that these have been bundled into a template named azure-deployment-setup.yml.

We also assume here that you have a variable group called Deployment Credentials that includes necessary credentials in variables named GITHUB_TOKEN, NPM_TOKEN, etc.

No matter which packaging system(s) you use, you should create tags and update the upstream release branch. This example assumes that it lives on GitHub:

- ${{ if eq(variables['Build.SourceBranchName'], 'rc') }}:
  - job: branch_and_tag
    pool:
      vmImage: ubuntu-latest
    variables:
    - group: Deployment Credentials
    steps:
    - template: azure-deployment-setup.yml

    - bash: |
        cranko github install-credential-helper
      displayName: Set up Git pushes
      env:
        GITHUB_TOKEN: $(GITHUB_TOKEN)

    - bash: |
        set -xeou pipefail
        cranko release-workflow tag
        git push --tags origin release:release
      displayName: Tag and push
      env:
        GITHUB_TOKEN: $(GITHUB_TOKEN)

GitHub releases

If you are indeed using GitHub, Cranko can automatically create GitHub releases for you. You must ensure that this task runs after the tags are pushed, because otherwise GitHub will auto-create incorrect tags for you:

- ${{ if eq(variables['Build.SourceBranchName'], 'rc') }}:
  - job: github_releases
    dependsOn: branch_and_tag # otherwise, GitHub creates the tags itself!
    pool:
      vmImage: ubuntu-latest
    variables:
    - group: Deployment Credentials

    steps:
    - template: azure-deployment-setup.yml

    - bash: cranko github install-credential-helper
      displayName: Set up Git pushes
      env:
        GITHUB_TOKEN: $(GITHUB_TOKEN)

    - bash: cranko github create-releases
      displayName: Create GitHub releases
      env:
        GITHUB_TOKEN: $(GITHUB_TOKEN)

You might also use cranko github upload-artifacts to upload artifacts associated with those releases, although if you have a monorepo you must use cranko show if-released to check at runtime whether the project in question was actually released.

Cargo publication

If your repository contains Cargo packages, you should publish them:

- ${{ if eq(variables['Build.SourceBranchName'], 'rc') }}:
  - job: cargo_publish
    pool:
      vmImage: ubuntu-latest
    variables:
    - group: Deployment Credentials

    steps:
    - template: azure-deployment-setup.yml

    - bash: cranko cargo foreach-released publish
      displayName: Publish updated Cargo crates
      env:
        CARGO_REGISTRY_TOKEN: $(CARGO_REGISTRY_TOKEN)

NPM publication

Likewise for NPM packages:

- ${{ if eq(variables['Build.SourceBranchName'], 'rc') }}:
  - job: npm_publish
    pool:
      vmImage: ubuntu-latest
    variables:
    - group: Deployment Credentials

    steps:
    - template: azure-deployment-setup.yml

    - bash: cranko npm install-token
      displayName: Set up NPM authentication
      env:
        NPM_TOKEN: $(NPM_TOKEN)

    # [ do any necessary build stuff here ]

    - bash: cranko npm foreach-released npm publish
      displayName: Publish to NPM

    - bash: shred ~/.npmrc
      displayName: Clean up credentials