Skip to content

fix(applier): never PUT an org-inherited ruleset at the repo endpoint - #1027

Merged
hyperpolymath merged 1 commit into
mainfrom
fix/branch-gates-org-inherited
Sep 22, 2026
Merged

hyperpolymath merged 1 commit into
mainfrom
fix/branch-gates-org-inherited

Conversation

@hyperpolymath

Copy link
Copy Markdown
Owner

The defect

repos/{r}/rulesets returns the organisation's rulesets alongside the repository's own. An inherited one reads back in full at repos/{r}/rulesets/{id} — so every GET succeeds and nothing warns you — while the PUT to that same path 404s.

scripts/apply-branch-gates.sh:333 selected:

.[]|select(.target=="branch" and .enforcement=="active")|.id

with no ownership filter. On every metadatastician repo that picks up the org-level EstateBranching (18225024) and then PUTs it to the repo path. That is the failure already measured 67 times on this estate, once per reached repo.

The discriminator

.source_type. Verified against the live API that the list endpoint does return it on every entry:

$ gh api repos/metadatastician/paint-type/rulesets --jq '[.[]|keys]'
[["_links","created_at","enforcement","id","name","node_id","source","source_type","target","updated_at"], …]

The population is now fetched and classified locally — a server-side select whose empty result is also its success result cannot fail closed.

Three outcomes, none a guess

Situation State Why
Only inherited rulesets active ORG-INHERITED Names the ids and the /orgs/{org}/rulesets/{id} endpoint. The cure is at the org, applied once; 67 doomed per-repo writes are not a smaller version of it.
Inherited beside a repo-level one repo-level selected, inherited reported Rulesets are additive — the inherited one goes on enforcing. Counting both as candidates would turn an ordinary repo into AMBIGUOUS and leave it permanently ungated.
.source_type absent UNKNOWN Writability is exactly what that field decides; defaulting a missing one to the writable arm is a silent 404.

Verified against the live estate, report-only

metadatastician/paint-type — all three active rulesets are source_type: Organization, zero repo-level:

metadatastician/paint-type	ORG-INHERITED	branch=main gate_files=5 contexts=19 … — the only
active branch ruleset(s) here are org-level (18225024); writable ONLY at
/orgs/{org}/rulesets/{id}, cured once at the org, never per repo

Before this change it would have selected 18225024 and issued the 404.

Tests — 46/46, and a mutant that died

  • CASE 12 org-inherited only → ORG-INHERITED, no PUT even with --apply.
  • CASE 13 inherited beside repo-level → WOULD-GATE on ruleset=9, inherited still reported.
  • CASE 14 .source_type absent → UNKNOWN, no PUT.
  • MUTANT F restores the original selection and dies by PUTting to repos/{r}/rulesets/18225024 — the 404 reproduced, not merely asserted.

Cases 12 and 13 deliberately ship no fixture for the inherited ruleset's own path: the shim exits 1 on a missing fixture, so that absence asserts the fixed applier never reads it.

The 14 existing rulesets-list fixtures omitted source_type, which the live API always sends; they are now faithful to it.

Scope

Read-only except for the two files above. No ruleset was written — every live run in this PR was report-only.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Ji1bq3TypfycfUPAR7hSxR

repos/{r}/rulesets RETURNS the organisation's rulesets alongside the
repository's own. An inherited one reads back IN FULL at
repos/{r}/rulesets/{id}, so every GET succeeds and nothing warns you --
and the PUT to that same path 404s.

apply-branch-gates.sh selected `.target=="branch" and .enforcement=="active"`
with no ownership filter, so on every metadatastician repo it picked up the
org-level EstateBranching (18225024) and then PUT to the repo path. That is
the failure already measured 67 times on this estate, once per reached repo.

The discriminator is .source_type, which the LIST endpoint does return --
verified against the live API, every entry carries it. The population is now
fetched and classified LOCALLY; a server-side select whose empty result is
also its success result cannot fail closed.

Three outcomes, none of them a guess:

  * only inherited rulesets active  -> ORG-INHERITED, naming the ids and the
    /orgs/{org}/rulesets/{id} endpoint. The cure is at the org, applied ONCE;
    67 doomed per-repo writes are not a smaller version of it.
  * inherited BESIDE a repo-level one -> the repo-level one is selected and
    the inherited one is REPORTED, because rulesets are additive and it goes
    on enforcing. Counting both as candidates would have turned an ordinary
    repo into AMBIGUOUS and left it permanently ungated.
  * .source_type absent -> UNKNOWN. Writability is exactly what that field
    decides; defaulting a missing one to the writable arm is a silent 404.

Verified report-only against the live estate: metadatastician/paint-type,
whose three active rulesets are ALL source_type=Organization, now reports
ORG-INHERITED instead of attempting the write.

Tests: CASE 12/13/14 plus MUTANT F, which restores the original selection and
dies by PUTting to repos/{r}/rulesets/18225024 -- the 404 reproduced. Cases 12
and 13 deliberately ship NO fixture for the inherited ruleset's own path, so
the shim's exit-1-on-missing-fixture asserts the fixed applier never reads it.

The 14 existing rulesets-list fixtures omitted source_type, which the live API
always sends; they are now faithful to it. 46/46 green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ji1bq3TypfycfUPAR7hSxR
@coderabbitai

coderabbitai Bot commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 0acaa735-1d81-4623-a065-bb6b0ae3e925

📥 Commits

Reviewing files that changed from the base of the PR and between 392e84d and 9d15a77.

📒 Files selected for processing (2)
  • scripts/apply-branch-gates.sh
  • scripts/tests/branch-gates-apply-test.sh
 ___________________________________________________________________________________________________________
< A CodeRabbit review a day keeps the merge cop away: https://x.com/ThePrimeagen/status/1986151686839116209 >
 -----------------------------------------------------------------------------------------------------------
  \
   \   \
        \ /\
        ( )
      .( o ).
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • 🔴 Error committing to branch - (🔄 Check to retry)
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sonarqubecloud

Copy link
Copy Markdown

@hyperpolymath
hyperpolymath enabled auto-merge (squash) September 22, 2026 23:46
@hyperpolymath
hyperpolymath merged commit 682fc60 into main Sep 22, 2026
44 of 45 checks passed
@hyperpolymath
hyperpolymath deleted the fix/branch-gates-org-inherited branch September 22, 2026 23:47
@coderabbitai

coderabbitai Bot commented Sep 22, 2026

Copy link
Copy Markdown
Contributor

❌ Failed to create Coding Agent finishing-touch task. Please try again.

hyperpolymath added a commit that referenced this pull request Sep 22, 2026
…cure needs (#1029)

Two hardening follow-ups on #1027, which landed the org-inherited fix.

**1. `grep -P` is a GNU extension.** Classifying active branch rulesets
into
repo-level vs org-inherited is the load-bearing step of this script — it
is
what stops the PUT that 404'd 67 times. It must not depend on which
`grep`
the runner ships. `awk -F'\t'` does the same field test portably.

Mutant F is retargeted at the awk line, so the control still dies:
without
the ownership filter the applier becomes `DRIFT` and PUTs to
`repos/acme/org-inherited/rulesets/18225024` — the measured 404,
reproduced.

**2. The `ORG-INHERITED` detail named the endpoint but not the
credential.**
An org-level ruleset write needs `admin:org`. A repo-scoped token reads
the
inherited ruleset *in full* and cannot write it — that asymmetry is the
whole
bug. Saying so in the report line saves the operator a second 403 chase.

Suite: **46/46**, mutant F killed.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01Ji1bq3TypfycfUPAR7hSxR

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant