A deep dive into the wild world of GitHub Actions' tagging formats

Featured image for sharing metadata for article

Last week, I was tagged in a LinkedIn post, where one of the Python Core developers was noting that Astral's setup-uv GitHub Action was not being updated by either Renovate or Dependabot, and a note to users that they would need to manually update.

I was a bit surprised about this - as it was the first time I'd heard about it - and this is never the scenario I'd want Renovate users to be in - manual bumping a version in a file should be a thing of the past!

After digging into a GitHub Issue about this on setup-uv, it was confirmed that was due to their move from mutable GitHub Actions releases to Immutable Releases, and that Renovate wasn't handling this upgrade in a reasonable way.

I had a bit of time on my hands, and some unfounded hubris for the time of day, so I thought "huh, how hard could this be?", and felt that a speedy fix would be a positive for Renovate and our users.

A GIF of the Key and Peele sketch "Substitute Teacher", where a teacher is exclaiming at a student "Ya done messed up A-a-ron!"

It turns out the way that tagging formats work with GitHub Actions is a little bit more complex than I'd first thought, and a few edge cases (as well as not-so-edge-cases) brought me back to reality. I was humbled by intermittently breaking things for folks for a couple of days, rolled it back, and started fresh.

Because this wasn't straightforward, I thought this would be a great opportunity to document it as a form of blogumentation.

Why Immutable Releases?

Before we get into what happened with the tag formats, it's worth talking briefly about what Immutable Releases are and why they're useful. This could very well be a separate post, but I'll try and cover it briefly here.

The folks over at Astral wrote about their approach to open source security which is worth a read as it includes how Immutable Releases protect them, as well as a tonne of other great practices.

GitHub's launch of Immutable releases in October last year was a long awaited feature, allowing a repository's pushed GitHub Releases (and the tags that underpin them) to never be updated again. This key protection ensures that once tag is pushed/the release is published, it cannot have its commit SHA updated, nor any attached release assets updated.

For instance, in Trivy's recent compromise, the attacker force-pushed over the tags of both the GitHub Action, and the trivy CLI. This meant that anyone downloading a version of trivy (or its GitHub Action) would now be pulling a compromised binary.

Anyone who had previously set up checksum validation against their downloaded trivy binary (with a checksum taken before the compromise) would be protected as the checksum validation would fail. However, anyone fetching the compromised binary fresh would also be served an updated checksum for the compromised binary, resulting in a "passing" check.

The other key way to avoid this would be to rely upon an existing, older, version of the trivy binary, for instance from your Linux distribution, or through Homebrew, or to build from source.

If the releases were marked immutable, this would not have been possible. It would have led to the attacker attempting to employ a different attack vector, as it would not be possible for them to overwrite existing tags. Aqua Security have now enabled immutable releases, which is great, and secures everyone using it!

A fun gotcha is that when enabling Immutable Releases, you have to go and edit each previous release (via the UI or API) for the immutable release to then take effect. If you do not, only new releases will be immutable!

So what's hard about GitHub Actions' versioning?

GitHub Actions relies - under the hood - on straightforward Git concepts:

  • you can specify a Git tag (which is recommended to be SemVer-like)
  • you can pin to a Git commit

In both cases, GitHub Actions will fetch the repo and check out the commit/tag (refspec) that you've specified.

Those of you more familiar with Git may wonder - does that also mean branches? Yes it does. Although it's a less common pattern, it's also possible to use a branch like @main, although it's not recommended as then there's very much no ability to control what changes you get.

The real complication I hit when working on these changes to GitHub Actions in Renovate was the structure of Git tags used by Actions authors.

You'll usually see an unpinned, "floating", tag like:

- uses: actions/checkout@v6

This isn't something magic which GitHub Actions uses to fetch the latest v6.x.y version at the given time. This is a mutable tag, v6, which points to the latest v6.x.y commit.

Every time the Action owner publishes a new version, they're force-pushing over the existing tag version(s), and pointing it to the new latest version.

That's not ideal for users, as you're expecting that there's no level of predictability between your Actions runs.

Renovate's best practices recommend that you pin this to a commit ("digest pin"), like so:

- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

Or preferably with full SemVer:

- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

Notice the comment syntax, which provides a readable indication of what that generic commit hash is, as well as a hint to dependency update tools on what the commit currently points to.

Renovate previously had its own syntax for how to perform this pinning, but as Dependabot looked to add support for digest pinning a year later, we collaborated with them to align on a reasonable set of syntax for all tools to use.

If the full digest pinning isn't for you, you can relax it a little bit and use the full SemVer tag:

- uses: actions/checkout@v6.0.2

There's still the risk that it's a mutable tag on the source repository, but at least pinning to the SemVer tag gives you a bit more confidence in predictability of what a version update looks like.

While working on Renovate's support, I also found that some repos use floating minor versions, such as:

- uses: azure/setup-helm@5119fcb9089d432beecbf79bb2c7915207344b78 # v3.5

This mutable tag is updated for each update to v3.5.y.

Summary of GitHub Actions version formats

We started with something fairly reasonable, but through investigating how the ecosystem at large works, it's not quite as straightforward.

So that means that we have to take into account:

FormatExample
Floating major tag@v5
Floating minor tag@v5.1
Stable semantic version tag@v5.1.8
Pre-release semantic version tag@0.0.0-rc.65
Branch reference@main, @feature/do-the-thing
Commit reference@de0fac2e4500dabe0009e67214ff5f5447ce83dd

Something we're not covering here is the fact that you may also have a tagged release that doesn't look like a Semantic Version.

We're ignoring those for the sake of Renovate, as it'll be something users will need to define what versioning to use for them, but may be something you want to consider, if you're implementing something to handle GitHub Actions versioning.

Making Renovate support this

I'll note that we'd already been recently talking about moving from our Docker versioning for GitHub Actions, with the original proposal being to move to Partial Semantic Versioning.

As I found when I initially tried this, it broke several usecases, and so we decided to rethink our approach.

When considering this functionality, there were a few other decisions we needed to make:

  • what happens if you're on a floating tag like @v7 and now the new version is @v8.0.0?
  • how do we handle an upgrade from @v7 where there is @v8, @v8.0 and @v8.1.0?
  • how do we know if the tag we're suggesting exists?

Renovate's new approach is to find the "shortest" tag name that matches what a user is currently using. For instance, if you're currently using @v7, we should suggest tags in the order @v8, @v8.0 and @v8.1.0.

An implementation detail for Renovate is that our versioning modules suggest version updates separate to the known versions available. This works predictably in ecosystems where the version numbers are predictable, but in cases where you may have a shorter or longer tag depending on what upstream publishes, we needed to wire in the "known release versions", to correctly suggest a release version that existed.

Feedback?

Have I missed anything in our support? Are there other spectres lurking in the background with how GitHub Actions versioning works? Let me know!

I hope that our friends over at Dependabot find this useful when they come to implement this support!

Written by Jamie Tanna's profile image Jamie Tanna on , and last updated on .

Content for this article is shared under the terms of the Creative Commons Attribution Non Commercial Share Alike 4.0 International, and code is shared under the Apache License 2.0.

#blogumentation #github #github-actions #renovate.

This post was filed under articles.

Interactions with this post

Interactions with this post

Below you can find the interactions that this page has had using WebMention.

Have you written a response to this post? Let me know the URL:

Do you not have a website set up with WebMention capabilities? You can use Comment Parade.