-
Notifications
You must be signed in to change notification settings - Fork 9
278 lines (256 loc) · 11.1 KB
/
codex-review.yml
File metadata and controls
278 lines (256 loc) · 11.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
name: Codex PR Review
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
paths-ignore:
- 'pnpm-lock.yaml'
- 'LICENSE'
- 'knip.json'
- 'CHANGELOG.md'
- 'data/**'
- 'snapshots/**'
- '**/*.snap'
- '**/*.log'
- '**/CHANGELOG.md'
- '**/dist/**'
- '**/.turbo/**'
- '**/node_modules/**'
- '**/snapshots/**'
concurrency:
group: codex-review-${{ github.event.pull_request.number }}
cancel-in-progress: true
permissions:
contents: read
pull-requests: write
jobs:
review:
name: Codex Review
runs-on: ubuntu-latest
# 30 min ceiling: gpt-5.4 / effort: medium can take ~25 min on a
# large diff, so 15 min was hitting the cap mid-stream. The
# concurrency group above already cancel-in-progress, so a new push
# will kill any still-running review — no downside to a generous
# ceiling.
timeout-minutes: 30
# Skip fork PRs (no access to secrets) and draft PRs (still iterating)
if: |
github.event.pull_request.head.repo.full_name == github.repository
&& github.event.pull_request.draft == false
steps:
- name: Checkout PR merge commit
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: refs/pull/${{ github.event.pull_request.number }}/merge
fetch-depth: 0
- name: Generate filtered PR diff
id: diff
run: |
# Filter out noise paths from the diff sent to the model.
git diff ${{ github.event.pull_request.base.sha }}...HEAD -- \
':!pnpm-lock.yaml' \
':!LICENSE' \
':!knip.json' \
':!CHANGELOG.md' \
':!data/**' \
':!snapshots/**' \
':!**/*.snap' \
':!**/*.log' \
':!**/CHANGELOG.md' \
':!**/dist/**' \
':!**/.turbo/**' \
':!**/node_modules/**' \
':!**/snapshots/**' \
> pr-diff.patch
DIFF_LINES=$(wc -l < pr-diff.patch)
DIFF_HASH=$(sha256sum pr-diff.patch | cut -d' ' -f1)
echo "diff_lines=${DIFF_LINES}" >> "$GITHUB_OUTPUT"
echo "diff_hash=${DIFF_HASH}" >> "$GITHUB_OUTPUT"
echo "Filtered diff: ${DIFF_LINES} lines, hash ${DIFF_HASH}"
- name: Check if diff already handled (hash skip)
id: hash_check
if: steps.diff.outputs.diff_lines != '0'
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
with:
script: |
const marker = `<!-- codex-review-diff-hash: ${{ steps.diff.outputs.diff_hash }} -->`;
const reviews = await github.paginate(
github.rest.pulls.listReviews,
{
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
per_page: 100,
}
);
const matched = reviews.some(r => r.body && r.body.includes(marker));
if (matched) {
console.log('Diff hash matches a prior review or skip notice — silently skipping.');
core.setOutput('skipped', 'true');
} else {
core.setOutput('skipped', 'false');
}
- name: Skip if diff exceeds size cap
id: size_check
if: |
steps.diff.outputs.diff_lines != '0'
&& steps.diff.outputs.diff_lines > 5000
&& steps.hash_check.outputs.skipped == 'false'
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
env:
DIFF_HASH: ${{ steps.diff.outputs.diff_hash }}
DIFF_LINES: ${{ steps.diff.outputs.diff_lines }}
with:
script: |
const hashMarker = `<!-- codex-review-diff-hash: ${process.env.DIFF_HASH} -->`;
const lines = process.env.DIFF_LINES;
await github.rest.pulls.createReview({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
body: `${hashMarker}\nCodex review skipped: filtered diff is ${lines} lines (cap: 5,000). Please consider splitting this into smaller PRs for reviewability.`,
event: 'COMMENT',
comments: [],
});
- name: Run Codex review
id: codex
if: |
steps.diff.outputs.diff_lines != '0'
&& steps.diff.outputs.diff_lines <= 5000
&& steps.hash_check.outputs.skipped == 'false'
uses: openai/codex-action@v1
with:
openai-api-key: ${{ secrets.OPENAI_API_KEY }}
model: gpt-5.4
prompt-file: .codex/review-prompt.md
output-schema-file: .codex/review-schema.json
effort: medium
sandbox: read-only
- name: Post PR review with inline comments
if: |
steps.diff.outputs.diff_lines != '0'
&& steps.diff.outputs.diff_lines <= 5000
&& steps.hash_check.outputs.skipped == 'false'
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
env:
REVIEW_JSON: ${{ steps.codex.outputs.final-message }}
DIFF_HASH: ${{ steps.diff.outputs.diff_hash }}
with:
script: |
const raw = process.env.REVIEW_JSON || '';
const hashMarker = `<!-- codex-review-diff-hash: ${process.env.DIFF_HASH} -->`;
console.log(`Raw Codex output (${raw.length} chars): ${raw.slice(0, 1000)}`);
let review;
try {
review = JSON.parse(raw);
} catch (e) {
console.error('Failed to parse Codex output:', e.message);
// Transient failure: do NOT embed the hash marker, so the next
// run on this same diff retries instead of silently skipping.
await github.rest.pulls.createReview({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
body: `Codex review failed to produce valid JSON output. Check the [workflow logs](${process.env.GITHUB_SERVER_URL}/${context.repo.owner}/${context.repo.repo}/actions/runs/${process.env.GITHUB_RUN_ID}) for details.`,
event: 'COMMENT',
comments: [],
});
return;
}
// Fetch all changed files (paginated for large PRs)
const files = await github.paginate(
github.rest.pulls.listFiles,
{
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
per_page: 100,
}
);
// Build set of valid (path:line) pairs from right-side diff hunk lines
// (added + context). This keeps comments bound to changed areas.
const validLines = new Set();
for (const file of files) {
// Skip binary/large/truncated files with no patch
if (!file.patch) continue;
const lines = file.patch.split('\n');
let currentLine = 0;
for (const line of lines) {
const hunkMatch = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)/);
if (hunkMatch) {
currentLine = parseInt(hunkMatch[1], 10);
continue;
}
// Added lines are valid comment targets
if (line.startsWith('+')) {
validLines.add(`${file.filename}:${currentLine}`);
currentLine++;
continue;
}
// Deleted lines do not exist in the new file
if (line.startsWith('-')) continue;
// Ignore hunk metadata lines
if (line.startsWith('\\')) continue;
// Context lines on the right side are also valid targets
validLines.add(`${file.filename}:${currentLine}`);
currentLine++;
}
}
console.log(`Valid comment locations: ${validLines.size}`);
// Partition comments into valid (on right-side diff lines) and dropped
const comments = Array.isArray(review.comments) ? review.comments : [];
const validComments = [];
const droppedComments = [];
for (const comment of comments) {
const key = `${comment.path}:${comment.line}`;
if (validLines.has(key)) {
validComments.push({
path: comment.path,
line: comment.line,
body: comment.body,
side: 'RIGHT',
});
} else {
droppedComments.push(comment);
}
}
if (droppedComments.length > 0) {
console.log('Dropped out-of-diff comments:');
for (const c of droppedComments) {
console.log(` ${c.path}:${c.line} — ${c.body.slice(0, 80)}`);
}
}
if (validComments.length === 0) {
console.log(`No valid inline comments (${comments.length} total from Codex, ${droppedComments.length} dropped).`);
// "All dropped" means the model produced comments but every one
// targeted lines outside the right-side diff — usually a line
// mapping failure, not a real no-issues signal. Treat it as
// transient: post the warning without the hash marker so the
// next run on the same diff retries instead of silently skipping.
const isLineMappingFailure = droppedComments.length > 0;
const summary = isLineMappingFailure
? `Codex review produced ${droppedComments.length} comment(s) but all targeted lines outside the diff and were dropped. Check the [workflow logs](${process.env.GITHUB_SERVER_URL}/${context.repo.owner}/${context.repo.repo}/actions/runs/${process.env.GITHUB_RUN_ID}) for details.`
: 'Codex review completed — no issues found.';
const body = isLineMappingFailure ? summary : `${hashMarker}\n${summary}`;
await github.rest.pulls.createReview({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
body,
event: 'COMMENT',
comments: [],
});
return;
}
// Inline comments + a small body carrying the diff-hash marker so
// future runs can detect "diff unchanged" and skip.
await github.rest.pulls.createReview({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
body: hashMarker,
event: 'COMMENT',
comments: validComments,
});
console.log(
`Review posted: ${validComments.length} inline comments, ${droppedComments.length} dropped out-of-diff comments`
);