From 105ada437164a650b007d7c2647bbe3ff725b3ac Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Mon, 3 Aug 2026 15:14:41 -0700 Subject: [PATCH] Take the ruleset bypass list out of the fleet config entirely Who may bypass a ruleset decides who the rules do not apply to, so it is a human decision taken per repository rather than a value the fleet config hands out. The three payloads declared RepositoryRole 5 with bypass_mode always, and applying them granted repository admins a standing exemption on main and develop across every repo. The docs are explicit that a ruleset applies to administrators by default and that the bypass list starts empty, so the declaration was a real grant rather than a restatement of an inherent privilege. Whether these repos first got it from the config or from earlier hand-made rulesets is unknown, and it does not matter: the config should not be the thing maintaining it either way. Removing the declaration alone would have made this worse. apply sends the payload as a PUT, which replaces the whole document, so a payload with no bypass_actors would clear the live list on every run. That is code deleting a bypass, which is the one thing that must never happen here. apply now reads the live list and writes it back unchanged, and aborts rather than proceeding if that read fails, since applying without it would silently clear what it was meant to preserve. On create there is no live list to read and none is sent, so a new ruleset starts at GitHub's own empty default. check reports the list on every run and asserts nothing about it, because no payload declares a value to compare against and inventing one would put code back in charge of the decision. Verified by composing the PUT body against this repo's live ruleset without sending it: the payload declares nothing, the live list is RepositoryRole 5 always, and the composed body carries that same list back. Co-Authored-By: Claude Opus 5 (1M context) --- repo-config/configure.sh | 24 ++++++++++++++++++++++-- repo-config/develop.json | 7 ------- repo-config/main.json | 7 ------- repo-config/operational/develop.json | 7 ------- 4 files changed, 22 insertions(+), 23 deletions(-) diff --git a/repo-config/configure.sh b/repo-config/configure.sh index 41a3a535..4842702e 100755 --- a/repo-config/configure.sh +++ b/repo-config/configure.sh @@ -111,7 +111,7 @@ ruleset_id() { # =============================== apply =============================== apply_ruleset() { # payload-file - create-or-update the ruleset by name - local file="$1" rname id + local file="$1" rname id live_bypass body if [ ! -e "$file" ]; then echo "Ruleset payload $file not found. Aborting to avoid a partially-applied configuration." >&2 exit 1 @@ -124,9 +124,23 @@ apply_ruleset() { # payload-file - create-or-update the ruleset by name id="$(ruleset_id "$rname")" if [ -n "$id" ]; then echo "Updating ruleset '$rname' (id $id) on $repo" - gh api --method PUT "repos/$repo/rulesets/$id" --input "$file" >/dev/null + # The bypass list is a human decision, so this script neither grants nor revokes it. + # A PUT replaces the whole document, so omitting the field would delete the live list rather than leave it alone. + # The live value is therefore read and written back unchanged, which is what "hands off" has to mean against a replacing API. + # A read failure aborts rather than proceeding, since applying without it would silently clear the list. + if ! live_bypass="$(gh api "repos/$repo/rulesets/$id" --jq '.bypass_actors // []')"; then + echo "Could not read the live bypass list for ruleset '$rname' on $repo. Aborting rather than applying a payload that would clear it." >&2 + exit 1 + fi + if ! body="$(jq --argjson b "$live_bypass" '.bypass_actors = $b' "$file")"; then + echo "Could not compose the ruleset payload for '$rname'. Aborting." >&2 + exit 1 + fi + gh api --method PUT "repos/$repo/rulesets/$id" --input - <<<"$body" >/dev/null else echo "Creating ruleset '$rname' on $repo" + # No bypass list is sent on create, so a new ruleset starts with GitHub's own empty default. + # Nothing is deleted here, because nothing existed to delete. gh api --method POST "repos/$repo/rulesets" --input "$file" >/dev/null fi } @@ -210,6 +224,12 @@ check_ruleset() { # payload-file - the live ruleset must match the committed pol if [ -z "$want_types" ]; then fail "ruleset payload $file declares no rules"; return; fi got_types="$(jq -r '[.rules[].type] | sort | join(",")' <<<"$live")" assert "'$rname' rule set = $want_types" test "$got_types" = "$want_types" + # The bypass list is reported and never asserted, because no payload declares one. + # Who may bypass a ruleset is a human decision taken in the UI, so code states what is there and judges nothing. + # It is surfaced on every run rather than left invisible, since it is the field that decides who the rules do not apply to. + local bypass + bypass="$(jq -r '[.bypass_actors[]? | "\(.actor_type) \(.actor_id) \(.bypass_mode)"] | join("; ")' <<<"$live")" + note "ruleset '$rname' bypass list: ${bypass:-none} (not managed by this script)" # Every parameterized rule is compared on its whole parameters object rather than on selected fields. # Naming fields one at a time meant a payload could declare a parameter the check never read. # Review-thread resolution, stale-review dismissal, and the status-check policy flags all went unverified that way. diff --git a/repo-config/develop.json b/repo-config/develop.json index efc4262b..d34d9f8f 100644 --- a/repo-config/develop.json +++ b/repo-config/develop.json @@ -1,11 +1,4 @@ { - "bypass_actors": [ - { - "actor_id": 5, - "actor_type": "RepositoryRole", - "bypass_mode": "always" - } - ], "conditions": { "ref_name": { "exclude": [], diff --git a/repo-config/main.json b/repo-config/main.json index 5d8d7b2d..57bd647d 100644 --- a/repo-config/main.json +++ b/repo-config/main.json @@ -1,11 +1,4 @@ { - "bypass_actors": [ - { - "actor_id": 5, - "actor_type": "RepositoryRole", - "bypass_mode": "always" - } - ], "conditions": { "ref_name": { "exclude": [], diff --git a/repo-config/operational/develop.json b/repo-config/operational/develop.json index 0d16930e..9cab7415 100644 --- a/repo-config/operational/develop.json +++ b/repo-config/operational/develop.json @@ -1,11 +1,4 @@ { - "bypass_actors": [ - { - "actor_id": 5, - "actor_type": "RepositoryRole", - "bypass_mode": "always" - } - ], "conditions": { "ref_name": { "exclude": [],