fix(scripts): ignore superseded target dispatch checks (#128704)

This commit is contained in:
Peter Steinberger
2026-08-24 05:09:28 -07:00
committed by GitHub
parent 41050b2f3b
commit e45e4ca69f
2 changed files with 104 additions and 8 deletions
+32 -8
View File
@@ -32,6 +32,7 @@ const RollupCheckSchema = z.object({
workflowRun: optionalNullable(
z.object({
databaseId: optionalNumber,
event: optionalString,
workflow: optional(z.object({ databaseId: optionalNumber })),
}),
),
@@ -66,7 +67,7 @@ const RollupResponseSchema = z.object({
repository: z.object({ pullRequest: RollupPageSchema.nullish() }).nullish(),
}),
});
const RunListItemSchema = z.object({ id: z.number(), created_at: z.string() });
const RunListItemSchema = z.object({ id: z.number(), workflow_id: optionalNumber });
const RunListSchema = z
.object({ workflow_runs: validArray(RunListItemSchema) })
.transform((response) => response.workflow_runs)
@@ -89,7 +90,7 @@ const FAILURE_CONCLUSIONS = new Set([
"STALE",
"TIMED_OUT",
]);
const ROLLUP_QUERY = `query($owner:String!,$name:String!,$pr:Int!,$cursor:String){repository(owner:$owner,name:$name){pullRequest(number:$pr){state mergeable headRefOid statusCheckRollup{state contexts(first:100,after:$cursor){totalCount pageInfo{hasNextPage endCursor} nodes{kind:__typename ... on CheckRun{name status conclusion databaseId checkSuite{workflowRun{databaseId workflow{databaseId}}}} ... on StatusContext{context state}}}}}}}`;
const ROLLUP_QUERY = `query($owner:String!,$name:String!,$pr:Int!,$cursor:String){repository(owner:$owner,name:$name){pullRequest(number:$pr){state mergeable headRefOid statusCheckRollup{state contexts(first:100,after:$cursor){totalCount pageInfo{hasNextPage endCursor} nodes{kind:__typename ... on CheckRun{name status conclusion databaseId checkSuite{workflowRun{databaseId event workflow{databaseId}}}} ... on StatusContext{context state}}}}}}}`;
const GH_READ_OPTIONS = {
stdio: ["ignore", "pipe", "pipe"],
timeout: 60_000,
@@ -192,7 +193,7 @@ function checkRunIdentity(check: RollupCheck) {
const newerJob = (a: JobIdentity, b: JobIdentity) =>
a.runId !== b.runId ? a.runId > b.runId : a.checkId > b.checkId;
export function classifyRollup(rollup: RollupPayload | null | undefined) {
export function classifyRollup(rollup: RollupPayload | null | undefined, runs: RunListItem[] = []) {
const rawNodes = rollup?.contexts?.nodes ?? [];
const hiddenContextCount = Math.max(
0,
@@ -221,8 +222,8 @@ export function classifyRollup(rollup: RollupPayload | null | undefined) {
let supersededCount = 0;
// Re-triggers leave every prior run's check runs on the SHA forever and GitHub's aggregate
// counts them. A check is superseded when a newer same-workflow check shares its name
// (GitHub's latest-name-wins semantics), or when it was cancelled and its workflow has a
// newer run (draft->ready cancels the old run before the replacement posts check runs).
// (GitHub's latest-name-wins semantics), or when its cancelled workflow has a newer run.
// Actions run metadata also proves target-run supersession before a newer run posts jobs.
// Older-run checks with unique names stay visible so distinct invocations are not dropped.
const nodes = rawNodes.filter((check) => {
const identity = checkRunIdentity(check);
@@ -236,8 +237,13 @@ export function classifyRollup(rollup: RollupPayload | null | undefined) {
return false;
}
}
const newestRun = newestRunByWorkflow.get(identity.workflowId);
if (check.conclusion === "CANCELLED" && newestRun !== undefined && newestRun > identity.runId) {
const newestRun = newestRunByWorkflow.get(identity.workflowId) ?? identity.runId;
if (
check.conclusion === "CANCELLED" &&
(newestRun > identity.runId ||
(check.checkSuite?.workflowRun?.event === "pull_request_target" &&
runs.some((run) => run.workflow_id === identity.workflowId && run.id > identity.runId)))
) {
supersededCount += 1;
return false;
}
@@ -310,6 +316,13 @@ const findRun = (repo: string, sha: string, after?: number) =>
RunListSchema.parse(execGhJson(buildFindRunArgs(repo, sha), GH_READ_OPTIONS)),
after,
);
const findTargetRuns = (repo: string, sha: string) =>
RunListSchema.parse(
execGhJson(
["api", `repos/${repo}/actions/runs?event=pull_request_target&head_sha=${sha}&per_page=100`],
GH_READ_OPTIONS,
),
);
const readRun = (repo: string, runId: number) =>
RunStatusSchema.parse(
execGhJson(
@@ -541,7 +554,18 @@ async function main(argv = process.argv.slice(2)) {
if (blocked !== null) {
return blocked;
}
const result = classifyRollup(pr.statusCheckRollup);
let result = classifyRollup(pr.statusCheckRollup);
if (
result.verdict === "FAILING" &&
pr.statusCheckRollup?.contexts?.nodes?.some(
(check) =>
check.conclusion === "CANCELLED" &&
check.checkSuite?.workflowRun?.event === "pull_request_target" &&
checkRunIdentity(check),
)
) {
result = classifyRollup(pr.statusCheckRollup, findTargetRuns(args.repo, args.headSha));
}
lastState = pr.statusCheckRollup?.state ?? "NONE";
lastPending = result.pendingCount;
console.log(
+72
View File
@@ -320,6 +320,78 @@ describe("watch-pr-ci", () => {
).toEqual({ verdict: "PENDING", pendingCount: 2, failingNames: [], supersededCount: 1 });
});
it.each<{
label: string;
name?: string;
conclusion?: string;
event?: string | null;
workflowId?: number | null;
newerRunId?: number;
newerWorkflowId?: number | null;
ciStatus?: string;
verdict?: string;
}>([
{ label: "pending CI", ciStatus: "IN_PROGRESS", verdict: "PENDING" },
{ label: "successful CI with a different check name", name: "target guard", verdict: "GREEN" },
{ label: "latest target cancellation", newerRunId: 100 },
{ label: "real target failure", conclusion: "FAILURE" },
{ label: "another event's cancellation", event: "pull_request" },
{ label: "another workflow", newerWorkflowId: 20 },
{ label: "unknown event", event: null },
{ label: "unknown visible workflow identity", workflowId: null },
{ label: "unknown replacement workflow identity", newerWorkflowId: null },
])(
"uses newer same-workflow target-run identity without hiding $label",
({
name = "dispatch",
conclusion = "CANCELLED",
event = "pull_request_target",
workflowId = 10,
newerRunId = 200,
newerWorkflowId = 10,
ciStatus = "COMPLETED",
verdict = "FAILING",
}) => {
expect(
classifyRollup(
{
state: "FAILURE",
contexts: {
nodes: [
{
kind: "CheckRun",
name,
status: "COMPLETED",
conclusion,
checkSuite: {
workflowRun: {
databaseId: 100,
event: event ?? undefined,
workflow: { databaseId: workflowId ?? undefined },
},
},
},
{
kind: "CheckRun",
name: "CI",
status: ciStatus,
conclusion: ciStatus === "COMPLETED" ? "SUCCESS" : null,
checkSuite: { workflowRun: { databaseId: 200, workflow: { databaseId: 20 } } },
},
],
},
},
[{ id: newerRunId, workflow_id: newerWorkflowId ?? undefined }],
),
).toEqual({
verdict,
pendingCount: ciStatus === "IN_PROGRESS" ? 1 : 0,
failingNames: verdict === "FAILING" ? [name] : [],
supersededCount: verdict === "FAILING" ? 0 : 1,
});
},
);
it("keeps only the newest same-run check attempt while its replacement is pending", () => {
expect(
classifyRollup({