Between July 24 and August 14, 2024, GitHub scheduled API brownouts for legacy tag-protection endpoints; a release pipeline that treated a successful tag push as proof of deploy authorization could fail at the next environment gate. The costly failure is not usually the missing rule itself—it is discovering during a production release that tag creation, workflow triggering, and environment access were controlled by three different mechanisms.
GitHub’s legacy tag protection rules were migrated to tag rulesets on August 30, 2024, and the legacy UI and API surface was deprecated as part of that transition. Treat this as a release-pipeline migration, not a settings-page cleanup: release tags are often inputs to GitHub Actions, package publishing, GitHub Releases, deployment environments, changelog jobs, and external CD systems.
1. Start with the three controls that teams routinely conflate
A protected tag rule answers one question: who may create, update, or delete a tag matching a pattern such as v* or release/*. A workflow trigger answers another: what happens after a matching ref changes. An environment protection policy answers a third: may this workflow job deploy to production?
Those controls may have appeared to work as one system when a team used a protected tag plus a tag-triggered deployment workflow. They are not interchangeable. GitHub introduced deployment tag protection rules specifically because relying on old deployment branch behavior for tags created a security and usability problem: deployment tag patterns are meant to match tags, not branches.
| Control | Example | Question it answers | Failure if omitted |
|---|---|---|---|
| Tag ruleset | v* | Who can move or create a release tag? | Any permitted contributor may create a tag that starts a release workflow. |
| Workflow trigger | push.tags: ['v*'] | Which tag events create a run? | A valid release tag exists but no build, publish, or deploy job runs. |
| Environment policy | production allows v* | Which refs may deploy to this environment? | The workflow starts, then deployment is blocked or authorized from an unintended ref. |
The migration decision rule is simple: preserve each control independently. Do not mark the task complete because a new tag ruleset exists if the production environment still has branch-only assumptions.
2. Inventory every dependency before changing a pattern
Begin with a repository-level inventory and then repeat it for every repository that can deploy to the same production environment. The most useful output is a one-page map of tag patterns, owners, workflows, and deploy targets—not a screenshot of repository settings.
Search the repository first. In a local clone, these commands find common tag-dependent workflow logic and release scripts:
git grep -nE "refs/tags|github.ref_type|github.ref_name|event.release.tag_name|tags:"
git grep -nE "git tag|git push.*--tags|git push origin.*refs/tags"
find .github/workflows -type f -maxdepth 2 -print
Then inspect the release process outside the repository. A tag may be created by a developer laptop, a GitHub Actions workflow, a release bot, a CLI script, or a separate CI system. The actor matters because the new ruleset must authorize the identity actually writing the ref, not merely the human who approved the release.
- Record each protected pattern:
v*,release/*,prod-*, and any legacy naming format. - Record whether tags are lightweight or annotated, and whether a release is created from the tag.
- List workflows triggered by
pushtags and workflows triggered byreleasepublication. - List package registries, deployment tools, and webhooks that receive a tag name.
- Identify the account, app, or token that creates, updates, and deletes tags.
3. Classify each tag pattern by release risk, not by repository size
One repository can need several rulesets. A broad * rule is easy to reason about but often grants release authority over scratch tags, build tags, and production tags at once. Split patterns when their consequences differ.
For example, an application may use v1.8.0 for published production releases, v1.9.0-rc.2 for release candidates, and build/8471 for CI metadata. The first pattern should normally have the strictest creation and update policy. The latter might not need a protected tag at all if it has no deployment or publication effect.
| Pattern | Typical downstream action | Recommended migration treatment |
|---|---|---|
v* | Create release, publish package, deploy production | Dedicated ruleset plus explicit production environment tag policy. |
v*-rc.* | Publish pre-release, deploy staging | Separate ruleset if a broader group may create candidates. |
release/* | Trigger a release workflow | Keep only if tools and workflow filters still use this format. |
build/* | Internal build reference | Prefer no protection unless it grants an external privilege. |
The second-order benefit is operational: on-call engineers can tell whether v2.4.1-rc.1 is intentionally allowed to reach staging without inferring that policy from a 200-line workflow file. Pattern names become part of your authorization model, so retire aliases that no longer have a consumer.
4. Build the target model around a concrete release path
Use one release path as the migration reference. Consider a service that publishes a container image and deploys it after a maintainer creates v3.12.0. Its desired sequence is: an authorized release manager creates the tag, the tag-triggered workflow builds from that exact commit, an image is published with an immutable identifier, and the production job is admitted only for an allowed tag pattern.
Write the policy in plain language before configuring anything:
- Only the release engineering team and the release automation identity can create or update
v*. - A push of
v*starts the release workflow. - The workflow deploys to
stagingfirst. - The production environment accepts deployments from tags matching
v*, with its existing reviewers or wait timer applied separately. - A release candidate pattern may reach staging but must not qualify for production unless that is an explicit business decision.
This exposes a common mismatch. If your production pattern is v*, it may also match pre-release tags such as v3.13.0-rc.1. Do not assume semantic-version punctuation creates a security boundary. If candidates and final releases have different authority, use patterns that make the difference unmistakable, such as release/* and candidate/*, or test the exact matching behavior in a non-production environment.
5. Migrate rulesets without silently changing who can release
GitHub’s migration of legacy tag protections to tag rulesets reduced the need to recreate every legacy setting manually, but automatic migration is not the same as pipeline validation. Compare the effective policy with the old operational reality: who could create a tag, which automation account pushed it, and whether anyone depended on deleting or moving a mistaken tag.
Before editing rules, export a human-readable record of the old state. Capture the pattern, authorized people or teams, and the date. Put that record in the release engineering ticket or repository administration documentation. It gives you an answer when a release bot receives a permission error two weeks later.
When configuring the target ruleset, verify these details deliberately:
- The ruleset targets tags rather than branches.
- The include pattern covers every intended release name and does not capture unrelated build tags.
- The permitted actors include the automation identity that performs the actual push.
- The policy for updating and deleting an existing tag is intentional, rather than inherited from habit.
- Repository administrators know whether they are subject to, or bypass, the policy under your organization’s configuration.
Do not solve a failed bot release by broadly bypassing the ruleset. That shortcut often turns a narrow release-tag control into an administrator-only convention. Fix the identity and authorization path instead.
6. Test tag-based deployments as an authorization chain
A successful git push tests only the first link in the chain. Your test must prove four observable facts: the tag is accepted, the intended workflow run starts, the run resolves the expected commit, and the environment admits or blocks deployment exactly as designed.
Create a test plan with a harmless artifact and a non-production environment. If production and staging use different environment rules, test both; a staging success does not prove that the production tag policy is correct.
- Create a commit containing a visible but safe version marker, such as a build metadata file.
- Create a tag that matches the production-format pattern only if your workflow has a safe test mode; otherwise use a dedicated test pattern and mirror the policy.
- Push the tag using the same identity used in real releases.
- Confirm the Actions run shows the tag ref and checks out the tagged commit.
- Confirm the staging deployment records the exact tag and commit SHA.
- Attempt a negative test: use an unauthorized identity or a non-matching tag and confirm it cannot reach the protected environment.
Preserve evidence from the run URL, deployment record, and commit SHA. A screenshot of a green workflow is weaker than a record demonstrating that v3.12.0 deployed the commit it names.
7. Audit workflow event semantics before declaring success
Tag-triggered workflows are usually based on push events, while a workflow that reacts to publication of a GitHub Release uses a different event and exposes different fields. Both can carry a tag name, but they happen at different points in the release process.
on:
push:
tags:
- 'v*'
A push-triggered pipeline can build and deploy immediately after tag creation. A release-triggered pipeline can wait until a release is published, which may fit teams that require release notes or a final human action. Do not accidentally swap one for the other during the ruleset migration.
Review conditions that inspect refs. A workflow may use github.ref_type, github.ref_name, refs/tags/, or a release event’s tag field. Replace broad conditions such as “not main” with an explicit tag test when the job has production credentials. Also inspect reusable workflows: a caller may be tag-triggered while the reusable deployment workflow assumes a branch name.
Finally, test a tag that already exists. A release process that retries by moving an existing tag has different requirements from one that creates a new immutable release identifier each time. GitHub’s immutable releases can protect published releases, tags, and release assets from later changes; that is a separate supply-chain choice, not a substitute for environment authorization.
8. Prepare for the legacy UI and API sunset in scripts and runbooks
The UI sunset matters most to manual release instructions. Search internal documentation for phrases such as “Tag protection,” screenshots of the old repository settings page, and URLs copied into runbooks. Replace them with instructions that name tag rulesets and explain the release pattern being governed.
The API sunset is more dangerous because scripts can keep working until a scheduled brownout or deprecation becomes an outage. Search automation repositories, Terraform modules, shell scripts, and internal release tooling for legacy tag-protection API calls. Do not limit the search to the application repository; the integration is often maintained by a platform team elsewhere.
For every API dependency, record the owner, call site, authentication method, and replacement plan. A migration ticket is incomplete if it says “update API” without naming the tool that applies the rule.
| Dependency found | Owner to assign | Acceptance check |
|---|---|---|
| Repository administration script | Platform engineering | Creates or reads the intended tag ruleset without legacy endpoints. |
| Terraform or infrastructure module | Infrastructure team | Plan output preserves tag pattern and authorized actors. |
| Release runbook | Release manager | A new maintainer can find the current settings and complete a test release. |
| External CI integration | Service owner | The integration can push an authorized test tag and trigger the expected pipeline. |
9. Run this migration checklist this week
Do not batch every repository into one untested policy change. Start with one service that has a predictable release cadence and a staging environment, then reuse the resulting checklist. The goal is a proven release path, not merely a completed administration task.
- Day 1: inventory tag patterns, workflow triggers, production environments, and tag-writing identities for one repository.
- Day 2: classify patterns into production release, candidate release, and internal tags; delete unused patterns from the proposed policy.
- Day 3: compare the migrated or configured tag ruleset with the documented release-author list.
- Day 4: run one positive tag deployment test and one negative authorization test in staging.
- Day 5: search shared automation and runbooks for legacy UI references and deprecated tag-protection API usage.
- Before production cutover: document the exact rollback decision: whether to pause releases, use a pre-authorized release operator, or deploy a previously validated artifact without creating a new tag.
The useful completion criterion is not “legacy protected tags are gone.” It is: an authorized actor can create the intended release tag, the correct commit is built, the correct environment policy evaluates the tag, and an unauthorized tag cannot turn into a production deployment.