A release can still serve a different ZIP, tarball, or binary after its version tag is protected if the release assets remain editable. That gap matters when a deployment script downloads v2.4.0 from GitHub Releases and assumes the file behind that familiar version name has never changed.

GitHub protected tags and immutable releases sound interchangeable because both use the language of “protection.” They protect different objects, at different points in the release lifecycle, and against different failure modes. Treating immutable releases as a replacement for tag protection can leave pre-release controls weak; treating tag protection as sufficient can leave published artifacts mutable.

1. Start by separating the Git reference from the published package

A Git tag is a ref: a name such as v2.4.0 that identifies a Git object, usually a commit. A GitHub release is a publishing record associated with that tag. It can include release notes and downloadable assets such as tool-linux-amd64.tar.gz, a Windows ZIP, a checksum file, or an SBOM.

Those are related objects, but they are not the same thing. A consumer might obtain source code by cloning the repository and checking out v2.4.0. Another consumer might use GitHub’s “latest release” endpoint, read its tag_name, and download a named asset. The second consumer depends on both the tag and the release asset remaining trustworthy.

Object Typical consumer action Failure if changed
Git tag v2.4.0 git checkout v2.4.0 The same version label resolves to different source code.
Release asset Download tool_2.4.0_linux_amd64.tar.gz The same download name returns a different binary.
Checksum asset Download checksums.txt A replaced binary can appear valid if its checksum is replaced too.
Release notes Read upgrade or security guidance Operators may act on revised instructions or advisories.

The key design question is therefore not “Do we protect releases?” It is: which release object must be frozen, who may change it before publication, and what do downstream users fetch?

2. Protected tags control mutations to a Git name

Tag protection is fundamentally a Git-reference control. It is intended to restrict who can create, update, or delete tags that match a selected naming pattern, such as v* or release/*. GitHub has been moving tag protection rules toward repository rulesets; GitHub’s tag-protection sunset notice says existing tag protections are migrated into rulesets if no action is taken before the sunset.

For a release team, the practical goal is simple: a developer should not be able to force-push a new meaning under an already-public version label. If v2.4.0 initially points to commit a1b2c3d, a later push should not silently make it point to e4f5g6h.

That solves an important reproducibility problem. A build system that runs this command should receive the same source revision whenever the tag is resolved:

git fetch --tags origin
git checkout v2.4.0

But tag protection does not, by itself, answer what happens to a separately uploaded file called tool_2.4.0_linux_amd64.tar.gz. A release asset is not a Git object stored in the repository’s commit history. That is the boundary many release policies overlook.

3. Immutable releases freeze the published release boundary

GitHub describes immutable releases as releases where assets and the associated Git tag cannot be changed after publication. The feature is aimed at supply-chain protection: an attacker should not be able to inject malware into an existing project release or alter an existing release in a way that breaks downstream workflows.

The word after is operationally important. Immutable releases are a publication-boundary control. Your team can prepare a draft, upload files, inspect them, and correct a packaging error before publication. Once the release is published, the release assets and its associated tag are frozen.

That means immutable releases protect a workflow that protected tags alone cannot fully protect:

  • A CI job creates cli_2.4.0_linux_amd64.tar.gz.
  • The job uploads the archive and checksums.txt to a GitHub release.
  • A customer downloads those files using the release tag or the latest-release API.
  • An account with release-editing access later attempts to replace the archive.

With an immutable published release, that replacement is blocked. The release remains a durable statement: this version was published with these assets and this associated tag.

4. The controls overlap, but neither is a drop-in synonym

Use protected tags to govern who can establish or alter version names in Git. Use immutable releases to govern whether a published distribution can be changed after the release moment. They overlap around the associated tag, but their timing and scope differ.

Question Protected tags or tag rulesets Immutable releases
Primary protected object A Git tag ref matching a rule. Published release assets and the associated Git tag.
Best timing Before and during tag creation or mutation. At the point a release is published.
Stops a tag being moved Yes, when the rule blocks the attempted ref mutation. Yes for the associated tag after immutable publication.
Stops a published binary being replaced Not by protecting the Git ref alone. Yes, for immutable release assets.
Useful for tags with no GitHub release Yes. No published release means no immutable release boundary.
Main policy decision Who may create, update, or delete release-like tags? When is a package final enough to become permanently published?

The second-to-last row is the reason “just turn on immutable releases” is incomplete advice. Many repositories use tags for internal build markers, deployment candidates, backports, or source-only releases. Those tags may need rules even if they never receive a GitHub release.

5. Walk through a failure that tag protection does not prevent

Imagine a project that publishes a command-line tool. Its installation guide contains this command:

curl -LO https://github.com/acme/widget/releases/download/v2.4.0/widget_2.4.0_linux_amd64.tar.gz

The repository protects v*, so no ordinary contributor can move v2.4.0 to a different commit. That is good: source users checking out the tag get stable source code.

Now assume a maintainer account with release-management access is compromised after publication. If the release is mutable, an attacker may try to remove the original archive and upload a replacement under the same asset name. The protected tag still points at the original commit, so an audit focused only on Git history may report no unexpected tag movement. Yet the installation command fetches the replaced binary.

Immutable releases close that particular gap by preventing the published asset and associated tag from being changed after publication. The security property is stronger for release consumers because the URL they use refers to an asset in a frozen publication.

There is a second-order consequence: immutable releases convert packaging mistakes into new-release work. If the Linux archive contains the wrong configuration file, the safe correction is not to swap files under v2.4.0. It is to publish a corrected version such as v2.4.1, explain the issue, and update documentation that points users to the flawed version.

6. Design for correction before publication, not mutation afterward

The most useful release-process change is to make “publish” a deliberate final gate. Do not build binaries, compute checksums, and publish a release in one opaque action with no inspection point. Create an unpublished release candidate, validate its contents, then publish only after the candidate passes review.

  1. Merge the intended commit to the protected default branch.
  2. Create a version tag from the reviewed commit, for example v2.4.0.
  3. Build all artifacts from that exact commit in CI.
  4. Generate a checksum file and, where relevant, an SBOM.
  5. Upload the artifacts to a draft or otherwise unpublished release stage.
  6. Have a reviewer compare the tag’s commit, filenames, checksums, and release notes.
  7. Publish the release only when the package is ready to be permanent.

This workflow makes the tradeoff explicit. The review is not ceremony: it is the last cheap point to catch an archive built from the wrong commit, an accidentally included credential file, or an artifact named for the wrong operating system.

For small teams, one reviewer may be enough. For a package downloaded by production automation, separate the person approving publication from the workflow identity that uploads artifacts. The goal is not to create a large approval process; it is to avoid one credential being able to alter source, artifact, and publication without a visible checkpoint.

7. Build the artifact from the tag you intend to publish

A release is only coherent if the binary, the checksum, and the version label describe the same source revision. Make CI prove that relationship rather than relying on a release manager’s local checkout.

A minimal shell pattern looks like this:

git fetch --tags --force origin
git rev-parse v2.4.0
git rev-parse HEAD
git diff --exit-code v2.4.0 HEAD

make build
sha256sum dist/widget_2.4.0_linux_amd64.tar.gz > dist/checksums.txt

The exact build command will vary by language: go build, npm pack, cargo build --release, and python -m build produce different package types. The invariant does not vary: build from the commit identified by the intended tag, and publish the output generated by that build.

Do not have the release job build from “whatever is currently on main.” Between a tag being created and CI starting, another merge can land. A binary built from the later branch tip may claim to be 2.4.0 while containing code that is not in v2.4.0.

Protected tags reduce the chance that the tag later changes. Immutable releases reduce the chance that the published output later changes. Deterministic build inputs connect the two.

8. Decide what downstream consumers should pin

Consumers make different trust decisions depending on how they retrieve software. If a deployment script asks GitHub for the latest release, then “latest” is a moving selector by design. Immutability protects each published release from later alteration; it does not turn the latest-release selector into a fixed version.

Use these rules for common cases:

  • Production deployment: pin v2.4.0 and verify a published checksum before installation.
  • Developer convenience installer: use the latest release, but make the moving behavior visible and retain a versioned download option.
  • Source dependency: pin a commit or protected tag, depending on your reproducibility requirements.
  • Binary dependency: pin the asset version and validate its checksum from a trusted release record.

Checksums are useful only if the expected checksum is obtained through a channel the attacker cannot alter along with the binary. Publishing both the archive and checksums.txt is valuable for integrity checking, but it does not magically create independent trust if both files can be replaced together. Immutable releases improve that situation by freezing the release assets once published.

For higher-assurance distribution, teams can additionally use signed tags, signed artifacts, or an external package registry. Those are complementary choices, not reasons to skip GitHub’s ref and release controls.

9. Audit your repository this week

You can identify the most important gap in about 30 minutes. Pick the release a real user installs, not an internal test tag, and trace the path from source commit to downloaded file.

  1. List the tag patterns your project treats as released versions: for example, v*, release/*, or cli-v*.
  2. Check the repository’s rulesets and confirm that those patterns have ref-mutation restrictions appropriate for your maintainers and CI identities.
  3. Check whether immutable releases are enabled for the repository and whether your next published release will use that protection.
  4. Inspect one existing release: note its tag, asset names, checksum file, and the CI workflow that created it.
  5. Find every script that downloads “latest” and decide whether a pinned version would be safer.
  6. Write a correction policy: publish v2.4.1 for a bad artifact rather than replacing the files for v2.4.0.

The practical target is easy to state: a version tag should keep identifying the reviewed source, and a published release should keep serving the reviewed assets. Protected tags defend the first statement. Immutable releases defend the second. A release process that uses both gives downstream users a stable thing to build from and a stable thing to download.