If v2.7.0 moves by one commit after a customer downloads its binary, the release page can still look legitimate while the source, checksum, and artifact no longer describe the same software. The expensive failure is not usually a compromised GitHub account; it is discovering weeks later that nobody can prove exactly what a customer installed.

GitHub gives release teams three similarly named but materially different controls: the Git tag, tag-focused rulesets, and immutable releases. Treating them as substitutes produces a common gap: a team protects v* tags but leaves release assets replaceable, or enables release immutability but has no disciplined way to decide which commit is allowed to become v2.7.0.

The useful model is simple: a tag identifies source history, a ruleset governs who may change matching tags, and an immutable release protects the published release package after publication. A trusted release normally needs all three concepts, even when a small project chooses to enforce only two of them.

1. Separate the tag, the release record, and the downloadable package

A GitHub release is based on a Git tag. The tag points to a specific point in repository history, while the release is GitHub’s publication record around that version: release notes, attached files, and the source archive GitHub makes available for the tag. A tag date and a release publication date can differ because they can be created at different times.

That timing difference matters operationally. Suppose an engineer creates and signs v2.7.0 on Monday, the CI system builds artifacts from that tag on Tuesday, and a release manager publishes the GitHub release on Wednesday. The source identity is Monday’s tag; the public distribution event is Wednesday’s release.

Object What it answers Example What can go wrong without controls
Git tag Which source revision is versioned? v2.7.0 points to commit abc123... The tag is moved or deleted.
GitHub release What publication does a user see and download? Release notes plus tool_2.7.0_linux_amd64.tar.gz Assets no longer match the intended source.
Tag ruleset Who may create, update, or delete matching tags? A rule targeting v* A privileged or accidental push changes release history.
Immutable release What remains protected after publication? The release’s tag and assets A published artifact set can be altered after users begin consuming it.

2. “Immutable tag” is an outcome, not one GitHub feature

Git itself does not make a normal tag permanently immutable. A tag is a ref: someone with sufficient repository permission can generally force-update it or delete it unless repository policy prevents that operation. A signed annotated tag helps recipients detect whether the tag object was signed by an expected key; it does not, by itself, stop an authorized user from replacing the ref.

That distinction explains why “we sign tags” is not an answer to “can v2.7.0 move?” Signing gives evidence about authorship and integrity of the signed tag object. Immutability requires a server-side control that rejects a later update or deletion, plus a process that limits who can create the tag in the first place.

On GitHub, tag-focused rulesets are the general policy layer for this job. Target a version namespace such as v* or a tighter namespace such as release/v*, then restrict tag creation, updates, and deletion according to your release model. GitHub aggregates rulesets that target the same ref; if overlapping rules define the same requirement differently, the more restrictive version applies.

That aggregation has a practical consequence: do not assume a permissive “release automation” ruleset overrides a broad security ruleset. Before changing automation permissions, inspect every ruleset matching the tag pattern. The effective policy is the combination, not the most recently created rule.

3. Immutable releases protect the published boundary

GitHub’s immutable-release capability addresses a different moment in the lifecycle: publication. Once an immutable release is published, GitHub protects the release’s assets and tag from tampering. This is especially important when the release is the delivery channel for compiled binaries rather than merely a page of release notes.

Consider a CLI project that distributes four files for each version: macOS, Linux, Windows, and a checksum manifest. Its users may fetch the “latest release” through a script, package manager integration, or direct download URL. For those users, the attached files are not supplementary documentation; they are the product.

A tag ruleset can stop v2.7.0 from being repointed, but it does not express the same release-level promise about already published assets. Immutable releases close that package boundary: after publication, the tag and release assets are protected together.

Conversely, immutable releases do not replace pre-publication governance. If the wrong commit is tagged, or CI builds from the wrong checkout, immutability preserves the mistake very effectively. The correct operational goal is not “make everything unchangeable.” It is “allow a narrow, reviewed path until publication, then make the published evidence and downloadable artifacts durable.”

4. A signed version workflow from commit to published release

Use a workflow in which the version tag is created deliberately, verified before the build, and treated as the only allowed source input for release artifacts. The example below assumes a version tag format of vMAJOR.MINOR.PATCH and a release branch that has already passed your normal review and test checks.

  1. Choose the exact commit that passed release validation, such as the current commit on main.
  2. Create an annotated, signed tag locally: git tag -s v2.7.0 -m "Release v2.7.0" <commit-sha>.
  3. Verify the signature before pushing: git tag -v v2.7.0.
  4. Push that one tag: git push origin v2.7.0.
  5. Have CI check out refs/tags/v2.7.0, build all release artifacts, and generate a checksum manifest.
  6. Publish a GitHub release based on that existing tag and upload exactly the CI-produced artifacts plus the checksum manifest.
  7. Publish it as an immutable release when the files and release notes are final.

The important choice is step 5: build from the tag, not from a floating branch name. A branch can legitimately advance between the decision to release and the build. A version tag should not.

5. Verify two different things: source identity and artifact identity

A signed tag and a checksum file solve different verification problems. The signed tag lets a verifier inspect whether the version marker was signed by a trusted signing key. The checksum manifest lets a downloader verify that a downloaded archive is byte-for-byte the file published for that release.

git fetch --tags origin
git tag -v v2.7.0
git checkout v2.7.0
sha256sum -c checksums.txt

The first three commands establish which source revision is being inspected. The final command checks downloaded artifacts against the release’s checksum file. If your project publishes tool_2.7.0_linux_amd64.tar.gz, put its hash in a plainly named file such as checksums.txt and attach both to the same release.

The tradeoff people skip is that checksums are only useful when users know how to trust the checksum manifest. Putting the archive and its checksum on the same mutable release page offers weaker evidence than a protected published release. Immutable releases improve that situation by protecting release assets after publication, but teams distributing high-risk software should still make signature-verification instructions and trusted public keys easy to find.

Do not describe a signed Git tag as a signature on the binary. It is not. If your threat model requires cryptographic proof for each executable, add an artifact-signing mechanism to the build pipeline rather than relying on the tag signature alone.

6. Choose controls with this decision table

Most repositories do not need the same policy. A documentation repository with occasional version markers has a different risk profile from a project distributing installers to thousands of machines. Start with what users consume, then select the smallest control set that protects that consumption path.

Repository situation Tag-focused ruleset Signed tags Immutable releases Practical decision
Internal library; consumers build from a commit SHA Recommended for v* Useful when release authorship matters Optional if no binary assets are consumed Protect tags first; release assets are not the primary boundary.
Open-source library; consumers install from tags or source archives Yes Yes Recommended Preserve a stable source version and a durable public publication record.
CLI, desktop app, or agent distributed as release binaries Yes Yes Yes Use all three: stable source, attributable tag, and protected downloads.
Nightly or preview builds Use a separate pattern such as nightly/* Optional Usually avoid for intentionally replaceable builds Keep mutable previews outside the versioned release namespace.
Emergency rollback likely during the first hour Yes, with a defined exception path Yes Publish only after final approval Use a release candidate before making a final release immutable.

7. Design tag rulesets around namespaces, not people’s memory

A rule targeting v* is easy to explain, easy to audit, and hard to bypass accidentally. It means that a command such as git push --force origin v2.7.0 should encounter the repository policy rather than relying on the release engineer remembering that version tags are special.

Keep pre-release and final-release names separate when their lifecycle differs. For example, use v2.7.0-rc.1 for release candidates and v2.7.0 for the final release, then decide whether both patterns deserve the same restrictions. If release candidates are expected to be superseded, give them a distinct naming pattern instead of weakening protections for every final tag.

  • Target final tags with a narrow pattern such as v* only if all tags beginning with v are release versions.
  • Restrict tag creation to the release automation identity or a small release-maintainer group.
  • Restrict updates and deletions for the final-release namespace.
  • Document the break-glass process before an incident, including who can approve a replacement version.

The least pleasant incident is discovering that the only way to correct a bad release is to weaken a broad ruleset under pressure. Prefer publishing v2.7.1 with a clear correction over silently repointing v2.7.0. The former leaves an auditable timeline; the latter makes downstream troubleshooting much harder.

8. Avoid the three release workflow mismatches

Mismatched source: CI builds from main after tagging v2.7.0. The artifact may contain a commit not represented by the tag. Fix it by passing the tag ref explicitly into checkout and recording the resolved commit SHA in build logs or release notes.

Mismatched permissions: an automation token can publish releases but cannot create a protected tag, or it can update tags more broadly than intended. Fix it by deciding whether humans create signed final tags and automation only publishes from them, or whether automation owns the entire tagging path. Mixing both without an explicit boundary causes failed release jobs and ad hoc permission exceptions.

Mismatched lifecycle: a team treats a published release as a staging area for replacing binaries while also telling users to trust “latest.” Fix it by using CI artifacts or a clearly labeled preview channel for pre-publication testing. Publish the final GitHub release only when the files are the files you intend users to retain.

Immutable release protection makes this lifecycle discipline visible. That friction is valuable: it moves uncertainty to the point where a release manager can still stop, rebuild, or publish a new version rather than asking users to guess which copy of v2.7.0 was real.

9. Put a trusted-release baseline in place this week

Start with one release, not a repository-wide policy rewrite. Pick the next version and write down its expected commit SHA, artifact filenames, and signing identity before CI runs. Then test the whole path in a noncritical release candidate.

  1. Create a tag ruleset that targets your final-version namespace and review any overlapping rulesets.
  2. Create and verify one signed annotated tag with git tag -s and git tag -v.
  3. Change the release workflow so checkout uses the tag, not the default branch.
  4. Attach a checksum manifest beside every distributed archive.
  5. Publish the completed version as an immutable release.
  6. From a clean clone, verify the tag and checksum as a user would.

If that final clean-clone test is awkward, your users will find it awkward too. The target state is modest but strong: one version name, one protected source reference, one reproducible build input, and one published artifact set that cannot quietly change after users begin downloading it.