Skip to main content

Actions and Action Sets

In Expeditor, an action is a reference to a discrete unit of logic that contains the pattern for how to accomplish a specific task. Actions help ensure that common processes (e.g. bumping the version or managing the changelog) are handled consistently across our wide variety of projects.

Most of the actions in Expeditor are related to the release life-cycle of GitHub software projects. As such, these actions act not only as the tools of automating those processes but documenting them as well. An interested party can look at the Expeditor configuration and be able to get an understanding of the release processes for a project by evaluating the “when, then” subscriptions.

The structure of an action

An action consists of three elements:

  1. The action type
  2. The action name
  3. The action filters

Let’s start by focusing on the first two, the action type and the action name, as those form the foundation of what an action is. You can think of an action as a method (the action type) that only accepts a single parameter (the action name) separated by a colon. Here are some examples that we’ll continue to use throughout this document.

Action Action Type Action Name
built_in:bump_version built_in bump_version
bash:.expeditor/update_version.sh bash .expeditor/bump_version.sh
built_in:update_changelog built_in update_changelog
trigger_pipeline:omnibus/release trigger_pipeline omnibus/release

A built-in action (i.e. an action with the type built_in) is a class of action that codifies a common, repeatable pattern that is leveraged across a wide variety of projects — in comparison to action types like bash or trigger_pipeline which are far more generic. Please check out the full list of supported actions.

The final element of an action is its action filters. An action filter is configuration that can be applied to an action to indicate whether or not the action should be executed — given the context of the workload to which it is responding. More details about action filters are covered in their reference documentation.

Organizing actions into action sets

An action set is a collection of one or more actions that are executed against a workload as part of a subscription. Let’s organize the actions we introduced in the section above into an action set that is responding to a familiar workload: pull_request_merged:{{github_repo}}:{{release_branch}}:*.

subscriptions:
  - workload: pull_request_merged:{{github_repo}}:{{release_branch}}:*
    actions:
      - built_in:bump_version
          not_if_labels:
            - "Expeditor: Skip Version Bump"
      - bash:.expeditor/update_version.sh
          only_if: built_in:bump_version
      - built_in:update_changelog
      - trigger_pipeline:omnibus/release

In this example, we’ve introduced both the concept of the action set and some examples of action filters. As mentioned above, action filters allow us to specify if an action should occur, tacking on the concept of “if” to our “when, then” framework.

Let’s take a moment to break down what exactly our action set is doing and think about it in terms of “when, then, if.”

When any pull request is merged into my project, then

  1. Bump the version of our software product but not if the Expeditor: Skip Version Bump GitHub label is applied to the pull request.
  2. Execute the .expeditor/update_version.sh bash script only if we’ve successfully bumped the version.
  3. Update the changelog
  4. Trigger the omnibus/release pipeline

Looking at this example, you may realize the addition of action filters makes things a bit more difficult to reason about — this is absolutely true! That is why we have the following suggestions and guidelines.

  1. Keep the number of actions in an action set to a minimum. The more actions you have, the more likely you are to need action filters to control behavior, and the more complex your subscription becomes. We recommended chaining together subscriptions if the cyclomatic complexity of your action set gets too high.
  2. Expeditor configuration is YAML, take advantage of comments. Remember, the .expeditor/config.yml file is not just configuration but also your documentation. Please use comments and white space as you see fit to increase the readability.

Understanding the different phases of the action set

There are three phases to the execution of an action set:

  1. pre-commit. Execute pre-commit actions, which modify files that are committed directly to the release branch.
  2. commit. Commit any modified files directly to the release branch, optionally create a tag, and push everything to GitHub. If the pre-commit actions do not modify any files, this phase is skipped.
  3. post-commit. Execute post-commit actions that depend on the results of the commit phase (including the git tag) having been pushed to GitHub.

All actions are will default to either the pre-commit or post-commit phase depending on which phase makes the most sense for their use case. The default value for every action is documented in the action reference documentation.

Here is how our example action set breaks out into pre- and post-commit actions:

Phase Actions
pre-commit built_in:bump_version
bash:.expeditor/update_version.sh
built_in:update_changelog
commit Commit the files modified by pre-commit actions to the release branch
Tag the commit (if appropriate)
Push to GitHub
post-commit trigger_pipeline:omnibus/release

Let’s walk through our full action set assuming that our action filters resolve such that all actions are going to be executed.

pre-commit

  1. built_in:bump_version: Bump the patch version of our application. For our example, we’ll assume we’re bumping from 1.0.0 to 1.0.1.
  2. bash:.expeditor/update_version.sh: Execute the .expeditor/update_version.sh bash script, located in the project’s GitHub repository. In most cases, iterations of this script use something like sed to replace references of the old 1.0.0 version to the new 1.0.1 version.
  3. built_in:update_changelog: Update our changelog with details of the pull request.

commit

  1. Commit the files modified as part of the pre-commit actions (e.g. VERSION, CHANGELOG.md, etc) directly to the the release branch.
  2. Tag the commit with the version number, typically in the form of v1.0.1 depending on your configuration.
  3. Push both the commit and the tag to GitHub.

post-commit

  1. trigger_pipeline:omnibus/release: Trigger the omnibus/release pipeline in Buildkite.