GitHub scheduled tag-protection API brownouts between July 24 and August 14, 2024, then migrated remaining tag protection rules into tag rulesets on August 30. The dangerous failure mode was not merely a broken automation call: an incorrectly translated release-tag pattern could leave tags such as v2.4.1 mutable, or unexpectedly block a release bot at the end of a deployment.

That deadline has passed, but the migration remains worth auditing. Repositories can retain old scripts, internal runbooks, and assumptions about who is allowed to create or move release tags. The practical task is to treat tag protection as a policy migration: inventory the old pattern, translate its intended behavior into a tag-targeting ruleset, verify its matching refs, and test the identity of the tags that matter to users and deployment systems.

Understand what changed: a policy rule became part of an aggregate

Legacy tag protection was a narrow feature: a repository had tag-protection rules with tag-name patterns. GitHub’s replacement is a repository ruleset whose target is tag. That sounds like a rename, but it changes how you reason about enforcement.

Rulesets have no priority order. When more than one ruleset targets the same tag, GitHub aggregates their rules. If two matching rulesets define the same rule differently, the most restrictive result applies. Rulesets also layer with other protection mechanisms that target the same refs.

This means “I created the replacement ruleset” is not a sufficient completion criterion. You must answer three more specific questions:

  • Which exact refs match the new ruleset’s include and exclude conditions?
  • Which other repository or organization rulesets also match those refs?
  • Can the actual release principal—human, GitHub App, or automation token—still perform the intended operation?

A migration that preserves the visible pattern but ignores overlapping rulesets can be stricter than the legacy policy. That is usually safer for production tags, but it can break a release pipeline during a version cut.

Start with an inventory, not a replacement click

Before changing anything, write down the current policy and the operational behavior behind it. A tag pattern alone does not tell you whether the protected tags are created by maintainers, a GitHub Actions workflow, a GitHub App, or an external release system.

Build an inventory for every repository with releases. Include both ordinary version tags and less obvious tag families such as release/*, prod-*, and signed build tags. A repository may have only one visible legacy rule while its CI system pushes three tag formats.

Inventory item Example Why it matters
Legacy pattern v* Defines the original intended tag scope.
Expected tag names v2.4.1, v2.4.1-rc.1 Provides concrete names for match tests.
Excluded names validate-v2.4.1 Detects patterns that are too broad.
Tag creator Release GitHub App Determines the bypass or authorization decision.
Allowed operations Create once; no update or deletion Maps to ruleset rules rather than a vague “protected” state.
Existing important tags v2.4.0, v2.4.1 Supplies refs and object IDs for post-migration verification.

Keep this inventory in the pull request or change ticket. Six months later, it explains why v* was intentionally chosen instead of a broader catch-all pattern.

Translate the old policy into explicit tag rules

The useful translation is behavioral, not cosmetic. A legacy protected tag typically represented a release invariant: once a release name exists, it must not be silently moved or removed. In a ruleset, make those operations explicit.

Legacy intent Tag ruleset design Migration check
Protect release tags matching v* Set target to tag; include refs/tags/v* Test stable and prerelease names.
Do not let a release name be reused Restrict tag creation where that matches your release process Confirm whether the release tool creates the tag itself.
Do not move an existing release tag Restrict updates Test a forced push only in a disposable repository.
Do not remove a published release tag Restrict deletions Confirm incident-response expectations for exceptions.
Allow a release automation identity Configure a deliberate bypass policy where needed Test using the real automation identity, not only an administrator account.

The tradeoff people skip is creation restriction. If your release system creates v2.4.1 during its final job, a rule that blocks creation without a correctly configured bypass will fail the release after artifacts have already been built. Conversely, if tags are created manually before a deployment workflow starts, restricting creation is usually part of the point.

Use this decision rule: if a tag name represents an immutable published release, protect creation, update, and deletion; then grant the smallest practical bypass to the identity that performs the approved release procedure. If tags are disposable build markers, put them under a separate prefix such as build/ rather than weakening the release-tag policy.

Be precise about ref names and glob patterns

A tag ruleset evaluates Git refs, not just the short tag strings shown in a release page. For a release tag named v2.4.1, the ref name is refs/tags/v2.4.1. Write and inspect the ruleset condition in that namespace.

For a simple semantic-version convention, an include condition such as refs/tags/v* is easy to review. It covers v1.0.0 and v1.0.0-rc.1, but it should not be assumed to cover unrelated prefixes such as release-v1.0.0.

Slash-containing tags deserve a separate test. A team using release/2026.08 and release/candidate/2026.08 should not infer the matching behavior from a single release/* example. Put both names in the inventory and test both against the actual ruleset. The cost of a five-minute pattern test is lower than discovering that a nested tag name is unprotected after a release.

Prefer separate release and temporary-tag namespaces

Do not make one broad policy serve two incompatible workflows. For example, keep immutable public tags under v*, and put CI-only tags under build/* or avoid pushing them altogether. That lets the release ruleset be strict without forcing a bypass exception for every build job.

Work through a release-tag migration example

Assume a repository currently protects the pattern v*. Maintainers publish tags such as v2.4.1; a release GitHub App creates the tag; and the team expects no one to move or delete it after publication.

  1. Create a repository ruleset named immutable-release-tags.
  2. Set its target to tag.
  3. Include refs/tags/v*.
  4. Configure restrictions for creation, update, and deletion according to the approved release workflow.
  5. Configure the release App as the narrowly scoped exception if the App must create the tag.
  6. Do not include build/*, test/*, or a generic wildcard merely for convenience.

Now test three outcomes. First, the release principal should be able to create an unused candidate such as v9999.0.0-migration-test in a non-production repository. Second, an ordinary contributor should receive a rejection when attempting the same action. Third, the test should show that an existing matching ref cannot be changed or deleted by a principal without the intended exception.

Perform destructive tests in a disposable repository or against a dedicated migration-test tag. Do not force-push a real release tag merely to prove that a policy exists.

Use the rulesets API and stop automating the retired path

GitHub’s sunset notice said that REST and GraphQL endpoints for legacy tag protection would be deprecated, alongside the old UI path. Any script that treats legacy tag protection as its source of truth should be retired or rewritten to inspect repository rulesets.

For an API-driven workflow, query repository rulesets and then retrieve the ruleset you intend to audit. The basic repository endpoint shape is:

GET /repos/OWNER/REPOSITORY/rulesets
GET /repos/OWNER/REPOSITORY/rulesets/RULESET_ID

A minimal inspection command can look like this:

curl -L \
  -H "Accept: application/vnd.github+json" \
  -H "Authorization: Bearer $GITHUB_TOKEN" \
  -H "X-GitHub-Api-Version: 2022-11-28" \
  https://api.github.com/repos/OWNER/REPOSITORY/rulesets

Do not build a migration job that first attempts a legacy tag-protection endpoint and only falls back to rulesets when it fails. That fallback bakes the retired path into your operational design and makes outages harder to diagnose. Instead, treat rulesets as the canonical configuration and store the expected ruleset name, target, include patterns, and expected restrictions in version-controlled policy documentation.

Verify tag targets, not just tag names

A protection policy guards a ref. Your deployment, package metadata, and release notes care about what that ref resolves to. After a migration, verify both the ruleset scope and the object identity of existing release tags.

Use git ls-remote to capture remote tag refs before and after the change:

git ls-remote --tags origin 'v*'

Annotated tags normally produce two lines: one for the tag object at refs/tags/v2.4.1 and one peeled line ending in ^{} , which identifies the object reached through the annotated tag. Lightweight tags generally have only the direct ref line. Save this output with the change record for the tags that represent shipped versions.

Then inspect a specific tag locally:

git fetch --tags origin
git show --no-patch --decorate v2.4.1
git rev-parse v2.4.1
git rev-parse v2.4.1^{}

The final command peels an annotated tag to its target. If a release pipeline deploys commit abc123, verify that the peeled tag target is the intended commit or signed object. A ruleset does not repair a tag that was already pointed at the wrong target; it only constrains later operations.

Check for collisions, bypasses, and organization policy

Repository-level review is incomplete when organization rulesets exist. Because matching rules aggregate and the most restrictive version applies, a repository rule that looks correct can still be blocked by an organization-level rule.

Review each matching policy through the lens of one real operation: “The release App creates v2.4.1.” Identify every ruleset that targets that ref and every restriction it contributes. Then make sure the intended bypass applies at the policy layer where the restriction is enforced.

Administrators are especially poor test subjects for this exercise. An administrator may have permissions or bypass behavior that a GitHub App, fine-grained token, or workflow identity does not. Run one controlled test with the same identity class used in production. Record the actor, tag name, expected outcome, and actual outcome.

  • Test a matching release tag such as v9999.0.0-test.
  • Test a non-matching tag such as build/migration-test.
  • Test creation with the release identity.
  • Test creation, update, and deletion with a non-bypass identity.
  • Review all rulesets that can target the same release ref.

Make the migration auditable in the same week

Finish the work with a small, repeatable checklist rather than relying on a screenshot of a settings page. The durable artifact is a record that another maintainer can use when the release workflow changes.

  1. List legacy tag patterns and at least two matching and two non-matching example tag names.
  2. Document whether tags may be created, updated, or deleted after publication.
  3. Create or inspect a tag-targeting ruleset with explicit refs/tags/... conditions.
  4. Review overlapping repository and organization rulesets for aggregate restrictions.
  5. Replace scripts that read or modify legacy tag protection with ruleset inspection or management.
  6. Capture git ls-remote --tags output for important existing release tags.
  7. Run controlled create, update, and delete tests using the production release identity and a non-bypass identity.
  8. Store the expected policy beside release documentation or infrastructure configuration.

The outcome is more than compliance with a retired API path. You get a release-tag policy that states exactly which refs are protected, which operations are prohibited, who can perform the approved exception, and which commit each published tag actually names.