A release pipeline that can create v2.4.0 but cannot repair a mistagged v2.4.0-rc.1 will usually fail at the least convenient moment: after artifacts, release notes, and downstream package references already exist. GitHub migrated legacy tag protection rules to tag rulesets on August 30, 2024, so the practical job in 2026 is not planning a migration—it is proving that the resulting ruleset, automation identities, and old API clients agree.

The 2024 retirement notice matters because it changed more than a settings-page label. GitHub stopped allowing repositories without tag protection rules to add them in the GitHub.com UI on May 30, ran API brownouts from July 24 through August 14, and migrated the remaining rules on August 30. If a script still calls a retired tag-protection endpoint, it may now produce a misleading “nothing to protect” result rather than a useful policy report.

1. Treat This as a Validation and Tooling-Cleanup Project

Start with the uncomfortable distinction: a migrated rule is not automatically a validated release control. A release tag is both a Git reference and, often, the trigger for GitHub Actions, package publishing, release creation, artifact signing, or deployment. The policy is correct only when the permitted writer can perform the intended operation and every other writer is blocked.

Use one repository as an end-to-end example before opening a broad cleanup ticket. Consider a fictional repository, acme/widgets, where:

  • Production releases use tags matching v*.
  • Release candidates use v*-rc.*.
  • A GitHub Actions workflow creates production tags.
  • Maintainers may create release candidates manually.
  • Deleting or moving a production tag should require an explicit exception.

The call for this repository is not “give maintainers broad bypass access.” It is to separate production and prerelease patterns, identify the workflow identity that creates production tags, and test creation, update, and deletion independently. That is more work than copying one old pattern, but it prevents a common migration-era mistake: allowing a release bot to create a tag while unintentionally allowing it to move or delete one.

2. Inventory Current Tag Rulesets, Not Retired Tag-Protection Endpoints

The retired legacy tag-protection REST and GraphQL endpoints are not a dependable current inventory source. Use repository rulesets to discover present enforcement. Legacy configuration can only be audited historically through retained migration records, organization audit evidence, internal change tickets, configuration exports, or logs captured before the endpoint retirement.

The following script inventories rulesets whose target is tag. It fetches each ruleset’s detail record so the report includes enforcement state, matching conditions, rule types, and bypass actors. It requires the GitHub CLI (gh), jq, an authenticated gh session, and repository access sufficient to read rulesets for the repositories being queried.

#!/usr/bin/env bash
set -euo pipefail

org="${1:?Usage: $0 ORGANIZATION}"
command -v gh >/dev/null
command -v jq >/dev/null

repo_list="$(mktemp)"
trap 'rm -f "$repo_list"' EXIT

gh api --paginate "orgs/$org/repos?per_page=100" \
  --jq '.[].name' > "$repo_list"

while IFS= read -r name; do
  repo="$org/$name"
  rulesets_json="$(mktemp)"
  errors_json="$(mktemp)"

  if ! gh api --paginate "repos/$repo/rulesets?per_page=100" \
    >"$rulesets_json" 2>"$errors_json"; then
    printf 'ERROR listing rulesets for %s: %s\n' \
      "$repo" "$(tr '\n' ' ' < "$errors_json")" >&2
    rm -f "$rulesets_json" "$errors_json"
    continue
  fi

  while IFS= read -r id; do
    gh api "repos/$repo/rulesets/$id" |
      jq -c --arg repo "$repo" '{
        repository: $repo,
        id,
        name,
        target,
        enforcement,
        conditions,
        rule_types: [.rules[].type],
        bypass_actors
      }'
  done < <(jq -r '.[] | select(.target == "tag") | .id' "$rulesets_json")

  rm -f "$rulesets_json" "$errors_json"
done < "$repo_list"

Save the newline-delimited JSON output to a dated file, such as tag-rulesets-2026-10-08.jsonl. Do not silently treat a 403 or 404 as a repository with no policy; it is an access gap in the inventory and deserves its own remediation row.

3. Prioritize Repositories by Release Blast Radius

Do not validate every repository in alphabetical order. Start where a changed tag has external consequences or where a failed tag push blocks a release train. A documentation repository with no release workflow is lower risk than a library whose version tag publishes a package consumed by hundreds of downstream builds.

Priority Repository signals Validation depth Example decision
1: release-critical Tag triggers package publishing, deploys, signed artifacts, or customer downloads Test create, blocked create, update, deletion, and release workflow behavior acme/widgets belongs here because v* is a release boundary
2: shared internal dependency Tag feeds internal package registries or reusable workflow versions Test creation and mutation controls; identify all consumers Prioritize if teams pin a tag rather than a commit SHA
3: operational tooling Tag triggers a build but artifacts are not externally distributed Validate the writer and rule matching Schedule with normal platform maintenance
4: dormant or archival No recent releases and no tag-driven workflow Confirm ownership and document an exception or retirement Do not grant bypass access merely to make an old rule look active

The tradeoff people skip is ongoing review cost. Every bypass actor is another identity whose permissions, token model, and ownership must be reconsidered when an employee leaves, a GitHub App is replaced, or a workflow is moved. A narrow ruleset may take an extra hour to design; an overly broad bypass can create years of policy debt.

4. Identify Tag Writers Before Changing Enforcement

A tag reader is not a tag writer. Release-note generators, artifact download scripts, and deployment systems may read tag_name from a GitHub release without ever pushing a ref. Your policy review should focus first on identities that create, update, or delete tags.

Ownership question Evidence to inspect What to record
Who creates production tags? Workflow YAML, release scripts, CI logs, GitHub App configuration Workflow name, actor or app identity, branch or commit source
Who creates prerelease tags? Maintainer runbooks, manual release procedures, recent tag history Team name and permitted pattern such as v*-rc.*
Who repairs mistakes? Incident runbooks and repository administration ownership Named escalation path for update or deletion requests
What consumes the tag? Actions workflows, package publishing configuration, deployment tooling Whether creation alone is enough to publish or deploy

For acme/widgets, the decision is to allow the release workflow identity to create stable tags, allow maintainers to create only prerelease tags if that is truly required, and reserve production-tag repair for a small release-engineering path. A team with no documented repair owner should not enable deletion bypass “just in case”; first write the escalation route.

5. Translate the Old Intent Into Verifiable Ruleset Controls

Legacy tag protections were commonly described as a pattern plus permitted creators. Rulesets require a more complete check because creation, update, and deletion can be controlled separately. Review the actual JSON returned by the inventory rather than assuming a migrated policy has the semantics you intended.

Legacy protection intent Ruleset behavior to inspect Expected result for acme/widgets
Only approved automation can publish releases Tag creation restriction and permitted bypass actor The release workflow identity can create stable release tags; ordinary writers cannot
Release tags follow a naming policy Matching tag pattern in conditions Stable and prerelease patterns are reviewed separately, not bundled accidentally
A published tag must not move Tag update restriction and enforcement status Updates are blocked under active enforcement except through the documented repair path
A published tag must not disappear Tag deletion restriction and permitted bypass actor Deletion is blocked; any exception is narrow and owned
Policy should be enforced, not merely observed enforcement value Production policy is active, not disabled or evaluation-only

Pattern review deserves its own pass. A broad v* pattern can include prereleases, build metadata, and experimental names depending on your naming convention. If prerelease writers have different permissions, use separate policy boundaries instead of trusting an informal convention that “maintainers will know which tag is production.”

6. Rehearse the Failure Path With a Disposable Tag

The July–August 2024 API brownouts are historical; you cannot use them as a present-day test window. Their useful lesson is reusable: test a deprecation or policy change by deliberately exercising the operation that should fail, then verify your tooling reports the failure clearly.

Create a neutral test convention such as policy-check-YYYYMMDD-HHMMSS, but only after confirming it cannot match a production tag pattern or trigger a publishing workflow. Check workflow triggers in the repository’s .github/workflows directory, release scripts, and any external CI configuration. If a workflow reacts to all tags with a wildcard, use a private disposable repository that has an equivalent ruleset instead.

set -euo pipefail

tag="policy-check-$(date -u +%Y%m%d-%H%M%S)"
git fetch origin
git tag -a "$tag" -m "Ruleset validation only; do not release"
git push origin "refs/tags/$tag"

Run this command with the identity you are testing, not with an administrator who can bypass controls. For the acme/widgets scenario, test a stable-looking tag only in a non-production test repository or under a deliberately safe pattern; never use a real v2.4.0-style name as a permission probe.

7. Test Creation, Update, and Deletion as Separate Operations

A successful tag creation test proves only one branch of the ruleset. The operationally important failures are often mutation attempts: someone tags the wrong commit, a pipeline retries after a partial failure, or a cleanup task tries to delete an invalid candidate.

  1. Use the approved release identity to create one disposable tag that is safe for the repository.
  2. Use an unapproved writer identity to attempt the same action and capture the error text and exit code.
  3. Attempt to move the approved disposable tag to another commit using a force push.
  4. Attempt to delete the disposable tag.
  5. Confirm that the outcome matches the documented policy for each operation.

For acme/widgets, the desired result is nuanced: the release workflow can create an approved release tag, a normal maintainer cannot create one, and neither identity can casually move or delete it. If a repair identity can bypass update and deletion restrictions, record why, who owns it, and how it is invoked. “It is an org admin account” is not an operational procedure.

8. Remove Legacy API Calls and Improve Error Handling

Search source repositories, shared shell libraries, Terraform or configuration-management code, and CI logs for references to legacy tag-protection APIs. A retired endpoint in a rarely used release script is dangerous because it may remain unnoticed until an urgent hotfix release.

git grep -n -E \
  'tags/protection|tag protection|protected tags|protection rules' \
  -- ':!vendor' ':!node_modules'

Replace old collection logic with ruleset inventory and explicit policy assertions. More importantly, make failures actionable. A release script should report the repository, tag name, authenticated actor, attempted operation, HTTP status if available, and the server response. It should not convert a rejected tag push into a generic “release failed” message after a 15-minute build.

For automation that creates a GitHub release after pushing a tag, make the tag push a checked step. GitHub’s release UI lets users select or create a tag while drafting a release, but your automation should not assume release creation proves the policy was satisfied. Keep tag creation, artifact publication, and release publication as observable steps with separate logs.

9. Finish This Week With a Policy Record and a Decision Rule

For each priority-one repository, create a short record containing the ruleset ID, target, enforcement value, matching patterns, relevant rule types, bypass actors, approved tag writers, repair owner, and date of the last rehearsal. Store it near the release runbook rather than in an administrator’s private notes.

  • Run the ruleset inventory for one organization and classify errors separately from zero-result repositories.
  • Pick the five repositories with the highest release blast radius.
  • Map each tag pattern to a named writer and a named repair owner.
  • Perform one safe creation test and one blocked-operation test for each priority-one repository.
  • Open cleanup issues for every legacy endpoint reference found by code search.

Apply this rule when deciding how much policy to build: if creating a matching tag can publish, deploy, or become a dependency reference, require an active tag ruleset with a named automation writer and a separately documented repair path; if it cannot, do not add bypass actors until a real release process needs them. That rule keeps controls proportional to consequence while avoiding the false comfort of a migrated setting nobody has actually exercised.