A compromised release token needs only one successful tag move to turn v2.4.1 from a known commit into a different build. Immutable releases close that hole after publication, but they do not answer the equally important question: who was allowed to create v2.4.1 in the first place?

That distinction matters for every repository affected by GitHub’s protected-tag sunset. GitHub migrated tag protection rules to tag rulesets in 2024, while immutable releases introduced a separate control over a published release. Treating either feature as a complete replacement creates a gap: a ruleset can control tag creation without locking release assets, while immutability can lock a published tag without expressing your team’s approval policy for creating future tags.

1. Start with the two controls you are actually migrating

Legacy protected tags were usually used for one job: stop unauthorized users or automation from creating, changing, or deleting names such as v*. Teams commonly expected that rule to protect the entire release process, even though a Git tag and a GitHub Release are related but separate objects.

Immutable releases add a different guarantee. Once an immutable release is published, GitHub locks its release assets and tag. You cannot add, replace, or delete assets, and you cannot move or delete that release’s tag while the release exists. You may still edit the release title and notes, or change whether it is a pre-release or latest release.

Question Tag ruleset Immutable release
Who may create v3.1.0? Defines the permitted actor or bypass path. Does not provide the creation policy.
Can v3.1.0 be moved before release publication? Can restrict tag updates. No protection exists until a release is published.
Can app.tar.gz be replaced after publication? No; rulesets govern Git references, not release assets. No; published assets are locked.
Can release notes be corrected? Not relevant. Yes; title and notes remain editable.

The migration target is therefore not “turn on immutable releases.” It is a two-layer design: tag rulesets govern creation and mutation of release names, while immutable releases preserve what users download after publication.

2. Inventory tag patterns before changing any rule

Do not begin with a single broad * rule. First list every tag namespace that appears in your repository and classify it by lifecycle. The same repository may use stable releases, release candidates, nightly builds, deployment markers, and temporary tags created by CI.

Run this locally to see the names your repository already exposes:

git fetch --tags origin
git tag --list | sort

Then inspect the release records on GitHub. A tag with no corresponding Release may be a deployment marker, a historical import, or an abandoned convention. Immutable releases only apply when a release is published, so these tag-only workflows require ruleset decisions even if you enable immutability.

  • Stable releases: v1.8.0, generally expected to remain resolvable forever.
  • Pre-releases: v1.9.0-rc.1, often created several times during testing.
  • Build tags: build-2026-09-25, often useful only for CI traceability.
  • Environment markers: production or staging, which intentionally move.
  • Temporary tags: names used by a release script and deleted afterward.

The overlooked cost is operational: locking all tag names makes routine deployment markers impossible to update. A moving production tag is a valid workflow; it simply must not share a policy with a permanent semantic-version release.

3. Convert legacy protection intent into tag rulesets

GitHub’s sunset migration moved existing tag protection rules into tag rulesets, but “migrated” does not mean “reviewed for current release practice.” Open each resulting ruleset and read it as an authorization policy, not as a checkbox inherited from the old UI.

For a normal versioned project, target stable release tags with a narrow pattern such as v*, then ensure the ruleset restricts the actions that violate your release contract: creation by unauthorized actors, updates to existing tags, and deletion. If your repository uses non-version tags that begin with v, use a more precise naming convention before enforcing the policy.

Use separate rulesets where lifecycle differs. The following is a practical starting point:

Tag pattern Creation policy Update/deletion policy Immutable release?
v* Release workflow or designated maintainers Restrict both after creation Yes for published releases
v*-rc.* Release workflow or release team Choose deliberately; many teams restrict both Use if candidates are distributed to users
build-* CI automation Often retain cleanup flexibility Usually no
production Deployment automation Must allow controlled updates No

A ruleset that prevents updates but permits every developer to create v9.9.9 protects history while still allowing release-name squatting. Make creation restrictions an explicit review item.

4. Decide which releases deserve immutability

Enable immutable releases where a published GitHub Release is part of the supply chain: downloadable binaries, source distributions, CLI archives, plugins, or any artifact a customer or downstream automation retrieves by release URL. The security value is concrete: a release asset cannot later be replaced under the same release record.

For a library that publishes only to a package registry, the answer depends on how consumers retrieve it. If users download GitHub release archives or installers, protect the release. If the Release is only a changelog pointer and the registry is the delivery mechanism, immutability may still be worthwhile, but it does not replace package-registry controls.

Do not confuse immutable releases with a complete repository freeze. GitHub documents that only the release assets and tag are locked. The source branch can continue advancing, and release notes can be corrected. That is useful for typo fixes, but it means release notes should not be your only security record of what was shipped.

A simple decision rule works well: if you would be alarmed to discover that an old download URL now returns different bytes, publish that item as an immutable release. If the tag is intentionally temporary or moving, keep it outside the immutable-release workflow.

5. Design the release boundary before automating it

The critical moment is the interval between tag creation and release publication. Immutable release protection begins after publication, so a workflow that creates v2.4.0, waits for manual packaging, and publishes tomorrow leaves a window in which the tag policy must carry all the protection.

For most repositories, use one release boundary: build and test a commit, create the version tag from that exact commit, upload the artifacts, and publish the release in the same controlled workflow. The tag ruleset protects the reference throughout; immutable-release protection takes over for the published tag and its assets.

Write down which input determines the commit. A release pipeline should use a full commit SHA or the event’s checked-out commit, not an ambiguous branch name such as main. If the workflow checks out main after a human has selected version v2.4.0, a new merge can silently change what the version represents.

git rev-parse HEAD
git tag -a v2.4.0 -m "Release v2.4.0"
git push origin v2.4.0

The commands are intentionally ordinary. The protection comes from who can run them against the remote and from publishing the corresponding release promptly, not from local Git syntax.

6. Use one concrete policy for stable releases

Consider a project with three maintainers, a GitHub Actions release workflow, stable versions such as v2.4.0, and test versions such as v2.5.0-rc.1. Its old protected-tag rule blocked unauthorized changes to v*. That was better than nothing, but it mixed stable and candidate lifecycles and did not lock uploaded archives.

A cleaner migration makes four decisions:

  1. Create a stable-tag ruleset for v*, with an explicit exception for the release candidate naming scheme if needed.
  2. Restrict stable tag creation to the release workflow identity and the small group responsible for emergency recovery.
  3. Restrict stable tag updates and deletion, rather than relying on maintainers to remember not to force-push a tag.
  4. Enable immutable releases and publish v2.4.0 with its checksum file and binary assets as one release event.

The team must then make one policy choice for release candidates. If customers or testers download the RC assets, make those releases immutable too; “candidate” does not make a tampered binary harmless. If RC tags are internal and routinely rebuilt, separate them with a pattern such as v*-rc.* and give them a deliberately different ruleset.

That naming split is not bureaucracy. It prevents an emergency request to retag an RC from becoming an exception that weakens the stable release policy.

7. Give CI the right authority, not broad administrator bypass

Once tag creation is restricted, the first failure commonly appears in automation: a release workflow can build artifacts but cannot push the version tag. Do not solve this by granting every workflow or every maintainer a bypass that defeats the ruleset.

Instead, identify the one release path that should be allowed to create stable tags. Document its trigger, required review, and credentials. A manual release command run from a developer laptop has a different risk profile from a reviewed GitHub Actions workflow tied to a protected branch.

Test the identity with a disposable version such as v0.0.0-test in a non-production repository or an agreed test namespace. Verify all three cases:

  • The approved release workflow can create the allowed tag.
  • A normal contributor cannot create the same tag pattern.
  • An attempted force-update or deletion is rejected according to the ruleset.

Also inspect scripts that use git push --tags. That command can attempt to push unrelated local tags, including tags outside the intended release. Prefer pushing the specific release name, as in git push origin v2.4.0, because the audit trail and failure mode are clearer.

8. Rehearse the failures immutability makes intentionally hard

Immutability is valuable precisely because it removes convenient repair options. A release manager who uploaded the wrong archive cannot replace that archive after an immutable release is published. A tag pointing at the wrong commit cannot be moved or deleted while its immutable release exists.

Your runbook should distinguish errors before publication from errors after publication. Before publication, the tag ruleset governs whether a controlled correction is possible. After publication, plan to issue a new version rather than treating a published version number as reusable.

Failure Preferred response Why
Wrong commit selected before publishing Stop the pipeline and correct it under the authorized tag process. No user-facing immutable release has been created yet.
Typo in release notes Edit the notes. GitHub permits note and title edits for immutable releases.
Wrong binary published Publish a corrected new version and communicate the issue. The immutable asset should not be silently replaced.
Need to change which release is highlighted Change latest or pre-release status where appropriate. Those release attributes remain editable.

This is the team-friction tradeoff people skip: immutable releases require version discipline. The fix for a bad artifact becomes v2.4.1, not “quietly re-upload v2.4.0.” For distributed software, that inconvenience is the point.

9. Complete this migration checklist this week

You can complete a useful first pass in one repository without redesigning every CI workflow. Start with the repository that publishes the most externally consumed artifacts, because its old versions are the versions an attacker would most want to reinterpret.

  1. List all existing tags with git tag --list | sort and group them by stable, pre-release, build, and moving-marker lifecycle.
  2. Review the tag rulesets created from legacy tag protection and record which patterns each one covers.
  3. For every stable release pattern, verify restrictions for unauthorized creation, updates, and deletion.
  4. Identify the exact human or CI identity allowed to create production tags; remove accidental broad paths where your policy permits.
  5. Enable immutable releases for repositories that distribute GitHub Release assets or rely on release tags as durable public references.
  6. Run one non-production release through the real workflow, including a tag-creation attempt by an unauthorized identity.
  7. Add a recovery note: release-note corrections are allowed, but a bad published asset requires a new release version.
  8. Document the policy next to the release workflow so the next maintainer understands why a “quick tag fix” is blocked.

The final check is simple: ask whether an unauthorized actor can create your next stable tag, whether anyone can move it before publication, and whether anyone can replace the bytes after publication. A tag ruleset, a controlled release identity, and immutable releases should each answer one part of that question.