Skip to content

feat(rulesets): add apply-branch-gates.sh, the branch-gates applier gates.json specified but nothing performed - #1011

Merged
hyperpolymath merged 1 commit into
mainfrom
feat/branch-gates-applier
Sep 22, 2026
Merged

hyperpolymath merged 1 commit into
mainfrom
feat/branch-gates-applier

Conversation

@hyperpolymath

Copy link
Copy Markdown
Owner

The applier gates.json specified and nothing performed

config/rulesets/README.adoc has said this outright since it was written:

the propagation mechanism is still missing by design, and this note is the
reminder that a template fix without an applier is a half fix.

That was literally true. Nothing in this repository read
config/rulesets/gates.json as data — every reference is prose, or
tests/test_governance_reusable_shape.sh asserting the file's contents.
gates.json specified a context derivation that no code performed, so
required_status_checks was never propagated anywhere.
apply-tag-ruleset-canon.sh is the tag applier; this is its branch sibling.

Closes the D76 / #956 workstream's missing component.

Contexts are derived, never typed

Per gates.json, contexts come from the check names the latest default-branch
run of each gate workflow actually emitted:

GET /repos/{o}/{r}/actions/workflows/{file}/runs?branch=<default>&per_page=1
GET /repos/{o}/{r}/actions/runs/{id}/jobs

A hand-typed context that nothing emits is a phantom: it can never turn
green, so it blocks the branch permanently.

The refusal that matters

Zero derivable contexts → report UNGATED, write nothing. A
required_status_checks rule carrying an empty list is a vacuous gate: it
reports "protected" in every summary view while requiring nothing. That is
strictly worse than having no rule at all, because it is indistinguishable from
a working one.

Exactness guard

A ruleset PUT replaces the whole object. The planned body — normalised
with the required_status_checks rule removed from both sides — must be
byte-identical to the source. Anything else moved is a refusal. Without this,
one jq slip strips required_signatures from every repo it touches, silently.

Deliberate non-actions

Situation Behaviour Why
No active branch ruleset NORULESET, reported Creating branch protection where none exists is a policy act, not a gate-fill
Retired rule types present Reported, not removed The estate census was ruled report-only; --strip-retired opts in
Two active branch rulesets AMBIGUOUS, fail closed Rulesets are additive — filling one of a pair leaves the other enforcing, and the repo stays blocked by a rule nothing announced
Gate workflow with no runs yet Omitted, named in no_run=[…] Never require a context nothing has emitted

--require-green N

Implements the standing ruling on #956, "require the reliably-green set". A
context that is currently red becomes a merge deadlock the instant it is
required. skipped and neutral count as green — GitHub treats both as
satisfying a required status check.

Measured on this repository, over the last 10 default-branch runs:

$ ./scripts/apply-branch-gates.sh --repo hyperpolymath/standards --require-green 10

hyperpolymath/standards  WOULD-GATE  branch=main gate_files=5 contexts=18
  not_green=[governance / Validate Hypatia Baseline]
  excluded=[governance / Allowlist Preflight,
            governance / Code quality + docs,
            governance / Live Actions policy (credentialed advisory)]

All three exclusions matched after the " / " — the reusable-job-name form
of never_required_contexts — proving that branch against the live API and not
only against a fixture. The one dropped context is the known-red Hypatia
baseline gate.

Tests — a passing suite would not have been evidence

scripts/tests/branch-gates-apply-test.sh runs 18 controls, two of which
are mutants that must go red:

Mutant Change Result
A delete the zero-context refusal applier writes the vacuous empty-list rule — verified by inspecting the captured PUT body. Killed.
B make the body-builder also drop required_signatures exactness guard refuses. Killed.

The suite drives the applier through a gh shim, so it needs no network and
no credentials
.

Two defects were found by these controls rather than in review:

  1. The --strip-retired jq filter used index(.type), where jq rebinds .
    to the array, so .type was always null and the filter errored. This is
    the same class of bug scripts/tests/apply-baseline-test.sh was written to
    pin — its header documents .file_pattern inside test(...) failing
    identically.
  2. The exactness guard rejected its own honest control, because adding the
    required_status_checks rule legitimately changes the rules array's length.

Both files are committed 100755 — a suite committed 0644 passes every
local run and dies in CI at exit 126 before a single control executes.

Not done here

Applying it to standards/main is a separate change: it converts
main from zero required checks to 18, which is a merge-policy decision for the
owner, not a side effect of landing a script. The ruleset that would be written
is 23787415, the repo's one active branch ruleset.

Verification

bash scripts/tests/branch-gates-apply-test.sh     # 18 passed, 0 failed
./scripts/apply-branch-gates.sh --repo hyperpolymath/standards --require-green 10
git ls-files -s scripts/apply-branch-gates.sh scripts/tests/branch-gates-apply-test.sh   # both 100755

🤖 Generated with Claude Code

https://claude.ai/code/session_01Ji1bq3TypfycfUPAR7hSxR

…cified but nothing performed

config/rulesets/README.adoc has said so since it was written: "the propagation
mechanism is still missing by design, and this note is the reminder that a
template fix without an applier is a half fix." That was literally true —
nothing in this repository read config/rulesets/gates.json as DATA. Every
reference was prose, or tests/test_governance_reusable_shape.sh asserting the
file's contents. gates.json specified a context derivation that no code
performed, so required_status_checks was never propagated to any repository.
apply-tag-ruleset-canon.sh is the TAG applier; this is its branch sibling.

CONTEXTS ARE DERIVED, NEVER TYPED
  Per gates.json, contexts come from the check names the latest default-branch
  run of each gate workflow actually emitted, via
    /actions/workflows/{file}/runs?branch=<default>&per_page=1
    /actions/runs/{id}/jobs
  A hand-typed context that nothing emits is a PHANTOM: it can never turn
  green, so it blocks the branch permanently.

THE REFUSAL THAT MATTERS
  Zero derivable contexts => report UNGATED and write NOTHING. A
  required_status_checks rule carrying an empty list is a VACUOUS GATE: it
  reports "protected" in every summary view while requiring nothing, which is
  strictly worse than having no rule, because it is indistinguishable from a
  working one.

EXACTNESS GUARD
  A ruleset PUT replaces the whole object. The planned body, normalised with
  the required_status_checks rule removed from both sides, must be
  byte-identical to the source; anything else moved is a refusal. Without it,
  one jq slip strips required_signatures from every repo it touches, silently.

DELIBERATE NON-ACTIONS
  * never creates a ruleset (NORULESET is reported, not repaired) — creating
    branch protection where none exists is a policy act, not a gate-fill;
  * never removes the four retired rule types unless --strip-retired is passed
    — the estate census was ruled report-only;
  * two active branch rulesets => AMBIGUOUS, fail closed. Rulesets are
    ADDITIVE: filling one of a pair leaves the other enforcing and the repo
    stays blocked by a rule nothing announced.

--require-green N implements the standing ruling on #956, "require the
reliably-green set": a context that is currently red becomes a merge deadlock
the instant it is required. `skipped` and `neutral` count as green, because
GitHub treats both as satisfying a required status check.

TESTS — a passing suite would not have been evidence
  scripts/tests/branch-gates-apply-test.sh runs 18 controls, two of which are
  MUTANTS that must go red:
    A. delete the zero-context refusal -> the applier writes the vacuous
       empty-list rule (verified by inspecting the captured PUT body);
    B. make the body-builder also drop required_signatures -> the exactness
       guard refuses.
  Both die. The suite exercises the applier through a `gh` shim, so it needs
  no network and no credentials.

Two defects were found by these controls rather than in review: the
--strip-retired jq filter used `index(.type)`, where jq rebinds `.` to the
array so `.type` was always null (the same class of bug apply-baseline-test.sh
was written to pin), and the exactness guard rejected its own honest control
because ADDING the required_status_checks rule legitimately changes the rules
array length.

Both files are committed 100755 — a suite committed 0644 passes every local
run and dies in CI at exit 126 before a single control executes.

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

Warning

Review limit reached

Next included review available in 23 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 5a7284ea-8d86-4c9a-b1e6-7aa09f64dc3e

📥 Commits

Reviewing files that changed from the base of the PR and between a55935e and 80c8c95.

📒 Files selected for processing (2)
  • scripts/apply-branch-gates.sh
  • scripts/tests/branch-gates-apply-test.sh

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 merged commit faed4dd into main Sep 22, 2026
45 checks passed
@hyperpolymath

Copy link
Copy Markdown
Owner Author

@coderabbitai review

The first attempt hit the review-capacity limit and posted the rate-limit banner, so this PR currently has a green CodeRabbit check with zero review comments — i.e. "did not look", not "found nothing". Requesting the real pass before merge.

Worth a close look: scripts/apply-branch-gates.sh performs PUT writes to GitHub branch rulesets. Its safety properties are (a) --apply defaults off, (b) a zero-derived-context repo is refused rather than written with an empty required-checks list (a vacuous gate), and (c) an exactness guard that removes the intentionally-changed required_status_checks rule from both sides and refuses the write unless everything else is byte-identical.

@coderabbitai

coderabbitai Bot commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

@hyperpolymath, I will run a new review of #1011.

I will inspect scripts/apply-branch-gates.sh with focus on default dry-run behavior, zero-context refusal, and preservation of all non-required_status_checks ruleset data during PUT requests.

⚠️ Action not completed

Pull request is closed.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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