Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save brianjbayer/2ff33c37fd6ec24326651e64202c5681 to your computer and use it in GitHub Desktop.

Select an option

Save brianjbayer/2ff33c37fd6ec24326651e64202c5681 to your computer and use it in GitHub Desktop.
Use git tags and loose versioning to manage your reusable github actions workflows and actions

Version GitHub Actions Workflows and Actions with Git Tags

Buehler's Clock - Wendy Bayer

📷 Photo: Buehler's Clock by Wendy Bayer


Versioning is crucial for maintaining the stability and consistency of your GitHub Actions Reusable Workflows and Actionss. By implementing a versioning strategy with git tags, you can ensure that updates are clearly communicated, reducing potential issues during automation.

💡 To learn how to create your own GitHub Actions Action and wrapper workflow, see this post

The High-Level Approach Using Git Tags and Semantic Versioning

Git tags are references to specific commits and are the most common way to version your code. Using tags in conjunction with semantic versioning (MAJOR.MINOR.PATCH) provides clear communication about the stability and impact of changes in your workflows or actions.

  • MAJOR version: Increments for breaking (non-backward-compatible) changes like renaming an action input or output
  • MINOR version: Increments for backward-compatible new features or improvements like new optional parameters
  • PATCH version: Increments for backward-compatible bug fixes

👀 Although Git tags themselves do not enforce semantic versioning, using this convention improves collaboration and helps others understand the state of the code.

When to Version

You should version your workflows and actions after a successful merge into the default (e.g. main) branch, marking the release point with a tag.

You can also Slip version tags when backward-compatible changes are introduced, allowing users to automatically get the new updates without modifying their workflows.

👉 Here the term "Slip" means to force the re-tag of an existing tag to a different commit

How to Tag

Here you can use mutable tags.

To correctly tag your repository you must...

  1. Tag the commit
  2. Push the tag to the remote

How To Create and Push a NEW Tag

  1. Create the new tag:

    • For the Current Commit...

      git tag <tagname>

      For example...

      git tag v0.2.5
    • For a Specific Commit...

      git tag <tagname> <commit-hash>

      For example...

      git tag v0.2.5 e05e63b07706f9f985270aa29858b8b645a833ba
  2. Push the new tags

    git push --tags

How to Slip/Re-Tag and Push an EXISTING Tag

To "Slip" or re-tag a tag that is already associated with a commit, you must use the --force or -f option with git tag and git push --tags.

  1. Force the re-tag:

    • For the Current Commit...

      git tag -f <tagname>

      For example...

      git -f tag v0.2.5
    • For a Specific Commit...

      git tag -f <tagname> <commit-hash>

      For example...

      git tag -f v0.2.5 e05e63b07706f9f985270aa29858b8b645a833ba
  2. Force the push of the re-tags

    git push --tags -f

Specifics of Versioning with GitHub Actions

When versioning GitHub Actions workflows and actions, there are a few specifics to consider depending on whether you're versioning a workflow or an action.

Workflows: Self-Contained Reusable Workflow Files

Reusable workflows, like .github/workflows/copy_image.yml can be fully versioned using a Git tag, as all the "code" resides in the workflow file running in the calling repository.

When you define a reusable workflow, you can reference it in other workflows using the uses keyword with an @ version tag:

  uses: user/repo/.github/workflows/[email protected]

Actions: Versioning Dockerfile-Based and Composite Actions

GitHub Actions can be built using Dockerfile-based actions or composite actions. These need to be referenced both by their action directory and when their source code is checked out in an explicit Git checkout step, as described in this post on Dockerfile-based actions.

Both types of actions must be versioned within the source code repository where they are developed. To execute an action within your workflow, you need to reference it by a version tag.

For example...

...
    - name: Checkout the action repository
      uses: actions/checkout@v5
      with:
        repository: someuser/some-github-action-repo
        path: action-repo
        ref: v1.0.0
...
    - name: Call the action
      id: call-the-action
      uses: ./action-repo/.github/actions/[email protected]
...

If you don’t specify a version, the action will always use the latest version (HEAD), which could introduce breaking changes. That is why it is generally better to specify exact versions to prevent any surprises.

Wrapper Workflows

In GitHub Actions, a wrapper workflow is a reusable workflow that calls another action or workflow. When you want to ensure that your wrapper workflow always uses a specific version of an action or workflow, you can define a ref input parameter in the wrapper workflow. This ref input specifies the Git reference (e.g. a specific tag or branch) that should be checked out when the action is called.

It is important to version the actions used in these wrapper workflows to avoid unexpected behavior from future updates. By referencing a specific version, you ensure consistency across different workflow runs.

⚠️ While you might think that you should assign a default value to your wrapper’s ref input, currently, there isn’t a tag that matches the latest change (at least one that exists yet)

For instance, in your wrapper workflow...

name: Some Wrapper

on:
  workflow_call:
    inputs:
...
      ref:
        description: "The git reference for the called action"
        required: false
        type: string
        # the default is '' which is HEAD
...

Then this reference is used for the git checkout of the called action...

For example...

...
    - name: Checkout the action repository
      uses: actions/checkout@v5
      with:
        repository: someuser/some-github-action-repo
        path: action-repo
        ref: ${{ inputs.ref }}
...
    - name: Call the action
      id: call-the-action
      uses: ./action-repo/.github/actions/delete-docker-hub-repository
...

This ensures that the action called by the wrapper is consistent and won’t change unexpectedly when the wrapper itself is executed.

💡 You do not need to version the action in the uses because it will be at HEAD by default which will be at ${{ inputs.ref }} from the actions/checkout@v5


Loose Versioning for Backward-Compatible Updates

Loose versioning, for lack of a better term, is a strategy that allows you to automatically pick up backward-compatible changes without needing to modify your workflows or actions each time a change is made. This involves "slipping" existing and used version tags, particularly minor and patch versions, to point to the latest merged commit that includes the new changes.

There are benefits and costs to this approach.

Benefits

🕛 💰 The main benefit of this approach is immediately picking up new features and bug fixes without manual intervention, saving time and money and ensuring your workflows stay current.

Costs

💥 Not all changes that are labeled as backward-compatible actually are. Sometimes, changes that are supposed to be non-breaking can unexpectedly break your CI/CD pipeline. For example, a minor change in an action called by a Reusable Workflow could unintentionally cause issues with existing configurations or inputs. As a result, while loose versioning usually saves you time, it introduces a risk of unexpected breaking changes requiring time and effort of updating the calling workflows.

✨ To mitigate this, it is crucial to test and carefully manage your actions and workflows, especially when relying on loose versioning

How Loose Versioning Works

  • Use patch and minor version tags (e.g. v1.2.0, v1.2)

    • Not so much major tags (e.g. v1)
  • When backward-compatible changes are made (such as bug fixes or minor feature additions), you...

    • Create a new patch tag e.g. v1.2.1 for the latest commit
    • Slip the minor tag e.g. v1.2 to the latest commit

This ensures that workflows automatically pick up the changes without requiring manual updates.

For example...

  • Your calling workflows use the minor version v1.2 of your reusable workflows and actions
  • You have an action v1.2.0 (v1.2) in your workflows
  • A backward-compatible feature is introduced and tagged v1.2.1
  • Instead of updating every workflow, you slip the v1.2 tag to the same latest commit as v1.2.1, and all workflows that reference v1.2 (@v1.2) automatically get the new feature

After-the-Fact Versioning in Wrapper Workflows (A "Hack")

One scenario where loose versioning can become tricky is when you have a wrapper workflow that calls another action.

The challenge here is that while you want to update the actions that your wrapper workflow calls, you don’t want to break workflows that depend on a specific version of that wrapper.

In some cases, you can actually use an after-the-fact versioning "hack" to make breaking changes to called actions and use git and slipping tags to make them not breaking changes and backwards-compatible after the changes are merged.

👉 This works for the previous minor (and patch) version and assuming that the wrapper workflow itself (inputs/secrets/outputs) has only backwards-compatible changes.

  1. In the PR making the breaking changes to the action, in the very first commit, "pin" the ref input default value to the current patch tag e.g. v1.2.0 (v1.2)

    name: Some Wrapper
    
    on:
      workflow_call:
        inputs:
    ...
          ref:
            description: "The git reference for the called action"
            required: false
            type: string
            # # Pin previous version of the action to avoid breaking changes
            default: v1.2.0
    ...

    For example, imagine this first pinning commit has commit sha e05e63b07706f9f985270aa29858b8b645a833ba

  2. After committing the first commit with the pinned version of the action, remove the "pinning" by removing the ref input's default value added in the first commit

    name: Some Wrapper
    
    on:
      workflow_call:
        inputs:
    ...
          ref:
            description: "The git reference for the called action"
            required: false
            type: string
    ...
  3. Make the non-backwards-compatible changes in the action and the backwards-compatible changes in the wrapper workflow in the PR

  4. After merging the PR, slip the current patch version (the one pinned in the first commit of the just-merged PR) e.g. v1.2.0 to that first commit pinning the action to that tag minor version

    For example, we would slip v1.2.0 to e05e63b07706f9f985270aa29858b8b645a833ba

  5. Create the new patch tag e.g. v1.2.1 at the latest merge commit

  6. Slip the minor tag e.g. v1.2 to the latest merge commit

🔖 You can see an example of this in this PR


Conclusion

Implementing versioning in GitHub Actions using Git tags and semantic versioning helps ensure stability and clarity, allowing for better communication and management of updates. It allows for balancing the benefits of loose versioning with the potential risks of unexpected breaking changes.


Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment