diff --git a/scripts/setup-pr-quality-ruleset.sh b/scripts/setup-pr-quality-ruleset.sh index 6c61b9bd..5caf3880 100755 --- a/scripts/setup-pr-quality-ruleset.sh +++ b/scripts/setup-pr-quality-ruleset.sh @@ -10,8 +10,8 @@ # What this script does: # 1. Checks whether the "pr-quality" ruleset already exists # 2. Creates it if missing, or updates it if present so drifted parameters -# (e.g. dismiss_stale_reviews_on_push) reconverge to the codified standard -# (idempotent — safe to re-run) +# (e.g. dismiss_stale_reviews_on_push, require_last_push_approval) +# reconverge to the codified standard (idempotent — safe to re-run) # # Prerequisites: gh (authenticated with repo admin rights) # Usage: @@ -79,7 +79,24 @@ JSON if [[ -n "$EXISTING_ID" ]]; then echo " ↻ '$RULESET_NAME' ruleset already exists (id: $EXISTING_ID) — updating to ensure compliance..." - printf '%s\n' "$RULESET_PAYLOAD" | gh api "repos/$REPO/rulesets/$EXISTING_ID" --method PUT --input - + # Fetch existing ruleset and preserve all fields not being updated + EXISTING_RULESET=$(gh api "repos/$REPO/rulesets/$EXISTING_ID") + # Merge new configuration into existing ruleset: preserve bypass_actors and other fields + UPDATE_PAYLOAD=$(printf '%s' "$RULESET_PAYLOAD" | jq --slurpfile existing <(echo "$EXISTING_RULESET") ' + . as $new + | $existing[0] + | .name = $new.name + | .target = $new.target + | .enforcement = $new.enforcement + | .conditions = $new.conditions + | .rules = $new.rules + ') + # Validate required fields are present + if ! printf '%s' "$UPDATE_PAYLOAD" | jq -e '.name and .target and .rules and (.rules | length > 0)' >/dev/null 2>&1; then + echo "Error: Invalid ruleset payload after merge (missing required fields)" >&2 + exit 1 + fi + printf '%s\n' "$UPDATE_PAYLOAD" | gh api "repos/$REPO/rulesets/$EXISTING_ID" --method PUT --input - echo " ✓ '$RULESET_NAME' ruleset updated successfully." echo "" echo "=== Done ===" diff --git a/scripts/tests/setup-pr-quality-ruleset.test.js b/scripts/tests/setup-pr-quality-ruleset.test.js index 589f4be5..4fa7ddfe 100644 --- a/scripts/tests/setup-pr-quality-ruleset.test.js +++ b/scripts/tests/setup-pr-quality-ruleset.test.js @@ -59,4 +59,34 @@ describe('setup-pr-quality-ruleset.sh codified ruleset', () => { const script = fs.readFileSync(scriptPath, 'utf8') expect(script).toMatch(/--method PUT/) }) + + it('script preserves existing bypass_actors when updating an existing ruleset', () => { + const scriptPath = path.join(__dirname, '..', 'setup-pr-quality-ruleset.sh') + const script = fs.readFileSync(scriptPath, 'utf8') + // The update path must fetch the existing ruleset and merge fields so a PUT + // preserves bypass_actors and other fields set by administrators. + expect(script).toMatch(/bypass_actors/) + expect(script).toMatch(/EXISTING_RULESET/) + }) + + it('script merges new configuration into existing ruleset preserving all fields', () => { + const scriptPath = path.join(__dirname, '..', 'setup-pr-quality-ruleset.sh') + const script = fs.readFileSync(scriptPath, 'utf8') + // The update flow must fetch the existing ruleset and selectively update + // fields (name, target, enforcement, conditions, rules) while preserving + // bypass_actors and other fields not in the new payload. + expect(script).toMatch(/EXISTING_RULESET=.*gh api.*rulesets.*EXISTING_ID/) + expect(script).toMatch(/slurpfile.*existing/) + // Verify field-by-field update pattern in jq to preserve all existing fields + expect(script).toMatch(/\.name = \$new\.name/) + expect(script).toMatch(/\.rules = \$new\.rules/) + expect(script).toMatch(/\$existing\[0\]/) + }) + + it('script validates merged payload before submitting', () => { + const scriptPath = path.join(__dirname, '..', 'setup-pr-quality-ruleset.sh') + const script = fs.readFileSync(scriptPath, 'utf8') + // Validation ensures the merged payload is well-formed and contains required fields + expect(script).toMatch(/jq -e.*\.name.*\.target.*\.rules/) + }) })