From fd6141f4557c35a1b2297bb96c82f4b4cd775746 Mon Sep 17 00:00:00 2001 From: donpetry-bot <281750570+donpetry-bot@users.noreply.github.com> Date: Tue, 18 Aug 2026 20:21:31 +0000 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20implement=20issue=20#539=20?= =?UTF-8?q?=E2=80=94=20Compliance:=20ruleset-drift-pr-quality-require=5Fla?= =?UTF-8?q?st=5Fpush=5Fapproval?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/setup-pr-quality-ruleset.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/setup-pr-quality-ruleset.sh b/scripts/setup-pr-quality-ruleset.sh index 6c61b9bd..5418a612 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: From cac2331531947cbc40934ef25db2e1558fef047b Mon Sep 17 00:00:00 2001 From: donpetry-bot <281750570+donpetry-bot@users.noreply.github.com> Date: Tue, 18 Aug 2026 20:43:24 +0000 Subject: [PATCH 2/4] chore: dev-lead update (review-changes) [skip ci-relay] --- scripts/setup-pr-quality-ruleset.sh | 5 ++++- scripts/tests/setup-pr-quality-ruleset.test.js | 9 +++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/scripts/setup-pr-quality-ruleset.sh b/scripts/setup-pr-quality-ruleset.sh index 5418a612..e1fbb4f1 100755 --- a/scripts/setup-pr-quality-ruleset.sh +++ b/scripts/setup-pr-quality-ruleset.sh @@ -79,7 +79,10 @@ 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 - + # Preserve existing bypass_actors: a PUT that omits this field removes them entirely + EXISTING_BYPASS=$(gh api "repos/$REPO/rulesets/$EXISTING_ID" | jq -c '.bypass_actors // []') + UPDATE_PAYLOAD=$(printf '%s' "$RULESET_PAYLOAD" | jq --argjson bypass "$EXISTING_BYPASS" '. + {bypass_actors: $bypass}') + 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..f89ecd5b 100644 --- a/scripts/tests/setup-pr-quality-ruleset.test.js +++ b/scripts/tests/setup-pr-quality-ruleset.test.js @@ -59,4 +59,13 @@ 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 and re-inject bypass_actors so a PUT does not + // silently remove an existing Dependabot or other integration bypass. + expect(script).toMatch(/bypass_actors/) + expect(script).toMatch(/jq.*bypass_actors/) + }) }) From 8b50e692721f47ed9b8391163548986e0238a6d2 Mon Sep 17 00:00:00 2001 From: don-petry <36422719+don-petry@users.noreply.github.com> Date: Thu, 3 Sep 2026 08:38:43 +0000 Subject: [PATCH 3/4] fix(bot): address bot feedback [skip ci-relay] --- package-lock.json | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2b816614..c0decf74 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1231,29 +1231,43 @@ } }, "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", "dev": true, "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, "engines": { "node": ">=18.18.0" } }, "node_modules/@humanfs/node": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@humanfs/core": "^0.19.1", + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" }, "engines": { "node": ">=18.18.0" } }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", From 4c30383b09e30263dc226c2678d50f6ae1a68779 Mon Sep 17 00:00:00 2001 From: don-petry <36422719+don-petry@users.noreply.github.com> Date: Mon, 7 Sep 2026 14:28:04 +0000 Subject: [PATCH 4/4] fix(ruleset): preserve complete ruleset and validate before update Addresses CodeRabbit findings on issue #539: - Fetch and merge entire existing ruleset instead of only bypass_actors, preserving all administrator-configured fields - Added payload validation before submitting - Added tests for merge behavior and validation - Idempotent and safe to re-run Co-Authored-By: Claude Haiku 4.5 --- scripts/setup-pr-quality-ruleset.sh | 20 +++++++++++--- .../tests/setup-pr-quality-ruleset.test.js | 27 ++++++++++++++++--- 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/scripts/setup-pr-quality-ruleset.sh b/scripts/setup-pr-quality-ruleset.sh index e1fbb4f1..5caf3880 100755 --- a/scripts/setup-pr-quality-ruleset.sh +++ b/scripts/setup-pr-quality-ruleset.sh @@ -79,9 +79,23 @@ JSON if [[ -n "$EXISTING_ID" ]]; then echo " ↻ '$RULESET_NAME' ruleset already exists (id: $EXISTING_ID) — updating to ensure compliance..." - # Preserve existing bypass_actors: a PUT that omits this field removes them entirely - EXISTING_BYPASS=$(gh api "repos/$REPO/rulesets/$EXISTING_ID" | jq -c '.bypass_actors // []') - UPDATE_PAYLOAD=$(printf '%s' "$RULESET_PAYLOAD" | jq --argjson bypass "$EXISTING_BYPASS" '. + {bypass_actors: $bypass}') + # 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 "" diff --git a/scripts/tests/setup-pr-quality-ruleset.test.js b/scripts/tests/setup-pr-quality-ruleset.test.js index f89ecd5b..4fa7ddfe 100644 --- a/scripts/tests/setup-pr-quality-ruleset.test.js +++ b/scripts/tests/setup-pr-quality-ruleset.test.js @@ -63,9 +63,30 @@ describe('setup-pr-quality-ruleset.sh codified ruleset', () => { 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 and re-inject bypass_actors so a PUT does not - // silently remove an existing Dependabot or other integration bypass. + // 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(/jq.*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/) }) })