diff --git a/.github/scripts/tests/apply-repo-settings-workflow.bats b/.github/scripts/tests/apply-repo-settings-workflow.bats index 28d8eb2a..446183e8 100644 --- a/.github/scripts/tests/apply-repo-settings-workflow.bats +++ b/.github/scripts/tests/apply-repo-settings-workflow.bats @@ -2,12 +2,16 @@ # Tests for .github/workflows/apply-repo-settings.yml — static YAML assertions, # no live API calls. # +# This workflow is now a THIN CALLER STUB synced from the org source of truth +# (petry-projects/.github/standards/workflows/apply-repo-settings.yml). All +# settings/ruleset logic — including the #420 GH_TOKEN preflight guard — moved +# into the org reusable workflow (apply-repo-settings-reusable.yml); the consumer +# repo carries no script copy and the caller job has no steps. These tests were +# rewritten to pin the stub contract instead of the retired inline job. +# # Regression guard for issue #403 (ruleset-drift-pr-quality-require_last_push_approval): -# the codified pr-quality ruleset already sets require_last_push_approval: true, but the -# live ruleset kept drifting back to false because apply-repo-settings.yml only re-applied -# the codified standard on manual dispatch or on a push touching the ruleset scripts. -# A scheduled trigger makes the codified rulesets self-heal drift automatically. These -# tests pin that the schedule trigger exists and that the pre-existing triggers survive. +# a scheduled trigger makes the codified rulesets self-heal live drift automatically. +# The schedule/workflow_dispatch guards below survive the migration to the stub. WORKFLOW="$(cd "$(dirname "$BATS_TEST_FILENAME")/../../.." && pwd)/.github/workflows/apply-repo-settings.yml" @@ -36,52 +40,50 @@ WORKFLOW="$(cd "$(dirname "$BATS_TEST_FILENAME")/../../.." && pwd)/.github/workf [ "$output" = "true" ] } -@test "workflow preserves the push trigger on main touching the ruleset scripts" { +@test "workflow preserves the push trigger on main touching the stub itself" { run yq '.on.push.branches[0]' "$WORKFLOW" [ "$status" -eq 0 ] [ "$output" = "main" ] - run yq '.on.push.paths | contains([".github/scripts/apply-pr-quality-ruleset.sh"])' "$WORKFLOW" + # The stub carries no script copy, so it re-applies on a push to its own file. + run yq '.on.push.paths | contains([".github/workflows/apply-repo-settings.yml"])' "$WORKFLOW" [ "$status" -eq 0 ] [ "$output" = "true" ] } -# Regression guard for issue #420 (ruleset-drift-pr-quality-require_last_push_approval -# recurred after #403). The #403 schedule cannot self-heal drift because every run — -# including the scheduled one — fails immediately with "GH_TOKEN is required": the -# GH_TOKEN_ADMIN secret is not configured, and the failure was buried in the script -# log where nobody noticed. These tests pin a preflight step that surfaces the missing -# secret loudly (GitHub error annotation + job summary) with actionable remediation, so -# the operational blocker is discoverable instead of silently recurring. +# Thin-caller-stub contract: the job delegates to the org reusable workflow and +# carries no inline steps of its own (settings/ruleset logic lives upstream). -@test "workflow has a preflight step that verifies GH_TOKEN_ADMIN before applying (issue #420)" { - run yq '[.jobs["apply-settings"].steps[] | select(.env.GH_TOKEN_ADMIN != null and ((.run // "") | test("GH_TOKEN_ADMIN")))] | length' "$WORKFLOW" +@test "apply job delegates to the org reusable workflow via uses:" { + run yq '.jobs.apply.uses' "$WORKFLOW" [ "$status" -eq 0 ] - [ "$output" -ge 1 ] + [ "$output" != "null" ] + printf '%s\n' "$output" | grep -q 'petry-projects/.github/.github/workflows/apply-repo-settings-reusable.yml@' +} + +@test "apply job carries no inline steps (logic lives in the reusable)" { + run yq '.jobs.apply | has("steps")' "$WORKFLOW" + [ "$status" -eq 0 ] + [ "$output" = "false" ] } -@test "preflight step surfaces the missing secret loudly and actionably (issue #420)" { - run yq '.jobs["apply-settings"].steps[] | select(.env.GH_TOKEN_ADMIN != null and ((.run // "") | test("GH_TOKEN_ADMIN"))) | .run' "$WORKFLOW" +@test "apply job inherits secrets so the reusable gets the admin token" { + run yq '.jobs.apply.secrets' "$WORKFLOW" [ "$status" -eq 0 ] - local preflight="$output" - # emits a GitHub error annotation so the failure is visible on the run page - printf '%s\n' "$preflight" | grep -q '::error' - # writes actionable remediation to the job summary - printf '%s\n' "$preflight" | grep -q 'GITHUB_STEP_SUMMARY' - # fails the job so the drift is not silently left unhealed - printf '%s\n' "$preflight" | grep -q 'exit 1' + [ "$output" = "inherit" ] } -@test "preflight runs before the apply-repo-settings step (issue #420)" { - run yq '.jobs["apply-settings"].steps | to_entries | map(select(.value.env.GH_TOKEN_ADMIN != null and ((.value.run // "") | test("GH_TOKEN_ADMIN")))) | .[0].key' "$WORKFLOW" +@test "checkout_ref forward stays in lockstep with the uses: channel pin" { + run yq '.jobs.apply.with.checkout_ref' "$WORKFLOW" [ "$status" -eq 0 ] - local preflight_idx="$output" + local checkout_ref="$output" + [ "$checkout_ref" != "null" ] + [ -n "$checkout_ref" ] - run yq '.jobs["apply-settings"].steps | to_entries | map(select((.value.run // "") | test("apply-repo-settings.sh"))) | .[0].key' "$WORKFLOW" + run yq '.jobs.apply.uses' "$WORKFLOW" [ "$status" -eq 0 ] - local apply_idx="$output" + local uses="$output" - [ -n "$preflight_idx" ] && [ "$preflight_idx" != "null" ] - [ -n "$apply_idx" ] && [ "$apply_idx" != "null" ] - [ "$preflight_idx" -lt "$apply_idx" ] + # `uses:` ends with @; checkout_ref must equal that same tag. + [ "${uses##*@}" = "$checkout_ref" ] } diff --git a/.github/workflows/apply-repo-settings.yml b/.github/workflows/apply-repo-settings.yml index 0c39f617..4c983ba3 100644 --- a/.github/workflows/apply-repo-settings.yml +++ b/.github/workflows/apply-repo-settings.yml @@ -1,71 +1,55 @@ -# Apply repository settings — disables check-suite auto-triggers (including -# Claude app 1236702) that permanently block auto-merge, and enforces all other -# required settings from standards/github-settings.md. +# ───────────────────────────────────────────────────────────────────────────── +# SOURCE OF TRUTH: petry-projects/.github/standards/workflows/apply-repo-settings.yml +# Standard: petry-projects/.github/standards/github-settings.md +# petry-projects/.github/standards/ruleset-remediation-runbook.md +# Reusable: petry-projects/.github/.github/workflows/apply-repo-settings-reusable.yml # -# Triggers: -# • workflow_dispatch — manual run to fix current state immediately -# • push to main on the settings script — re-applies on every future change -# • schedule — weekly re-apply so codified rulesets self-heal live drift. -# Without this, a ruleset that is manually changed in the GitHub UI (e.g. -# pr-quality's require_last_push_approval flipped to false) stays drifted -# until someone edits a script path, which is why compliance drift findings -# recurred (#325 → #403). The apply scripts are idempotent, so re-running is -# safe. +# AGENTS — READ BEFORE EDITING: +# • This file is a THIN CALLER STUB. All settings/ruleset logic lives in the +# reusable workflow above; consumer repos carry NO script copy. +# • You MAY change: the cron offset only (stagger org API load). +# • You MUST NOT change: the `uses:` line, `secrets: inherit`, the job-level +# `permissions:` block, or the `checkout_ref` forward — reusable workflows can +# be granted no more permissions than the calling job has, and forwarding an +# input the pinned channel does not declare breaks every run at startup (#1034). +# • If you need different behaviour, open a PR against the reusable in the +# central repo. +# ───────────────────────────────────────────────────────────────────────────── # -# Required secret: GH_TOKEN_ADMIN — a classic PAT or fine-grained token with -# administration:write (repo admin) scope for petry-projects/markets. +# Apply repo settings — thin caller for the org-level compliance self-heal. +# To adopt: copy this file verbatim to .github/workflows/apply-repo-settings.yml. +# Required org secret (inherited): +# GH_PAT_DON_PETRY — classic PAT, repo scope, owner has admin on this repo. name: Apply repo settings on: - workflow_dispatch: schedule: - - cron: "23 5 * * 1" + # Weekly self-heal. The cron offset MAY be changed to stagger org API load. + - cron: '17 6 * * 1' + workflow_dispatch: + inputs: + dry_run: + description: "Preview changes without applying" + type: boolean + required: false + default: false push: branches: [main] 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 + - '.github/workflows/apply-repo-settings.yml' permissions: {} +concurrency: + group: apply-repo-settings + cancel-in-progress: false + jobs: - apply-settings: - name: Apply repo settings - runs-on: ubuntu-latest + apply: permissions: contents: read - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - # Preflight — without GH_TOKEN_ADMIN the apply step dies with a generic - # "GH_TOKEN is required" buried in the script log, so the weekly self-heal - # fails unnoticed and ruleset drift recurs (#325 → #403 → #420). Surface the - # missing secret loudly (error annotation + job summary) so it gets fixed. - - name: Verify admin token is configured - env: - GH_TOKEN_ADMIN: ${{ secrets.GH_TOKEN_ADMIN }} - run: | - if [ -z "${GH_TOKEN_ADMIN}" ]; then - { - echo "## Apply repo settings cannot run" - echo "" - echo "The \`GH_TOKEN_ADMIN\` secret is not configured for this repository." - echo "Without it the pr-quality / code-quality rulesets cannot self-heal, so" - echo "compliance drift (e.g. \`require_last_push_approval\` flipping to false)" - echo "recurs unnoticed." - echo "" - echo "**Fix:** provision a repo/org admin token secret named \`GH_TOKEN_ADMIN\`" - echo "with \`administration:write\` scope (see standards/github-settings.md)," - echo "then re-run this workflow." - } >> "$GITHUB_STEP_SUMMARY" - echo "::error title=GH_TOKEN_ADMIN missing::Provision the GH_TOKEN_ADMIN secret so ruleset drift can self-heal (see job summary)." - exit 1 - fi - echo "GH_TOKEN_ADMIN is configured." - - - name: Apply repo settings - env: - GH_TOKEN: ${{ secrets.GH_TOKEN_ADMIN }} - run: bash .github/scripts/apply-repo-settings.sh + uses: petry-projects/.github/.github/workflows/apply-repo-settings-reusable.yml@apply-repo-settings/v1-ring1 # NOSONAR(githubactions:S7637) first-party channel ref + with: + dry_run: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run == true }} + checkout_ref: apply-repo-settings/v1-ring1 # keep in lockstep with the uses: channel pin + secrets: inherit # NOSONAR(githubactions:S7635) first-party trusted reusable