Skip to content

Immutable GitHub Releases

Lesson 10 of 10Intermediate → Advanced13 min readGit Security & DevSecOps · Supply Chain SecurityVerified: GitHub immutable releases documentation, September 2026

A GitHub release is, by default, editable. The tag can be moved to a different commit. Assets can be replaced or deleted. Somebody downloading v2.1.0 today and somebody downloading it next month can receive different files, and nothing in either download indicates that.

Immutable releases close that. Once published, the tag is locked to its commit and the assets are locked to their content.

Publishing an immutable release enforces four things:

Git tag protection. The tag is locked to a specific commit, cannot be changed, and cannot be deleted while the release exists.

Asset protection. Every file attached to the release — binaries, archives, checksums — is protected from modification and deletion.

A release attestation. Publishing generates a cryptographically verifiable record of the release containing the release tag, the commit SHA and the release assets.

Repository resurrection protection. Even if you delete the repository and create a new one with the same name, tags associated with immutable releases in the original cannot be reused.

Enable it for a repository or across an organisation.

Consumers pin to release tags. That is how every package manager, every curl in an installation script, and every documentation page referencing a download works.

A tag is a pointer. Without protection:

  • The tag can be moved to a different commit, so v2.1.0 no longer means what it meant
  • Assets can be replaced, so the binary at a given URL changes
  • Assets can be deleted, so a reproducible install stops working
  • The release can be edited after people have relied on it

None of these require a compromise. A maintainer fixing a packaging mistake by replacing an asset does the same thing an attacker would, and neither is visible to somebody who downloaded yesterday.

For a consumer, the consequence is that “we audited version 2.1.0” is a statement with no fixed referent. The version you audited and the version somebody installs today may not be the same artifact.

The property that makes this a supply-chain control rather than a repository setting.

Publishing an immutable release automatically generates a release attestation: a cryptographically verifiable record containing the release tag, the commit SHA and the release assets.

That is a different claim from a build provenance attestation, and the difference is worth being precise about.

A build provenance attestation is about one artifact: these bytes were produced by this workflow from this commit.

A release attestation is about the release as a whole: this tag, pointing at this commit, published this set of assets.

The second is the claim a consumer downloading from a releases page actually needs. They are not verifying a binary they built; they are verifying that what they have is the release the project published, as a set. An attacker who could add a fourth asset to a three-asset release would be producing something each individual signature might still cover.

The two compose. A release whose assets each carry build provenance, and which itself carries a release attestation, lets a consumer establish both that each asset came from a known build and that the set is the published release.

The subtlest of the four guarantees and worth understanding, because the attack it prevents is not obvious.

Delete a repository, and the name becomes available. Somebody — you, or somebody else — creates a new repository with the same name, creates a tag v2.1.0, and publishes a release. Every reference to github.com/org/repo at tag v2.1.0 now resolves to content nobody vetted, and the URL is identical.

This has been a real problem for abandoned or transferred projects, and it is a route by which a long-dead reference becomes an attacker-controlled one.

Immutable releases prevent it: tags associated with immutable releases in the original repository cannot be reused, even after the repository is deleted and recreated under the same name.

The guarantee therefore extends beyond your control of the repository, which is unusual and is exactly what a consumer relying on a URL needs.

Immutability begins at publication, which makes the order of operations matter.

  1. Create the release as a draft. Nothing is locked yet.

  2. Attach every asset. Binaries, archives, checksums, SBOMs — everything the release should contain.

  3. Check the release notes and the target commit. Both are much easier to change now.

  4. Publish. Immutability applies from this moment.

The failure this avoids: publishing first and attaching assets afterwards. A CI pipeline that creates the release then uploads artifacts in a later step is publishing an empty release and then attempting to modify it — which is the operation immutability blocks.

- name: Create a draft release with all assets
run: |
gh release create "${TAG}" \
--draft \
--title "${TAG}" \
--notes-file release-notes.md \
dist/*.tar.gz dist/*.sha256 sbom.spdx.json
env:
TAG: ${{ github.ref_name }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Publish it
run: gh release edit "${TAG}" --draft=false
env:
TAG: ${{ github.ref_name }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Two steps rather than one, and everything is in place before the second.

Threat. A consumer installs something other than the release they intended, from a reference they believe is stable.

Attack surface. The Git tag, which is a movable pointer. The release assets, which are files that can be replaced. The repository name itself, which becomes available if the repository is deleted. And the whole set of consumers who fetched earlier and have no way to notice.

Impact. Whatever the substituted artifact does, on every machine that installs it — which for a widely-used project is a very large number, delivered by the mechanism everybody trusts.

Control. Fix the tag to a commit, fix the assets to their content, generate an attestation, and reserve the tag names beyond the repository’s lifetime.

Verification. Attempt the attack on a disposable repository: publish a release, then try to replace an asset and move the tag. Then verify an asset’s attestation and confirm it names the tag and commit you expect.

The part of this model worth emphasising is the last item in the attack surface. Most integrity controls protect the artifact; this one also protects people who already fetched it, by ensuring the thing they referenced cannot become something else.

The setting exists at repository level and at organisation level, and the organisation version is the one that matters for coverage.

Two considerations before turning it on broadly.

Existing releases are not retroactively immutable. Enabling the setting affects releases published after it. Historical releases remain editable, which is worth knowing if you were relying on the setting to fix something already published.

Some workflows genuinely publish then attach. A pipeline that creates a release early — to get a URL, or to trigger downstream automation — and uploads assets afterwards will break. That is the draft-first pattern above, and the fix is a two-line change to the workflow rather than an exception to the policy.

The rollout that avoids surprises:

  1. Enable on a repository whose release process you own, and publish one release end to end.

  2. Fix the workflow if it published before attaching. This is the only common breakage.

  3. Enable across repositories that publish externally consumed artifacts — those are where consumers rely on the tag.

  4. Enable organisation-wide, once the workflow pattern is established.

  5. Document the guarantee for consumers, with the verification command.

Step 5 is the one that turns the setting into something anybody outside your organisation benefits from.

Both stop a tag moving, from different directions and with different scope.

Tag rulesetImmutable releases
ProtectsThe Git tagThe tag and the release assets
ScopeAny tag matching a patternTags associated with published releases
AttestationNoneGenerated automatically
Survives repository deletionNoYes
Applies toAll matching tags, released or notPublished releases only
Configured atRepository or organisation rulesetRepository or organisation setting

They overlap and they are not redundant.

A tag ruleset protects every tag matching v*, including tags with no release attached — useful if you tag things that are not releases, or if you want creation restricted to release automation.

Immutable releases protect the assets, which a tag ruleset does not touch at all, and generate an attestation, which a ruleset does not.

Using both is reasonable and is what a mature repository generally ends up with.

It does not make the released software safe. An immutable release of a vulnerable binary is an immutable vulnerable binary. Immutability is about integrity, not quality.

It does not prevent a new release. You can publish v2.1.1 with whatever you like. It prevents v2.1.0 changing, which is the point — consumers of v2.1.0 are protected, and consumers who upgrade are making a new decision.

It does not delete-protect the repository. The repository can still be deleted; the tags become unusable rather than the repository becoming permanent.

It does not reach mirrors and caches. Anything that copied an asset before publication has its own copy, and package managers that mirror release assets have theirs.

It does not verify anything by itself. The release attestation is evidence. Somebody has to check it, and the check has to be able to refuse.

Immutable releases are the last integrity control in the pipeline, and they close a specific gap that everything upstream leaves open.

The chain up to this point produces an artifact with provenance and a signature: you can establish that a given binary was built by a given workflow from a given commit. What that does not fix is the reference — the tag consumers use to find it.

An artifact can have impeccable provenance and still be unreachable through a stable name, because the tag pointing at it is movable. Conversely, a fixed tag pointing at an artifact with no provenance tells you the reference is stable and nothing about where the bytes came from.

ControlFixes
Build provenanceWhere the artifact came from
SigningThat the bytes have not changed and who produced them
SBOMWhat is inside
Immutable releasesThat the name keeps pointing at the same thing

All four together give a consumer a reference they can rely on, contents they can inventory, an origin they can check, and integrity they can verify — which is the complete set, and it is rare.

For deployments inside your own infrastructure, deploying by digest achieves the referential stability directly and immutable releases add little. For software other people download by tag, there is no digest in the workflow — they follow a URL — and this is the control that makes that URL mean something.

The attestation is only a control if it is consumed. Verifying an asset you downloaded:

Terminal window
gh attestation verify ./downloaded-asset.tar.gz -R YOUR_ORG/YOUR_REPO

As everywhere in this cluster, the -R flag is what makes the check meaningful. Without it you are asking “does an attestation exist for these bytes?”, which almost anything can satisfy.

For a consumer, the useful pattern in an installation script or a CI job:

  1. Download by release tag, which is now a stable reference.
  2. Verify the attestation against the expected repository, and fail if it does not match.
  3. Record the digest of what you got, so a future fetch can be compared.

Step 3 is worth doing even with immutability, because it is what turns “we trust the guarantee” into “we can check the guarantee held”.

When you need to change a published release

Section titled “When you need to change a published release”

Immutability means the answer is always “publish a new version”, and it is worth walking through the cases because the instinct to edit is strong.

A packaging mistake — a broken or missing binary. Publish a patch version. v2.1.1 with the correct assets, and a note on v2.1.0 explaining the problem. This is better than silently replacing the asset even when replacing is possible, because consumers who already installed v2.1.0 learn that something was wrong.

A mistake in the release notes. Release notes are metadata rather than content, and how editable they are depends on the platform’s rules — check rather than assume. Where notes cannot be edited, a pinned issue or a note on the next release carries the correction.

A secret in a release asset. This is an incident, not an edit. Rotate the credential first. Then publish a clean version. Removing the asset would be the instinct and it is both blocked and insufficient — anyone who downloaded it has it, which is the same reasoning as removing secrets from Git history.

The wrong commit was tagged. Publish a new version from the right commit. The wrong one stays, which is unattractive and is exactly the property consumers are relying on.

The pattern across all four: the release record is append-only, so corrections are additions. That is a discipline rather than a limitation, and it is the same one that makes Git history trustworthy.

If you publish software others install, immutable releases are among the cheapest meaningful things you can offer them. It is a setting, it costs nothing operationally beyond the draft-first workflow, and it converts your release tags from pointers into fixed references.

Publish the expectation too: a line in your README saying releases are immutable and how to verify them. A guarantee consumers do not know about is a guarantee nobody uses.

If you consume software from GitHub releases, prefer projects that publish immutably and verify what you download. Where a project does not, record the digest of what you fetched and compare on subsequent fetches — the manual version of the same property.

Publishing before attaching assets. Immutability applies at publication, and the later upload is the operation it blocks.

Expecting to fix a release afterwards. You cannot. Publish a new patch version, which is the correct response anyway.

Treating it as a substitute for tag rules. It covers tags with releases. Tags without releases need a ruleset.

Believing it makes the software safe. It fixes what the release contains, not whether the contents are good.

Not verifying the attestation. Generated automatically and consumed by nobody is the usual outcome.

Not telling consumers. A guarantee nobody knows about changes nobody’s behaviour.

Assuming it reaches mirrors. Anything already copied is outside its scope.

Immutability converts a release from a pointer into a fixed reference. v2.1.0 stops being “whatever that tag points at today” and becomes a specific commit and a specific set of files — which is what everybody already assumed it was.

  • A default GitHub release is editable: the tag can move and assets can be replaced or deleted
  • Immutable releases lock the tag to its commit and the assets to their content
  • Publishing generates a release attestation covering the tag, the commit SHA and the assets
  • A release attestation is a claim about the release as a set, distinct from per-artifact provenance
  • Repository resurrection protection prevents tag reuse after a repository is deleted and recreated
  • Immutability applies at publication, so create a draft, attach everything, then publish
  • Tag rulesets protect tags; immutable releases protect tags and assets and produce evidence
  • It does not make the software safe, prevent new releases, or reach mirrors and caches
  • The attestation is only a control once somebody verifies it and can refuse

Use a disposable repository.

  1. Create a release without immutability enabled. Attach a file. Then replace the file and move the tag to a different commit. Predict: does anything stop you?

  2. Enable immutable releases for the repository.

  3. Publish a new release with an asset. Try to replace the asset. Predict: what error appears?

  4. Try to move the tag with git tag -f and git push --force. Predict: does it fail at the Git level or at the release level?

  5. Publish a release without attaching assets, then try to attach one. Predict: does it work? This is the draft-first lesson.

  6. Verify the release attestation with gh attestation verify. Read what it reports about the tag and commit.

  7. Delete the repository and create a new one with the same name. Try to create a release with the same tag. Predict: what happens?

GitHub Actions Security ChecklistToken permissions, fork pull requests, script injection and supply chain — with the attack each item prevents.

The repository security templates — secrets management and least-privilege token guides — are in the Professional Toolkit.