{
  "$type": "site.standard.document",
  "bskyPostRef": {
    "cid": "bafyreia6qbktbuu443dvuvgmzax5ortjuemfmmt4yjurshu3htv6l3o42m",
    "uri": "at://did:plc:wszrgoqdwy3i2dfeub2mt3wf/app.bsky.feed.post/3mkaeyc53lqt2"
  },
  "coverImage": {
    "$type": "blob",
    "ref": {
      "$link": "bafkreiep5dsa2ip64fanl23ckaegh7pnbzyisteupyynsmg2humj63gwc4"
    },
    "mimeType": "image/png",
    "size": 13001
  },
  "description": "Inside the ways that GitHub Actions' versioning works, and how we improved Renovate's support.",
  "path": "/posts/2026/04/24/github-actions-tagging/",
  "publishedAt": "2026-04-24T10:53:55.000Z",
  "site": "https://www.jvt.me",
  "tags": [
    "blogumentation",
    "github",
    "github-actions",
    "renovate",
    "Astral's setup-uv GitHub Action",
    "a GitHub Issue about this on setup-uv",
    "Immutable Releases",
    "document it as a form of blogumentation",
    "wrote about their approach to open source security",
    "October last year",
    "Trivy's recent compromise",
    "Docker versioning",
    "the original proposal",
    "Partial Semantic Versioning",
    "when they come to implement this support",
    "@main",
    "@v5",
    "@v5.1",
    "@v5.1.8",
    "@0.0.0-rc.65",
    "@feature",
    "@de0fac2e4500dabe0009e67214ff5f5447ce83dd",
    "@v7",
    "@v8.0.0",
    "@v8",
    "@v8.0",
    "@v8.1.0"
  ],
  "textContent": "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.\n\nI 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!\n\nAfter 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.\n\nI 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.\n\nIt turns out the way that 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.\n\nBecause this wasn't straightforward, I thought this would be a great opportunity to document it as a form of blogumentation.\n\n## Why Immutable Releases?\n\nBefore 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.\n\nThe 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.\n\nGitHub'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.\n\nFor 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.\n\nAnyone 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.\n\nThe 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.\n\nIf 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!\n\nA 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!\n\n## So what's hard about GitHub Actions' versioning?\n\nGitHub Actions relies - under the hood - on straightforward Git concepts:\n\n  * you can specify a Git tag (which is recommended to be SemVer-like)\n  * you can pin to a Git commit\n\n\n\nIn both cases, GitHub Actions will fetch the repo and check out the commit/tag (refspec) that you've specified.\n\nThose 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.\n\nThe real complication I hit when working on these changes to GitHub Actions in Renovate was the structure of Git tags used by Actions authors.\n\nYou'll usually see an unpinned, \"floating\", tag like:\n\n\n    - uses: actions/checkout@v6\n\n\nThis 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.\n\nEvery 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.\n\nThat's not ideal for users, as you're expecting that there's no level of predictability between your Actions runs.\n\nRenovate's best practices recommend that you pin this to a commit (\"digest pin\"), like so:\n\n\n    - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6\n\n\nOr preferably with full SemVer:\n\n\n    - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n\n\nNotice 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.\n\nRenovate 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.\n\nIf the full digest pinning isn't for you, you can relax it a little bit and use the full SemVer tag:\n\n\n    - uses: actions/checkout@v6.0.2\n\n\nThere'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.\n\nWhile working on Renovate's support, I also found that some repos use floating _minor_ versions, such as:\n\n\n    - uses: azure/setup-helm@5119fcb9089d432beecbf79bb2c7915207344b78 # v3.5\n\n\nThis mutable tag is updated for each update to `v3.5.y`.\n\n### Summary of GitHub Actions version formats\n\nWe started with something fairly reasonable, but through investigating how the ecosystem at large works, it's not quite as straightforward.\n\nSo that means that we have to take into account:\n\nFormat| Example\n---|---\nFloating major tag| `@v5`\nFloating minor tag| `@v5.1`\nStable semantic version tag| `@v5.1.8`\nPre-release semantic version tag| `@0.0.0-rc.65`\nBranch reference| `@main`, `@feature/do-the-thing`\nCommit reference| `@de0fac2e4500dabe0009e67214ff5f5447ce83dd`\n\nSomething we're _not_ covering here is the fact that you may also have a tagged release that doesn't look like a Semantic Version.\n\nWe'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.\n\n## Making Renovate support this\n\nI'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.\n\nAs I found when I initially tried this, it broke several usecases, and so we decided to rethink our approach.\n\nWhen considering this functionality, there were a few other decisions we needed to make:\n\n  * what happens if you're on a floating tag like `@v7` and now the new version is `@v8.0.0`?\n  * how do we handle an upgrade from `@v7` where there is `@v8`, `@v8.0` and `@v8.1.0`?\n  * how do we know if the tag we're suggesting exists?\n\n\n\nRenovate'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`.\n\nAn 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.\n\n# Feedback?\n\nHave I missed anything in our support? Are there other spectres lurking in the background with how GitHub Actions versioning works? Let me know!\n\nI hope that our friends over at Dependabot find this useful when they come to implement this support!",
  "title": "A deep dive into the wild world of GitHub Actions' tagging formats",
  "updatedAt": "2026-04-24T10:53:55.000Z"
}