Skip to content

Commit a2b3b46

Browse files
don-petryclaude
andauthored
feat: make pr-review-mention an org standard (#237)
* feat: make pr-review-mention an org standard with reusable workflow - Extract all logic from pr-review-mention.yml into pr-review-mention-reusable.yml (org single source of truth) - Slim pr-review-mention.yml down to a thin caller stub (local ref pattern, matching auto-rebase.yml) - Add standards/workflows/pr-review-mention.yml canonical template for other repos (@v1 reference) - Add pr-review-mention.yml to REQUIRED_WORKFLOWS and centralized stub checks in compliance-audit.sh - Document in ci-standards.md: template table, required-workflow count (6→7), and §10 with full spec - Add scripts/deploy-standard-workflows.sh to push standard stubs to all org repos in one command Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> * fix: remove unused counter vars (SC2034), add trailing newline to codeowners-standard Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> * fix: address Gemini review comments on deploy-standard-workflows.sh - Fix claude.yml compliance check: derive uses: from template (not stem-reusable heuristic), so the claude→claude-code-reusable name exception is handled automatically - Combine two API calls (SHA + content) into one fetch_existing call with tab-split output - Fix base64 portability: try -w 0 (GNU), fall back to -b 0 (BSD/macOS) - Increase repo list limit to 500 for larger orgs - Remove unused counter variables (already fixed in prior commit; this replaces the old approach) Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> * fix: address Copilot review comments - Declare GH_PAT_WORKFLOWS in workflow_call secrets block (matching other reusables) - Clarify fork-PR guard docs: only review_requested path excludes forks; comment triggers are base-repo-only by GitHub's event model, protected by trust check - Fix 'SHA' → 'tag' in standards/workflows/pr-review-mention.yml header comment - Add --no-archived to gh repo list in deploy script - Switch --field to --raw-field for content/sha/message to avoid form-encoding issues with base64's + and / characters Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
1 parent d3ad991 commit a2b3b46

7 files changed

Lines changed: 436 additions & 123 deletions

File tree

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
# Reusable PR Review — Mention Trigger workflow.
2+
# Single source of truth for the org: all review-dispatch logic lives here.
3+
# Repo-level pr-review-mention.yml files are thin caller stubs.
4+
# Standard: https://github.com/petry-projects/.github/blob/main/standards/ci-standards.md
5+
#
6+
# Fires when @petry-review-bot is mentioned in a PR comment / review comment,
7+
# or when donpetry-bot is assigned as a reviewer on a PR. Dispatches a
8+
# repository_dispatch event to petry-projects/.github-private to trigger the
9+
# pr-review agent cascade.
10+
#
11+
# Requires: GH_PAT_WORKFLOWS org secret (classic PAT with repo scope).
12+
name: PR Review — Mention Trigger (Reusable)
13+
14+
on:
15+
workflow_call:
16+
secrets:
17+
GH_PAT_WORKFLOWS:
18+
description: "Classic PAT with repo scope used to post comments and dispatch the review agent"
19+
required: true
20+
21+
jobs:
22+
handle-mention:
23+
runs-on: ubuntu-latest
24+
permissions:
25+
pull-requests: write
26+
# Fire when:
27+
# (a) donpetry-bot is added as a reviewer on a same-repo PR (fork PRs
28+
# don't receive org secrets so we exclude them), OR
29+
# (b) @petry-review-bot is mentioned in a PR comment / review comment.
30+
#
31+
# Guard each branch against missing fields: requested_reviewer is null for
32+
# team review requests; comment fields don't exist on pull_request events.
33+
if: |
34+
(github.event_name == 'pull_request' &&
35+
github.event.requested_reviewer != null &&
36+
github.event.requested_reviewer.login == 'donpetry-bot' &&
37+
github.event.pull_request.head.repo.full_name == github.repository) ||
38+
(github.event_name != 'pull_request' &&
39+
contains(github.event.comment.body, '@petry-review-bot') &&
40+
(github.event_name == 'pull_request_review_comment' ||
41+
github.event.issue.pull_request != null))
42+
43+
steps:
44+
- name: Check commenter trust level
45+
id: trust
46+
env:
47+
GH_TOKEN: ${{ secrets.GH_PAT_WORKFLOWS }}
48+
run: |
49+
# Only OWNER, MEMBER, and COLLABORATOR can trigger reviews.
50+
# This prevents external contributors or bots from burning review quota.
51+
#
52+
# For review_requested events author_association is absent from the
53+
# sender payload, so query the collaborator permission API instead.
54+
# The endpoint returns a top-level .permission string:
55+
# "admin" | "write" | "read" | "none"
56+
if [ "${{ github.event_name }}" = "pull_request" ]; then
57+
ACTOR="${{ github.event.sender.login }}"
58+
PERM=$(gh api /repos/${{ github.repository }}/collaborators/${{ github.event.sender.login }}/permission \
59+
--jq '.permission' 2>/dev/null || echo "none")
60+
case "$PERM" in
61+
admin|write)
62+
echo "trusted=true" >> "$GITHUB_OUTPUT"
63+
echo "Actor @$ACTOR has permission $PERM — trusted"
64+
;;
65+
*)
66+
echo "trusted=false" >> "$GITHUB_OUTPUT"
67+
echo "Actor @$ACTOR has permission $PERM — not trusted, skipping"
68+
;;
69+
esac
70+
else
71+
ACTOR="${{ github.event.comment.user.login }}"
72+
ASSOC="${{ github.event.comment.author_association }}"
73+
case "$ASSOC" in
74+
OWNER|MEMBER|COLLABORATOR)
75+
echo "trusted=true" >> "$GITHUB_OUTPUT"
76+
echo "Actor @$ACTOR has association $ASSOC — trusted"
77+
;;
78+
*)
79+
echo "trusted=false" >> "$GITHUB_OUTPUT"
80+
echo "Actor @$ACTOR has association $ASSOC — not trusted, skipping"
81+
;;
82+
esac
83+
fi
84+
85+
- name: Resolve PR URL
86+
id: pr
87+
if: steps.trust.outputs.trusted == 'true'
88+
env:
89+
GH_TOKEN: ${{ secrets.GH_PAT_WORKFLOWS }}
90+
run: |
91+
if [ "${{ github.event_name }}" = "pull_request" ]; then
92+
PR_URL="${{ github.event.pull_request.html_url }}"
93+
elif [ "${{ github.event_name }}" = "pull_request_review_comment" ]; then
94+
PR_URL="${{ github.event.pull_request.html_url }}"
95+
else
96+
# issue_comment: resolve the PR URL from the issue's pull_request link
97+
PR_URL=$(gh api "${{ github.event.issue.pull_request.url }}" --jq '.html_url')
98+
fi
99+
echo "pr_url=$PR_URL" >> "$GITHUB_OUTPUT"
100+
echo "PR URL: $PR_URL"
101+
102+
- name: Post acknowledgement comment
103+
if: steps.trust.outputs.trusted == 'true'
104+
env:
105+
GH_TOKEN: ${{ secrets.GH_PAT_WORKFLOWS }}
106+
PR_URL: ${{ steps.pr.outputs.pr_url }}
107+
run: |
108+
if [ "${{ github.event_name }}" = "pull_request" ]; then
109+
ACTOR="${{ github.event.sender.login }}"
110+
MSG="@${ACTOR} assigned me as reviewer — starting a fresh review now. Results will appear in a few minutes."
111+
else
112+
ACTOR="${{ github.event.comment.user.login }}"
113+
MSG="@${ACTOR} I'm on it — starting a fresh review now. Results will appear in a few minutes."
114+
fi
115+
gh pr comment "$PR_URL" --body "$(printf '<!-- pr-review-agent mention-ack -->\n%s' "${MSG}")"
116+
117+
- name: Trigger review agent
118+
if: steps.trust.outputs.trusted == 'true'
119+
env:
120+
GH_TOKEN: ${{ secrets.GH_PAT_WORKFLOWS }}
121+
PR_URL: ${{ steps.pr.outputs.pr_url }}
122+
run: |
123+
# repository_dispatch requires only Contents: write, not Actions: write.
124+
# pr-review.yml in .github-private listens for "pr-review-mention" and
125+
# extracts client_payload.pr_url to run the cascade.
126+
gh api \
127+
--method POST \
128+
--header "Accept: application/vnd.github+json" \
129+
/repos/petry-projects/.github-private/dispatches \
130+
--field event_type=pr-review-mention \
131+
--field "client_payload[pr_url]=$PR_URL"
132+
echo "Dispatch sent for $PR_URL"
Lines changed: 26 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,25 @@
1-
# Deploy this file to petry-projects/.github as:
2-
# .github/workflows/pr-review-mention.yml
1+
# ─────────────────────────────────────────────────────────────────────────────
2+
# SOURCE OF TRUTH: petry-projects/.github/standards/workflows/pr-review-mention.yml
3+
# Standard: petry-projects/.github/standards/ci-standards.md
4+
# Reusable: petry-projects/.github/.github/workflows/pr-review-mention-reusable.yml
35
#
4-
# Purpose: Trigger the pr-review-agent whenever @petry-review-bot is mentioned
5-
# in a PR comment or review comment, OR when donpetry-bot is assigned
6-
# as a reviewer on a PR within the petry-projects org.
6+
# AGENTS — READ BEFORE EDITING:
7+
# • This file is a THIN CALLER STUB. All review-dispatch logic lives in the
8+
# reusable workflow above.
9+
# • You MAY change: nothing in normal use. NOTE: this file intentionally uses
10+
# a LOCAL ref (`./`) instead of a pinned SHA — this repo IS the source of
11+
# truth, so a local ref is always current. Other repos use @v1
12+
# (see standards/workflows/pr-review-mention.yml).
13+
# • You MUST NOT change: trigger events or the job-level `permissions:` block —
14+
# reusable workflows can be granted no more permissions than the calling job,
15+
# so removing the stanza breaks the reusable's gh API calls.
16+
# • If you need different behaviour, open a PR against the reusable in the
17+
# central repo.
18+
# ─────────────────────────────────────────────────────────────────────────────
719
#
8-
# Prerequisites:
9-
# Uses GH_PAT_WORKFLOWS (org secret, classic PAT with repo scope) — already
10-
# present in petry-projects org. No additional secret setup required.
11-
20+
# PR Review Mention — thin caller for the org-level reusable.
21+
# To adopt: copy standards/workflows/pr-review-mention.yml to your repo.
22+
# Requires: GH_PAT_WORKFLOWS org secret (already present in petry-projects org).
1223
name: PR Review — Mention Trigger
1324

1425
on:
@@ -19,116 +30,11 @@ on:
1930
pull_request:
2031
types: [review_requested]
2132

22-
permissions:
23-
pull-requests: write
33+
permissions: {}
2434

2535
jobs:
26-
handle-mention:
27-
runs-on: ubuntu-latest
28-
# Fire when:
29-
# (a) donpetry-bot is added as a reviewer on a same-repo PR (fork PRs
30-
# don't receive org secrets so we exclude them), OR
31-
# (b) @petry-review-bot is mentioned in a PR comment / review comment.
32-
#
33-
# Guard each branch against missing fields: requested_reviewer is null for
34-
# team review requests; comment fields don't exist on pull_request events.
35-
if: |
36-
(github.event_name == 'pull_request' &&
37-
github.event.requested_reviewer != null &&
38-
github.event.requested_reviewer.login == 'donpetry-bot' &&
39-
github.event.pull_request.head.repo.full_name == github.repository) ||
40-
(github.event_name != 'pull_request' &&
41-
contains(github.event.comment.body, '@petry-review-bot') &&
42-
(github.event_name == 'pull_request_review_comment' ||
43-
github.event.issue.pull_request != null))
44-
45-
steps:
46-
- name: Check commenter trust level
47-
id: trust
48-
env:
49-
GH_TOKEN: ${{ secrets.GH_PAT_WORKFLOWS }}
50-
run: |
51-
# Only OWNER, MEMBER, and COLLABORATOR can trigger reviews.
52-
# This prevents external contributors or bots from burning review quota.
53-
#
54-
# For review_requested events author_association is absent from the
55-
# sender payload, so query the collaborator permission API instead.
56-
# The endpoint returns a top-level .permission string:
57-
# "admin" | "write" | "read" | "none"
58-
if [ "${{ github.event_name }}" = "pull_request" ]; then
59-
ACTOR="${{ github.event.sender.login }}"
60-
PERM=$(gh api /repos/${{ github.repository }}/collaborators/${{ github.event.sender.login }}/permission \
61-
--jq '.permission' 2>/dev/null || echo "none")
62-
case "$PERM" in
63-
admin|write)
64-
echo "trusted=true" >> "$GITHUB_OUTPUT"
65-
echo "Actor @$ACTOR has permission $PERM — trusted"
66-
;;
67-
*)
68-
echo "trusted=false" >> "$GITHUB_OUTPUT"
69-
echo "Actor @$ACTOR has permission $PERM — not trusted, skipping"
70-
;;
71-
esac
72-
else
73-
ACTOR="${{ github.event.comment.user.login }}"
74-
ASSOC="${{ github.event.comment.author_association }}"
75-
case "$ASSOC" in
76-
OWNER|MEMBER|COLLABORATOR)
77-
echo "trusted=true" >> "$GITHUB_OUTPUT"
78-
echo "Actor @$ACTOR has association $ASSOC — trusted"
79-
;;
80-
*)
81-
echo "trusted=false" >> "$GITHUB_OUTPUT"
82-
echo "Actor @$ACTOR has association $ASSOC — not trusted, skipping"
83-
;;
84-
esac
85-
fi
86-
87-
- name: Resolve PR URL
88-
id: pr
89-
if: steps.trust.outputs.trusted == 'true'
90-
env:
91-
GH_TOKEN: ${{ secrets.GH_PAT_WORKFLOWS }}
92-
run: |
93-
if [ "${{ github.event_name }}" = "pull_request" ]; then
94-
PR_URL="${{ github.event.pull_request.html_url }}"
95-
elif [ "${{ github.event_name }}" = "pull_request_review_comment" ]; then
96-
PR_URL="${{ github.event.pull_request.html_url }}"
97-
else
98-
# issue_comment: resolve the PR URL from the issue's pull_request link
99-
PR_URL=$(gh api "${{ github.event.issue.pull_request.url }}" --jq '.html_url')
100-
fi
101-
echo "pr_url=$PR_URL" >> "$GITHUB_OUTPUT"
102-
echo "PR URL: $PR_URL"
103-
104-
- name: Post acknowledgement comment
105-
if: steps.trust.outputs.trusted == 'true'
106-
env:
107-
GH_TOKEN: ${{ secrets.GH_PAT_WORKFLOWS }}
108-
PR_URL: ${{ steps.pr.outputs.pr_url }}
109-
run: |
110-
if [ "${{ github.event_name }}" = "pull_request" ]; then
111-
ACTOR="${{ github.event.sender.login }}"
112-
MSG="@${ACTOR} assigned me as reviewer — starting a fresh review now. Results will appear in a few minutes."
113-
else
114-
ACTOR="${{ github.event.comment.user.login }}"
115-
MSG="@${ACTOR} I'm on it — starting a fresh review now. Results will appear in a few minutes."
116-
fi
117-
gh pr comment "$PR_URL" --body "$(printf '<!-- pr-review-agent mention-ack -->\n%s' "${MSG}")"
118-
119-
- name: Trigger review agent
120-
if: steps.trust.outputs.trusted == 'true'
121-
env:
122-
GH_TOKEN: ${{ secrets.GH_PAT_WORKFLOWS }}
123-
PR_URL: ${{ steps.pr.outputs.pr_url }}
124-
run: |
125-
# Use repository_dispatch (requires only Contents: write, not Actions: write).
126-
# pr-review.yml listens for type "pr-review-mention" and extracts
127-
# client_payload.pr_url to run the cascade.
128-
gh api \
129-
--method POST \
130-
--header "Accept: application/vnd.github+json" \
131-
/repos/petry-projects/.github-private/dispatches \
132-
--field event_type=pr-review-mention \
133-
--field "client_payload[pr_url]=$PR_URL"
134-
echo "Dispatch sent for $PR_URL"
36+
pr-review-mention:
37+
permissions:
38+
pull-requests: write
39+
uses: ./.github/workflows/pr-review-mention-reusable.yml # local ref — always current
40+
secrets: inherit

‎scripts/compliance-audit.sh‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ FINDINGS_FILE="$REPORT_DIR/findings.json"
3434
SUMMARY_FILE="$REPORT_DIR/summary.md"
3535
ISSUES_FILE="$REPORT_DIR/issues.json"
3636

37-
REQUIRED_WORKFLOWS=(ci.yml sonarcloud.yml claude.yml dependabot-automerge.yml dependency-audit.yml agent-shield.yml)
37+
REQUIRED_WORKFLOWS=(ci.yml sonarcloud.yml claude.yml dependabot-automerge.yml dependency-audit.yml agent-shield.yml pr-review-mention.yml)
3838
# Note: codeql.yml is intentionally NOT in REQUIRED_WORKFLOWS. CodeQL is now
3939
# configured via GitHub-managed default setup (Settings → Code security →
4040
# Code scanning), not a per-repo workflow file. The check_codeql_default_setup
@@ -665,6 +665,7 @@ check_centralized_workflow_stubs() {
665665
"dependabot-rebase.yml:dependabot-rebase-reusable"
666666
"agent-shield.yml:agent-shield-reusable"
667667
"feature-ideation.yml:feature-ideation-reusable"
668+
"pr-review-mention.yml:pr-review-mention-reusable"
668669
)
669670

670671
# List the repo's workflow directory once instead of probing each file.

0 commit comments

Comments
 (0)