Compare commits

...
Author SHA1 Message Date
bincxz b3224b6dc4 fix(ci): dedupe @codex only via head-pinned markers
Drop plain-unpinned timestamp heuristics — they cannot safely know which
SHA a bare @codex review targeted. Skip re-request when a trusted author
already pinned this head with cursor-codex-head or cursor-external-codex.
2026-08-05 23:27:44 +08:00
bincxz 874ab86ce2 fix(ci): use PR head-change time for plain @codex dedupe
Commit author/committer dates predate cherry-picks and rebases, so a
plain @codex for the previous head could suppress re-request after a
new synchronize. Pass pull_request.updated_at (moves with the push)
as notBefore instead.
2026-08-05 23:20:35 +08:00
bincxz 56bb359ee7 fix(ci): only dedupe plain @codex against the current head push
Plain unpinned requests no longer skip solely by age — that blocked
re-request after a new synchronize when an older plain @codex still
looked "recent". Callers pass the head commit time as notBefore so
only post-push plain requests suppress a duplicate.
2026-08-05 23:14:03 +08:00
bincxz cda7a308fa fix(ci): dedupe same-head @codex review requests
Plain maintainer/Cursor-bot @codex review comments did not plant the
cursor-external-codex marker, so own_rerequest on synchronize posted a
second request for the same SHA. That concurrent pair produced a clean
issue comment from one job and a findings review from the other.

Skip re-request when any trusted or Cursor-bot author already requested
this head via external marker, cursor-codex-head pin, or a recent plain
@codex review.
2026-08-05 23:06:10 +08:00
3 changed files with 165 additions and 9 deletions

No files matched your search

+1 -2
View File
@@ -5361,8 +5361,7 @@ jobs:
},
);
const forceRetry =
process.env.GITHUB_EVENT_NAME === 'workflow_dispatch';
if (
process.env.GITHUB_EVENT_NAME === 'workflow_dispatch'; if (
!forceRetry &&
auto.shouldSkipExternalCodexRerequest({
existingComments: comments,
+69 -7
View File
@@ -1805,18 +1805,80 @@ function buildExternalCodexRerequestComment(headSha) {
].join('\n');
}
/**
* True when a comment body is a Codex review *request* (not a clean summary).
* Matches "@codex review" as a mention token, not "Codex Review: …" summaries.
*/
function isCodexReviewRequestBody(body) {
const text = String(body || '');
if (!/(^|[^A-Za-z0-9_@])@codex\s+review\b/i.test(text)) return false;
// Connector clean/noise posts never use the @codex request form.
return true;
}
/**
* Authors whose @codex review posts count for same-head dedupe.
* Includes maintainers/GITHUB_TOKEN and Cursor's PR bot (often posts a plain
* @codex review before our own_rerequest job lands).
*/
function isCodexRequestDedupeAuthor(login, { ownActors } = {}) {
if (isTrustedAutomationControlAuthor(login, { ownActors })) return true;
const name = String(login || '')
.trim()
.toLowerCase()
.replace(/^@/, '');
return name === 'cursor[bot]' || name === 'cursor';
}
/**
* Skip posting another @codex review when this head was already requested.
*
* Historical bug: only `cursor-external-codex:SHA` counted. Automation
* comments that pin `cursor-codex-head:SHA` (and maintainer requests built the
* same way) did not, so a second synchronize-path request could fire for the
* same SHA while an earlier job was still in flight — clean issue comment from
* one job, findings review from the other.
*
* Skip when a trusted/dedupe author already requested this head via:
* - `cursor-external-codex:SHA`, or
* - `cursor-codex-head:SHA` pin matching headSha.
*
* Plain unpinned "@codex review" is intentionally ignored: without a head pin
* we cannot tell which SHA it targeted, and timestamp heuristics (age, commit
* date, PR.updated_at) all have false-positive races. Prefer always planting
* a head pin (buildCodexReviewRequestComment) so re-request is safe.
*
* `notBefore` is accepted but unused (kept so older workflow callers do not
* throw if they still pass it).
*/
function shouldSkipExternalCodexRerequest({
existingComments = [],
headSha,
ownActors,
notBefore: _notBefore = null,
notBeforeSlackMs: _notBeforeSlackMs = 5_000,
} = {}) {
const marker = `<!-- cursor-external-codex:${sanitizeUntrustedText(headSha, 64)} -->`;
return existingComments.some(
(c) =>
isTrustedAutomationControlAuthor(c?.user?.login || c?.login, {
ownActors,
}) && String(c.body || '').includes(marker),
);
const want = String(headSha || '')
.trim()
.toLowerCase();
if (!want) return false;
const externalMarker = `<!-- cursor-external-codex:${sanitizeUntrustedText(want, 64)} -->`;
return existingComments.some((c) => {
const login = c?.user?.login || c?.login;
if (!isCodexRequestDedupeAuthor(login, { ownActors })) return false;
const body = String(c?.body || '');
if (!isCodexReviewRequestBody(body)) return false;
// Explicit external marker for this SHA (legacy + current automation).
if (body.includes(externalMarker)) return true;
// Automation / human request that pins this head.
const pinned = extractRequestedHeadSha(body);
if (pinned && commitShasMatch(want, pinned)) return true;
return false;
});
}
function buildSlackPayload({
+95
View File
@@ -4139,6 +4139,101 @@ test('shouldSkipExternalCodexRerequest matches trusted head sha marker only', ()
);
});
test('shouldSkipExternalCodexRerequest honors head pins; ignores plain unpinned @codex', () => {
const sha = 'deadbeefcafebabe000000000000000000000001';
const short = sha.slice(0, 12);
// Automation request with head pin but no external marker still dedupes.
assert.equal(
auto.shouldSkipExternalCodexRerequest({
headSha: sha,
ownActors: 'binaricat,netcatty-bot,github-actions[bot]',
existingComments: [
{
user: { login: 'binaricat' },
body: auto.buildCodexReviewRequestComment(1, sha),
},
],
}),
true,
);
// Short SHA pin matches full head.
assert.equal(
auto.shouldSkipExternalCodexRerequest({
headSha: sha,
ownActors: 'binaricat',
existingComments: [
{
user: { login: 'binaricat' },
body: `<!-- cursor-automation -->\n\n@codex review\n\n<!-- cursor-codex-head:${short} -->`,
},
],
}),
true,
);
// Plain unpinned @codex never suppresses — cannot know which SHA it meant.
assert.equal(
auto.shouldSkipExternalCodexRerequest({
headSha: sha,
ownActors: 'binaricat',
notBefore: '2026-08-05T14:00:00Z',
existingComments: [
{
user: { login: 'cursor[bot]' },
created_at: '2026-08-05T14:00:30Z',
body: '@codex review',
},
],
}),
false,
);
assert.equal(
auto.shouldSkipExternalCodexRerequest({
headSha: sha,
ownActors: 'binaricat',
existingComments: [
{
user: { login: 'binaricat' },
created_at: '2026-08-05T14:00:10Z',
body: '@codex review',
},
],
}),
false,
);
// Connector clean summary is not a request.
assert.equal(
auto.shouldSkipExternalCodexRerequest({
headSha: sha,
ownActors: 'binaricat',
existingComments: [
{
user: { login: 'chatgpt-codex-connector[bot]' },
created_at: '2026-08-05T14:00:59Z',
body: "Codex Review: Didn't find any major issues.\n\n**Reviewed commit:** `deadbeef`",
},
],
}),
false,
);
// Different head pin does not skip.
assert.equal(
auto.shouldSkipExternalCodexRerequest({
headSha: sha,
ownActors: 'binaricat',
existingComments: [
{
user: { login: 'binaricat' },
body: auto.buildCodexReviewRequestComment(
1,
'ffffffffffffffffffffffffffffffffffffffff',
),
},
],
}),
false,
);
});
test('parseCodexReviewOutcome accepts clean reaction without summary text', () => {
const outcome = auto.parseCodexReviewOutcome({
summaryText: '',