A single forced push can move v2.4.1 to a different commit while the GitHub Release page keeps the same familiar tag name. If customers downloaded binaries, deployment automation triggered from tags, or CI published packages from that release, the damage is not limited to one Git reference.

GitHub’s retired protected-tag feature covered a narrow but important job: stop the wrong people from creating, changing, or deleting names that represent releases. Rulesets are the replacement, but an automatic migration is not the same thing as reviewing your release policy. The useful migration work is translating each existing tag pattern into an intentional ruleset with the right target, restrictions, and bypass path.

1. Start with the policy behind each protected-tag pattern

Before opening the ruleset UI, list every existing protected-tag pattern and answer one operational question: what must never happen to a tag matching this pattern? “Protect v*” is not a policy; it is only a matcher.

For a conventional release repository, the actual policy is often: a release tag may be created by a release workflow, but nobody may update or delete it afterward. A repository that uses tags for deployment markers may instead need a movable tag such as production, while still preventing developers from deleting it.

Existing tag family Likely intent Ruleset decision
v* Immutable public versions such as v1.8.0 Restrict creation, updates, and deletions; allow only the release actor to create.
release/* Named release tags such as release/2026-09 Protect the family, then decide whether nested names are valid.
production A movable deployment marker Restrict updates and deletion; permit the deployment automation that moves it.
build-* Disposable CI build markers Usually do not protect, unless another system consumes them as permanent evidence.

This inventory prevents the common migration error: copying a broad legacy pattern into a ruleset and unintentionally treating temporary CI tags like permanent releases. A protected tag should represent a promise that a name will retain its meaning.

2. Know which tags deserve immutability

Use the consumer of the tag to choose the protection level. A tag used only by a developer’s local script has different consequences from a tag that drives a package publish job or anchors a GitHub Release containing downloadable binaries.

For example, assume an application releases from tags named vMAJOR.MINOR.PATCH. The release workflow checks out the tag, builds a Linux archive and Windows ZIP file, and attaches both to a GitHub Release. Once users have downloaded those artifacts, moving the tag makes later source inspection unreliable: the tag can point at code different from the code that produced the original files.

That scenario merits three controls:

  • Block unauthorized creation, so an unreviewed commit cannot be labeled as an official version.
  • Block updates, so an existing release name cannot be moved to another commit.
  • Block deletion, so release tooling and users retain a stable reference.

In contrast, a tag called staging is often deliberately movable. Treating it like an immutable version will create friction for every deployment. The appropriate rule is not “protect all tags”; it is “make the mutation rule match the tag’s meaning.”

3. Translate legacy patterns into GitHub fnmatch patterns

Rulesets target branches or tags using fnmatch syntax. For migration purposes, begin with the smallest pattern that captures the intended naming convention. A literal tag name is the safest matcher; a wildcard is a policy boundary and should be reviewed like one.

These patterns are useful starting points for common tag schemes:

Desired target Pattern What to check before using it
All version tags beginning with v v* Confirm that tags such as v-next or verify-build are not in the same namespace.
One release tag only v2.4.1 A literal has no wildcard spillover, but requires a new rule or different process for each version.
Release names under one level release/* Decide whether names such as release/mobile/v2 should be included.
Release names with nested segments release/** Use this only when the repository intentionally supports multiple path segments.
Calendar versions for 2026 2026.* Check whether prerelease names such as 2026.10-rc1 belong in the protected set.

The tradeoff people skip is that v* protects both v1.2.3 and any future tag beginning with v. If your repository creates verification tags such as validate-123, use that separate prefix rather than relying on a broad release matcher.

4. Build the replacement ruleset for immutable releases

For the most common migration case, create a tag-targeting ruleset for release versions and configure it as an immutable namespace. Give it an operational name such as Immutable v* release tags, not a vague name such as Tag protection. Six months later, the name should tell a maintainer why the rule exists.

  1. Select tags as the target reference type.
  2. Add the include pattern v*, or your narrower release convention.
  3. Enable restrictions on tag creation, tag updates, and tag deletion.
  4. Add only the release team or automation identity that must create a release tag to the bypass configuration.
  5. Record the expected workflow in the ruleset description: for example, “Release workflow creates v*; tags are immutable after publication.”

Creation deserves special attention. If developers currently create a version tag locally and push it, restricting creation changes the release procedure. That may be desirable: it forces release creation through a reviewed workflow. But it is a process change, not merely a technical migration, so tell the people who cut releases before activating the rule.

Do not grant a broad administrator bypass merely because a release occasionally needs repair. A repaired release should normally receive a new version, such as v2.4.2, rather than silently repointing v2.4.1.

5. Handle movable deployment tags as a separate ruleset

Do not combine immutable versions and movable environment markers into one pattern. If v* means “permanent release” while production means “currently deployed,” they have opposite update policies and should have separate rulesets.

A practical configuration for a movable production tag is a literal tag target with updates and deletions restricted. Put the deployment system, rather than every engineer, on the allowed bypass path. That permits a promotion job to move production from one approved commit to another while blocking ad hoc local pushes.

The command that changes an annotated or lightweight tag can look deceptively ordinary:

git tag -f production <approved-commit>
git push --force origin production

That is exactly why the bypass should be narrow. A developer with a broadly permitted bypass can move the deployment marker without leaving the same audit trail as the deployment process. If your workflow does not genuinely need a movable tag, prefer immutable version tags and store deployment state in the deployment system instead.

Keep release rules separate from environment approval rules. GitHub environment protections are not a substitute for tag mutation controls: a release is based on a tag, and branch-oriented assumptions do not automatically protect that tag reference.

6. Decide who may bypass before you import or activate

A ruleset is only as strict as its bypass design. Start from the actor that performs the required action, then grant the smallest possible exception. For an organization with five maintainers, “all maintainers can bypass” may be convenient, but it makes every maintainer an exception to release immutability.

Use this decision rule: if an action can be performed by one repeatable automation path, grant the bypass to that path; if it requires human judgment, grant it to the smallest named release group. Avoid a bypass for every repository contributor.

Release operation Preferred actor Bypass scope
Create v2.4.1 Release workflow or release team Only the actor that cuts approved releases
Move production Deployment automation Only the deployment actor
Delete an accidental unpublished tag Small release-admin group Exceptional and documented
Create build-481 CI system No release ruleset match, if builds are disposable

Also check the identity actually used by your workflow. A job using a GitHub App, a personal access token, or the default workflow token may appear as different actors. Test the real release workflow rather than assuming “GitHub Actions” is a universal bypass identity.

7. Use the migration feature as an inventory, not approval

GitHub provides a migration feature for existing tag protection rules, and the sunset notice states that existing protections can be imported. That is helpful for preserving coverage, especially in repositories with several old patterns, but import results still need a policy review.

After importing, inspect each generated ruleset against three questions:

  • Does its target pattern match exactly the tags you intend to govern?
  • Does it restrict the operations that matter: creation, update, deletion, or a deliberate subset?
  • Does its bypass configuration reflect the people and automation that release software today?

Suppose an old rule protected release-* when releases were cut manually. Since then, a workflow may have taken over publishing, while the old release-manager group still exists. A mechanically migrated rule can preserve the old group’s access even though the current process no longer needs it. The migration succeeded technically but retained unnecessary authority.

For repository fleets, create a simple review sheet with the repository name, old pattern, new pattern, mutation policy, bypass actor, and owner. This is faster than trying to reconstruct release intent during an incident.

8. Test rules with harmless tags before relying on a release

Ruleset mistakes tend to appear at the worst possible time: during a release window, after artifacts are built and approvals are complete. Test the rule in a non-production repository or with a clearly non-release tag that matches the intended pattern.

For a planned v* policy, test these three outcomes:

  1. A non-bypassed contributor cannot create a matching test tag such as v0.0.0-ruleset-test.
  2. The approved release actor can create the tag if your process requires it.
  3. After creation, an unauthorized actor cannot force-push or delete that tag.
git tag v0.0.0-ruleset-test
git push origin v0.0.0-ruleset-test

git tag -f v0.0.0-ruleset-test HEAD~1
git push --force origin v0.0.0-ruleset-test

git push origin :refs/tags/v0.0.0-ruleset-test

Remove the test tag through the approved process afterward. The point is not to practice destructive Git commands; it is to verify that creation, update, and deletion are independently governed as your policy expects.

9. Complete this migration week with a release-path drill

Do not make this a settings-only task. Pick the next real release path and walk it from commit approval through tag creation, artifact build, GitHub Release publication, and package or deployment publication. A tag ruleset is successful when that path is both protected and usable.

Use this short completion checklist:

  • Export or record every legacy protected-tag pattern before changing it.
  • Classify each pattern as immutable release, movable deployment marker, or disposable CI tag.
  • Create separate tag rulesets where those classes have different mutation rules.
  • Use literal patterns for single markers and narrow fnmatch patterns for tag families.
  • Review bypasses against the actual workflow identity and release team.
  • Run a create, force-update, and delete test on a non-production tag.
  • Write the final policy beside the release runbook, including who can create a version tag.

The durable outcome is not simply that retired tag protections are gone. It is that v2.4.1, production, and build-481 now have explicit, different rules because they represent different promises to your users and your automation.