Skip to content

fix(node-ui): canonicalize context graph layer counts #2555

fix(node-ui): canonicalize context graph layer counts

fix(node-ui): canonicalize context graph layer counts #2555

Workflow file for this run

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
# Default everything to read-only; the one job that posts review comments
# escalates `pull-requests: write` at the job level only.
permissions:
contents: read
jobs:
review:
name: Codex Review
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
# 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.3.1
with:
ref: refs/pull/${{ github.event.pull_request.number }}/merge
fetch-depth: 0
persist-credentials: false
- name: Generate filtered PR diff
id: diff
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: |
# Filter out noise paths from the diff sent to the model.
git diff "$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
env:
DIFF_HASH: ${{ steps.diff.outputs.diff_hash }}
with:
script: |
const marker = `<!-- codex-review-diff-hash: ${process.env.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@e0fdf01220eb9a88167c4898839d273e3f2609d1 # v1.8
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`
);