-
Notifications
You must be signed in to change notification settings - Fork 0
feat: auto-trigger PR review when all readiness criteria are met #323
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
787c546
ebb1ccf
8543b1b
956a309
416d1d6
5a02048
6d058d9
adfd77d
e4aa1e9
7b7876f
c829492
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,213 @@ | ||
| # 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. Effective review decision is not CHANGES_REQUESTED | ||
| # 4. No unresolved review threads | ||
| # | ||
| # 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 | ||
| 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 | ||
| checks: read | ||
| actions: read | ||
|
|
||
| steps: | ||
| - name: Resolve PR URL | ||
| id: pr | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GH_PAT_WORKFLOWS }} | ||
| 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) | ||
| # 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 | ||
| 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) | ||
| # 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 }}" | ||
| ;; | ||
| *) | ||
| 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 | ||
|
|
||
| # 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,reviewDecision) | ||
| STATE=$(echo "$PR_META" | jq -r '.state') | ||
| IS_DRAFT=$(echo "$PR_META" | jq -r '.isDraft') | ||
| PR_NUMBER=$(echo "$PR_META" | jq -r '.number') | ||
| REVIEW_DECISION=$(echo "$PR_META" | jq -r '.reviewDecision // ""') | ||
|
|
||
| # 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 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 | ||
|
|
||
| # 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 "") | ||
|
|
||
| # 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). $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") | ||
|
|
||
| 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. 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 review decision ✓" | ||
|
|
||
| # 4. No unresolved review threads. | ||
| # REST API has no resolved field on review comments; GraphQL is | ||
| # 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. | ||
|
Check warning on line 184 in .github/workflows/pr-auto-review-reusable.yml
|
||
| 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" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| # ───────────────────────────────────────────────────────────────────────────── | ||
| # 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: | ||
| # 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] | ||
|
Comment on lines
+33
to
+34
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The stated CI-finished trigger will not run for the org's normal GitHub Actions checks: GitHub's Actions docs for Useful? React with 👍 / 👎. |
||
| # Re-evaluate readiness after review state changes. | ||
| pull_request_review: | ||
| types: [submitted, dismissed] | ||
| # Re-evaluate when the PR is first opened, updated, or comes out of draft. | ||
| pull_request: | ||
| types: [opened, reopened, synchronize, ready_for_review] | ||
|
|
||
| permissions: {} | ||
|
|
||
| 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: | ||
| GH_PAT_WORKFLOWS: ${{ secrets.GH_PAT_WORKFLOWS }} | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| # ───────────────────────────────────────────────────────────────────────────── | ||
| # 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), 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 | ||
| # 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: | ||
| # 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"] | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
With the standard workflow set I checked ( Useful? React with 👍 / 👎. |
||
| 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] | ||
|
Comment on lines
+39
to
+40
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the only remaining blocker is an unresolved review thread, resolving the final thread does not submit or dismiss a PR review, so this workflow never re-runs to observe Useful? React with 👍 / 👎. |
||
| # Re-evaluate when the PR is first opened, updated, or comes out of draft. | ||
| pull_request: | ||
| types: [opened, reopened, synchronize, ready_for_review] | ||
|
Comment on lines
+42
to
+43
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For Dependabot PRs, GitHub treats Useful? React with 👍 / 👎.
Comment on lines
+42
to
+43
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
On same-repository PRs, the Useful? React with 👍 / 👎. |
||
|
|
||
| permissions: {} | ||
|
|
||
| jobs: | ||
| pr-auto-review: | ||
| permissions: | ||
| pull-requests: read | ||
|
Comment on lines
+49
to
+50
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The current permissions are insufficient for the readiness criteria described in the PR summary. The permissions:
pull-requests: read
checks: read
statuses: read
actions: read |
||
| checks: read | ||
| actions: read | ||
| uses: petry-projects/.github/.github/workflows/pr-auto-review-reusable.yml@v2 | ||
| secrets: | ||
| GH_PAT_WORKFLOWS: ${{ secrets.GH_PAT_WORKFLOWS }} | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a large PR has more than 100 review threads and the first page is resolved but a later page still has an unresolved thread, this query only inspects
reviewThreads(first:100)and then dispatches the review agent even though the stated readiness criterion is not met. Please loop onpageInfo.hasNextPage/endCursoror otherwise query all review threads before treatingUNRESOLVED=0as authoritative.Useful? React with 👍 / 👎.