A release called v2.4.1 can keep its familiar GitHub URL while the Git object behind that tag is moved to a different commit. If your deployment, package build, or customer support process treats that tag as a release contract, one unprotected force-push can turn a routine hotfix into a supply-chain incident.

GitHub’s legacy tag protection rules were sunset and migrated to tag rulesets. The published timeline matters because it changed more than a page in repository settings: repositories without existing legacy protection could no longer add it through the GitHub.com UI beginning May 30, API brownouts followed in July and August, and GitHub stated that all tag protection rules would migrate to a new tag ruleset on August 30, 2024.

The useful response in 2026 is not to reconstruct the old feature. It is to establish which tags represent published promises, verify that the replacement control protects those promises, and make one person or team accountable when the release process changes.

1. Start with the release contract, not the old setting

A Git tag is a pointer to a particular point in repository history. A GitHub release is based on a tag, but the tag date and release date can differ because they may be created at different times. That distinction is operationally important: a repository can have many tags without each tag being a public, deployable release.

Do not begin with the question, “Which repositories had legacy tag protection?” Begin with, “Which tag names must never silently point somewhere else?” The answer is usually much smaller and more valuable than every tag in the organization.

Tag example Typical meaning Protection decision
v3.8.2 Published production release with release notes or downloadable binaries Protect creation, updates, and deletion according to the release process
cli-v3.8.2 Release tag for one component in a monorepo Protect if consumers install or deploy this component independently
rc-3.9.0-1 Release candidate rebuilt during testing Use a separate policy; immutability may be premature
build-8421 Ephemeral CI marker Usually do not protect; retention and cleanup matter more
demo-2026-09 Demo or internal checkpoint Protect only if another system treats it as immutable input

The tradeoff people skip is recovery. A rule that blocks changes to v3.8.2 is valuable precisely because a rushed engineer cannot “fix” a bad tag by moving it. Your release process must provide another path: publish v3.8.3, or formally revoke the release while preserving evidence of what was shipped.

2. Build a repository inventory with evidence columns

Legacy REST and GraphQL endpoints were deprecated as part of the sunset, so do not make an obsolete endpoint the foundation of your audit. Instead, create a spreadsheet, issue tracker table, or internal database that records present-day facts: repository, release tag patterns, ruleset status, release owner, and evidence link.

First get the repository universe. For an organization, the GitHub CLI can provide a practical starting list:

gh repo list YOUR-ORG --limit 1000 --json nameWithOwner,isArchived,visibility \
  --jq '.[] | select(.isArchived == false) | .nameWithOwner'

For each active repository, inspect the Releases page and the tag list. GitHub exposes both from the repository’s Releases area, and the chronological release history often reveals which naming conventions are actually used instead of merely documented. Record the last three published release tags, not only the latest one; old exceptions are where incomplete patterns show up.

Your inventory should distinguish “no relevant tags” from “not checked.” The second status is a risk, while the first is a decision.

  • Repository: acme/payments-api
  • Release tag patterns: v*, hotfix-*
  • Last releases checked: v4.12.0, v4.11.1, hotfix-4.10.4
  • Current rule evidence: URL or screenshot of the tag-targeting ruleset
  • Release mechanism: manual GitHub release, GitHub Actions, or external pipeline
  • Owner: named team, not “Engineering”
  • Decision: protect, explicitly do not protect, or pending pilot

3. Find the tags that downstream systems actually consume

A repository’s visible releases are only one signal. The tag that needs protection is often named in a workflow, deployment manifest, package recipe, documentation snippet, or customer installation command. Search the default branch and deployment repositories for release-oriented strings before writing patterns.

git grep -nE 'refs/tags/|tag_name|release-|v[0-9]+\.[0-9]+' HEAD -- \
  .github docs deploy scripts

Then inspect remote tags directly. This command lists tag references without cloning the full repository:

git ls-remote --tags https://github.com/YOUR-ORG/YOUR-REPO.git

For example, suppose acme/agent has v*, nightly-*, and installer-v*. Documentation tells users to download binaries from GitHub Releases using installer-v2.6.0, while the package registry pipeline uses v2.6.0. Both patterns are release contracts. The nightly tags are not, even if they appear more frequently in the tag list.

Use a simple decision rule: protect a pattern when moving, deleting, or creating a matching tag outside the approved release process could cause a user, deployment, artifact consumer, or audit record to receive the wrong code. This rule catches component-prefixed tags that a generic v* policy misses.

4. Verify the migrated ruleset instead of assuming it survived intact

GitHub said existing tag protection rules would be migrated to a new tag ruleset. Migration is not the same as validation. A matching pattern can be too broad, too narrow, or assigned to the wrong people after repository ownership and automation have changed.

In each repository with release tags, open its rules configuration and locate rulesets that target tags. Review the rule by testing it against real names, not abstract intent. A pattern that appears to mean “all semantic versions” may fail to cover a prefix such as server-v1.7.0; a pattern intended for releases may also capture v1.8.0-rc.1.

For every matching rule, record these four checks:

  1. Scope: Which tag names match? Test a stable release, a prerelease, and an unrelated tag.
  2. Blocked actions: Does the ruleset prevent the unwanted creation, update, and deletion operations for protected names?
  3. Allowed actors: Can the release automation and designated maintainers complete the legitimate release path?
  4. Bypass accountability: If an exception exists, is it attributable to a small, named group rather than every repository administrator?

Do not infer protection from the existence of a GitHub Release. Releases may contain source archives and binary assets, but their presence does not by itself prove the underlying tag cannot be changed. Treat the release page as consumer-facing evidence and the tag ruleset as the repository-side control.

5. Design patterns around stable releases, prereleases, and monorepos

One broad tag rule is tempting because it reduces settings work. It also creates the most common failure mode: release engineers discover during an incident that their emergency or prerelease naming convention is blocked, then request a permanent broad bypass.

Split policies when the lifecycle differs. Stable releases generally deserve the strongest immutability policy. Release candidates and nightly builds often need more flexibility, but that flexibility should be explicit rather than accidentally inherited from a catch-all rule.

Repository shape Suggested pattern strategy Reasoning
Single application Separate stable v* from a prerelease convention such as rc-* Stable deployment inputs and temporary test builds have different recovery needs
Library with GitHub Releases Protect the exact published convention, such as v* Consumers may build installation and artifact URLs around these tags
Monorepo Use component prefixes such as api-v* and web-v* A component release can be independently consumed even when commits are shared
CI-heavy repository Keep immutable release tags separate from disposable build tags Protecting thousands of temporary references adds friction without improving release integrity

A tag convention is therefore part of your security boundary. If one repository uses v1.4.0, another uses release/1.4.0, and a third uses backend-v1.4.0, do not force a uniform pattern during this migration. First protect what exists. Standardize naming later as a separate change with its own compatibility review.

6. Pilot the replacement controls in a noncritical repository

Choose one repository that has a real release workflow but low blast radius: an internal CLI, a documentation site, or a sample service. Avoid an empty test repository. An empty repository proves that a settings screen accepts a pattern; it does not prove that a release workflow, bot identity, and human maintainer can operate under the rule.

Create a pilot tag pattern that mirrors production, such as pilot-v*, and run the entire release path. Have the usual release automation create the tag and GitHub Release, then validate the release from the consumer perspective by downloading or referencing the release artifact exactly as your users do.

  1. Create a test release from a known commit using the normal automation identity.
  2. Confirm the tag and release resolve to the expected commit.
  3. Attempt an unauthorized update of the protected tag from a non-approved account.
  4. Attempt deletion from the same account.
  5. Run the documented emergency path with an approved actor, if your policy permits one.
  6. Publish a new patch tag such as pilot-v1.0.1 rather than changing pilot-v1.0.0.

Capture exact outcomes, including denial messages and unexpected permissions. The key pilot question is not “Can we block a tag?” It is “Can the authorized workflow publish a release while everyone else is prevented from changing the immutable reference?”

7. Check automation permissions before enforcing production rules

Tag protection commonly exposes an ownership problem: releases are made by a GitHub Actions workflow, but nobody knows whether the workflow runs with a repository token, a GitHub App, or a human-maintained secret. Do not enable a restrictive production rule until you can name the actor that creates the release tag.

Read the workflow files that publish releases. Look for steps that run git tag, git push --tags, invoke the GitHub CLI, or call a release action. Also inspect external build systems that may push back into GitHub after a successful build.

git grep -nE 'git tag|push --tags|refs/tags|gh release|create release' HEAD -- .github scripts

Test the workflow on the pilot repository after the ruleset is active. If it fails, do not solve the problem by granting every administrator a bypass. Decide whether the workflow identity needs explicit permission, whether a release maintainer should trigger a controlled manual step, or whether the pipeline should create a new patch version after validation.

This is the second-order cost of immutability: it converts informal “just retag it” repairs into versioning decisions. That is a feature for published software, but it must be budgeted into incident runbooks and support communication.

8. Document ownership, exceptions, and the evidence trail

A protected tag rule with no owner decays quickly. Teams rename repositories, replace CI systems, and change release naming conventions. Six months later, the rule looks arbitrary and someone broadens it to make a deployment work.

Add a short RELEASES.md file or an entry in your engineering handbook for every release-producing repository. Keep it concise enough to update during a release-process change.

  • Protected patterns: for example, v* and installer-v*
  • What each pattern represents: package release, binary installer, or production deployment
  • Release owner: the team responsible for policy and access review
  • Release actor: the workflow, GitHub App, or approved maintainers that create tags
  • Recovery rule: publish a replacement tag and revoke affected artifacts; do not move the published tag
  • Review trigger: changes to release automation, tag naming, repository transfer, or new distribution channel

Link this document from the inventory row and link the applicable ruleset from the document. That two-way link gives an auditor enough context to understand why a pattern exists without rediscovering the release system from workflow YAML.

9. Complete the migration work this week

Do not turn this into a quarter-long repository cleanup. A small organization can complete a first pass in five working sessions; a larger one can use the same sequence per business unit. The output is a defensible decision for every release repository, including an explicit decision not to protect disposable tags.

  1. Day 1: Export active repositories with gh repo list and assign each repository to an initial owner.
  2. Day 2: Inspect Releases, tags, workflow files, and deployment references. Classify tag patterns as stable, prerelease, or ephemeral.
  3. Day 3: Review existing tag-targeting rulesets and mark missing patterns, overly broad patterns, and unknown release actors.
  4. Day 4: Run the full pilot in one noncritical repository, including denied update and deletion attempts.
  5. Day 5: Apply verified patterns to production repositories, document ownership, and create follow-up tickets for naming or automation redesign.

Finish by sampling one protected production release from the last 90 days: identify its tag, commit, GitHub Release, workflow run, and owner. If you can trace those five items in under five minutes, your migration has produced a usable release control rather than another forgotten repository setting.

For GitHub’s background on releases and their relationship to tags, see GitHub’s release documentation. For the historical sunset timeline, retain a link to the GitHub Changelog entry in your internal migration record alongside your inventory.