Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 43 additions & 12 deletions .github/prompts/codex-pr-review.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,10 @@ Before any analysis, load PR metadata, latest head SHA, and diff from the GitHub
Workflow-provided env:

- `CURRENT_HEAD_SHA` - PR head SHA for this run
- `LATEST_BOT_REVIEW_ID` - most recent prior bot review id, if any
- `LATEST_BOT_REVIEW_COMMIT` - commit SHA reviewed by that prior bot review, if any
- `IS_FOLLOW_UP_REVIEW` - `true` when contributor pushed new commits after the last bot review
- `LATEST_BOT_REVIEW_ID` - latest reusable prior bot review id; empty when no prior context is safe to reuse
- `LATEST_BOT_REVIEW_COMMIT` - commit SHA of that reusable prior review; empty when no prior context is safe to reuse
- `IS_FOLLOW_UP_REVIEW` - `true` only when the prior reviewed head is a verified ancestor on a complete, merge-free linear extension
- `PRIOR_CONTEXT_DISCARDED` - `true` when prior context was discarded after a force-push, rebase, merge commit, non-linear rollback/history rewrite, incomplete comparison, or ancestry-check failure

```bash
pr_number=$(jq -r '.pull_request.number' "$GITHUB_EVENT_PATH")
Expand All @@ -50,41 +51,68 @@ current_head_sha="${CURRENT_HEAD_SHA:-$(jq -r '.pull_request.head.sha' "$GITHUB_
latest_bot_review_id="${LATEST_BOT_REVIEW_ID:-}"
latest_bot_review_commit="${LATEST_BOT_REVIEW_COMMIT:-}"
is_follow_up_review="${IS_FOLLOW_UP_REVIEW:-false}"
prior_context_discarded="${PRIOR_CONTEXT_DISCARDED:-false}"

gh pr view "$pr_number" -R "$repo" --json number,title,body,labels,author,additions,deletions,changedFiles,files,headRefOid
gh pr diff "$pr_number" -R "$repo"

if [ "$is_follow_up_review" = "true" ] && [ -n "$latest_bot_review_id" ]; then
gh api "repos/$repo/pulls/$pr_number/reviews/$latest_bot_review_id"
gh api "repos/$repo/pulls/$pr_number/reviews/$latest_bot_review_id/comments"

if [ -n "$latest_bot_review_commit" ] && [ "$latest_bot_review_commit" != "$current_head_sha" ]; then
gh pr diff "$pr_number" -R "$repo" --name-only

if [ "$is_follow_up_review" = "true" ] && \
[ "$prior_context_discarded" != "true" ] && \
[ -n "$latest_bot_review_id" ] && \
[ -n "$latest_bot_review_commit" ] && \
[ "$latest_bot_review_commit" != "$current_head_sha" ]; then
# Defense in depth: independently verify linear ancestry before loading any
# old review text. A compare with status=diverged after a force-push/rebase
# contains base-branch changes and is not a valid incremental PR diff.
comparison=$(gh api \
"repos/$repo/compare/$latest_bot_review_commit...$current_head_sha" \
--jq '{status: .status, ahead_by: .ahead_by, behind_by: .behind_by, merge_base_sha: .merge_base_commit.sha, commit_count: (.commits | length), has_merge_commit: ([.commits[] | select((.parents | length) > 1)] | length > 0)}')
comparison_status=$(printf '%s' "$comparison" | jq -r '.status')
comparison_ahead=$(printf '%s' "$comparison" | jq -r '.ahead_by')
comparison_behind=$(printf '%s' "$comparison" | jq -r '.behind_by')
comparison_merge_base=$(printf '%s' "$comparison" | jq -r '.merge_base_sha')
comparison_commit_count=$(printf '%s' "$comparison" | jq -r '.commit_count')
comparison_has_merge=$(printf '%s' "$comparison" | jq -r '.has_merge_commit')

if [ "$comparison_status" = "ahead" ] && \
[ "$comparison_behind" = "0" ] && \
[ "$comparison_merge_base" = "$latest_bot_review_commit" ] && \
[ "$comparison_ahead" = "$comparison_commit_count" ] && \
[ "$comparison_has_merge" = "false" ]; then
gh api "repos/$repo/pulls/$pr_number/reviews/$latest_bot_review_id"
gh api "repos/$repo/pulls/$pr_number/reviews/$latest_bot_review_id/comments"
gh api -H "Accept: application/vnd.github.v3.diff" \
"repos/$repo/compare/$latest_bot_review_commit...$current_head_sha"
else
is_follow_up_review=false
prior_context_discarded=true
echo "Prior review context discarded: PR history is not a linear extension."
fi
fi
```

## Task

1. **Load context (progressive)**: `CLAUDE.md`, `README.md`, then only needed source files.
2. **Determine review mode**: `initial` when no prior bot review exists for another commit, otherwise `follow-up after new commits`.
2. **Determine review mode**: use `initial` when there is no reusable prior review, `follow-up after new commits` only for a verified linear update, and `full review after prior context reset` when `PRIOR_CONTEXT_DISCARDED=true` or the defense-in-depth ancestry check fails.
3. **Review the latest PR diff in full**: correctness, security (OWASP top 10), regressions, data loss, performance, and maintainability.
4. **File context**: the workflow checks out the trusted base branch and pre-fetches the PR head. Use `gh pr diff` for changed hunks; when you need PR-head file contents, read them with `git show "refs/remotes/pull/$pr_number/head:path/to/file"` rather than assuming the working tree is the PR head.
5. **Follow-up context**: when `IS_FOLLOW_UP_REVIEW=true`, use the previous bot review and compare diff only as context for what changed since the last bot pass. Do not limit the review to those changes.
5. **Follow-up context**: only for a verified linear update, use the previous bot review and compare diff as context for what changed since the last bot pass. Do not limit the review to those changes. After prior context is reset, do not load, repeat, or cite any prior review finding.
6. **Check tests**: note missing or inadequate coverage. Tests should be in `src/tests/` mirroring the source structure.
7. **Respond** with an evidence-based review comment (no code changes).

## Response Guidelines

- **Findings first**: order by severity (Blocker/Major/Minor/Nit).
- **Mode line**: summary must start with `Review mode: initial` or `Review mode: follow-up after new commits`.
- **Mode line**: summary must start with `Review mode: initial`, `Review mode: follow-up after new commits`, or `Review mode: full review after prior context reset`.
- **Evidence**: cite specific files and line numbers using `path:line`.
- **No speculation**: if uncertain, say so; if not found, say "Not found in repo/docs".
- **Missing info**: ask only when required; max 4 questions.
- **Language**: match the PR's language (Chinese or English); if mixed, use the dominant language.
- **Signature**: end with `*Open Cowork Bot*`.
- **Diff focus**: only comment on added/modified lines; use unchanged code only for context.
- **Authoritative scope**: the current `gh pr diff` and current Files Changed list are the only authoritative PR scope. Before reporting or repeating a finding, verify its path is currently changed and its anchor is an added or modified line; otherwise discard it.
- **Fresh-head only**: before posting, re-fetch live PR head SHA; if it differs from `CURRENT_HEAD_SHA`, stop without posting a stale review.
- **Attribution**: report only issues introduced or directly triggered by the diff; anchor comments to diff lines, citing related context if needed.
- **High signal**: if confidence < 80%, do not report; ask a question if needed.
Expand All @@ -110,6 +138,7 @@ fi
**Summary**

- Must begin with the review mode line
- Must include `Review policy: advisory — the check reflects automation health/completion only; it does not approve the PR or resolve findings.`
- If no issues: explicitly say so and mention residual risks/testing gaps

**Testing**
Expand All @@ -120,6 +149,8 @@ fi

Submit exactly one review for this run. Use a single atomic `create review` API call so summary and inline comments stay attached to the same `CURRENT_HEAD_SHA`.

This review is advisory. Keep `event: "COMMENT"`; findings do not change the workflow conclusion. The check reflects automation health/completion only; it does not approve the PR or resolve findings.

```bash
live_head_sha=$(gh pr view "$pr_number" -R "$repo" --json headRefOid -q .headRefOid)
if [ "$live_head_sha" != "$current_head_sha" ]; then
Expand Down
27 changes: 25 additions & 2 deletions .github/scripts/deepseek-common.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,23 @@ export function runGh(args, options = {}) {
}
}

/**
* Return true only when a follow-up review is a linear extension of the
* previously reviewed head. A force-push/rebase produces a diverged compare;
* using that diff as "new commits" would mix changes from the rewritten base
* into the PR review context.
*/
export function isLinearReviewUpdate(comparison, previousHeadSha) {
return Boolean(
previousHeadSha &&
comparison?.status === 'ahead' &&
Number(comparison?.behind_by) === 0 &&
comparison?.merge_base_sha === previousHeadSha &&
Number(comparison?.ahead_by) === Number(comparison?.commit_count) &&
comparison?.has_merge_commit === false
);
}

function buildGitGrepArgsFromRgArgs(args) {
const gitArgs = ['grep'];
const pathspecs = [];
Expand Down Expand Up @@ -400,8 +417,14 @@ export function loadRepoDocs(relativePaths, maxChars = 6000) {
}

export function listPullRequestFiles(repo, prNumber) {
const raw = runGh(['api', `repos/${repo}/pulls/${prNumber}/files?per_page=100`]);
return JSON.parse(raw);
const raw = runGh([
'api',
'--paginate',
'--slurp',
`repos/${repo}/pulls/${prNumber}/files?per_page=100`,
]);
const pages = JSON.parse(raw);
return pages.flatMap((page) => (Array.isArray(page) ? page : []));
}

export function loadPullRequestFileExcerpts(prNumber, filePaths, maxFiles = 6, maxChars = 4000) {
Expand Down
88 changes: 66 additions & 22 deletions .github/scripts/deepseek-pr-review.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
assertNonEmptyParsedString,
callDeepSeekJsonWithRetries,
ensureBotSignature,
isLinearReviewUpdate,
loadEventPayload,
loadPullRequestFileExcerpts,
loadRepoDocs,
Expand All @@ -22,6 +23,9 @@ Implementation note:
- Return ONLY valid JSON with the shape {"body":"FULL_MARKDOWN_REVIEW_BODY"}.
- Put every finding directly in the review body itself.
- Do not assume inline review comments are available.
- Treat the authoritative current changed-file list and unified diff as the only
source of PR-attributed findings. Never carry a prior finding forward unless
it is re-verified against a currently changed line.
- Keep the markdown body ready to post as a summary-only GitHub PR review.`;
}

Expand Down Expand Up @@ -72,6 +76,7 @@ async function main() {
const latestBotReviewId = process.env.LATEST_BOT_REVIEW_ID || '';
const latestBotReviewCommit = process.env.LATEST_BOT_REVIEW_COMMIT || '';
const isFollowUpReview = process.env.IS_FOLLOW_UP_REVIEW === 'true';
const priorContextDiscarded = process.env.PRIOR_CONTEXT_DISCARDED === 'true';

const prompt = readTextFileIfExists('.github/prompts/codex-pr-review.md');
if (!prompt) {
Expand Down Expand Up @@ -99,45 +104,82 @@ async function main() {
4000
);

let followUpContext = 'None.';
if (isFollowUpReview && latestBotReviewId) {
const review = runGh(['api', `repos/${repo}/pulls/${prNumber}/reviews/${latestBotReviewId}`]);
const reviewComments = runGh([
'api',
`repos/${repo}/pulls/${prNumber}/reviews/${latestBotReviewId}/comments`,
]);
let compareDiff = '';
if (latestBotReviewCommit && latestBotReviewCommit !== currentHeadSha) {
compareDiff = runGh([
let reviewModeHint = priorContextDiscarded ? 'full review after prior context reset' : 'initial';
let followUpContext = priorContextDiscarded
? 'Prior review context was discarded because the update was not a safe linear extension. Review only the ' +
'authoritative current PR diff below.'
: 'None.';
if (
isFollowUpReview &&
!priorContextDiscarded &&
latestBotReviewId &&
latestBotReviewCommit &&
latestBotReviewCommit !== currentHeadSha
) {
let comparison = null;
try {
comparison = JSON.parse(
runGh([
'api',
`repos/${repo}/compare/${latestBotReviewCommit}...${currentHeadSha}`,
'--jq',
'{status: .status, ahead_by: .ahead_by, behind_by: .behind_by, merge_base_sha: .merge_base_commit.sha, commit_count: (.commits | length), has_merge_commit: ([.commits[] | select((.parents | length) > 1)] | length > 0)}',
])
);
} catch (error) {
console.warn(
`Could not verify previous review ancestry; using a fresh full review: ${error.message}`
);
}

if (isLinearReviewUpdate(comparison, latestBotReviewCommit)) {
reviewModeHint = 'follow-up after new commits';
const review = runGh(['api', `repos/${repo}/pulls/${prNumber}/reviews/${latestBotReviewId}`]);
const reviewComments = runGh([
'api',
`repos/${repo}/pulls/${prNumber}/reviews/${latestBotReviewId}/comments`,
]);
const compareDiff = runGh([
'api',
'-H',
'Accept: application/vnd.github.v3.diff',
`repos/${repo}/compare/${latestBotReviewCommit}...${currentHeadSha}`,
]);
followUpContext = [
'Previous bot review from a verified ancestor on a merge-free linear extension:',
truncate(review, 8000, 'previous review'),
'Previous bot review comments from that verified ancestor:',
truncate(reviewComments, 8000, 'previous review comments'),
`Linear compare diff since previous review:\n${truncate(compareDiff, 20000, 'compare diff')}`,
].join('\n\n');
} else {
reviewModeHint = 'full review after prior context reset';
followUpContext =
'Prior review and old-head compare intentionally omitted: the previously reviewed head ' +
'is not a verified linear ancestor of the current head (force-push, rebase, merge commit, ' +
'or incomplete/unavailable history). ' +
'Review only the authoritative current PR diff below.';
}
followUpContext = [
'Previous bot review:',
truncate(review, 8000, 'previous review'),
'Previous bot review comments:',
truncate(reviewComments, 8000, 'previous review comments'),
compareDiff ? `Compare diff since previous review:\n${truncate(compareDiff, 20000, 'compare diff')}` : '',
]
.filter(Boolean)
.join('\n\n');
}

const userPrompt = [
`Repo: ${repo}`,
`PR number: ${prNumber}`,
`Current head SHA: ${currentHeadSha}`,
`Review mode hint: ${isFollowUpReview ? 'follow-up after new commits' : 'initial'}`,
`Review mode hint: ${reviewModeHint}`,
'',
'PR metadata:',
JSON.stringify(prMeta, null, 2),
'',
'Repository docs:',
serializeDocs(docs),
'',
'Follow-up context:',
followUpContext,
'',
'AUTHORITATIVE CURRENT PR CHANGED PATHS:',
files.map((file) => file.filename).join('\n') || '(none)',
'',
'Changed files and patches:',
serializeFiles(files),
'',
Expand All @@ -147,8 +189,10 @@ async function main() {
'Unified diff:',
truncate(diff, 120000, 'PR diff'),
'',
'Follow-up context:',
followUpContext,
'FINAL SCOPE GUARD:',
'Report only issues introduced or directly triggered by the authoritative current PR diff. ' +
'Do not report a prior finding unless its path is listed above and the issue is re-verified ' +
'on a currently added or modified line.',
].join('\n');

const { parsed, usage } = await callDeepSeekJsonWithRetries({
Expand Down
7 changes: 5 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@ jobs:

- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 22
cache: npm
node-version: 24.11.1
# cache: npm

- name: Generate package lock
run: npm install --package-lock-only --ignore-scripts --no-audit --no-fund

- name: Install dependencies
run: npm ci --ignore-scripts
Expand Down
Loading
Loading