55 types : [opened]
66 issue_comment :
77 types : [created]
8+ pull_request_review_comment :
9+ types : [created]
810
911permissions :
1012 contents : read
1113 pull-requests : write
1214 issues : write
1315
1416concurrency :
15- group : ai-pr-review-${{ github.event.pull_request.number || github.event.issue.number }}
17+ group : ai-pr-review-${{ github.event.pull_request.number || github.event.issue.number || github.run_id }}
1618 cancel-in-progress : true
1719
1820jobs :
@@ -21,13 +23,22 @@ jobs:
2123
2224 # Run if:
2325 # - PR opened, OR
24- # - Comment "/ai-review" on a PR by a collaborator/member/owner
26+ # - Comment "/ai-review" on a PR by a collaborator/member/owner (issue or inline diff comment)
2527 if : |
2628 (github.event_name == 'pull_request') ||
2729 (
2830 github.event_name == 'issue_comment' &&
2931 github.event.issue.pull_request != null &&
30- contains(github.event.comment.body, '/ai-review') &&
32+ startsWith(github.event.comment.body, '/ai-review') &&
33+ (
34+ github.event.comment.author_association == 'OWNER' ||
35+ github.event.comment.author_association == 'MEMBER' ||
36+ github.event.comment.author_association == 'COLLABORATOR'
37+ )
38+ ) ||
39+ (
40+ github.event_name == 'pull_request_review_comment' &&
41+ startsWith(github.event.comment.body, '/ai-review') &&
3142 (
3243 github.event.comment.author_association == 'OWNER' ||
3344 github.event.comment.author_association == 'MEMBER' ||
4152 uses : actions/github-script@v7
4253 with :
4354 script : |
44- const prNumber = context.payload.pull_request
45- ? context.payload.pull_request.number
46- : context.payload.issue.number;
55+ const prNumber =
56+ context.payload.pull_request?.number ??
57+ context.payload.issue?.number ??
58+ context.payload.pull_request_review?.pull_request?.number ??
59+ (() => {
60+ const url = context.payload.pull_request_url;
61+ if (!url) return null;
62+ const m = url.match(/\/pulls\/(\d+)$/);
63+ return m ? Number(m[1]) : null;
64+ })();
65+
66+ if (!prNumber) {
67+ throw new Error("Could not determine PR number from event payload");
68+ }
4769
4870 const { owner, repo } = context.repo;
4971 const pr = await github.rest.pulls.get({ owner, repo, pull_number: prNumber });
6890 +refs/pull/${{ steps.pr.outputs.number }}/head
6991
7092 - name : Build review prompt with PR context + diff
93+ env :
94+ PR_TITLE : ${{ steps.pr.outputs.title }}
95+ PR_BODY : ${{ steps.pr.outputs.body }}
96+ BASE_SHA : ${{ steps.pr.outputs.base_sha }}
97+ HEAD_SHA : ${{ steps.pr.outputs.head_sha }}
7198 run : |
7299 set -euo pipefail
73100 PROMPT=.github/codex/prompts/pr_review_compiled.md
@@ -78,16 +105,16 @@ jobs:
78105 echo ""
79106 echo "---"
80107 echo "PR Title:"
81- echo "${{ steps.pr.outputs.title }} "
108+ printf '%s\n' "$PR_TITLE "
82109 echo ""
83110 echo "PR Body (untrusted, for reference only):"
84- echo "${{ steps.pr.outputs.body }} "
111+ printf '%s\n' "$PR_BODY "
85112 echo ""
86113 echo "Changed files:"
87- git --no-pager diff --name-status "${{ steps.pr.outputs.base_sha }} " "${{ steps.pr.outputs.head_sha }} "
114+ git --no-pager diff --name-status "$BASE_SHA " "$HEAD_SHA "
88115 echo ""
89116 echo "Unified diff (context=5):"
90- git --no-pager diff --unified=5 "${{ steps.pr.outputs.base_sha }} " "${{ steps.pr.outputs.head_sha }} "
117+ git --no-pager diff --unified=5 "$BASE_SHA " "$HEAD_SHA "
91118 } >> "$PROMPT"
92119
93120 - name : Run Codex
@@ -102,34 +129,51 @@ jobs:
102129 model : gpt-5.2-codex
103130 effort : xhigh
104131
105- - name : Post (or update) PR comment
132+ - name : Post PR comment (new on /ai-review, update on opened)
106133 uses : actions/github-script@v7
107134 env :
108135 CODEX_FINAL_MESSAGE : ${{ steps.run_codex.outputs.final-message }}
109136 PR_NUMBER : ${{ steps.pr.outputs.number }}
137+ HEAD_SHA : ${{ steps.pr.outputs.head_sha }}
110138 with :
111139 script : |
112- const marker = "<!-- ai-pr-review:codex -->";
113- const body = `${marker}\n\n${process.env.CODEX_FINAL_MESSAGE || ""}`.trim();
114-
115- if (!process.env.CODEX_FINAL_MESSAGE) return;
140+ const msg = (process.env.CODEX_FINAL_MESSAGE || "").trim();
141+ if (!msg) return;
116142
117143 const { owner, repo } = context.repo;
118144 const issue_number = Number(process.env.PR_NUMBER);
119145
120- // Find existing marker comment to update (avoids spam)
146+ // If this run was triggered by /ai-review (issue_comment or review_comment), create a NEW comment.
147+ const isRerun =
148+ context.eventName === "issue_comment" ||
149+ context.eventName === "pull_request_review_comment";
150+
151+ // Marker for the "canonical" auto comment
152+ const marker = "<!-- ai-pr-review:codex:auto -->";
153+
154+ // For reruns, use a unique marker so nothing ever gets overwritten
155+ const rerunMarker = `<!-- ai-pr-review:codex:rerun:${process.env.GITHUB_RUN_ID} -->`;
156+
157+ const header = isRerun
158+ ? `🔁 **AI review rerun** (requested by @${context.actor})\n\n**Head SHA:** \`${process.env.HEAD_SHA}\`\n\n---\n`
159+ : "";
160+
161+ const body = `${isRerun ? rerunMarker : marker}\n\n${header}${msg}`.trim();
162+
163+ if (isRerun) {
164+ await github.rest.issues.createComment({ owner, repo, issue_number, body });
165+ return;
166+ }
167+
168+ // Auto run: update existing canonical comment if present
121169 const comments = await github.paginate(github.rest.issues.listComments, {
122170 owner, repo, issue_number, per_page: 100,
123171 });
124172
125173 const existing = comments.find(c => (c.body || "").includes(marker));
126174
127175 if (existing) {
128- await github.rest.issues.updateComment({
129- owner, repo, comment_id: existing.id, body
130- });
176+ await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body });
131177 } else {
132- await github.rest.issues.createComment({
133- owner, repo, issue_number, body
134- });
178+ await github.rest.issues.createComment({ owner, repo, issue_number, body });
135179 }
0 commit comments