A release engineer can publish v2.4.0 at commit A and, in an ordinary mutable setup, a later tag move can make git checkout v2.4.0 retrieve commit B. That is one incident with two separate control failures: somebody was able to alter a release tag, and the published release did not preserve the artifact consumers were meant to receive.
GitHub offers controls for both moments, but they are not interchangeable. Tag rulesets govern what selected actors may do to matching Git refs. Immutable releases preserve a published GitHub release’s tag and attached assets. A team that enables only one of them can still leave a meaningful gap in its production workflow.
There are two gates in a release, not one
Think of a production release as crossing two gates. The first is the ref governance gate: before publication, who may create v2.4.0, force-update it, or delete it? The second is the published-record gate: after GitHub presents that version as a release, can its tag or downloadable files change?
Tag protection rules were GitHub’s earlier mechanism for the first gate. GitHub announced that those rules were migrated to tag rulesets on August 30, 2024, so teams should treat tag rulesets as the current model when designing new controls. The important question is not whether a setting is labelled “protected”; it is which operation it denies and at what point in the release lifecycle.
| Release moment | Primary concern | Control that addresses it |
|---|---|---|
| Before publication | Only the release workflow should create or change v2.* tags. |
Tag ruleset with creation, update, and deletion restrictions. |
| At publication | The tag must identify the reviewed commit and release assets must be complete. | Release automation, checks, signing policy, and a release checklist. |
| After publication | Consumers must keep receiving the same tag target and release files. | Immutable releases. |
This separation matters most when developers, release managers, and CI identities have different permissions. A policy that says “nobody can move the tag” may be too strict while a release is being assembled, yet too weak if it stops applying after the release is publicly distributed.
What a tag ruleset actually governs
A tag ruleset matches a naming pattern such as v*, release/*, or prod-*. Its job is to restrict ref operations on tags that match the pattern. In a production workflow, the useful operations to reason about are creation, update, and deletion—not merely whether the word “protection” appears in repository settings.
For example, a library can reserve v* for a release workflow. A developer may create local tags while testing, but should not be able to push v3.1.0 from a laptop. The CI identity, or a small release-admin group, creates that tag only after the release commit has passed the required checks.
- Create restriction: stops an unapproved actor from claiming a version such as
v1.8.0. - Update restriction: stops an actor from repointing an existing version tag to another commit.
- Deletion restriction: stops an actor from removing a version tag to make room for a replacement.
- Bypass governance: defines the exceptional identities that can perform otherwise denied operations.
The bypass list is the tradeoff teams skip. Every bypass identity is a path around the normal release process. Keep it small, use a dedicated automation identity where practical, and review it with the same care as production deployment access. “Administrators can fix it later” is not a release policy; it is a decision to preserve an emergency mutation path.
What becomes fixed when an immutable release is published
Immutable releases start at a different event: publication of a GitHub release. When immutable releases are enabled for a repository, GitHub locks the published release’s assets and tag. Assets cannot be added, replaced, or deleted after publication, and the tag cannot be moved or deleted while that release exists.
That gives consumers a concrete property they can rely on. If a user downloads an archive, binary, checksum file, or other release asset attached to published v2.4.0, a later editor cannot silently swap it for a different file through that release. Likewise, the release tag cannot be repointed while the immutable release remains in place.
The lock is intentionally narrower than “nothing about the release can ever change.” GitHub documentation states that maintainers can still edit the release title and notes, and can change whether the release is a pre-release or the latest release. That is useful when a changelog has a typo or a release needs a different presentation status, but it means release notes should not be your only integrity record.
| Published release field or object | Behavior with immutable releases enabled |
|---|---|
| Attached assets | Cannot be added, modified, replaced, or deleted after publication. |
| Release tag | Cannot be moved or deleted while the release exists. |
| Title and release notes | Can still be edited. |
| Pre-release or latest status | Can still be changed. |
Why one control does not replace the other
A tag ruleset does not, by itself, preserve release assets. Suppose v4.0.0 is protected from updates, but a maintainer can edit a normal GitHub release and replace tool-linux-amd64.tar.gz. The Git commit remains stable while the downloadable artifact changes. That is a different supply-chain failure than a moved tag.
Immutable releases do not decide who gets to create a tag before publication. A release manager might accidentally publish v2.0.0 from the wrong commit, or an overly broad CI token might create a version tag before tests complete. Once published as immutable, the outcome is preserved—but it may be the wrong outcome.
There is also a timing gap. Immutable-release protection is connected to a published release, not to every tag that happens to exist in the repository. If your workflow creates a tag at 10:00 and publishes the GitHub release at 10:20, the tag ruleset is the control that governs the 20-minute interval. The safer workflow is not “choose one”; it is to assign each mechanism to the failure it can actually prevent.
Walk through a library release from commit to consumer
Consider an open-source CLI that releases v1.12.0. Its consumers may fetch the source by tag, download a binary from the GitHub release page, or pin the version in automation. The team needs both pre-publication authority control and post-publication consistency.
- A release pull request updates the version and changelog. Required review and CI establish which commit is eligible.
- The release workflow creates
v1.12.0at that approved commit. A tag ruleset forv*prevents ordinary contributors from independently creating, updating, or deleting production version tags. - The workflow builds the binary and checksum files from the tagged commit, rather than from an unrelated branch head.
- The workflow creates and publishes the GitHub release with those assets.
- Immutable-release protection preserves both the tag and the published assets after publication.
The decision at step 2 is about authority: which identity is allowed to name a commit as a release? The decision at step 5 is about persistence: can a later actor alter what that published name and download page mean? Treating these as separate approvals catches a common mistake: granting CI the ability to push any matching tag, then assuming publication immutability will correct a bad tag. It will not.
Choose differently for libraries, services, and deployment repositories
The right strictness depends on what a tag represents. A public library tag is often a long-lived contract with unknown downstream users. A service repository’s tag may be an internal audit marker while the live deployment is controlled elsewhere. A deployment repository may use tags as promotions between environments, where deliberate movement could be part of an established process.
| Repository type | Recommended tag-ruleset posture | Immutable-release posture | Reasoning |
|---|---|---|---|
| Public or widely consumed library | Restrict create, update, and delete for v*; limit bypass to the release process and a very small emergency group. |
Enable for published releases with distributed binaries, archives, checksums, or SBOMs. | Consumers can rely on a version long after the original team has moved on. |
| Internal service | Restrict production version tags or release-candidate tags to CI and release owners. | Use when GitHub release assets are part of the internal delivery or audit record. | A stable deployment audit trail matters, but GitHub assets may not be the service’s artifact source. |
| Deployment repository | Protect tags that represent approved production states; explicitly decide whether promotion tags are allowed to move. | Use for release records that package manifests, bundles, or deployment evidence for later retrieval. | Some teams need moving environment labels, so do not confuse production with an immutable version tag. |
The deployment-repository row is where blanket advice breaks down. A movable tag such as production can be a pointer to the currently deployed revision. A version tag such as deploy-2026-09-27.1 is an audit record. Give those two names different rules rather than forcing one tag convention to perform both jobs.
Do not use release notes as evidence of what shipped
Because immutable releases still permit edits to titles and release notes, those fields are communications channels, not immutable attestations. A corrected installation instruction is legitimate; a rewritten note that changes the apparent scope of a release is also technically possible. The tag and asset lock does not make prose historically fixed.
For a production workflow, put verifiable facts in artifacts and automation output. Examples include a checksum file attached before publication, a build identifier in the artifact filename, and a release workflow that records the commit SHA it tagged. The goal is not to make every sentence uneditable; it is to make the identity of the distributed bytes independently checkable.
This is especially relevant for teams that publish both a package to a registry and a GitHub release. Immutable release assets protect the GitHub release assets. They do not, by themselves, state anything about whether another package registry permits replacement, deletion, or republishing under the same version. Assess each distribution channel separately.
Design an exception path before the first bad release
Immutability changes incident response. With a mutable release, the tempting response to a bad binary is to replace the file or repoint the tag. With immutable releases, the system prevents that repair pattern. That friction is the feature: it makes a quiet rewrite harder. But the team must have a visible alternative.
For a faulty published version, the usual operational answer is to publish a new version, for example moving from v2.4.0 to v2.4.1, and update release notes or release status to guide users. The exact process should distinguish a build failure, a security issue, and a documentation correction.
- For an incorrect binary, publish a corrected new version rather than treating the old version as replaceable.
- For a note typo, edit the permitted release text and keep the asset and tag unchanged.
- For a wrongly targeted tag discovered before publication, use the tag-ruleset’s authorized release path rather than granting broad permanent bypass access.
- For compromised credentials, review every identity able to bypass tag rules before relying on release immutability as the containment measure.
The maintenance cost is real: version numbers may be consumed faster, and release managers lose a convenient cleanup action. In exchange, downstream users do not have to guess whether yesterday’s v2.4.0 is the same object as today’s.
Make the controls testable in CI
A policy that has never denied an unauthorized operation is only assumed to work. Add a release-process test to your repository operations checklist. You do not need to test against a real production version tag; use a non-production tag pattern in a controlled repository or verify the ruleset configuration through your normal infrastructure review process.
The practical tests are simple: confirm that a developer identity cannot create a protected release-pattern tag, confirm that the approved release identity can perform the intended operation, and confirm that an already published immutable release cannot accept a replacement asset or a moved tag. Record the expected failure as part of the runbook, not as tribal knowledge in a chat thread.
Also inspect your automation boundaries. If one workflow token can both build unreviewed code and publish immutable releases, the process may faithfully preserve an unreviewed artifact. Split the workflow logically: establish the approved commit, create the tag through the authorized path, build from that tag, then publish the release. The purpose is traceability as much as access control.
What to do this week
Start with one production repository and inventory its existing tags. Separate mutable pointers such as staging or production from permanent version identifiers such as v1.7.3. If both types match one broad pattern today, create a naming plan before imposing a rule that breaks legitimate promotions.
- List every tag pattern used for releases and state whether it is a pointer or a permanent version.
- For each permanent pattern, identify the only identities allowed to create, update, and delete it before release publication.
- Review bypass access as production access, including automation identities.
- Enable immutable releases where GitHub release assets are part of what consumers download or audit.
- Write the replacement-version procedure for a bad release and test it with a non-production release.
The decision rule is straightforward: use tag rulesets when your question begins with “who is allowed to change this tag?” Use immutable releases when it begins with “can this published tag or file ever change afterward?” Production release workflows need both questions answered, because users experience both failures as the same broken promise: a version name no longer identifies the software they expected.