Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
213 changes: 213 additions & 0 deletions .github/workflows/pr-auto-review-reusable.yml
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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Complete the task associated to this "TODO" comment.

See more on https://sonarcloud.io/project/issues?id=petry-projects_.github&issues=AZ49znQ_nQ8skf4BKTDy&open=AZ49znQ_nQ8skf4BKTDy&pullRequest=323
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")
Comment on lines +185 to +190
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Paginate review threads before dispatching

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 on pageInfo.hasNextPage/endCursor or otherwise query all review threads before treating UNRESOLVED=0 as authoritative.

Useful? React with 👍 / 👎.

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"
52 changes: 52 additions & 0 deletions .github/workflows/pr-auto-review.yml
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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Do not rely on check_suite for Actions CI

The stated CI-finished trigger will not run for the org's normal GitHub Actions checks: GitHub's Actions docs for check_suite say the event does not trigger workflows when the suite was created by GitHub Actions or the head SHA is associated with GitHub Actions. For PRs whose readiness depends on these workflows, no auto-review run is started when CI turns green, so this needs a trigger that actually fires for Actions CI completion.

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 }}
55 changes: 55 additions & 0 deletions standards/workflows/pr-auto-review.yml
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"]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Trigger on all gated Actions workflows

With the standard workflow set I checked (Dependency audit, AgentShield, and this repo's SonarCloud Analysis are also GitHub Actions workflows), gh pr checks gates on checks beyond CI. If CI completes while one of those checks is still pending, this run skips; when that later Actions workflow turns green, this workflow_run filter does not fire and check_suite still will not cover GitHub Actions-created suites, so ready PRs can remain undispatched until some unrelated PR/review event. Include every Actions workflow that can appear in the readiness gate, or trigger on all completed workflow runs and let the existing gate decide readiness.

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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Recheck readiness when review threads are resolved

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 UNRESOLVED=0; GitHub exposes thread resolution as separate pull_request_review_thread webhook activity rather than as pull_request_review. In that scenario the PR satisfies all criteria but no auto-review is dispatched until another unrelated PR or CI event occurs, so add a supported mechanism to re-evaluate on thread resolution (for example a webhook/dispatch bridge or scheduled fallback).

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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid PAT-backed runs on Dependabot PR events

For Dependabot PRs, GitHub treats pull_request/pull_request_review workflows as fork-originated and does not expose normal Actions secrets, so this trigger can invoke the reusable without GH_PAT_WORKFLOWS; the next gh pr view call then fails authentication instead of cleanly skipping. Because this organization relies on Dependabot workflows, opening or updating a Dependabot PR will produce a red auto-review workflow run unless the event is skipped before the PAT is needed or the credential is also supplied as a Dependabot secret.

Useful? React with 👍 / 👎.

Comment on lines +42 to +43
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Do not expose the PAT to PR branch workflow code

On same-repository PRs, the pull_request run has access to normal Actions secrets while executing the workflow definition from the PR merge ref, so a branch that modifies this stub or the referenced reusable can read GH_PAT_WORKFLOWS before the change is merged. Because this token is described as a classic repo-scoped PAT, use a default-branch context such as pull_request_target/workflow_run for the secret-bearing dispatch path or split the untrusted PR trigger from the privileged API call.

Useful? React with 👍 / 👎.


permissions: {}

jobs:
pr-auto-review:
permissions:
pull-requests: read
Comment on lines +49 to +50
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current permissions are insufficient for the readiness criteria described in the PR summary. The gh pr checks command (and the underlying GitHub API calls) requires checks: read and statuses: read to retrieve the status of CI runs. Additionally, actions: read is recommended to allow the reusable workflow to inspect other workflow runs, aligning with the permissions granted to other review-related agents in the organization (e.g., Claude Code as documented in standards/ci-standards.md).

    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 }}