fix(ci): accept neutral draft gate runs (#120310)

* fix(ci): accept neutral draft gate runs

* fix(ci): retain decisive gate success

* fix(ci): keep pending gate runs authoritative

* fix(ci): order hosted gates by creation
This commit is contained in:
Vincent Koc
2026-08-08 04:53:10 +08:00
committed by GitHub
parent 588d8bb853
commit 3a0216ce29
2 changed files with 119 additions and 16 deletions
+15 -8
View File
@@ -174,8 +174,10 @@ function matchingAuthoritativeRuns(runs, workflowName, sha, allowManual = true)
}
function latestRun(runs) {
return runs.toSorted((left, right) =>
String(right.updated_at ?? "").localeCompare(String(left.updated_at ?? "")),
// GitHub run_number is creation order; updated_at moves as jobs finish.
return runs.toSorted(
(left, right) =>
Number(right.run_number ?? right.id ?? 0) - Number(left.run_number ?? left.id ?? 0),
)[0];
}
@@ -370,18 +372,23 @@ function isGateProvenInProgressRun(run, ciGateJobs, nowMs) {
function preferredCiRun(runs, nowMs) {
const scheduledRuns = runs.filter((run) => run.event === "pull_request");
const latestScheduledRun = latestRun(scheduledRuns);
const latestCompletedScheduledRun = latestRun(
scheduledRuns.filter((run) => run.status === "completed"),
const latestDecision = latestRun(
scheduledRuns.filter(
(run) => run.status === "completed" && !["cancelled", "skipped"].includes(run.conclusion),
),
);
const latestManualRun = latestRun(runs.filter((run) => run.event === "workflow_dispatch"));
// Manual proof may replace stale scheduled success or a pending run,
// never an unresolved terminal non-success.
if (latestCompletedScheduledRun && latestCompletedScheduledRun.conclusion !== "success") {
return latestCompletedScheduledRun;
if (latestDecision && latestDecision.conclusion !== "success") {
return latestDecision;
}
if (latestScheduledRun?.status === "completed" && isRecentRun(latestScheduledRun, nowMs)) {
return latestScheduledRun;
if (latestScheduledRun && latestScheduledRun.status !== "completed") {
return latestRun([latestScheduledRun, latestManualRun].filter(Boolean));
}
if (latestScheduledRun?.status === "completed" && isSuccessfulRecentRun(latestDecision, nowMs)) {
return latestDecision;
}
return latestManualRun ?? latestScheduledRun;
}
+104 -8
View File
@@ -31,6 +31,7 @@ const requiredCliArgs = [
type WorkflowRunFixture = {
id: number;
run_number: number;
name: string;
event: string;
status: string;
@@ -49,6 +50,7 @@ type WorkflowRunFixture = {
function successfulRun(name: string, id: number, updatedAt: string): WorkflowRunFixture {
return {
id,
run_number: id,
name,
event: "pull_request",
status: "completed",
@@ -72,6 +74,10 @@ function releaseGateRun(id: number, updatedAt: string) {
};
}
function pendingCiRun(id: number, updatedAt: string, status = "queued") {
return { ...successfulRun("CI", id, updatedAt), status, conclusion: null };
}
function queuedBuildArtifactFallbackRuns() {
return [
releaseGateRun(1, "2026-06-17T10:49:00Z"),
@@ -1060,15 +1066,9 @@ describe("verify-pr-hosted-gates", () => {
});
it.each([
[
"queued",
{
...successfulRun("CI", 1, "2026-06-17T10:50:00Z"),
status: "queued",
conclusion: null,
},
],
["stale", successfulRun("CI", 1, "2026-06-16T10:54:59Z")],
["cancelled", { ...successfulRun("CI", 1, "2026-06-17T10:50:00Z"), conclusion: "cancelled" }],
["skipped", { ...successfulRun("CI", 1, "2026-06-17T10:50:00Z"), conclusion: "skipped" }],
])("prefers a fresh exact release gate while scheduled CI is %s", (_state, scheduledRun) => {
expect(
collectHostedGateEvidence({
@@ -1081,6 +1081,86 @@ describe("verify-pr-hosted-gates", () => {
});
});
it.each(["cancelled", "skipped"])("rejects neutral-only scheduled CI (%s)", (conclusion) => {
expect(() =>
collectHostedGateEvidence({
sha,
workflowRuns: [{ ...successfulRun("CI", 1, "2026-06-17T10:50:00Z"), conclusion }],
}),
).toThrow("Missing successful recent CI workflow");
});
it.each(["cancelled", "skipped"])(
"retains a recent scheduled success after a newer neutral run (%s)",
(conclusion) => {
const success = successfulRun("CI", 1, "2026-06-17T10:47:00Z");
const neutral = { ...successfulRun("CI", 2, "2026-06-17T10:48:00Z"), conclusion };
const collect = () => collectHostedGateEvidence({ sha, workflowRuns: [success, neutral] });
expect(collect()).toEqual({
headSha: sha,
workflows: [expect.objectContaining({ name: "CI", id: 1 })],
});
for (const status of ["queued", "in_progress"]) {
Object.assign(neutral, { status, conclusion: null });
expect(collect).toThrow("Missing successful recent CI workflow");
}
},
);
it.each([
[
"queued over older manual",
[releaseGateRun(1, "2026-06-17T10:49:00Z"), pendingCiRun(2, "2026-06-17T10:50:00Z")],
null,
],
[
"in-progress over older manual",
[
releaseGateRun(1, "2026-06-17T10:49:00Z"),
pendingCiRun(2, "2026-06-17T10:50:00Z", "in_progress"),
],
null,
],
[
"pending over neutral and manual",
[
{ ...successfulRun("CI", 1, "2026-06-17T10:48:00Z"), conclusion: "skipped" },
releaseGateRun(2, "2026-06-17T10:49:00Z"),
pendingCiRun(3, "2026-06-17T10:50:00Z"),
],
null,
],
[
"newer manual over older pending",
[
pendingCiRun(1, "2026-06-17T10:48:00Z", "in_progress"),
releaseGateRun(2, "2026-06-17T10:49:00Z"),
],
2,
],
])("applies the %s policy", (_case, workflowRuns, expectedId) => {
const collect = () => collectHostedGateEvidence({ sha, workflowRuns });
if (expectedId === null) {
expect(collect).toThrow("Missing successful recent CI workflow");
return;
}
expect(collect()).toEqual({
headSha: sha,
workflows: [expect.objectContaining({ name: `CI release gate ${sha}`, id: expectedId })],
});
});
it("orders runs by creation sequence when completion updates invert", () => {
const olderSuccess = successfulRun("CI", 1, "2026-06-17T10:54:00Z");
const newerFailure = {
...successfulRun("CI", 2, "2026-06-17T10:49:00Z"),
conclusion: "failure",
};
expect(() =>
collectHostedGateEvidence({ sha, workflowRuns: [olderSuccess, newerFailure] }),
).toThrow("Missing successful recent CI workflow");
});
it("rejects a completed scheduled CI failure even when a fallback passed", () => {
expect(() =>
collectHostedGateEvidence({
@@ -1100,6 +1180,22 @@ describe("verify-pr-hosted-gates", () => {
).toThrow("Missing successful recent CI workflow");
});
it.each(["cancelled", "skipped"])(
"keeps an older scheduled failure blocking after a newer neutral run (%s)",
(conclusion) => {
expect(() =>
collectHostedGateEvidence({
sha,
workflowRuns: [
{ ...successfulRun("CI", 1, "2026-06-17T10:47:00Z"), conclusion: "failure" },
{ ...successfulRun("CI", 2, "2026-06-17T10:48:00Z"), conclusion },
releaseGateRun(3, "2026-06-17T10:49:00Z"),
],
}),
).toThrow("Missing successful recent CI workflow");
},
);
it("does not mask a failed CI run with a queued rerun and release-gate fallback", () => {
expect(() =>
collectHostedGateEvidence({