diff --git a/.github/scripts/apply-pr-quality-ruleset.sh b/.github/scripts/apply-pr-quality-ruleset.sh new file mode 100755 index 00000000..6d038966 --- /dev/null +++ b/.github/scripts/apply-pr-quality-ruleset.sh @@ -0,0 +1,102 @@ +#!/usr/bin/env bash +# apply-pr-quality-ruleset.sh — Idempotently create/update the pr-quality ruleset +# +# This script creates (or updates) the `pr-quality` repository ruleset that +# enforces pull-request quality gates on the default branch of +# petry-projects/markets. +# +# Pull-request rule parameters configured (codified standard, source of truth: +# petry-projects/.github/standards/rulesets/pr-quality.json — see #324): +# - required_approving_review_count: 1 +# - require_code_owner_review: true +# - required_review_thread_resolution: true +# - dismiss_stale_reviews_on_push: true (the drifted parameter — issue #324) +# - require_last_push_approval: true +# - allowed_merge_methods: ["squash"] +# +# Standard reference: +# https://github.com/petry-projects/.github/blob/main/standards/github-settings.md#pr-quality--standard-ruleset-all-repositories +# +# Usage: +# GH_TOKEN= bash .github/scripts/apply-pr-quality-ruleset.sh +# +# The org-level script (petry-projects/.github/scripts/apply-rulesets.sh) is the +# canonical tool for managing rulesets across all repos. This script exists as a +# repo-local reference and fallback for the markets repository specifically. + +set -euo pipefail + +REPO="petry-projects/markets" +RULESET_NAME="pr-quality" + +if [ -z "${GH_TOKEN:-}" ]; then + echo "ERROR: GH_TOKEN is required with administration:write scope" >&2 + exit 1 +fi + +export GH_TOKEN + +# Fetch existing rulesets +EXISTING_ID=$(gh api "repos/$REPO/rulesets" \ + --jq ".[] | select(.name == \"$RULESET_NAME\") | .id") + +PAYLOAD=$(jq -n '{ + name: "pr-quality", + target: "branch", + enforcement: "active", + conditions: { + ref_name: { + include: ["~DEFAULT_BRANCH"], + exclude: [] + } + }, + rules: [ + { + type: "pull_request", + parameters: { + required_approving_review_count: 1, + require_code_owner_review: true, + required_review_thread_resolution: true, + dismiss_stale_reviews_on_push: true, + require_last_push_approval: true, + allowed_merge_methods: ["squash"] + } + } + ], + bypass_actors: [ + { + actor_type: "OrganizationAdmin", + bypass_mode: "always" + }, + { + actor_id: 3167543, + actor_type: "Integration", + bypass_mode: "always" + } + ] +}') + +if [ -n "$EXISTING_ID" ]; then + echo "Updating existing $RULESET_NAME ruleset (id=$EXISTING_ID) ..." + echo "$PAYLOAD" | gh api -X PUT "repos/$REPO/rulesets/$EXISTING_ID" --input - > /dev/null + echo "Done — ruleset updated: https://github.com/$REPO/rules/$EXISTING_ID" +else + echo "Creating $RULESET_NAME ruleset ..." + # Capture stderr so a 422 conflict from concurrent creation doesn't terminate the script. + # On conflict, re-fetch the ID created by the concurrent run and update instead. + if RESULT=$(echo "$PAYLOAD" | gh api -X POST "repos/$REPO/rulesets" --input - 2>&1); then + NEW_ID=$(echo "$RESULT" | jq -r '.id') + echo "Done — ruleset created: https://github.com/$REPO/rules/$NEW_ID" + else + EXISTING_ID=$(gh api "repos/$REPO/rulesets" \ + --jq ".[] | select(.name == \"$RULESET_NAME\") | .id") + if [ -z "$EXISTING_ID" ]; then + echo "ERROR: POST failed and no existing $RULESET_NAME ruleset found" >&2 + echo "$RESULT" >&2 + exit 1 + fi + echo "Concurrent creation detected — updating $RULESET_NAME ruleset (id=$EXISTING_ID) ..." + echo "$PAYLOAD" | gh api -X PUT "repos/$REPO/rulesets/$EXISTING_ID" --input - > /dev/null + echo "Done — ruleset updated: https://github.com/$REPO/rules/$EXISTING_ID" + fi +fi diff --git a/.github/scripts/apply-repo-settings.sh b/.github/scripts/apply-repo-settings.sh index c15cfbdf..773bac90 100755 --- a/.github/scripts/apply-repo-settings.sh +++ b/.github/scripts/apply-repo-settings.sh @@ -61,4 +61,8 @@ echo "Applying code-quality required-status-checks ruleset ..." "$SCRIPT_DIR/apply-code-quality-ruleset.sh" +echo "Applying pr-quality required-review ruleset ..." + +"$SCRIPT_DIR/apply-pr-quality-ruleset.sh" + echo "Done — repository settings applied: https://github.com/$REPO/settings" diff --git a/.github/scripts/tests/apply-pr-quality-ruleset.bats b/.github/scripts/tests/apply-pr-quality-ruleset.bats new file mode 100644 index 00000000..ca67c5fe --- /dev/null +++ b/.github/scripts/tests/apply-pr-quality-ruleset.bats @@ -0,0 +1,49 @@ +#!/usr/bin/env bats +# Tests for apply-pr-quality-ruleset.sh — static content assertions, no live API calls. +# +# The pull_request rule parameters asserted below are the codified standard +# (petry-projects/.github/standards/rulesets/pr-quality.json), the source of +# truth referenced by issue #324. + +SCRIPT="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)/apply-pr-quality-ruleset.sh" + +@test "script exists and is executable" { + [ -f "$SCRIPT" ] + [ -x "$SCRIPT" ] +} + +@test "script uses set -euo pipefail" { + grep -q 'set -euo pipefail' "$SCRIPT" +} + +@test "script targets petry-projects/markets repo" { + grep -qE '^REPO="petry-projects/markets"$' "$SCRIPT" +} + +@test "script declares the pr-quality ruleset" { + grep -qE '^[[:space:]]+name[[:space:]]*:[[:space:]]*"pr-quality"' "$SCRIPT" +} + +@test "script sets dismiss_stale_reviews_on_push to true (issue #324)" { + grep -qE '^[[:space:]]+dismiss_stale_reviews_on_push[[:space:]]*:[[:space:]]*true[[:space:]]*,?[[:space:]]*$' "$SCRIPT" +} + +@test "script requires one approving review" { + grep -qE '^[[:space:]]+required_approving_review_count[[:space:]]*:[[:space:]]*1[[:space:]]*,?[[:space:]]*$' "$SCRIPT" +} + +@test "script requires code owner review" { + grep -qE '^[[:space:]]+require_code_owner_review[[:space:]]*:[[:space:]]*true[[:space:]]*,?[[:space:]]*$' "$SCRIPT" +} + +@test "script requires review thread resolution" { + grep -qE '^[[:space:]]+required_review_thread_resolution[[:space:]]*:[[:space:]]*true[[:space:]]*,?[[:space:]]*$' "$SCRIPT" +} + +@test "script requires last push approval" { + grep -qE '^[[:space:]]+require_last_push_approval[[:space:]]*:[[:space:]]*true[[:space:]]*,?[[:space:]]*$' "$SCRIPT" +} + +@test "script allows only the squash merge method" { + grep -qE '^[[:space:]]+allowed_merge_methods[[:space:]]*:[[:space:]]*\[[[:space:]]*"squash"[[:space:]]*\][[:space:]]*$' "$SCRIPT" +} diff --git a/.github/scripts/tests/apply-repo-settings.bats b/.github/scripts/tests/apply-repo-settings.bats index c9aa4156..6ab7c21c 100755 --- a/.github/scripts/tests/apply-repo-settings.bats +++ b/.github/scripts/tests/apply-repo-settings.bats @@ -33,3 +33,7 @@ SCRIPT="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)/apply-repo-settings.s @test "script wires in code-quality ruleset application (issue #375)" { grep -q 'apply-code-quality-ruleset.sh' "$SCRIPT" } + +@test "script wires in pr-quality ruleset application (issue #324)" { + grep -q 'apply-pr-quality-ruleset.sh' "$SCRIPT" +} diff --git a/.github/workflows/apply-repo-settings.yml b/.github/workflows/apply-repo-settings.yml index 51d95e01..3f462fa4 100644 --- a/.github/workflows/apply-repo-settings.yml +++ b/.github/workflows/apply-repo-settings.yml @@ -17,6 +17,7 @@ on: paths: - .github/scripts/apply-repo-settings.sh - .github/scripts/apply-code-quality-ruleset.sh + - .github/scripts/apply-pr-quality-ruleset.sh - .github/workflows/apply-repo-settings.yml permissions: {}