From 8d3f85ea6e569ab3aed291405de6b9ab8b1f1321 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Mon, 3 Aug 2026 16:05:16 -0700 Subject: [PATCH] Re-vendor the ruleset payloads and configure.sh after the promotion The hub promoted `develop` to `main` as `7194f91` while the previous two convergence pull requests were open, so the ground truth this repo is measured against moved under them. ProjectTemplate#545 takes `bypass_actors` out of all three payloads, and rewrites `configure.sh` around that decision. The rewrite is the substantive half. Apply now reads the live bypass list and writes it back unchanged, because a PUT replaces the whole document and omitting the field would delete the list rather than leave it alone, and it aborts rather than proceeding if that read fails. Check reports the live list and asserts nothing about it. Who may bypass a ruleset decides who the rules do not apply to, which is a decision for the maintainer rather than a value the fleet config hands out. ## What this deliberately does not do Remove the live bypass. Both rulesets here carry the `RepositoryRole` admin entry the old payload granted every repo, and nothing in this change touches it. Removing it is a protected-branch settings write and a decision the hub just declared human, so it is the maintainer's. Worth knowing that the two tools now disagree about that state. `configure.sh check ptr727/Blog release` exits 0 and reports the bypass as unmanaged, while `spec/audit.py` still compares `bypass_actors` and calls the same state a DEFECT on both branches. Raised as ProjectTemplate#554, since it makes every fleet repo carrying a bypass permanently not operational on a field the fleet decided not to manage. ## Verification - All four verbatim units byte-match hub `main` `7194f91` after LF normalization: the three re-vendored here plus `.markdownlint-cli2.jsonc`. - `repo-config/configure.sh check ptr727/Blog release` exits 0 under the rewritten check mode, and reports "bypass list: RepositoryRole 5 always (not managed by this script)" on both rulesets rather than asserting it. - The JSON and editorconfig gates are clean. Co-Authored-By: Claude Opus 5 (1M context) --- repo-config/configure.sh | 24 ++++++++++++++++++++++-- repo-config/develop.json | 7 ------- repo-config/main.json | 7 ------- 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/repo-config/configure.sh b/repo-config/configure.sh index 41a3a53..4842702 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 dcb0faf..16c89f4 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 9168c50..a99ed24 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": [],