Skip to content

Tag rulesets restrict creation with no bypass actors — an unknown number of repos can never ship a release #1028

Description

@hyperpolymath

Finding

A repository ruleset named Immutable-Tags that carries the creation rule with an empty bypass_actors list means no tag can ever be created in that repository by anyone — the owner included. The repo can never ship a release, and there is no workaround: gh release create creates the same ref and is rejected identically.

Surfaced by a real rejection while cutting launch-scaffolder v0.1.0:

remote: error: GH013: Repository rule violations found for refs/tags/v0.1.0.
remote: - Cannot create ref due to creations being restricted.
 ! [remote rejected] v0.1.0 -> v0.1.0

"Immutable tags" means a tag cannot be changed or deleted once cut — that is update + deletion + non_fast_forward. creation does not belong to that intent, and with an empty bypass list it authorises nobody, so the rule can only ever refuse.

Measured (8 repos sampled — NOT a census)

8 of 8 restricted tag creation. The difference was entirely the bypass list:

repo bypass current_user_can_bypass tags
hyperpolymath/standards 2 always 7
metadatastician/* (org EstateTagging) 3 always 0
hyperpolymath/hypatia 0 never 14
hyperpolymath/launch-scaffolder 0 never 0
hyperpolymath/rsr-template-repo 0 never 0
hyperpolymath/trigger 0 never 0

Existing tags do not prove a repo can tag. hypatia held 14 tags and was still refused a new one — every one predated the ruleset (created 2026-06-25). The count of existing tags is not a probe for tag-creation capability; current_user_can_bypass is.

The blocking rulesets are substantively identical to standards' working one — same five rules; only the name casing and the rule array order differ. The sole real difference is bypass_actors. This is the same skipped-apply-step class as the marid F25 finding, not a policy decision.

Already cured (owner-ruled 2026-09-23)

Four repos had the standards bypass list mirrored onto them — repository-admin role + the Integration app standards already trusts — applied as a full reconstructed body, one call each:

  • hyperpolymath/launch-scaffolderv0.1.0 cut and released
  • hyperpolymath/rsr-template-repo
  • hyperpolymath/trigger
  • hyperpolymath/hypatia

All four now read bypass=2 / can=always, with the full ruleset shape diffed before-vs-after and unchanged apart from bypass_actors.

What remains — the reason this issue exists

The other ~160 hyperpolymath repos are unmeasured. It is already recorded that 167 of 170 "no rules" repos carry an active tag ruleset. If those also carry creation with no bypass, none of them can ever ship a release, and nobody will discover it until someone tries to tag.

Acceptance criteria

  1. A census over every hyperpolymath repo recording, per repo: whether an active tag-target ruleset exists, whether its rules include creation, its bypass_actors length, and current_user_can_bypass. Throttled to avoid the REST secondary limit.
  2. The census output names the set where creation is restricted and bypass_actors is empty — the set that cannot release.
  3. A ruling on that set: mirror the standards bypass list, or drop creation from the ruleset. One decision applied uniformly, not per repo.
  4. A positive control carried in the same run: hyperpolymath/standards must read bypass=2 / can=always, so a reported zero is provably real absence rather than an auth artefact.
  5. The check is written against the applied ruleset via repos/O/R/rulesets/{id}, never against a committed .github/rulesets/*.json — committing the JSON does not create the ruleset.

Two traps worth recording in whatever does the sweep

  • A ruleset PUT returned HTTP/2.0 200 OK with an empty body and had not applied. Three sibling repos succeeded in the same minute, so it was not permissions. An unchanged retry applied it. A 200 is not evidence a ruleset write landed — re-read it.
  • Verify the whole shape, not the field you expected to move. Diff the ruleset before and after with bypass_actors and the volatile fields deleted, and require the result empty, so a silently dropped rules entry cannot pass as success.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WPSJ7fBhVAMcpSffCBWUDo

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions