Skip to main content

Manage your project version

Every Expeditor project falls into one of three versioning paradigms:

  1. Single Component; Semantic Version: Projects like chef/chef, which consist of a single component that is semantically versioned.
  2. Multiple Components; Semantic Version: Projects like habitat-sh/habitat, which contains multiple components — each of which may have their own versioning scheme — but all roll up under a single semantic version.
  3. Multiple Components; Non-standard Version: Projects like chef/automate, which contains multiple components — each of which may have their own versioning scheme — but not roll up under a single a semantic version, instead opting for an alternative versioning scheme (such as a timestamp).

This guide is going to cover the first two types of projects — ones which have a single semantic version like chef/chef and habitat-sh/habitat. If your project requires an alternate (non-standard) versioning scheme like chef/automate, please reach out to Release Engineering.

Note

Chef-flavored Semantic Versioning

At Chef, we treat what is normally considered the patch version of the semantic version instead as a build version. This build version is automatically incremented by Expeditor — typically when a GitHub pull request is merged. Each project may dictate the cadence by which they bump their major and minor versions, but we ask that you respect the rules of semantic versioning when it comes to breaking and/or incompatible API changes.

There are four aspects to version bumping that we will cover in this guide:

  1. Basic management of your VERSION file
  2. How version git tags are generated
  3. Controlling major and minor increments using GitHub labels
  4. Automating code changes when you bump your version

Basic management of your VERSION file

At Chef, we manage our version using the VERSION file. This file — the location of which is configurable using github.version_file, but defaults to the root of your GitHub repository — is the source of truth for your version. It is a plain-text file that contains only your version in MAJOR.MINOR.BUILD format. Expeditor does not support version metadata suffixes such as -dev.

Expeditor is able to modify the content of your VERSION file using the built_in:bump_version action in response to the pull_request_merged workload.

.expeditor/config.yml
subscriptions:
  - workload: pull_request_merged:{{github_repo}}:{{release_branch}}:*
    actions:
      - built_in:bump_version

You can control the behavior of built_in:bump_version action using a variety of Expeditor configuration values (which we will discuss in a moment). Right now, there are two important things to remember about the built_in:bump_version action:

  1. If the VERSION file was not modified as part of your pull request, the built_in:bump_version action will update the VERSION file.
  2. If the VERSION file was manually modified as part of your pull request, the built_in:bump_version action will not update the VERSION file and instead continue with the existing value as-is.

There are many situations where you will have a pull request that does not warrant a modification of the VERSION file. Common examples of these situations include modifications to non-application-related files such as:

  • READMEs or other user-facing documentation
  • Test and development environment files like Terraform or TravisCI configuration
  • Release configuration files like your .expeditor/config.yml

Whatever the situation may be, if you do not want to bump the version as a result of a pull request, you’ll want to set up and configure a not_if_labels action filter.

.expeditor/config.yml
subscriptions:
  - workload: pull_request_merged:{{github_repo}}:{{release_branch}}:*
    actions:
      - built_in:bump_version:
          not_if_labels:
            - "Expeditor: Skip Version Bump"
            - "Expeditor: Skip All"

The common practice is to configure two labels:

  • Expeditor: Skip Version Bump is used exclusively to prevent the version from being bumped.
  • Expeditor: Skip All is a label we recommend configuring on all actions, allowing you to easily skip multiple Expeditor actions with a single label.

Now, if you apply one of these two labels to your GitHub pull request, Expeditor will know that you do not want to bump your version.

How version git tags are generated

In our introduction to action sets we introduced the three phases of action sets: pre-commit, commit, and post-commit. When your VERSION file is modified as part of a pre-commit action like built_in:bump_version or as part of the pull request itself, Expeditor will automatically create a git tag and push it up to GitHub. The format of your git tag is controlled by the github.version_tag_format configuration value.

We recommend you stay with the default format for the tag which is v{{version}} (which will result in tags like v1.0.0). You may specify an alternate format using the github.version_tag_format configuration setting.

Controlling major and minor version increments using GitHub labels

In addition to using GitHub labels to indicate that you do not want to bump the version of your project, you can also use them to indicate when you’d like to bump your major or minor versions. Building on our example from above, let’s see what that configuration would look like.

.expeditor/config.yml
---
github:
  minor_bump_labels:
    - "Expeditor: Bump Version Minor"
  major_bump_labels:
    - "Expeditor: Bump Version Major"

subscriptions:
  - workload: pull_request_merged:{{github_repo}}:{{release_branch}}:*
    actions:
      - built_in:bump_version:
          ignore_labels:
            - "Expeditor: Skip Version Bump"
            - "Expeditor: Skip All"

In this example we’ve introduced two new Expeditor configuration values: github.minor_bump_labels and github.major_bump_labels. These configuration values are handy on open source projects when a contribution from an external contributor warrants a change in the major or minor version. Rather than having to work with the contributor to make the needed modifications to their pull request, you can instead simply apply a GitHub label to the pull request and allow Expeditor to handle the rest.

Automating code changes when you bump your version

Most applications expose their version via some API. Given that our version is now managed in our VERSION file, engineers have two options:

  1. Use a bash action to run a script that replaces all occurrences of your old version in your source code with your new version.
  2. Configure your application to dynamically read the VERSION file.

We recommend you go with the first option: writing a bash action. If you have not written a bash action before, we recommend you check out our article on best practices for writing Bash scripts.

The example below shows what such a configuration might look like.

.expeditor/config.yml
---
github:
  minor_bump_labels:
    - "Expeditor: Bump Version Minor"
  major_bump_labels:
    - "Expeditor: Bump Version Major"

subscriptions:
  - workload: pull_request_merged:{{github_repo}}:{{release_branch}}:*
    actions:
      - built_in:bump_version:
          ignore_labels:
            - "Expeditor: Skip Version Bump"
            - "Expeditor: Skip All"
      - bash:.expeditor/update_version.sh:
          only_if: built_in:bump_version

Let’s quickly run down the new configuration we’ve added to our example.

  1. bash:.expeditor/update_version.sh. This is the file that will replace all occurrences of your old version in your source code with the new version (using a utility like sed).
  2. only_if: built_in:bump_version. The only_if action filter indicates that we should only execute the bash action if we’ve first executed the built_in:bump_version action.

Note

The built_in:bump_version will execute with a no-op if you’ve manually modified the VERSION file as part of the pull request. With the configuration above, the only time our bash action would be skipped is if the Expeditor: Skip Version Bump or Expeditor: Skip All label is applied to your GitHub pull request.

The content inside your .expeditor/update_version.sh file will vary depending on your project. We encourage you to look at other projects similar to yours as examples, but for our example we’ll use a pattern commonly used for RubyGems:

#!/bin/bash
set -eou pipefail

# Grab the current version
VERSION=$(cat VERSION)

# Use sed and regular expressions to replace the versions in the relevant files
sed -i -r "s/^(\s*)VERSION = \".+\"/\1VERSION = \"${VERSION}\"/" lib/my-gem/version.rb

Managing multiple release branches

A “release branch” is a git branch on your GitHub repository from which you wish to maintain versions of your software project.

Expeditor works under the assumption that your software project is developed using the GitHub Flow. This means that the repository’s default branch is associated with the latest (or “currently in development”) release of your software project. However, your GitHub repository may also have additional release branches which contain long-lived branches for previous releases of your software project that are kept around for any number of reasons.

The most common scenario for needing to add a new release branch bumping the major version of your software project. Your latest major version lives on the repository’s default branch while your previous major version(s) go on to live on a new, long-lived branch. For example, we maintain multiple release branches of Chef Infra Client (master and chef-14) and Chef InSpec (master and 3-stable).

Adding a new release branch requires four manual steps. Please note, the git branch does not need to exist before adding it to .expeditor/config.yml.

In our example below, we will update the main branch for our fictional project from 1.x to 2.x while maintaining a 1-stable branch.

  1. Merge a pull request that adds the new release branch to your configuration on the main branch.

    Expeditor uses the release_branches setting to determine which branches in a GitHub repository are release branches. Being specified as a release branch in the .expeditor/config.yml file on the main branch allows that branch to have its own agent and act as an independent project.

    .expeditor/config.yml
      release_branches:
        - main:
    -       version_constraint: "1.*"
    +       version_constraint: "2.*"
    +   - 1-stable:
    +       version_constraint: "1.*"
    

    After you merge this pull request, Expeditor will allow an agent for the 1-stable branch to exist. The act of adding a release branch to your .expeditor/config.yml does not create the associated agent or the branch — that will be taken care of in subsequent steps.

  2. Create the new release branch off of main.

    After defining the release branch in your .expeditor/config.yml and merging the pull request to main, you can now create the branch in git.

    git branch 1-stable
    git push origin 1-stable

    After executing these steps we’ll have a 1-stable branch on GitHub with the same configuration as your main branch. We’ll now be able to manage the configuration for our main and 1-stable release branches independently.

  3. Update the major version on main.

    There are two ways to bump the major version of your project:

    1. Merge a pull request that leverages a label specified in github.major_bump_labels.
    2. Merge a pull request that updates your github.version_file accordingly.
  4. Create the agent for the 1-stable release branch.

    There are two ways to create the agent for your new release branch:

    • Contact Release Engineering and have them load the new agent for you.
    • Merge a pull request into your new release branch modifying the .expeditor/config.yml file.