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.
The short answer
Section titled “The short answer”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.
The problem this solves
Section titled “The problem this solves”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.0no 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 release attestation
Section titled “The release attestation”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.
Repository resurrection protection
Section titled “Repository resurrection protection”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.
The draft-first workflow
Section titled “The draft-first workflow”Immutability begins at publication, which makes the order of operations matter.
-
Create the release as a draft. Nothing is locked yet.
-
Attach every asset. Binaries, archives, checksums, SBOMs — everything the release should contain.
-
Check the release notes and the target commit. Both are much easier to change now.
-
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.
The threat model
Section titled “The threat model”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.
Enabling it across an organisation
Section titled “Enabling it across an organisation”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:
-
Enable on a repository whose release process you own, and publish one release end to end.
-
Fix the workflow if it published before attaching. This is the only common breakage.
-
Enable across repositories that publish externally consumed artifacts — those are where consumers rely on the tag.
-
Enable organisation-wide, once the workflow pattern is established.
-
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.
Immutable releases or tag rulesets?
Section titled “Immutable releases or tag rulesets?”Both stop a tag moving, from different directions and with different scope.
| Tag ruleset | Immutable releases | |
|---|---|---|
| Protects | The Git tag | The tag and the release assets |
| Scope | Any tag matching a pattern | Tags associated with published releases |
| Attestation | None | Generated automatically |
| Survives repository deletion | No | Yes |
| Applies to | All matching tags, released or not | Published releases only |
| Configured at | Repository or organisation ruleset | Repository 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.
What it does not do
Section titled “What it does not do”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.
Where this sits in the wider chain
Section titled “Where this sits in the wider chain”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.
| Control | Fixes |
|---|---|
| Build provenance | Where the artifact came from |
| Signing | That the bytes have not changed and who produced them |
| SBOM | What is inside |
| Immutable releases | That 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.
Verifying a release
Section titled “Verifying a release”The attestation is only a control if it is consumed. Verifying an asset you downloaded:
gh attestation verify ./downloaded-asset.tar.gz -R YOUR_ORG/YOUR_REPOAs 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:
- Download by release tag, which is now a stable reference.
- Verify the attestation against the expected repository, and fail if it does not match.
- 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.
For maintainers, and for consumers
Section titled “For maintainers, and for consumers”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.
Common mistakes
Section titled “Common mistakes”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.
Mental model
Section titled “Mental model”Immutability converts a release from a pointer into a fixed reference.
v2.1.0stops 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.
What you learned
Section titled “What you learned”- 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
Exercise
Section titled “Exercise”Use a disposable repository.
-
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?
-
Enable immutable releases for the repository.
-
Publish a new release with an asset. Try to replace the asset. Predict: what error appears?
-
Try to move the tag with
git tag -fandgit push --force. Predict: does it fail at the Git level or at the release level? -
Publish a release without attaching assets, then try to attach one. Predict: does it work? This is the draft-first lesson.
-
Verify the release attestation with
gh attestation verify. Read what it reports about the tag and commit. -
Delete the repository and create a new one with the same name. Try to create a release with the same tag. Predict: what happens?