From 787c5465b92d8b32b043cc875696c935c0559ec2 Mon Sep 17 00:00:00 2001 From: Don Petry <36422719+don-petry@users.noreply.github.com> Date: Mon, 18 May 2026 18:23:17 -0500 Subject: [PATCH 01/11] feat: add pr-auto-review reusable workflow Fires a review-agent dispatch when a PR meets all readiness criteria: - All CI checks passing - No CHANGES_REQUESTED reviews - No unresolved review threads Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/pr-auto-review-reusable.yml | 167 ++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 .github/workflows/pr-auto-review-reusable.yml diff --git a/.github/workflows/pr-auto-review-reusable.yml b/.github/workflows/pr-auto-review-reusable.yml new file mode 100644 index 0000000..3d61b9c --- /dev/null +++ b/.github/workflows/pr-auto-review-reusable.yml @@ -0,0 +1,167 @@ +# Reusable PR Auto-Review — Ready Check workflow. +# Single source of truth for the org: all readiness-gate logic lives here. +# Repo-level pr-auto-review.yml files are thin caller stubs. +# Standard: https://github.com/petry-projects/.github/blob/main/standards/ci-standards.md +# +# Fires a review-agent dispatch when a PR meets ALL of the following criteria: +# 1. PR is open and not a draft +# 2. All CI checks are completed and passing (no pending / failing) +# 3. No reviews in CHANGES_REQUESTED state +# 4. No unresolved review threads +# +# Triggered by: +# check_suite:completed — a check suite finished (fast-exits if not success) +# pull_request_review:submitted — a review was submitted (may clear CHANGES_REQUESTED) +# pull_request_review:dismissed — a changes-requested review was dismissed +# pull_request_review_thread:resolved — a review thread was resolved +# +# Requires: +# GH_PAT_WORKFLOWS — org secret, classic PAT (repo scope) for API calls and dispatch +name: PR Auto-Review — Ready Check (Reusable) + +on: + workflow_call: + secrets: + GH_PAT_WORKFLOWS: + description: "Classic PAT with repo scope used for API calls and dispatching the review agent" + required: true + +jobs: + check-and-dispatch: + runs-on: ubuntu-latest + permissions: + pull-requests: read + + steps: + - name: Resolve PR URL + id: pr + env: + GH_TOKEN: ${{ secrets.GH_PAT_WORKFLOWS }} + run: | + set -euo pipefail + case "${{ github.event_name }}" in + check_suite) + # Short-circuit: only worth proceeding if the suite itself succeeded. + CONCLUSION="${{ github.event.check_suite.conclusion }}" + if [ "$CONCLUSION" != "success" ]; then + echo "Check suite conclusion is '$CONCLUSION' — skipping" + echo "skip=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + # check_suite.pull_requests only covers same-repo PRs; the commits + # API also surfaces cross-fork PRs. + HEAD_SHA="${{ github.event.check_suite.head_sha }}" + PR_URL=$(gh api "/repos/${{ github.repository }}/commits/${HEAD_SHA}/pulls" \ + --jq '[.[] | select(.state == "open" and .draft == false)][0].html_url // empty' \ + 2>/dev/null || true) + ;; + pull_request_review | pull_request_review_thread) + PR_URL="${{ github.event.pull_request.html_url }}" + ;; + *) + echo "Unhandled event '${{ github.event_name }}' — skipping" + echo "skip=true" >> "$GITHUB_OUTPUT" + exit 0 + ;; + esac + + if [ -z "${PR_URL:-}" ]; then + echo "No open non-draft PR found for this event — skipping" + echo "skip=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + + echo "pr_url=$PR_URL" >> "$GITHUB_OUTPUT" + echo "skip=false" >> "$GITHUB_OUTPUT" + echo "PR URL: $PR_URL" + + - name: Check PR readiness criteria + id: criteria + if: steps.pr.outputs.skip != 'true' + env: + GH_TOKEN: ${{ secrets.GH_PAT_WORKFLOWS }} + PR_URL: ${{ steps.pr.outputs.pr_url }} + run: | + set -euo pipefail + + # Fetch basic PR metadata in one call. + PR_META=$(gh pr view "$PR_URL" --json state,isDraft,number,baseRepository) + STATE=$(echo "$PR_META" | jq -r '.state') + IS_DRAFT=$(echo "$PR_META" | jq -r '.isDraft') + PR_NUMBER=$(echo "$PR_META" | jq -r '.number') + REPO=$(echo "$PR_META" | jq -r '.baseRepository.nameWithOwner') + + # 1. PR must be open and not a draft. + if [ "$STATE" != "OPEN" ] || [ "$IS_DRAFT" = "true" ]; then + echo "PR is $STATE (draft=$IS_DRAFT) — skipping" + echo "ready=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + echo "PR is open and not a draft ✓" + + # 2. All CI checks must be completed and passing. + # gh pr checks --json returns objects with a "bucket" field: + # "pass" | "skipping" | "fail" | "pending" | "cancel" + CHECKS=$(gh pr checks "$PR_URL" --json bucket,name 2>/dev/null || echo "[]") + TOTAL=$(echo "$CHECKS" | jq 'length') + if [ "$TOTAL" -eq 0 ]; then + echo "No CI checks found on this PR — skipping" + echo "ready=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + NOT_PASSING=$(echo "$CHECKS" | \ + jq '[.[] | select(.bucket != "pass" and .bucket != "skipping")] | length') + if [ "$NOT_PASSING" -gt 0 ]; then + echo "$NOT_PASSING of $TOTAL check(s) are not yet passing — skipping" + echo "ready=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + echo "All $TOTAL CI check(s) passing ✓" + + # 3. No reviews in CHANGES_REQUESTED state. + CHANGES_REQ=$(gh api "/repos/${REPO}/pulls/${PR_NUMBER}/reviews" \ + --jq '[.[] | select(.state == "CHANGES_REQUESTED")] | length') + if [ "$CHANGES_REQ" -gt 0 ]; then + echo "$CHANGES_REQ review(s) in CHANGES_REQUESTED state — skipping" + echo "ready=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + echo "No CHANGES_REQUESTED reviews ✓" + + # 4. No unresolved review threads (REST API has no resolved field; use GraphQL). + UNRESOLVED=$(gh api graphql \ + -f query='query($owner:String!,$repo:String!,$number:Int!){ + repository(owner:$owner,name:$repo){ + pullRequest(number:$number){ + reviewThreads(first:100){nodes{isResolved}} + } + } + }' \ + -f owner="${REPO%%/*}" \ + -f repo="${REPO##*/}" \ + -F number="${PR_NUMBER}" \ + --jq '[.data.repository.pullRequest.reviewThreads.nodes[] + | select(.isResolved == false)] | length') + if [ "$UNRESOLVED" -gt 0 ]; then + echo "$UNRESOLVED unresolved review thread(s) — skipping" + echo "ready=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + echo "No unresolved review threads ✓" + + echo "All readiness criteria met — dispatching review agent" + echo "ready=true" >> "$GITHUB_OUTPUT" + + - name: Dispatch review agent + if: steps.criteria.outputs.ready == 'true' + env: + GH_TOKEN: ${{ secrets.GH_PAT_WORKFLOWS }} + PR_URL: ${{ steps.pr.outputs.pr_url }} + run: | + gh api \ + --method POST \ + --header "Accept: application/vnd.github+json" \ + /repos/petry-projects/.github-private/dispatches \ + --field event_type=pr-review-mention \ + --field "client_payload[pr_url]=$PR_URL" + echo "::notice::Auto-review dispatched for $PR_URL" From ebb1ccf3c8c39e2537bbbf2b821007b6dbec3647 Mon Sep 17 00:00:00 2001 From: Don Petry <36422719+don-petry@users.noreply.github.com> Date: Mon, 18 May 2026 18:23:35 -0500 Subject: [PATCH 02/11] feat: add pr-auto-review thin caller for .github repo Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/pr-auto-review.yml | 40 ++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 .github/workflows/pr-auto-review.yml diff --git a/.github/workflows/pr-auto-review.yml b/.github/workflows/pr-auto-review.yml new file mode 100644 index 0000000..ebf9a8a --- /dev/null +++ b/.github/workflows/pr-auto-review.yml @@ -0,0 +1,40 @@ +# ───────────────────────────────────────────────────────────────────────────── +# SOURCE OF TRUTH: petry-projects/.github/standards/workflows/pr-auto-review.yml +# Standard: petry-projects/.github/standards/ci-standards.md +# Reusable: petry-projects/.github/.github/workflows/pr-auto-review-reusable.yml +# +# AGENTS — READ BEFORE EDITING: +# • This file is a THIN CALLER STUB. All readiness-gate logic lives in the +# reusable workflow above. +# • You MAY change: nothing in normal use. NOTE: this file intentionally uses +# a LOCAL ref (`./`) instead of a pinned SHA — this repo IS the source of +# truth, so a local ref is always current. Other repos use @v2 +# (see standards/workflows/pr-auto-review.yml). +# • You MUST NOT change: trigger events or the job-level `permissions:` block — +# reusable workflows can be granted no more permissions than the calling job, +# so removing the stanza breaks the reusable's gh API calls. +# • If you need different behaviour, open a PR against the reusable in the +# central repo. +# ───────────────────────────────────────────────────────────────────────────── +# +# PR Auto-Review — thin caller for the org-level reusable. +# To adopt: copy standards/workflows/pr-auto-review.yml to your repo. +# Requires: GH_PAT_WORKFLOWS org secret (already present in petry-projects org). +name: PR Auto-Review — Ready Check + +on: + check_suite: + types: [completed] + pull_request_review: + types: [submitted, dismissed] + pull_request_review_thread: + types: [resolved] + +permissions: {} + +jobs: + pr-auto-review: + permissions: + pull-requests: read + uses: ./.github/workflows/pr-auto-review-reusable.yml # local ref — always current + secrets: inherit From 8543b1be5a0cf0663e97a05329f91425e5690134 Mon Sep 17 00:00:00 2001 From: Don Petry <36422719+don-petry@users.noreply.github.com> Date: Mon, 18 May 2026 18:23:46 -0500 Subject: [PATCH 03/11] feat: add pr-auto-review standard template for org repos Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- standards/workflows/pr-auto-review.yml | 41 ++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 standards/workflows/pr-auto-review.yml diff --git a/standards/workflows/pr-auto-review.yml b/standards/workflows/pr-auto-review.yml new file mode 100644 index 0000000..800a935 --- /dev/null +++ b/standards/workflows/pr-auto-review.yml @@ -0,0 +1,41 @@ +# ───────────────────────────────────────────────────────────────────────────── +# SOURCE OF TRUTH: petry-projects/.github/standards/workflows/pr-auto-review.yml +# Standard: petry-projects/.github/standards/ci-standards.md +# Reusable: petry-projects/.github/.github/workflows/pr-auto-review-reusable.yml +# +# AGENTS — READ BEFORE EDITING: +# • This file is a THIN CALLER STUB. All readiness-gate logic lives in the +# reusable workflow above. +# • You MAY change: the tag in the `uses:` line when upgrading the reusable +# workflow version (e.g. bump `@v2` → `@v3` when petry-projects/.github cuts +# a new release). +# • You MUST NOT change: trigger events or the job-level `permissions:` block — +# reusable workflows can be granted no more permissions than the calling job, +# so removing the stanza breaks the reusable's gh API calls. +# • If you need different behaviour, open a PR against the reusable in the +# central repo. +# • When publishing a new version of this reusable, also update this template +# and open a fanout PR across all caller repos. +# ───────────────────────────────────────────────────────────────────────────── +# +# PR Auto-Review — thin caller for the org-level reusable. +# To adopt: copy this file to .github/workflows/pr-auto-review.yml in your repo. +# Requires: GH_PAT_WORKFLOWS org secret (already present in petry-projects org). +name: PR Auto-Review — Ready Check + +on: + check_suite: + types: [completed] + pull_request_review: + types: [submitted, dismissed] + pull_request_review_thread: + types: [resolved] + +permissions: {} + +jobs: + pr-auto-review: + permissions: + pull-requests: read + uses: petry-projects/.github/.github/workflows/pr-auto-review-reusable.yml@v2 + secrets: inherit From 956a30919c490cbd8f7fe82616ea61fd7bdda076 Mon Sep 17 00:00:00 2001 From: Don Petry <36422719+don-petry@users.noreply.github.com> Date: Mon, 18 May 2026 20:15:56 -0500 Subject: [PATCH 04/11] fix: address all PR review comments on pr-auto-review-reusable - Replace pull_request_review_thread (invalid GH Actions event) with workflow_run and pull_request event handling - Add workflow_run:completed trigger for Actions-based CI (check_suite does not fire for GH Actions runs) - Add pull_request:[opened,reopened,synchronize,ready_for_review] trigger - Use reviewDecision from gh pr view instead of REST reviews list to correctly reflect effective review state (avoids false positives from historical CHANGES_REQUESTED reviews) - Fix gh pr checks capture: use || true instead of || echo '[]' so the JSON payload is preserved when checks are failing/pending - Exclude own in-progress check run from CI gate to prevent self-blocking - Add checks:read and actions:read job permissions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/pr-auto-review-reusable.yml | 94 ++++++++++++++----- 1 file changed, 70 insertions(+), 24 deletions(-) diff --git a/.github/workflows/pr-auto-review-reusable.yml b/.github/workflows/pr-auto-review-reusable.yml index 3d61b9c..293fd0f 100644 --- a/.github/workflows/pr-auto-review-reusable.yml +++ b/.github/workflows/pr-auto-review-reusable.yml @@ -6,14 +6,14 @@ # Fires a review-agent dispatch when a PR meets ALL of the following criteria: # 1. PR is open and not a draft # 2. All CI checks are completed and passing (no pending / failing) -# 3. No reviews in CHANGES_REQUESTED state +# 3. Effective review decision is not CHANGES_REQUESTED # 4. No unresolved review threads # -# Triggered by: -# check_suite:completed — a check suite finished (fast-exits if not success) -# pull_request_review:submitted — a review was submitted (may clear CHANGES_REQUESTED) -# pull_request_review:dismissed — a changes-requested review was dismissed -# pull_request_review_thread:resolved — a review thread was resolved +# Triggered by (events forwarded from the thin caller): +# workflow_run:completed — a named CI workflow finished green +# check_suite:completed — a third-party check suite finished green +# pull_request:opened/reopened/synchronize/ready_for_review +# pull_request_review:submitted/dismissed # # Requires: # GH_PAT_WORKFLOWS — org secret, classic PAT (repo scope) for API calls and dispatch @@ -31,6 +31,8 @@ jobs: runs-on: ubuntu-latest permissions: pull-requests: read + checks: read + actions: read steps: - name: Resolve PR URL @@ -40,22 +42,44 @@ jobs: run: | set -euo pipefail case "${{ github.event_name }}" in + workflow_run) + # workflow_run fires when a named CI workflow completes (Actions-aware). + CONCLUSION="${{ github.event.workflow_run.conclusion }}" + if [ "$CONCLUSION" != "success" ]; then + echo "Workflow run conclusion is '$CONCLUSION' — skipping" + echo "skip=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + HEAD_SHA="${{ github.event.workflow_run.head_sha }}" + PR_URL=$(gh api "/repos/${{ github.repository }}/commits/${HEAD_SHA}/pulls" \ + --jq '[.[] | select(.state == "open" and .draft == false)][0].html_url // empty' \ + 2>/dev/null || true) + ;; check_suite) - # Short-circuit: only worth proceeding if the suite itself succeeded. + # check_suite covers third-party CI (e.g. SonarCloud). + # NOTE: this event does NOT fire for GitHub Actions runs; use + # workflow_run in the caller for Actions-based CI. CONCLUSION="${{ github.event.check_suite.conclusion }}" if [ "$CONCLUSION" != "success" ]; then echo "Check suite conclusion is '$CONCLUSION' — skipping" echo "skip=true" >> "$GITHUB_OUTPUT" exit 0 fi - # check_suite.pull_requests only covers same-repo PRs; the commits - # API also surfaces cross-fork PRs. HEAD_SHA="${{ github.event.check_suite.head_sha }}" PR_URL=$(gh api "/repos/${{ github.repository }}/commits/${HEAD_SHA}/pulls" \ --jq '[.[] | select(.state == "open" and .draft == false)][0].html_url // empty' \ 2>/dev/null || true) ;; - pull_request_review | pull_request_review_thread) + pull_request) + # Skip draft PRs early to avoid unnecessary API calls. + if [ "${{ github.event.pull_request.draft }}" = "true" ]; then + echo "PR is a draft — skipping" + echo "skip=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + PR_URL="${{ github.event.pull_request.html_url }}" + ;; + pull_request_review) PR_URL="${{ github.event.pull_request.html_url }}" ;; *) @@ -84,12 +108,15 @@ jobs: run: | set -euo pipefail - # Fetch basic PR metadata in one call. - PR_META=$(gh pr view "$PR_URL" --json state,isDraft,number,baseRepository) + # Fetch basic PR metadata in one call, including the effective review + # decision so we don't need a separate REST call for reviews history. + PR_META=$(gh pr view "$PR_URL" \ + --json state,isDraft,number,baseRepository,reviewDecision) STATE=$(echo "$PR_META" | jq -r '.state') IS_DRAFT=$(echo "$PR_META" | jq -r '.isDraft') PR_NUMBER=$(echo "$PR_META" | jq -r '.number') REPO=$(echo "$PR_META" | jq -r '.baseRepository.nameWithOwner') + REVIEW_DECISION=$(echo "$PR_META" | jq -r '.reviewDecision // ""') # 1. PR must be open and not a draft. if [ "$STATE" != "OPEN" ] || [ "$IS_DRAFT" = "true" ]; then @@ -100,17 +127,31 @@ jobs: echo "PR is open and not a draft ✓" # 2. All CI checks must be completed and passing. - # gh pr checks --json returns objects with a "bucket" field: - # "pass" | "skipping" | "fail" | "pending" | "cancel" - CHECKS=$(gh pr checks "$PR_URL" --json bucket,name 2>/dev/null || echo "[]") + # gh pr checks --json may exit non-zero when checks are + # failing/pending but still writes the JSON payload to stdout; + # use || true so set -e doesn't discard that output. + CHECKS=$(gh pr checks "$PR_URL" --json bucket,name 2>/dev/null || true) + if [ -z "${CHECKS}" ]; then + CHECKS="[]" + fi TOTAL=$(echo "$CHECKS" | jq 'length') if [ "$TOTAL" -eq 0 ]; then echo "No CI checks found on this PR — skipping" echo "ready=false" >> "$GITHUB_OUTPUT" exit 0 fi - NOT_PASSING=$(echo "$CHECKS" | \ - jq '[.[] | select(.bucket != "pass" and .bucket != "skipping")] | length') + + # Exclude this workflow's own in-progress check so it doesn't + # block itself on every trigger. + SELF_CHECK=$(gh api \ + "/repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/jobs" \ + --jq '.jobs[0].name // empty' 2>/dev/null || echo "") + + NOT_PASSING=$(echo "$CHECKS" | jq \ + --arg self "$SELF_CHECK" \ + '[.[] | select(.bucket != "pass" and .bucket != "skipping") + | select($self == "" or .name != $self)] | length') + if [ "$NOT_PASSING" -gt 0 ]; then echo "$NOT_PASSING of $TOTAL check(s) are not yet passing — skipping" echo "ready=false" >> "$GITHUB_OUTPUT" @@ -118,17 +159,22 @@ jobs: fi echo "All $TOTAL CI check(s) passing ✓" - # 3. No reviews in CHANGES_REQUESTED state. - CHANGES_REQ=$(gh api "/repos/${REPO}/pulls/${PR_NUMBER}/reviews" \ - --jq '[.[] | select(.state == "CHANGES_REQUESTED")] | length') - if [ "$CHANGES_REQ" -gt 0 ]; then - echo "$CHANGES_REQ review(s) in CHANGES_REQUESTED state — skipping" + # 3. Effective review decision must not be CHANGES_REQUESTED. + # reviewDecision reflects the aggregate current state (accounts for + # dismissals and superseding reviews), unlike the REST reviews list + # which returns full history and can produce false positives. + if [ "$REVIEW_DECISION" = "CHANGES_REQUESTED" ]; then + echo "Effective review decision is CHANGES_REQUESTED — skipping" echo "ready=false" >> "$GITHUB_OUTPUT" exit 0 fi - echo "No CHANGES_REQUESTED reviews ✓" + echo "No CHANGES_REQUESTED review decision ✓" - # 4. No unresolved review threads (REST API has no resolved field; use GraphQL). + # 4. No unresolved review threads. + # REST API has no resolved field on review comments; GraphQL is + # required. Paginating with first:100 covers the vast majority of + # PRs; threads beyond 100 are an edge case noted in the TODO below. + # TODO: paginate reviewThreads for PRs with >100 threads. UNRESOLVED=$(gh api graphql \ -f query='query($owner:String!,$repo:String!,$number:Int!){ repository(owner:$owner,name:$repo){ From 416d1d601c028dbb9f34f86e4df254e31599f89c Mon Sep 17 00:00:00 2001 From: Don Petry <36422719+don-petry@users.noreply.github.com> Date: Mon, 18 May 2026 20:16:21 -0500 Subject: [PATCH 05/11] fix: update pr-auto-review thin caller with corrected triggers - Remove pull_request_review_thread (invalid GH Actions event) - Add workflow_run:[CI] for Actions-based CI completion - Add pull_request:[opened,reopened,synchronize,ready_for_review] - Add checks:read and actions:read job permissions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/pr-auto-review.yml | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-auto-review.yml b/.github/workflows/pr-auto-review.yml index ebf9a8a..f975fa3 100644 --- a/.github/workflows/pr-auto-review.yml +++ b/.github/workflows/pr-auto-review.yml @@ -23,12 +23,21 @@ name: PR Auto-Review — Ready Check on: + # workflow_run fires when a named GitHub Actions workflow completes. + # check_suite does NOT trigger for GitHub Actions runs, so this is required + # to catch CI turning green on a PR. + workflow_run: + workflows: ["CI"] + types: [completed] + # check_suite covers third-party CI checks (e.g. SonarCloud, external apps). check_suite: types: [completed] + # Re-evaluate readiness after review state changes. pull_request_review: types: [submitted, dismissed] - pull_request_review_thread: - types: [resolved] + # Re-evaluate when the PR is first opened, updated, or comes out of draft. + pull_request: + types: [opened, reopened, synchronize, ready_for_review] permissions: {} @@ -36,5 +45,7 @@ jobs: pr-auto-review: permissions: pull-requests: read + checks: read + actions: read uses: ./.github/workflows/pr-auto-review-reusable.yml # local ref — always current secrets: inherit From 5a020486d5852c5aa01f479e0e5ecaf48fd63fbb Mon Sep 17 00:00:00 2001 From: Don Petry <36422719+don-petry@users.noreply.github.com> Date: Mon, 18 May 2026 20:16:39 -0500 Subject: [PATCH 06/11] fix: update pr-auto-review standards template with corrected triggers - Remove pull_request_review_thread (invalid GH Actions event) - Add workflow_run with TODO comment for CI workflow name customization - Add pull_request:[opened,reopened,synchronize,ready_for_review] - Add checks:read and actions:read job permissions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- standards/workflows/pr-auto-review.yml | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/standards/workflows/pr-auto-review.yml b/standards/workflows/pr-auto-review.yml index 800a935..de504dc 100644 --- a/standards/workflows/pr-auto-review.yml +++ b/standards/workflows/pr-auto-review.yml @@ -8,10 +8,11 @@ # reusable workflow above. # • You MAY change: the tag in the `uses:` line when upgrading the reusable # workflow version (e.g. bump `@v2` → `@v3` when petry-projects/.github cuts -# a new release). -# • You MUST NOT change: trigger events or the job-level `permissions:` block — -# reusable workflows can be granted no more permissions than the calling job, -# so removing the stanza breaks the reusable's gh API calls. +# a new release), and the workflow name(s) in `workflow_run.workflows` to +# match your repository's CI workflow name(s). +# • You MUST NOT change: trigger event types or the job-level `permissions:` +# block — reusable workflows can be granted no more permissions than the +# calling job, so removing the stanza breaks the reusable's gh API calls. # • If you need different behaviour, open a PR against the reusable in the # central repo. # • When publishing a new version of this reusable, also update this template @@ -24,12 +25,22 @@ name: PR Auto-Review — Ready Check on: + # workflow_run fires when a named GitHub Actions workflow completes. + # check_suite does NOT trigger for GitHub Actions runs, so this is required + # to catch CI turning green on a PR. + # TODO: replace "CI" with your repository's CI workflow name(s). + workflow_run: + workflows: ["CI"] + types: [completed] + # check_suite covers third-party CI checks (e.g. SonarCloud, external apps). check_suite: types: [completed] + # Re-evaluate readiness after review state changes. pull_request_review: types: [submitted, dismissed] - pull_request_review_thread: - types: [resolved] + # Re-evaluate when the PR is first opened, updated, or comes out of draft. + pull_request: + types: [opened, reopened, synchronize, ready_for_review] permissions: {} @@ -37,5 +48,7 @@ jobs: pr-auto-review: permissions: pull-requests: read + checks: read + actions: read uses: petry-projects/.github/.github/workflows/pr-auto-review-reusable.yml@v2 secrets: inherit From 6d058d9274ac4d6e5069c0a9c16bab4e2ba56eac Mon Sep 17 00:00:00 2001 From: Don Petry <36422719+don-petry@users.noreply.github.com> Date: Mon, 18 May 2026 20:20:35 -0500 Subject: [PATCH 07/11] fix: resolve SC2016 lint error and invalid baseRepository field - Add '# shellcheck disable=SC2016' before jq --arg self usage; the $self token is a jq variable (passed via --arg), not a shell variable - Replace baseRepository (not a valid gh pr view --json field) with URL parsing: sed 's|https://github.com/||; s|/pull/.*||' Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/pr-auto-review-reusable.yml | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/.github/workflows/pr-auto-review-reusable.yml b/.github/workflows/pr-auto-review-reusable.yml index 293fd0f..17d1ba6 100644 --- a/.github/workflows/pr-auto-review-reusable.yml +++ b/.github/workflows/pr-auto-review-reusable.yml @@ -108,14 +108,17 @@ jobs: run: | set -euo pipefail - # Fetch basic PR metadata in one call, including the effective review - # decision so we don't need a separate REST call for reviews history. + # Derive the base repository (owner/repo) from the PR URL. + # gh pr view --json does not expose baseRepository; parsing the URL + # is simpler and works for both same-repo and fork PRs. + REPO=$(echo "$PR_URL" | sed 's|https://github.com/||; s|/pull/.*||') + + # Fetch PR metadata in one call, including the effective review decision. PR_META=$(gh pr view "$PR_URL" \ - --json state,isDraft,number,baseRepository,reviewDecision) + --json state,isDraft,number,reviewDecision) STATE=$(echo "$PR_META" | jq -r '.state') IS_DRAFT=$(echo "$PR_META" | jq -r '.isDraft') PR_NUMBER=$(echo "$PR_META" | jq -r '.number') - REPO=$(echo "$PR_META" | jq -r '.baseRepository.nameWithOwner') REVIEW_DECISION=$(echo "$PR_META" | jq -r '.reviewDecision // ""') # 1. PR must be open and not a draft. @@ -141,12 +144,15 @@ jobs: exit 0 fi - # Exclude this workflow's own in-progress check so it doesn't - # block itself on every trigger. + # Get the name of this workflow's own check run so it can be excluded + # from the gate — an in-progress run shows as "pending" and would + # otherwise block itself on every trigger. SELF_CHECK=$(gh api \ "/repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/jobs" \ --jq '.jobs[0].name // empty' 2>/dev/null || echo "") + # SC2016: $self is a jq variable (passed via --arg), not a shell variable. + # shellcheck disable=SC2016 NOT_PASSING=$(echo "$CHECKS" | jq \ --arg self "$SELF_CHECK" \ '[.[] | select(.bucket != "pass" and .bucket != "skipping") From adfd77de9a17e50285ceda53b6e6142294983de9 Mon Sep 17 00:00:00 2001 From: Don Petry <36422719+don-petry@users.noreply.github.com> Date: Mon, 18 May 2026 20:23:21 -0500 Subject: [PATCH 08/11] fix: use double-quoted jq expression to avoid SC2016 Replace single-quoted jq filter containing $self (a jq variable) with a double-quoted string using \\, which produces a literal dollar sign for jq without triggering shellcheck SC2016 (expressions don't expand in single quotes). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/pr-auto-review-reusable.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pr-auto-review-reusable.yml b/.github/workflows/pr-auto-review-reusable.yml index 17d1ba6..7afa705 100644 --- a/.github/workflows/pr-auto-review-reusable.yml +++ b/.github/workflows/pr-auto-review-reusable.yml @@ -151,12 +151,12 @@ jobs: "/repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/jobs" \ --jq '.jobs[0].name // empty' 2>/dev/null || echo "") - # SC2016: $self is a jq variable (passed via --arg), not a shell variable. - # shellcheck disable=SC2016 + # Use double-quoted jq expression with \$self so the shell produces + # a literal "$self" for jq without triggering SC2016 (which flags + # shell variables in single-quoted strings). NOT_PASSING=$(echo "$CHECKS" | jq \ --arg self "$SELF_CHECK" \ - '[.[] | select(.bucket != "pass" and .bucket != "skipping") - | select($self == "" or .name != $self)] | length') + "map(select(.name != \$self)) | map(select(.bucket != \"pass\" and .bucket != \"skipping\")) | length") if [ "$NOT_PASSING" -gt 0 ]; then echo "$NOT_PASSING of $TOTAL check(s) are not yet passing — skipping" From e4aa1e9170218864ee2df10aea4836598abb8bd4 Mon Sep 17 00:00:00 2001 From: Don Petry <36422719+don-petry@users.noreply.github.com> Date: Mon, 18 May 2026 20:26:58 -0500 Subject: [PATCH 09/11] fix: escape GraphQL variable refs to avoid SC2016 GraphQL variables ($owner, $repo, $number) in the -f query= string triggered SC2016. Use a double-quoted string with \\$ escaping so the shell produces literal dollar signs for GraphQL without shellcheck interpreting them as unexpanded shell variables in single quotes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/pr-auto-review-reusable.yml | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/.github/workflows/pr-auto-review-reusable.yml b/.github/workflows/pr-auto-review-reusable.yml index 7afa705..e1ffbd2 100644 --- a/.github/workflows/pr-auto-review-reusable.yml +++ b/.github/workflows/pr-auto-review-reusable.yml @@ -153,7 +153,7 @@ jobs: # Use double-quoted jq expression with \$self so the shell produces # a literal "$self" for jq without triggering SC2016 (which flags - # shell variables in single-quoted strings). + # shell variables in single-quoted strings). $self is a jq variable. NOT_PASSING=$(echo "$CHECKS" | jq \ --arg self "$SELF_CHECK" \ "map(select(.name != \$self)) | map(select(.bucket != \"pass\" and .bucket != \"skipping\")) | length") @@ -178,22 +178,16 @@ jobs: # 4. No unresolved review threads. # REST API has no resolved field on review comments; GraphQL is - # required. Paginating with first:100 covers the vast majority of - # PRs; threads beyond 100 are an edge case noted in the TODO below. + # required. \$owner/\$repo/\$number are GraphQL variable references; + # the backslash-dollar escaping prevents shell expansion while + # keeping the literal $ that GraphQL expects. # TODO: paginate reviewThreads for PRs with >100 threads. UNRESOLVED=$(gh api graphql \ - -f query='query($owner:String!,$repo:String!,$number:Int!){ - repository(owner:$owner,name:$repo){ - pullRequest(number:$number){ - reviewThreads(first:100){nodes{isResolved}} - } - } - }' \ + -f "query=query(\$owner:String!,\$repo:String!,\$number:Int!){repository(owner:\$owner,name:\$repo){pullRequest(number:\$number){reviewThreads(first:100){nodes{isResolved}}}}}" \ -f owner="${REPO%%/*}" \ -f repo="${REPO##*/}" \ -F number="${PR_NUMBER}" \ - --jq '[.data.repository.pullRequest.reviewThreads.nodes[] - | select(.isResolved == false)] | length') + --jq "[.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false)] | length") if [ "$UNRESOLVED" -gt 0 ]; then echo "$UNRESOLVED unresolved review thread(s) — skipping" echo "ready=false" >> "$GITHUB_OUTPUT" From 7b7876f0f24751727fe374aceba36abe95418863 Mon Sep 17 00:00:00 2001 From: Don Petry <36422719+don-petry@users.noreply.github.com> Date: Mon, 18 May 2026 20:30:30 -0500 Subject: [PATCH 10/11] fix(pr-auto-review): pass only required secret instead of secrets: inherit Resolves SonarCloud security hotspot S7635 (githubactions:S7635). Only GH_PAT_WORKFLOWS is needed by the reusable workflow. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/pr-auto-review.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pr-auto-review.yml b/.github/workflows/pr-auto-review.yml index f975fa3..f3e9781 100644 --- a/.github/workflows/pr-auto-review.yml +++ b/.github/workflows/pr-auto-review.yml @@ -48,4 +48,5 @@ jobs: checks: read actions: read uses: ./.github/workflows/pr-auto-review-reusable.yml # local ref — always current - secrets: inherit + secrets: + GH_PAT_WORKFLOWS: ${{ secrets.GH_PAT_WORKFLOWS }} From c829492edfb8561ad2c567c3e4ab0a7cc7a0d276 Mon Sep 17 00:00:00 2001 From: Don Petry <36422719+don-petry@users.noreply.github.com> Date: Mon, 18 May 2026 20:30:38 -0500 Subject: [PATCH 11/11] fix(pr-auto-review): pass only required secret in standards template too Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- standards/workflows/pr-auto-review.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/standards/workflows/pr-auto-review.yml b/standards/workflows/pr-auto-review.yml index de504dc..7409476 100644 --- a/standards/workflows/pr-auto-review.yml +++ b/standards/workflows/pr-auto-review.yml @@ -51,4 +51,5 @@ jobs: checks: read actions: read uses: petry-projects/.github/.github/workflows/pr-auto-review-reusable.yml@v2 - secrets: inherit + secrets: + GH_PAT_WORKFLOWS: ${{ secrets.GH_PAT_WORKFLOWS }}