Generating releases for your project shouldn’t be a chore, yet many times it does prove to be a pain. If you don’t release very often or on a regular schedule, you have to go back and remember how to do it. This can result in inconsistencies in your releases, which makes it harder on your users.

I recently set up automated releases for a C++ project in Bitbucket. This process automates the steps to create a release branch, bump the version per Semantic Versioning, generate a changelog according to Conventional Commits, and push the release back to git, fully tagged and ready to go. Here is how I did it.

Define the common steps in your pipeline

# Default image to use - version 3.x
image: atlassian/default-image:3
definitions:
  services:
    docker:
      memory: 7128
  steps:
      - step: &Build-Application
          name: Build Application
          image: rikorose/gcc-cmake
          size: 2x
          script:
            # Update the submodules
            - git submodule update --recursive --init
            # Install the dependencies
            - apt-get update && export DEBIAN_FRONTEND=noninteractive
            - apt-get -y install --no-install-recommends uuid-dev libssl-dev libz-dev libzmq5 libzmq3-dev
            # Print the Linux version.
            - uname -a
            # Print the gcc version.
            - gcc --version

            # Print the CMake version.
            - cmake --version
            # Setup the build
            - mkdir _bld && cd _bld
            # Call CMake
            - cmake -DCMAKE_BUILD_TYPE=Debug ..
            # Build project
            - make -j10
      - step: &Build-Container
          name: Test Container Build
          size: 2x
          script:
            # Update the submodules
            - git submodule update --recursive --init
            # Build the container
            - docker build --file ./Dockerfile .
          services:
            - dockerCode language: PHP (php)

In this snippet, I define which image to use by default for all the steps. I chose to use the default Atlassian image, but specified version 3. If you do not specify a version here (with the :3) you wind up with a really old version of the image that is kept around for backwards compatibility.

I also define two common build steps, called Build-Application and Build-Container, which I use later on in my pipeline.

Define the pipeline(s)

pipelines:
  custom:
    generate-release:
      - step:
          name: Generate release branch
          script:
            - git checkout master
            - git pull --ff-only
            - git checkout -b release/next
            - git push -u origin release/next
  pull-requests:
    '**': # all PRs
      - step: *Build-Application
      - step: *Build-Container
  branches:
    master:
      - step: *Build-Application
      - step: *Build-ContainerCode language: PHP (php)

This snippet generates a few pipelines, one that runs each time the master branch is updated on the server, one that runs for every pull request created, and one custom pipeline that must be run manually.

The master branch and pull-requests pipelines are identical and simply utilize the defined steps from the previous step. The custom pipeline however has a single role: create a new branch called release/next and push that back to the server. As you’ll see in the next section, this will trigger another branch pipeline.

Define the release generation pipeline

  branches:
    # master branch defined here previously
    release/next:
      - step:
          name: Generate Release
          script:
            # Configure npm to work properly as root user in Ubuntu
            - npm config set user 0
            - npm config set unsafe-perm true
            # Install necessary release packages and generate release, pushing back to repo
            - npm install -g release-it @release-it/conventional-changelog @j-ulrich/release-it-regex-bumper --save-dev
            - release-it --ci
      - parallel:
        - step:
            name: Publish to External Continuous Delivery System
            script:
              - export APP="name_of_app"
              - git clone --recursive https://url.of.your.cd.com/your-cd-repo.git
              - cd containers
              - git checkout testing
              - git submodule update --recursive --init
              - cd ${APP} && git checkout master
              - git pull
              - export VERSION=$(git tag | sort -V | tail -1)
              - >
                echo "Updating ${APP} to Release Version: ${VERSION}"
              - git checkout ${VERSION}
              - cd ../
              - git add ${APP}
              - >
                git -c user.name='Bitbucket Pipeline' -c user.email='bitbucket-pipeline@witricity.com' commit -m "${APP}: update to version ${VERSION}"
              - git push
        - step:
            name: Create Pull Request
            caches:
              - node
            script:
              - apt-get update
              - apt-get -y install curl jq
              - export DESTINATION_BRANCH="master"
              - export CLOSE_ME="true"
              - >
                export BB_TOKEN=$(curl -s -S -f -X POST -u "${BB_AUTH_STRING}" \
                  https://bitbucket.org/site/oauth2/access_token \
                  -d grant_type=client_credentials -d scopes="repository" | jq --raw-output '.access_token')
              - >
                export DEFAULT_REVIEWERS=$(curl https://api.bitbucket.org/2.0/repositories/${BITBUCKET_REPO_OWNER}/${BITBUCKET_REPO_SLUG}/default-reviewers \
                  -s -S -f -X GET \
                  -H "Authorization: Bearer ${BB_TOKEN}" | jq '.values' | jq 'map({uuid})' )
              - >
                curl https://api.bitbucket.org/2.0/repositories/${BITBUCKET_REPO_OWNER}/${BITBUCKET_REPO_SLUG}/pullrequests \
                  -s -S -f -X POST \
                  -H 'Content-Type: application/json' \
                  -H "Authorization: Bearer ${BB_TOKEN}" \
                  -d '{
                        "title": "Release '"${BITBUCKET_BRANCH}"'",
                        "description": "Automated PR release :)",
                        "source": {
                          "branch": {
                            "name": "'"${BITBUCKET_BRANCH}"'"
                          }
                        },
                        "destination": {
                          "branch": {
                            "name": "'"${DESTINATION_BRANCH}"'"
                          }
                        },
                        "close_source_branch": '"${CLOSE_ME}"',
                        "reviewers": '"${DEFAULT_REVIEWERS}"'
                      }'
Code language: PHP (php)

This is a rather large block, but is pretty straight-forward.

The first step, called “Generate Release”, is where the release magic happens. It uses the NPM tool called release-it to generate the release. Basically, this utilizes a configuration file in the repository named .release-it.json. Based on that file, it will automatically do the following:

  • Bump the version, based on how you define it in .release-it.json
  • Generate and update a changelog
  • Git commit, tag, and push
  • And much more if you so choose…

Since this is run in the release/next branch, the version, changelog and all other changes are made and pushed here. At that point, I wanted to do two things: first, publish this new release to my external continuous delivery system; and second, automatically generate a pull request in Bitbucket to get the release back in the master branch.

My .release-it.json file looks like this:

{
  "git": {
    "commitMessage": "[skip ci] ci: release v${version}"
  },
  "plugins": {
    "@release-it/conventional-changelog": {
        "preset": {
            "name": "conventionalcommits",
            "commitUrlFormat": "{{host}}/{{owner}}/{{repository}}/commits/{{hash}}",
            "compareUrlFormat": "{{host}}/{{owner}}/{{repository}}/compare/{{currentTag}}..{{previousTag}}",
            "types": [
              {
                "type": "feat",
                "section": "Features"
              },
              {
                "type": "fix",
                "section": "Bug Fixes"
              },
              {
                "type": "perf",
                "section": "Performance Improvements"
              }
            ]
        },
        "infile": "CHANGELOG.md"
    },
    "@j-ulrich/release-it-regex-bumper": {
        "out": [
            {
                "file": "CMakeLists.txt",
                "search": "VERSION {{semver}}",
                "replace": "VERSION {{versionWithoutPrerelease}}"
            },
            {
                "file": "Dockerfile",
                "search": "Version={{semver}}",
                "replace": "Version={{versionWithoutPrerelease}}"
            }
        ]
    }
  }
}
Code language: JSON / JSON with Comments (json)

At this point, to generate a release, just go to the pipelines page for your repository and select Run pipeline. Then choose what branch you want to use for the basis of your release (I typically release from master) and choose the ‘custom: generate-release’ pipeline and off you go!

Conclusion

This process greatly simplifies my life when it comes to release a new version of my projects. Could this be fully automated? Absolutely — I’m just not there quite yet.

I hope you find this useful!

Last modified: January 30, 2023

Author

Comments

Write a Reply or Comment

Your email address will not be published.