A release pipeline can publish a correct binary at 09:00 and point the same version tag at different source code at 09:05. That five-minute gap is exactly why protecting a Git tag and making a GitHub Release immutable solve different failures.
The confusing part is that both controls mention tags, and both can be described as “release protection.” But their enforcement boundary is different: protected tags govern what may happen to a tag before a release exists, while immutable releases preserve a published release package and its tag after publication. A team that chooses only one can still leave a meaningful stage of its release lifecycle unprotected.
Start with the failure you are trying to prevent
A Git tag is a ref that names a commit. A GitHub Release is a publication object built around a tag, with a title, notes, and optionally downloadable assets such as my-cli_2.4.0_linux_amd64.tar.gz. These are related objects, but they are not the same object and they do not fail in the same way.
Protected tags are primarily a source-control policy. They answer: “Who may create, update, or delete tags matching v*?” That matters before publication, when a CI account or a maintainer is turning a commit into a candidate version.
Immutable releases are a publication-integrity policy. GitHub’s immutable-release documentation says that, once enabled, the release assets and tag are locked after publishing. The same documentation explicitly preserves edits to the release title, release notes, pre-release status, and latest-release designation. That distinction is deliberate: prose can be corrected without changing the code or files users download.
The practical decision is not “tags or releases?” It is whether you need to control the creation path, the published artifact, or both.
Use the release-lifecycle matrix, not a feature checklist
Release protection should follow the artifact through its lifecycle. The matrix below is the fastest way to locate the control that matters for your workflow. “Primary” means the control directly addresses the state transition; “limited” means it may help indirectly but should not be your only guardrail.
| Release stage | Example event | Protected tags | Immutable releases | Why it matters |
|---|---|---|---|---|
| Version planning | Choose v2.4.0 for a commit |
Limited | Not applicable | No tag or published release exists yet. |
| Tag creation | CI pushes v2.4.0 |
Primary | Not applicable | Rules determine who can create a matching version tag. |
| Pre-publication correction | Replace a mistaken candidate tag before publishing | Primary | Not applicable | The team needs an intentional, auditable path to correct a ref. |
| Release publication | Create release notes and upload archives | Helpful for the tag | Not yet active | Assets are still being assembled into the published release. |
| Post-publication distribution | User downloads the v2.4.0 archive |
Limited | Primary | The tag and release assets need to remain unchanged. |
| Metadata correction | Fix a typo in notes | Not relevant | Allowed | Immutable releases lock assets and tag, not title or notes. |
| Withdrawal or replacement | A critical packaging defect is found | Depends on policy | Requires a new release strategy | Immutability makes “silently replace the files” unavailable. |
The important row is post-publication distribution. A protected-tag policy can stop a tag move, but it does not by itself express that every file attached to an already published release must remain fixed. Conversely, an immutable release does nothing while your pipeline is deciding whether it is allowed to create v2.4.0 in the first place.
What protected tags are best at before publication
Use protected tags when version names have authority inside your engineering process. For example, a repository may reserve v* for production versions while allowing developers to create non-release tags such as test/alice-api-refactor. A tag policy can distinguish those namespaces rather than treating every tag as equally sensitive.
A healthy release job commonly has a sequence like this:
git tag -a v2.4.0 -m "Release v2.4.0"
git push origin v2.4.0
Those two commands are easy to run from a laptop, a compromised token, or a misconfigured CI workflow. Protected tags narrow who is authorized to perform that push for release-shaped names. They are especially valuable when your release tag triggers downstream jobs: package publishing, image builds, documentation deployment, or customer notifications.
- Use a narrow pattern such as
v*if production versions consistently start withv. - Give the release automation identity the minimum access it needs to create the tag.
- Decide explicitly who can override or administer the policy before an incident, not during one.
- Keep experimental tags out of the production tag namespace.
The tradeoff people skip is recovery friction. If a release engineer tags the wrong commit, a strong policy correctly makes the “just force-push it” fix harder. That is not a reason to remove protection; it is a reason to rehearse a correction path and require the pipeline to validate the commit before it creates the version tag.
What immutable releases add after publication
GitHub describes immutable releases as a supply-chain security layer: after a release is published, assets cannot be added, modified, or deleted, and the associated tag cannot be moved or deleted. That creates a stable statement for consumers: the release named v2.4.0 continues to identify the same source point and the same uploaded files.
This is particularly relevant for repositories where users fetch release assets directly. A shell installer may resolve a release tag and download a named archive; a build script may use the GitHub Releases API to discover the latest release; a deployment tool may cache a URL to a release asset. If an asset can later be replaced under the same release version, those consumers can receive different bytes without changing the version string they requested.
Immutability does not freeze every field a maintainer might want to adjust. GitHub documents that you can still edit release titles and notes and change whether the release is a pre-release or the latest release. That is useful for correcting installation instructions or reclassifying a release, but it has a second-order consequence: consumers must not treat editable prose or “latest” status as a cryptographic identity.
For identity, use the version, the release asset name, and—where your distribution process provides one—a checksum or signature. The immutable-release control protects the publication object; it does not make a vague installer instruction or an unpinned “latest” lookup reproducible.
Walk through a realistic CLI release decision
Consider a team publishing a cross-platform command-line tool. Its workflow builds three files: acme_2.4.0_darwin_arm64.tar.gz, acme_2.4.0_linux_amd64.tar.gz, and checksums.txt. The release starts from commit 8f3c... on the main branch.
- The CI workflow runs tests and builds all three files from
8f3c.... - After validation, its release identity creates
v2.4.0. Protected-tag rules ensure an ordinary developer token cannot create or redirect a production-version tag. - The workflow creates the GitHub Release, uploads the archives and checksum file, and publishes it.
- Immutable-release protection locks the uploaded files and the
v2.4.0tag after publication. - A day later, a maintainer spots a typo in the release notes. They edit the notes without changing distributed bytes.
Now consider a real defect: the Linux archive was built with the wrong configuration. Under an immutable-release model, the team should not silently upload a replacement under v2.4.0. Instead, publish a corrected version such as v2.4.1, clearly explain the issue in release notes or an advisory process appropriate to the project, and direct users to the corrected version.
That approach costs an extra version and extra communication. It also prevents the worse outcome: two users both reporting “2.4.0” while holding different files.
Choose one control, or both, with this decision table
Most production software should use both controls because the costs are complementary: protected tags constrain who can create release identity, and immutable releases preserve what that identity means once published. There are still legitimate cases where one is the more urgent first step.
| Your situation | Recommended control | Reason |
|---|---|---|
| Tags trigger deployments or package publication | Protected tags first, then immutable releases | Unauthorized tag creation is the earliest dangerous transition. |
| Users download binaries from GitHub Releases | Immutable releases first, then protected tags | Published archives need a stable identity for consumers. |
| Library repository publishes source-only version tags | Protected tags at minimum | The version-to-commit mapping is the key release contract. |
| Internal repository with no GitHub Release assets | Protected tags, evaluate immutability if releases are published later | There may be no published asset surface to lock today. |
| Open-source project with release notes and downloadable installers | Both | Contributors need controlled version creation and users need stable downloads. |
| Team routinely “rebuilds” the same version | Protected tags now; redesign versioning before enabling immutability | Immutability exposes a release discipline problem that replacement uploads previously hid. |
The decisive question is simple: can a user or an automated system consume this version after publication? If yes, use immutable releases. Then ask: can a tag push trigger a consequential action before publication? If yes, protect the tag namespace too.
Design CI so the controls do not fight automation
The most common implementation failure is enabling a restrictive tag policy and discovering that the release workflow can no longer push v2.4.0. Treat the release workflow as a named production actor, not as an exception you fix hurriedly after the first failed release.
Separate the workflow into two trust boundaries. The build stage should produce artifacts and test results from a specific commit. The publication stage should receive that validated commit, create the protected tag through the authorized path, and publish the release. Do not let a later job rebuild from an unpinned branch name such as main; branch heads can change between the test and publication stages.
Also make the version immutable in your pipeline inputs. For a version like v2.4.0, derive archive names, checksum entries, release title, and tag from one value. A workflow that computes 2.4.0 in three different scripts eventually publishes mismatched names, even if GitHub prevents post-publication changes.
Keep “latest” separate from version identity
GitHub allows the latest-release designation to change on an immutable release. That makes it a convenience pointer, not a substitute for a pinned dependency. Installation documentation can offer a latest-release route for interactive use, but production deployment instructions should record the exact version and verify the intended asset.
This is the same operational distinction as mutable versus immutable infrastructure references: a moving label is useful for discovery; a fixed identifier is necessary for repeatability.
Plan the exception path before enabling immutability
Immutable releases turn an informal practice—fixing a published file in place—into an explicit release-management decision. That is beneficial, but only if the team agrees on what happens when a release is wrong.
Write down answers to these four questions:
- Who can create a corrected patch version such as
v2.4.1? - Where will users see that
v2.4.0has a known issue: release notes, documentation, a repository advisory process, or all three? - Which downstream systems should be updated when the latest release changes?
- How will support staff identify whether a customer installed the faulty version?
Do not use editable release notes as a substitute for a versioned correction. Notes are intentionally editable under immutable releases, which is useful for context but insufficient for distinguishing bytes already downloaded by users. A new version creates a clean line between the defective package and the fixed package.
For pre-releases, make the policy equally explicit. GitHub allows a published immutable release’s pre-release status to change, so release automation should not assume that status alone captures all approval state. Use your CI checks and internal approval process to decide when a candidate is ready to publish.
Audit the boundary you actually expose to users
A quick audit reveals whether your current controls match your distribution behavior. Start with your last three releases and answer concrete questions rather than reviewing settings in isolation: Which commit does each tag reference? Which files were attached? Which URLs or API calls do users and scripts rely on? Does a tag push cause a deployment?
Then inspect your release instructions. If they tell users to download an archive from GitHub Releases, immutable-release protection is directly relevant. If they instruct automation to run git checkout v2.4.0, protected tags are directly relevant. If both statements are true, the answer is not a tradeoff: use both protections around the same versioning contract.
Finally, distinguish data that can safely change from data that cannot. Release notes can be corrected. The “latest” marker can be reassigned. A published file called acme_2.4.0_linux_amd64.tar.gz should not quietly become a different file.
What to do this week
You can improve release integrity in one short maintenance session without redesigning your entire pipeline. Begin with the release namespace your users already recognize, usually v*, and document the identity allowed to create those tags.
- List the last five release tags and the automation or people that created them.
- Protect the production tag pattern, while preserving a documented recovery route for a mistaken pre-publication tag.
- Run one dry release through CI and confirm the authorized workflow can create the protected tag.
- Enable immutable releases for a repository whose GitHub Release assets are consumed by users, after notifying maintainers that published files cannot be replaced.
- Add a release checklist item: “A packaging error gets a new version; it does not get an in-place asset replacement.”
- Update deployment documentation to pin versions instead of relying on a moving latest-release selection.
Protected tags give your release process a controlled front door. Immutable releases make sure the package cannot be swapped after it leaves that door. Combining them turns a version string from a convention into a durable promise about both source code and downloadable artifacts.