@cleocode/core 2026.10.22 → 2026.10.23-rc.ci.694
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-provenance.json +2 -2
- package/dist/orchestrate/spawn-ops.d.ts +7 -1
- package/dist/orchestrate/spawn-ops.d.ts.map +1 -1
- package/dist/orchestrate/spawn-ops.js +13 -1
- package/dist/orchestrate/spawn-ops.js.map +1 -1
- package/dist/release/ci-evidence.d.ts +114 -1
- package/dist/release/ci-evidence.d.ts.map +1 -1
- package/dist/release/ci-evidence.js +265 -45
- package/dist/release/ci-evidence.js.map +1 -1
- package/dist/resources/admission-ledger.d.ts +8 -2
- package/dist/resources/admission-ledger.d.ts.map +1 -1
- package/dist/resources/admission-ledger.js +14 -4
- package/dist/resources/admission-ledger.js.map +1 -1
- package/dist/tasks/complete-ci.d.ts.map +1 -1
- package/dist/tasks/complete-ci.js +12 -4
- package/dist/tasks/complete-ci.js.map +1 -1
- package/dist/tasks/component-pr.d.ts +35 -0
- package/dist/tasks/component-pr.d.ts.map +1 -1
- package/dist/tasks/component-pr.js +64 -0
- package/dist/tasks/component-pr.js.map +1 -1
- package/dist/tasks/evidence.d.ts.map +1 -1
- package/dist/tasks/evidence.js +119 -7
- package/dist/tasks/evidence.js.map +1 -1
- package/dist/tasks/gate-runner.d.ts.map +1 -1
- package/dist/tasks/gate-runner.js +48 -13
- package/dist/tasks/gate-runner.js.map +1 -1
- package/dist/tasks/heavy-tool-env.d.ts +42 -4
- package/dist/tasks/heavy-tool-env.d.ts.map +1 -1
- package/dist/tasks/heavy-tool-env.js +132 -4
- package/dist/tasks/heavy-tool-env.js.map +1 -1
- package/dist/tasks/test-run-binding.js +3 -1
- package/dist/tasks/test-run-binding.js.map +1 -1
- package/dist/tasks/tool-cache.d.ts.map +1 -1
- package/dist/tasks/tool-cache.js +6 -2
- package/dist/tasks/tool-cache.js.map +1 -1
- package/package.json +13 -13
- package/templates/CLEO-INJECTION.md +1 -1
- package/templates/CLEO-REFERENCE.md +7 -5
|
@@ -39,6 +39,14 @@
|
|
|
39
39
|
* {@link nonTestFailuresOn}. The stand-in search also looks past later runs red
|
|
40
40
|
* for that reason alone, up to {@link CI_DESCENDANT_MAX_CANDIDATES} commits.
|
|
41
41
|
*
|
|
42
|
+
* A fourth (T14032, owner decision D11192), for a WORKSPACE-WIDE change only
|
|
43
|
+
* (one a targeted test-run cannot speak for: a lockfile, a root config): when
|
|
44
|
+
* the merge commit is not green and no T12742 descendant stands in, a green
|
|
45
|
+
* push run of every required check on the default branch's CURRENT head (or
|
|
46
|
+
* on a commit differing from it only in documentation) that descends from the
|
|
47
|
+
* merge stands in, whatever the merge's own run concluded and whatever later
|
|
48
|
+
* commits touched. See {@link findGreenCurrentHead}.
|
|
49
|
+
*
|
|
42
50
|
* @task T12634
|
|
43
51
|
*/
|
|
44
52
|
import { execFileSync } from 'node:child_process';
|
|
@@ -384,6 +392,7 @@ export function evaluateMergeCommitChecks(required, checks, mergeCommitSha, opts
|
|
|
384
392
|
if (descendantSha !== undefined &&
|
|
385
393
|
descendantSha !== mergeCommitSha &&
|
|
386
394
|
(SUPERSEDED_VERDICTS.has(onMerge.verdict) ||
|
|
395
|
+
opts.currentHead === true ||
|
|
387
396
|
(opts.inheritedFailures === true &&
|
|
388
397
|
onMerge.verdict !== 'missing' &&
|
|
389
398
|
!onMerge.verdict.startsWith('pending')))) {
|
|
@@ -544,6 +553,18 @@ function latestRuns(checks, sha, scope) {
|
|
|
544
553
|
}
|
|
545
554
|
return [...latest.values()];
|
|
546
555
|
}
|
|
556
|
+
/**
|
|
557
|
+
* Why a merge-commit run did not test the merge as it was when it landed
|
|
558
|
+
* (T14026): a re-run, or a start after a later default-branch commit landed.
|
|
559
|
+
* Null when neither is known.
|
|
560
|
+
*/
|
|
561
|
+
function lateRun(run, nextMainCommitAt) {
|
|
562
|
+
if ((run.runAttempt ?? 1) > 1)
|
|
563
|
+
return `is re-run attempt ${run.runAttempt}`;
|
|
564
|
+
if (run.startedAt && nextMainCommitAt && Date.parse(run.startedAt) > Date.parse(nextMainCommitAt))
|
|
565
|
+
return `started at ${run.startedAt}, after a later default-branch commit landed (${nextMainCommitAt})`;
|
|
566
|
+
return null;
|
|
567
|
+
}
|
|
547
568
|
/**
|
|
548
569
|
* Whether the merge commit's failures were all inherited from main (T13453).
|
|
549
570
|
*
|
|
@@ -555,15 +576,23 @@ function latestRuns(checks, sha, scope) {
|
|
|
555
576
|
* the PR may have introduced and refuses. A pending merge-commit run refuses.
|
|
556
577
|
* Pure: the ancestors' checks are passed in, nearest first.
|
|
557
578
|
*
|
|
579
|
+
* The refusal blames the PR only when the failing run could have seen nothing
|
|
580
|
+
* but the PR's change (T14026): a re-run, or a run that started after a later
|
|
581
|
+
* default-branch commit landed, ran in a world that moved on since the merge
|
|
582
|
+
* (rotated secrets, external services), so it still refuses but says the
|
|
583
|
+
* failure may not be the PR's.
|
|
584
|
+
*
|
|
558
585
|
* @param required - Required check names.
|
|
559
586
|
* @param checks - Checks fetched for the merge commit.
|
|
560
587
|
* @param mergeCommitSha - The merge commit.
|
|
561
588
|
* @param ancestors - First-parent ancestors of the merge, nearest first, with their checks.
|
|
562
589
|
* @param pins - Required-name to app/workflow pin.
|
|
590
|
+
* @param nextMainCommitAt - When the next default-branch commit after the merge landed, if known.
|
|
563
591
|
* @returns The required names to stand in for and the inherited jobs, or why not.
|
|
564
592
|
* @task T13453
|
|
593
|
+
* @task T14026
|
|
565
594
|
*/
|
|
566
|
-
export function inheritedOnMerge(required, checks, mergeCommitSha, ancestors, pins) {
|
|
595
|
+
export function inheritedOnMerge(required, checks, mergeCommitSha, ancestors, pins, nextMainCommitAt = null) {
|
|
567
596
|
const scope = pinnedScope(required, pins);
|
|
568
597
|
const onMerge = latestRuns(checks, mergeCommitSha, scope);
|
|
569
598
|
const pending = onMerge.find((c) => c.status !== 'completed');
|
|
@@ -597,9 +626,13 @@ export function inheritedOnMerge(required, checks, mergeCommitSha, ancestors, pi
|
|
|
597
626
|
};
|
|
598
627
|
}
|
|
599
628
|
if (base.run.conclusion === 'success') {
|
|
629
|
+
const at = `${job.name}: ${job.conclusion} on merge commit ${mergeCommitSha.slice(0, 12)} but success on its base ${base.sha.slice(0, 12)}`;
|
|
630
|
+
const late = lateRun(job, nextMainCommitAt);
|
|
600
631
|
return {
|
|
601
632
|
ok: false,
|
|
602
|
-
reason:
|
|
633
|
+
reason: late
|
|
634
|
+
? `${at}; the failing run ${late}, so it may have failed on something that changed after the merge (secrets, external services) rather than on the PR`
|
|
635
|
+
: `${at} — the PR introduced this failure`,
|
|
603
636
|
};
|
|
604
637
|
}
|
|
605
638
|
inherited.push({
|
|
@@ -958,7 +991,7 @@ export async function findGreenDescendant(input) {
|
|
|
958
991
|
break;
|
|
959
992
|
ancestors.push({ sha: at, checks: fetched.checks });
|
|
960
993
|
}
|
|
961
|
-
const result = inheritedOnMerge(required, input.checks, merge, ancestors, pins);
|
|
994
|
+
const result = inheritedOnMerge(required, input.checks, merge, ancestors, pins, input.nextMainCommitAt?.(merge, input.ref, cwd) ?? null);
|
|
962
995
|
if (result.ok) {
|
|
963
996
|
superseded = { ok: true, names: result.names };
|
|
964
997
|
inherited = result.inherited;
|
|
@@ -1050,59 +1083,140 @@ export async function findGreenDescendant(input) {
|
|
|
1050
1083
|
reason: `${failedJob.name}: ${failedJob.conclusion} on later ${input.ref} commit ${sha.slice(0, 12)}`,
|
|
1051
1084
|
};
|
|
1052
1085
|
}
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1086
|
+
const verdict = judgeStandIn(fetched.checks, sha, supersededNames, pins, input.jobs, input.ref);
|
|
1087
|
+
if (verdict === 'undecided')
|
|
1088
|
+
continue;
|
|
1089
|
+
if (!verdict.ok)
|
|
1090
|
+
return verdict;
|
|
1091
|
+
return {
|
|
1092
|
+
ok: true,
|
|
1093
|
+
sha,
|
|
1094
|
+
checks: [...fetched.checks, ...headChecks],
|
|
1095
|
+
range: `${merge}..${sha}`,
|
|
1096
|
+
...(head ? { prHeadSha: head } : {}),
|
|
1097
|
+
...(inherited ? { inherited } : {}),
|
|
1098
|
+
...(nonTest ? { nonTest } : {}),
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
return {
|
|
1102
|
+
ok: false,
|
|
1103
|
+
reason: `no later ${input.ref} commit (first ${CI_DESCENDANT_MAX_CANDIDATES} within 7 days) has a completed push run of ${supersededNames.join(', ')}`,
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
/**
|
|
1107
|
+
* Judge one later default-branch commit's own `push` runs as a stand-in
|
|
1108
|
+
* (T12742, T14032): every named check green, and the gates' jobs run and
|
|
1109
|
+
* green (T13846). A pending run refuses with "wait"; a failed one refuses —
|
|
1110
|
+
* a green run is never shopped for past one that is red or may still go red.
|
|
1111
|
+
* Cancelled, skipped and missing (never-started) runs leave it undecided, so
|
|
1112
|
+
* the caller looks at the next candidate.
|
|
1113
|
+
*/
|
|
1114
|
+
function judgeStandIn(checks, sha, names, pins, jobs, ref) {
|
|
1115
|
+
for (const name of names) {
|
|
1116
|
+
const pinned = checks.filter((c) => c.name === name && pinMatches(c, pins[name]));
|
|
1117
|
+
const verdict = judgeOnSha(pinned, sha, 'push');
|
|
1118
|
+
if (verdict.ok)
|
|
1119
|
+
continue;
|
|
1120
|
+
if (verdict.verdict.startsWith('pending')) {
|
|
1069
1121
|
return {
|
|
1070
1122
|
ok: false,
|
|
1071
|
-
reason: `${name}: ${verdict.verdict} on later ${
|
|
1123
|
+
reason: `${name}: ${verdict.verdict} on later ${ref} commit ${sha.slice(0, 12)} — wait for ${sha.slice(0, 12)} to finish, then verify again`,
|
|
1072
1124
|
};
|
|
1073
1125
|
}
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1126
|
+
if (verdict.verdict === 'missing' || SUPERSEDED_VERDICTS.has(verdict.verdict)) {
|
|
1127
|
+
return 'undecided';
|
|
1128
|
+
}
|
|
1129
|
+
return {
|
|
1130
|
+
ok: false,
|
|
1131
|
+
reason: `${name}: ${verdict.verdict} on later ${ref} commit ${sha.slice(0, 12)}`,
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
// T13846: the gates' own jobs must have run on the stand-in too.
|
|
1135
|
+
let decided = true;
|
|
1136
|
+
for (const job of jobs ?? []) {
|
|
1137
|
+
const onSha = evaluateJobs(job.globs, checks, sha, job.scope, 'push');
|
|
1138
|
+
if (onSha.ok)
|
|
1139
|
+
continue;
|
|
1140
|
+
const pending = onSha.reasons.find((r) => r.includes(': pending ('));
|
|
1141
|
+
if (pending) {
|
|
1142
|
+
return {
|
|
1143
|
+
ok: false,
|
|
1144
|
+
reason: `${pending} — wait for ${sha.slice(0, 12)} to finish, then verify again`,
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
const decisive = onSha.reasons.find((r) => !JOB_NOT_RUN.test(r));
|
|
1148
|
+
if (decisive)
|
|
1149
|
+
return { ok: false, reason: `${decisive} (later ${ref} commit)` };
|
|
1150
|
+
decided = false;
|
|
1151
|
+
}
|
|
1152
|
+
return decided ? { ok: true } : 'undecided';
|
|
1153
|
+
}
|
|
1154
|
+
/**
|
|
1155
|
+
* Find the green required-CI push run on the default branch's CURRENT head
|
|
1156
|
+
* that stands in for a workspace-wide change's merge-commit CI (T14032, owner
|
|
1157
|
+
* decision D11192). A workspace-wide change (a lockfile, a root config, a CI
|
|
1158
|
+
* definition) only a full run speaks for; when its merge-commit run was red
|
|
1159
|
+
* for a reason that is not the change (a secrets job failing on a re-run) and
|
|
1160
|
+
* later commits touched its files, {@link findGreenDescendant} refuses, yet a
|
|
1161
|
+
* full green CI on today's HEAD tests the merged change as it is now — the
|
|
1162
|
+
* same reasoning as the targeted test-run stand-in (T13673), which also
|
|
1163
|
+
* allows later edits.
|
|
1164
|
+
*
|
|
1165
|
+
* Candidates are HEAD, then its first parents while they differ from HEAD only
|
|
1166
|
+
* in documentation ({@link isCiDocumentPath}, the T13784 rule: nothing a test
|
|
1167
|
+
* reads moved), up to {@link CI_DESCENDANT_MAX_CANDIDATES}. Each must descend
|
|
1168
|
+
* from the merge commit. The first decisive verdict decides, as in
|
|
1169
|
+
* {@link findGreenDescendant}: every required check green on its own `push`
|
|
1170
|
+
* runs (and the gates' jobs run, T13846) accepts; a failed or pending one
|
|
1171
|
+
* refuses; cancelled, skipped or missing runs move to the parent.
|
|
1172
|
+
*
|
|
1173
|
+
* @param input - Required checks, pins, merge commit and injected I/O.
|
|
1174
|
+
* @returns The stand-in commit, its checks and the head, or the refusal reason.
|
|
1175
|
+
* @task T14032
|
|
1176
|
+
*/
|
|
1177
|
+
export async function findGreenCurrentHead(input) {
|
|
1178
|
+
const { mergeCommitSha: merge, ref, cwd } = input;
|
|
1179
|
+
const head = input.defaultBranchHead(ref, cwd);
|
|
1180
|
+
if (!head)
|
|
1181
|
+
return { ok: false, reason: `cannot read the current head of ${ref}` };
|
|
1182
|
+
let sha = head;
|
|
1183
|
+
for (let n = 0; n < CI_DESCENDANT_MAX_CANDIDATES && sha; n++) {
|
|
1184
|
+
const at = sha === head ? `${ref} HEAD ${head.slice(0, 12)}` : `${sha.slice(0, 12)}`;
|
|
1185
|
+
if (sha === merge || !input.isAncestor(merge, sha, cwd)) {
|
|
1186
|
+
return {
|
|
1187
|
+
ok: false,
|
|
1188
|
+
reason: sha === head
|
|
1189
|
+
? `${at} does not descend from merge commit ${merge.slice(0, 12)} here (git fetch origin, then verify again)`
|
|
1190
|
+
: `no push run between merge commit ${merge.slice(0, 12)} and ${ref} HEAD ${head.slice(0, 12)} decided the required checks`,
|
|
1191
|
+
};
|
|
1192
|
+
}
|
|
1193
|
+
if (sha !== head) {
|
|
1194
|
+
const moved = input.changedBetween(sha, head, cwd);
|
|
1195
|
+
if (moved === null) {
|
|
1196
|
+
return { ok: false, reason: `cannot diff ${sha.slice(0, 12)}..${head.slice(0, 12)}` };
|
|
1197
|
+
}
|
|
1198
|
+
const testInput = moved.find((p) => !isCiDocumentPath(p));
|
|
1199
|
+
if (testInput) {
|
|
1081
1200
|
return {
|
|
1082
1201
|
ok: false,
|
|
1083
|
-
reason: `${
|
|
1202
|
+
reason: `${ref} HEAD ${head.slice(0, 12)} has no completed push run of the required checks, and ${sha.slice(0, 12)}..${head.slice(0, 12)} changes ${testInput}, so an earlier run does not test HEAD — wait for HEAD's push CI to finish, then verify again`,
|
|
1084
1203
|
};
|
|
1085
1204
|
}
|
|
1086
|
-
const decisive = onSha.reasons.find((r) => !JOB_NOT_RUN.test(r));
|
|
1087
|
-
if (decisive)
|
|
1088
|
-
return { ok: false, reason: `${decisive} (later ${input.ref} commit)` };
|
|
1089
|
-
decided = false;
|
|
1090
1205
|
}
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
...(nonTest ? { nonTest } : {}),
|
|
1100
|
-
};
|
|
1206
|
+
const fetched = await input.fetchChecks(sha, cwd);
|
|
1207
|
+
if (!fetched.ok)
|
|
1208
|
+
return { ok: false, reason: fetched.reason };
|
|
1209
|
+
const verdict = judgeStandIn(fetched.checks, sha, input.required, input.pins, input.jobs, ref);
|
|
1210
|
+
if (verdict !== 'undecided') {
|
|
1211
|
+
if (!verdict.ok)
|
|
1212
|
+
return verdict;
|
|
1213
|
+
return { ok: true, sha, checks: fetched.checks, range: `${merge}..${sha}`, head };
|
|
1101
1214
|
}
|
|
1215
|
+
sha = input.firstParentOf(sha, cwd);
|
|
1102
1216
|
}
|
|
1103
1217
|
return {
|
|
1104
1218
|
ok: false,
|
|
1105
|
-
reason: `no
|
|
1219
|
+
reason: `no commit within ${CI_DESCENDANT_MAX_CANDIDATES} first parents of ${ref} HEAD ${head.slice(0, 12)} has a completed push run of ${input.required.join(', ')}`,
|
|
1106
1220
|
};
|
|
1107
1221
|
}
|
|
1108
1222
|
/**
|
|
@@ -1222,6 +1336,48 @@ export function ghDefaultBranch(cwd) {
|
|
|
1222
1336
|
return null;
|
|
1223
1337
|
}
|
|
1224
1338
|
}
|
|
1339
|
+
/** Default {@link ResolveCiEvidenceOptions.nextMainCommitAt} (T14026). */
|
|
1340
|
+
function defaultNextMainCommitAt(mergeSha, ref, cwd) {
|
|
1341
|
+
const out = gitRead(cwd, [
|
|
1342
|
+
'log',
|
|
1343
|
+
'--first-parent',
|
|
1344
|
+
'--ancestry-path',
|
|
1345
|
+
'--reverse',
|
|
1346
|
+
'--format=%cI',
|
|
1347
|
+
`${mergeSha}..${ref}`,
|
|
1348
|
+
]);
|
|
1349
|
+
return out?.split('\n')[0] || null;
|
|
1350
|
+
}
|
|
1351
|
+
/**
|
|
1352
|
+
* Default {@link ResolveCiEvidenceOptions.defaultBranchHead} (T14032): the
|
|
1353
|
+
* branch head GitHub reports, so a stale local ref never passes for the
|
|
1354
|
+
* current one; `git rev-parse <ref>` only when `gh` cannot answer.
|
|
1355
|
+
*/
|
|
1356
|
+
function defaultBranchHeadOf(ref, cwd) {
|
|
1357
|
+
const branch = ref.replace(/^origin\//, '');
|
|
1358
|
+
if (isGitWorkTree(cwd) && isGhCliAvailable()) {
|
|
1359
|
+
const commit = ghApi(`repos/{owner}/{repo}/commits/${encodeURIComponent(branch)}`, cwd);
|
|
1360
|
+
if (typeof commit === 'object' &&
|
|
1361
|
+
commit !== null &&
|
|
1362
|
+
'sha' in commit &&
|
|
1363
|
+
typeof commit.sha === 'string' &&
|
|
1364
|
+
/^[0-9a-f]{40}$/.test(commit.sha)) {
|
|
1365
|
+
return commit.sha;
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
return gitRead(cwd, ['rev-parse', '--verify', '--quiet', `${ref}^{commit}`]);
|
|
1369
|
+
}
|
|
1370
|
+
/** Default {@link ResolveCiEvidenceOptions.changedBetween} (T14032). */
|
|
1371
|
+
function defaultChangedBetween(from, to, cwd) {
|
|
1372
|
+
const out = gitRead(cwd, ['diff', '--name-only', '--no-renames', from, to]);
|
|
1373
|
+
return out === null ? null : out.split('\n').filter(Boolean);
|
|
1374
|
+
}
|
|
1375
|
+
/** Default {@link ResolveCiEvidenceOptions.workspaceWideReason}: the `test-run:` rule (T14032). */
|
|
1376
|
+
async function defaultWorkspaceWideReason(root, paths) {
|
|
1377
|
+
const { testRunRequirement } = await import('../tasks/test-run-binding.js');
|
|
1378
|
+
const need = testRunRequirement(root, paths);
|
|
1379
|
+
return need.kind === 'full' ? need.reason : null;
|
|
1380
|
+
}
|
|
1225
1381
|
function defaultFirstParentOf(sha, cwd) {
|
|
1226
1382
|
return gitRead(cwd, ['rev-parse', '--verify', '--quiet', `${sha}^1`]);
|
|
1227
1383
|
}
|
|
@@ -1310,8 +1466,10 @@ async function defaultFetchChecks(sha, cwd) {
|
|
|
1310
1466
|
for (const r of wf.workflow_runs ?? []) {
|
|
1311
1467
|
const event = typeof r.event === 'string' ? r.event : undefined;
|
|
1312
1468
|
const path = typeof r.path === 'string' ? r.path.replace(/@.*$/, '') : undefined;
|
|
1469
|
+
const attempt = typeof r.run_attempt === 'number' ? r.run_attempt : undefined;
|
|
1470
|
+
const startedAt = typeof r.run_started_at === 'string' ? r.run_started_at : undefined;
|
|
1313
1471
|
if (typeof r.check_suite_id === 'number')
|
|
1314
|
-
bySuite.set(r.check_suite_id, { event, path });
|
|
1472
|
+
bySuite.set(r.check_suite_id, { event, path, attempt, startedAt });
|
|
1315
1473
|
checks.push({
|
|
1316
1474
|
name: String(r.name ?? ''),
|
|
1317
1475
|
source: 'workflow-run',
|
|
@@ -1322,6 +1480,8 @@ async function defaultFetchChecks(sha, cwd) {
|
|
|
1322
1480
|
appSlug: 'github-actions',
|
|
1323
1481
|
...(path ? { workflowPath: path } : {}),
|
|
1324
1482
|
...(event ? { event } : {}),
|
|
1483
|
+
...(attempt !== undefined ? { runAttempt: attempt } : {}),
|
|
1484
|
+
...(startedAt ? { startedAt } : {}),
|
|
1325
1485
|
});
|
|
1326
1486
|
}
|
|
1327
1487
|
for (let page = 1; page <= 10; page++) {
|
|
@@ -1333,6 +1493,14 @@ async function defaultFetchChecks(sha, cwd) {
|
|
|
1333
1493
|
const app = r.app;
|
|
1334
1494
|
const suite = r.check_suite?.id;
|
|
1335
1495
|
const origin = typeof suite === 'number' ? bySuite.get(suite) : undefined;
|
|
1496
|
+
const startedAt = typeof r.started_at === 'string' ? r.started_at : undefined;
|
|
1497
|
+
// A job started before its workflow's latest attempt belongs to an earlier one.
|
|
1498
|
+
const attempt = origin?.attempt !== undefined &&
|
|
1499
|
+
startedAt &&
|
|
1500
|
+
origin.startedAt &&
|
|
1501
|
+
Date.parse(startedAt) >= Date.parse(origin.startedAt)
|
|
1502
|
+
? origin.attempt
|
|
1503
|
+
: undefined;
|
|
1336
1504
|
checks.push({
|
|
1337
1505
|
name: String(r.name ?? ''),
|
|
1338
1506
|
source: 'check-run',
|
|
@@ -1344,6 +1512,8 @@ async function defaultFetchChecks(sha, cwd) {
|
|
|
1344
1512
|
...(typeof app?.id === 'number' ? { appId: app.id } : {}),
|
|
1345
1513
|
...(origin?.path ? { workflowPath: origin.path } : {}),
|
|
1346
1514
|
...(origin?.event ? { event: origin.event } : {}),
|
|
1515
|
+
...(attempt !== undefined ? { runAttempt: attempt } : {}),
|
|
1516
|
+
...(startedAt ? { startedAt } : {}),
|
|
1347
1517
|
});
|
|
1348
1518
|
}
|
|
1349
1519
|
if (runs.length < 100)
|
|
@@ -1371,7 +1541,8 @@ export async function recheckCiDescendantAtom(atom, cwd, fetchChecks = defaultFe
|
|
|
1371
1541
|
if (!descendant)
|
|
1372
1542
|
return { ok: true };
|
|
1373
1543
|
const head = atom.descendantPrHeadSha;
|
|
1374
|
-
|
|
1544
|
+
// T14032: a workspace-wide current-HEAD stand-in consulted main's push runs only.
|
|
1545
|
+
if (!head && !atom.mainOnly && !atom.workspaceWide) {
|
|
1375
1546
|
return {
|
|
1376
1547
|
ok: false,
|
|
1377
1548
|
reason: `ci:${atom.prNumber} leans on ${descendant.slice(0, 12)} but records no PR head; verify again`,
|
|
@@ -1650,6 +1821,7 @@ export async function resolveCiEvidenceAtom(prNumber, roots, opts = {}) {
|
|
|
1650
1821
|
isAncestor: opts.isAncestor ?? defaultIsAncestor,
|
|
1651
1822
|
firstParentOf: opts.firstParentOf ?? defaultFirstParentOf,
|
|
1652
1823
|
fetchJobLog: opts.fetchJobLog ?? defaultFetchJobLog,
|
|
1824
|
+
nextMainCommitAt: opts.nextMainCommitAt ?? defaultNextMainCommitAt,
|
|
1653
1825
|
});
|
|
1654
1826
|
if (found.ok) {
|
|
1655
1827
|
const withDescendant = [...checks, ...found.checks];
|
|
@@ -1678,6 +1850,52 @@ export async function resolveCiEvidenceAtom(prNumber, roots, opts = {}) {
|
|
|
1678
1850
|
descendantReason = found.reason;
|
|
1679
1851
|
}
|
|
1680
1852
|
}
|
|
1853
|
+
// T14032 (owner decision D11192): a workspace-wide change, which only a full
|
|
1854
|
+
// run speaks for, is stood in for by a green push run of every required
|
|
1855
|
+
// check on the default branch's current head, descending from the merge,
|
|
1856
|
+
// whatever the merge's own run concluded and whatever later commits touched:
|
|
1857
|
+
// that run tested the merged change as it is now (as the T13673 test-run
|
|
1858
|
+
// stand-in does). A scoped change keeps the T12742 rule alone.
|
|
1859
|
+
let workspaceWide;
|
|
1860
|
+
let currentHeadReason;
|
|
1861
|
+
if (!judgedChecks.ok && onDefault.ref !== null && !docsOnlyDiff) {
|
|
1862
|
+
const wide = await (opts.workspaceWideReason ?? defaultWorkspaceWideReason)(cwd, pr.changedPaths);
|
|
1863
|
+
if (wide !== null) {
|
|
1864
|
+
const found = await findGreenCurrentHead({
|
|
1865
|
+
...(jobSpecs.length > 0 ? { jobs: jobSpecs } : {}),
|
|
1866
|
+
required: required.workflows,
|
|
1867
|
+
mergeCommitSha: pr.mergeCommitSha,
|
|
1868
|
+
pins,
|
|
1869
|
+
ref: onDefault.ref,
|
|
1870
|
+
cwd,
|
|
1871
|
+
fetchChecks,
|
|
1872
|
+
defaultBranchHead: opts.defaultBranchHead ?? defaultBranchHeadOf,
|
|
1873
|
+
changedBetween: opts.changedBetween ?? defaultChangedBetween,
|
|
1874
|
+
firstParentOf: opts.firstParentOf ?? defaultFirstParentOf,
|
|
1875
|
+
isAncestor: opts.isAncestor ?? defaultIsAncestor,
|
|
1876
|
+
});
|
|
1877
|
+
const rejudged = found.ok
|
|
1878
|
+
? evaluateMergeCommitChecks(required.workflows, [...checks, ...found.checks], pr.mergeCommitSha, {
|
|
1879
|
+
...(treeEqualHead ? { treeEquivalentSha: treeEqualHead } : {}),
|
|
1880
|
+
pins,
|
|
1881
|
+
descendantSha: found.sha,
|
|
1882
|
+
currentHead: true,
|
|
1883
|
+
})
|
|
1884
|
+
: null;
|
|
1885
|
+
if (found.ok && rejudged?.ok) {
|
|
1886
|
+
checks = [...checks, ...found.checks];
|
|
1887
|
+
judgedChecks = rejudged;
|
|
1888
|
+
descendant = { sha: found.sha, range: found.range };
|
|
1889
|
+
workspaceWide = wide;
|
|
1890
|
+
}
|
|
1891
|
+
else {
|
|
1892
|
+
const why = found.ok
|
|
1893
|
+
? `${found.sha.slice(0, 12)} was green, but re-judging with it still fails: ${rejudged && !rejudged.ok ? rejudged.reasons.join('; ') : ''}`
|
|
1894
|
+
: found.reason;
|
|
1895
|
+
currentHeadReason = `the change is workspace-wide (${wide}), so a green push run of the required checks on the current ${onDefault.ref} HEAD would stand in (T14032), but ${why}`;
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1681
1899
|
if (!judgedChecks.ok) {
|
|
1682
1900
|
const waiting = judgedChecks.reasons.some((r) => /pending|not found|missing/.test(r));
|
|
1683
1901
|
return {
|
|
@@ -1686,6 +1904,7 @@ export async function resolveCiEvidenceAtom(prNumber, roots, opts = {}) {
|
|
|
1686
1904
|
reason: `Required CI on PR #${prNumber}'s merge commit ${pr.mergeCommitSha.slice(0, 12)} is not green ` +
|
|
1687
1905
|
`(source: ${describeRequiredWorkflowsSource(required.source)}):\n - ${judgedChecks.reasons.join('\n - ')}` +
|
|
1688
1906
|
(descendantReason ? `\n No later main run stands in: ${descendantReason}` : '') +
|
|
1907
|
+
(currentHeadReason ? `\n No current-HEAD run stands in: ${currentHeadReason}` : '') +
|
|
1689
1908
|
(mainOnly
|
|
1690
1909
|
? `\n PR #${prNumber} edits the pinned workflow ${editedWorkflows.join(', ')}, so only main's push CI attests it (T13174)` +
|
|
1691
1910
|
(waiting
|
|
@@ -1806,6 +2025,7 @@ export async function resolveCiEvidenceAtom(prNumber, roots, opts = {}) {
|
|
|
1806
2025
|
...(descendant.nonTest ? { nonTestFailures: descendant.nonTest } : {}),
|
|
1807
2026
|
}
|
|
1808
2027
|
: {}),
|
|
2028
|
+
...(workspaceWide ? { workspaceWide } : {}),
|
|
1809
2029
|
...(mainOnly ? { mainOnly: true } : {}),
|
|
1810
2030
|
requiredSource: required.source.tier,
|
|
1811
2031
|
taskId: context.task.id,
|