Skip to content

source value 8 is obsolete #6

source value 8 is obsolete

source value 8 is obsolete #6

Workflow file for this run

name: Codex PR Review
on:
pull_request_target:
types: [opened, reopened]
issue_comment:
types: [created]
permissions:
contents: read
pull-requests: write
concurrency:
group: codex-review-${{ github.event.pull_request.number || github.event.issue.number || github.ref }}
cancel-in-progress: true
jobs:
gate-report:
if: |
(github.event_name != 'issue_comment' || (
github.event.issue.pull_request &&
github.event.comment.body == '#codex-review'
))
runs-on: ubuntu-latest
steps:
- name: Print pre-gate decision
shell: bash
run: |
echo "Pre-gate report"
echo " event_name: ${{ github.event_name }}"
echo " issue_comment_on_pr: ${{ github.event.issue.pull_request && 'true' || 'false' }}"
echo " comment_body: ${{ github.event.comment.body || '' }}"
echo " event_author_association: ${{ github.event.pull_request.author_association || github.event.comment.author_association || '' }}"
resolve-pr-context:
needs: gate-report
if: needs.gate-report.result == 'success'
runs-on: ubuntu-latest
outputs:
pull_number: ${{ steps.resolve.outputs.pull_number }}
head_ref: ${{ steps.resolve.outputs.head_ref }}
base_ref: ${{ steps.resolve.outputs.base_ref }}
head_sha: ${{ steps.resolve.outputs.head_sha }}
base_sha: ${{ steps.resolve.outputs.base_sha }}
pr_body: ${{ steps.resolve.outputs.pr_body }}
author_association: ${{ steps.resolve.outputs.author_association }}
milestone_number: ${{ steps.resolve.outputs.milestone_number }}
milestone_title: ${{ steps.resolve.outputs.milestone_title }}
should_run_downstream: ${{ steps.resolve.outputs.should_run_downstream }}
gate_reason: ${{ steps.resolve.outputs.gate_reason }}
steps:
- name: Resolve PR context
id: resolve
uses: actions/github-script@v7
with:
script: |
const eventName = context.eventName;
const expectedCommand = '#codex-review';
const expectedAssociations = ['MEMBER', 'OWNER'];
const isIssueComment = eventName === 'issue_comment';
const isPullRequestComment = Boolean(context.payload.issue?.pull_request);
const receivedCommentBody = context.payload.comment?.body ?? '';
const receivedCommentAuthorAssociation = context.payload.comment?.author_association ?? '';
const commentCommandMatches = receivedCommentBody === expectedCommand;
let pr = context.payload.pull_request;
if (!pr && eventName === 'issue_comment') {
if (!context.payload.issue?.pull_request) {
core.setFailed('issue_comment is not on a pull request');
return;
}
const pull_number = context.issue.number;
const response = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number,
});
pr = response.data;
}
if (!pr) {
core.setFailed('Unable to resolve pull request context from event payload');
return;
}
const prAuthorAssociation = pr.author_association || '';
const headRef = pr.head?.ref || '';
const baseRef = pr.base?.ref || '';
const isMilestoneBranch = (ref) => ref.startsWith('milestone/') || ref.startsWith('milestones/');
const milestoneBranchGatePassed = isMilestoneBranch(headRef) || isMilestoneBranch(baseRef);
const commandGatePassed = !isIssueComment || (isPullRequestComment && commentCommandMatches);
const shouldRunDownstream = commandGatePassed && milestoneBranchGatePassed;
let gateReason = 'ok';
if (isIssueComment && !isPullRequestComment) {
gateReason = 'not_pr_comment';
} else if (isIssueComment && !commentCommandMatches) {
gateReason = 'bad_command';
} else if (!milestoneBranchGatePassed) {
gateReason = 'non_milestone_branch';
}
core.info(`Gate check: expected event trigger command for issue_comment "${expectedCommand}", received "${receivedCommentBody}"`);
core.info(`Gate check: expected issue_comment on PR=true, received ${String(isPullRequestComment)}`);
core.info(`Gate check: expected comment author association in [${expectedAssociations.join(', ')}], received "${receivedCommentAuthorAssociation}"`);
core.info(`Gate check: expected PR author association in [${expectedAssociations.join(', ')}], received "${prAuthorAssociation}"`);
core.info(`Gate check: expected head/base ref under milestone/*, received head_ref="${headRef}", base_ref="${baseRef}", milestoneBranchGatePassed=${String(milestoneBranchGatePassed)}`);
core.info(`Gate evaluation: commandGatePassed=${String(commandGatePassed)}, shouldRunDownstream=${String(shouldRunDownstream)}, gateReason="${gateReason}"`);
core.setOutput('pull_number', String(pr.number));
core.setOutput('head_ref', headRef);
core.setOutput('base_ref', baseRef);
core.setOutput('head_sha', pr.head?.sha || '');
core.setOutput('base_sha', pr.base?.sha || '');
core.setOutput('pr_body', pr.body || '');
core.setOutput('author_association', prAuthorAssociation);
core.setOutput('milestone_number', pr.milestone?.number ? String(pr.milestone.number) : '');
core.setOutput('milestone_title', pr.milestone?.title || '');
core.setOutput('should_run_downstream', String(shouldRunDownstream));
core.setOutput('gate_reason', gateReason);
rebase-check:
needs: resolve-pr-context
if: needs.resolve-pr-context.outputs.should_run_downstream == 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout PR head
uses: actions/checkout@v4
with:
ref: ${{ needs.resolve-pr-context.outputs.head_sha }}
fetch-depth: 0
- name: Rebase check bypass for milestone head branches
if: |
startsWith(needs.resolve-pr-context.outputs.head_ref, 'milestone/') ||
startsWith(needs.resolve-pr-context.outputs.head_ref, 'milestones/')
shell: bash
run: |
echo "Bypassing rebase-check for milestone head branch: ${{ needs.resolve-pr-context.outputs.head_ref }}"
- name: Ensure PR is rebased onto base
if: |
!startsWith(needs.resolve-pr-context.outputs.head_ref, 'milestone/') &&
!startsWith(needs.resolve-pr-context.outputs.head_ref, 'milestones/')
shell: bash
run: |
git fetch --no-tags origin ${{ needs.resolve-pr-context.outputs.base_sha }}
if ! git merge-base --is-ancestor \
${{ needs.resolve-pr-context.outputs.base_sha }} \
${{ needs.resolve-pr-context.outputs.head_sha }}; then
echo "PR head is not rebased onto base."
echo "Base SHA: ${{ needs.resolve-pr-context.outputs.base_sha }}"
echo "Head SHA: ${{ needs.resolve-pr-context.outputs.head_sha }}"
exit 1
fi
codex-review:
needs: [resolve-pr-context, rebase-check]
if: needs.resolve-pr-context.outputs.should_run_downstream == 'true'
runs-on: ubuntu-latest
outputs:
summary: ${{ steps.parse_review.outputs.summary }}
findings_json: ${{ steps.parse_review.outputs.findings_json }}
steps:
- name: Checkout PR merge ref
uses: actions/checkout@v4
with:
ref: ${{ needs.resolve-pr-context.outputs.head_sha }}
fetch-depth: 0
- name: Debug workflow refs
shell: bash
run: |
echo "GITHUB_WORKFLOW_REF=$GITHUB_WORKFLOW_REF"
echo "GITHUB_REF=$GITHUB_REF"
echo "GITHUB_SHA=$GITHUB_SHA"
echo "--- workflow file (first 120 lines) ---"
sed -n '1,120p' .github/workflows/codex-review.yml
- name: Fetch PR base and head
shell: bash
run: |
git fetch --no-tags origin ${{ needs.resolve-pr-context.outputs.base_sha }}
git fetch --no-tags origin ${{ needs.resolve-pr-context.outputs.head_sha }}
- name: Resolve migration guide
id: guide
shell: bash
run: |
num="${{ needs.resolve-pr-context.outputs.milestone_number }}"
if [ -z "$num" ]; then
echo "PR has no milestone set."
exit 1
fi
title="${{ needs.resolve-pr-context.outputs.milestone_title }}"
if [ -z "$title" ]; then
echo "PR has no milestone title set."
exit 1
fi
# Sanitize title for filename matching
safe=$(echo "$title" | sed -E 's/[^A-Za-z0-9._-]+/_/g' | sed -E 's/^_+|_+$//g')
glob="docs/migrations/${safe}.md"
matches=$(ls $glob 2>/dev/null || true)
count=$(echo "$matches" | sed '/^$/d' | wc -l | tr -d ' ')
if [ "$count" -ne 1 ]; then
echo "Expected exactly one migration guide for milestone title: $title"
echo "Sanitized title: $safe"
echo "Attempted match: $glob"
echo "Match count: $count"
if [ -n "$matches" ]; then
echo "Matches:"
echo "$matches"
fi
exit 1
fi
echo "GUIDE_PATH=$matches" >> "$GITHUB_ENV"
- name: Build Codex prompt
shell: bash
run: |
cat .github/codex/prompts/review.md > /tmp/codex_prompt.md
echo "" >> /tmp/codex_prompt.md
echo "AGENTS instructions (from HEAD):" >> /tmp/codex_prompt.md
cat AGENTS.md >> /tmp/codex_prompt.md
echo "" >> /tmp/codex_prompt.md
echo "Migration guide (from HEAD): $GUIDE_PATH" >> /tmp/codex_prompt.md
cat "$GUIDE_PATH" >> /tmp/codex_prompt.md
echo "" >> /tmp/codex_prompt.md
echo "PR description (from GitHub):" >> /tmp/codex_prompt.md
cat <<'EOF' >> /tmp/codex_prompt.md
${{ needs.resolve-pr-context.outputs.pr_body }}
EOF
echo "" >> /tmp/codex_prompt.md
echo "Review only the PR changes using:" >> /tmp/codex_prompt.md
echo "git diff ${{ needs.resolve-pr-context.outputs.base_sha }}...${{ needs.resolve-pr-context.outputs.head_sha }}" >> /tmp/codex_prompt.md
echo "" >> /tmp/codex_prompt.md
echo "For any inline comment, first find the exact line number with: nl -ba <file>." >> /tmp/codex_prompt.md
- name: Run Codex review
id: codex
uses: openai/codex-action@v1
with:
openai-api-key: ${{ secrets.OPENAI_API_KEY }}
prompt-file: /tmp/codex_prompt.md
sandbox: read-only
- name: Parse Codex review output
id: parse_review
uses: actions/github-script@v7
env:
REVIEW_BODY: ${{ steps.codex.outputs.final-message }}
with:
script: |
const raw = process.env.REVIEW_BODY || '';
let summary = raw || 'Codex review produced no output.';
let comments = [];
try {
const parsed = JSON.parse(raw);
summary = parsed.summary || summary;
comments = Array.isArray(parsed.comments) ? parsed.comments : [];
} catch (err) {
core.setFailed(`Codex output is not valid JSON: ${err.message}`);
return;
}
const findings = comments
.filter((c) => c && c.path && Number.isInteger(c.line))
.map((c, idx) => ({
finding_index: idx,
path: c.path,
line: c.line,
body: c.body || c.summary || c.message || 'Codex review comment.',
}));
core.info(`Parsed findings: ${findings.length}`);
core.setOutput('summary', summary);
core.setOutput('findings_json', JSON.stringify(findings));
codex-match-and-post:
needs: [resolve-pr-context, rebase-check, codex-review]
if: needs.resolve-pr-context.outputs.should_run_downstream == 'true'
runs-on: ubuntu-latest
steps:
- name: Fetch unresolved review threads
id: unresolved
uses: actions/github-script@v7
with:
script: |
const pullNumber = Number('${{ needs.resolve-pr-context.outputs.pull_number }}');
const query = `
query($owner: String!, $repo: String!, $number: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $number) {
reviewThreads(first: 100) {
nodes {
id
isResolved
comments(first: 100) {
nodes {
databaseId
body
path
line
originalLine
author {
login
}
}
}
}
}
}
}
}
`;
const result = await github.graphql(query, {
owner: context.repo.owner,
repo: context.repo.repo,
number: pullNumber,
});
const nodes = result.repository.pullRequest.reviewThreads.nodes || [];
const unresolved = nodes
.filter((t) => !t.isResolved)
.map((t) => {
const comments = t.comments?.nodes || [];
const last = comments[comments.length - 1];
const first = comments[0];
return {
thread_id: t.id,
comment_id: last?.databaseId || first?.databaseId || null,
path: last?.path || first?.path || '',
line: last?.line ?? last?.originalLine ?? first?.line ?? first?.originalLine ?? null,
body: last?.body || '',
author: last?.author?.login || 'unknown',
};
})
.filter((t) => t.comment_id !== null);
core.info(`Unresolved threads loaded: ${unresolved.length}`);
core.setOutput('threads_json', JSON.stringify(unresolved));
- name: Build Codex matching prompt
shell: bash
run: |
cat > /tmp/codex_match_prompt.md <<'EOF'
You are matching new Codex findings against existing unresolved PR review threads.
Goal:
- For each new finding, decide whether it should:
1) open a new thread (`new_thread`)
2) reply to an existing unresolved thread (`reply_existing`)
Matching guidance:
- Prefer reply_existing when the unresolved thread and new finding are similar or related in intent.
- Consider path, nearby line, and semantic meaning of the issue.
- If uncertain, choose new_thread.
Output strict JSON only:
{
"actions": [
{
"finding_index": 0,
"action": "new_thread | reply_existing",
"target_comment_id": 123456789,
"reason": "short reason"
}
]
}
Rules:
- Include exactly one action per finding_index.
- For action=new_thread, omit target_comment_id.
- For action=reply_existing, include target_comment_id from unresolved threads input.
EOF
{
echo ""
echo "New findings JSON:"
echo '${{ needs.codex-review.outputs.findings_json }}'
echo ""
echo "Unresolved review threads JSON:"
echo '${{ steps.unresolved.outputs.threads_json }}'
} >> /tmp/codex_match_prompt.md
- name: Run Codex matcher
id: codex_match
uses: openai/codex-action@v1
with:
openai-api-key: ${{ secrets.OPENAI_API_KEY }}
prompt-file: /tmp/codex_match_prompt.md
sandbox: read-only
- name: Post matched review updates
uses: actions/github-script@v7
env:
SUMMARY: ${{ needs.codex-review.outputs.summary }}
FINDINGS_JSON: ${{ needs.codex-review.outputs.findings_json }}
MATCH_RAW: ${{ steps.codex_match.outputs.final-message }}
PULL_NUMBER: ${{ needs.resolve-pr-context.outputs.pull_number }}
with:
script: |
const summary = process.env.SUMMARY || 'Codex review completed.';
const pullNumber = Number(process.env.PULL_NUMBER);
const findings = JSON.parse(process.env.FINDINGS_JSON || '[]');
let actions = [];
try {
const parsed = JSON.parse(process.env.MATCH_RAW || '{}');
actions = Array.isArray(parsed.actions) ? parsed.actions : [];
} catch (err) {
core.warning(`Codex matcher output parse failed. Falling back to new_thread for all findings: ${err.message}`);
}
const actionByIndex = new Map();
for (const a of actions) {
if (!a || !Number.isInteger(a.finding_index)) continue;
actionByIndex.set(a.finding_index, a);
}
const newReviewComments = [];
const groupedReplies = new Map();
for (const finding of findings) {
const action = actionByIndex.get(finding.finding_index);
const shouldReply = action &&
action.action === 'reply_existing' &&
Number.isInteger(action.target_comment_id);
if (shouldReply) {
const key = String(action.target_comment_id);
const existing = groupedReplies.get(key) || [];
existing.push({
body: finding.body,
path: finding.path,
line: finding.line,
reason: action.reason || '',
});
groupedReplies.set(key, existing);
core.info(`Finding ${finding.finding_index} matched existing comment ${key}`);
} else {
newReviewComments.push({
path: finding.path,
line: finding.line,
side: 'RIGHT',
body: finding.body,
});
core.info(`Finding ${finding.finding_index} treated as new_thread`);
}
}
for (const [commentId, matchedFindings] of groupedReplies.entries()) {
const lines = matchedFindings.map((f) => `- \`${f.path}:${f.line}\` ${f.body}`);
const reasons = matchedFindings
.map((f) => f.reason)
.filter(Boolean)
.map((r) => `- ${r}`);
const replyBody = [
'Codex follow-up: this issue still appears related to this unresolved thread.',
'',
'**Current findings**',
...lines,
reasons.length ? '' : null,
reasons.length ? '**Matching rationale**' : null,
...reasons,
].filter(Boolean).join('\n');
await github.rest.pulls.createReplyForReviewComment({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pullNumber,
comment_id: Number(commentId),
body: replyBody,
});
}
if (newReviewComments.length > 0) {
try {
await github.rest.pulls.createReview({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pullNumber,
event: 'REQUEST_CHANGES',
body: summary,
comments: newReviewComments,
});
} catch (err) {
const message = err?.message || '';
const isUnresolvedLine = err?.status === 422 && message.includes('Line could not be resolved');
if (!isUnresolvedLine) {
throw err;
}
core.warning(`Inline review comment could not be resolved. Falling back to body-only review: ${message}`);
const fallbackFindings = newReviewComments.map((c) => `- \`${c.path}:${c.line}\` ${c.body}`);
const fallbackBody = [
summary,
'',
'Inline comment positions could not be resolved for one or more findings, so they are listed below instead:',
'',
'**Unresolved inline findings**',
...fallbackFindings,
].join('\n');
await github.rest.pulls.createReview({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pullNumber,
event: 'REQUEST_CHANGES',
body: fallbackBody,
comments: [],
});
}
return;
}
if (findings.length > 0) {
const matchedOnlySummary = [
summary,
'',
'No new inline findings were identified in this run.',
'Existing unresolved issues remain, and follow-up comments were posted on related existing review threads.',
].join('\n');
await github.rest.pulls.createReview({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pullNumber,
event: 'COMMENT',
body: matchedOnlySummary,
comments: [],
});
return;
}
if (findings.length === 0) {
await github.rest.pulls.createReview({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pullNumber,
event: 'APPROVE',
body: summary,
comments: [],
});
}