@cleocode/core 2026.10.25 → 2026.10.26
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/dispatch/contracts/output-contracts.d.ts +13 -0
- package/dist/dispatch/contracts/output-contracts.d.ts.map +1 -1
- package/dist/dispatch/contracts/output-contracts.js +29 -0
- package/dist/dispatch/contracts/output-contracts.js.map +1 -1
- package/dist/docs/docs-ops.d.ts +8 -0
- package/dist/docs/docs-ops.d.ts.map +1 -1
- package/dist/docs/docs-ops.js +24 -1
- package/dist/docs/docs-ops.js.map +1 -1
- package/dist/docs/docs-update.d.ts +8 -5
- package/dist/docs/docs-update.d.ts.map +1 -1
- package/dist/docs/docs-update.js +16 -5
- package/dist/docs/docs-update.js.map +1 -1
- package/dist/gateway-client/generated/types.gen.d.ts +27 -0
- package/dist/gateway-client/generated/types.gen.d.ts.map +1 -1
- package/dist/hooks/handlers/watchdog-hooks.d.ts.map +1 -1
- package/dist/hooks/handlers/watchdog-hooks.js +5 -0
- package/dist/hooks/handlers/watchdog-hooks.js.map +1 -1
- package/dist/internal.d.ts +3 -2
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +5 -2
- package/dist/internal.js.map +1 -1
- package/dist/lifecycle/verification/satisfies-validator.d.ts.map +1 -1
- package/dist/lifecycle/verification/satisfies-validator.js +22 -10
- package/dist/lifecycle/verification/satisfies-validator.js.map +1 -1
- package/dist/memory/brain-writer-thread.d.ts +30 -0
- package/dist/memory/brain-writer-thread.d.ts.map +1 -1
- package/dist/memory/brain-writer-thread.js +46 -1
- package/dist/memory/brain-writer-thread.js.map +1 -1
- package/dist/memory/brain-writer-worker.js +13 -2
- package/dist/memory/brain-writer-worker.js.map +1 -1
- package/dist/memory/embedding-backfill.d.ts +12 -0
- package/dist/memory/embedding-backfill.d.ts.map +1 -1
- package/dist/memory/embedding-backfill.js +51 -31
- package/dist/memory/embedding-backfill.js.map +1 -1
- package/dist/orchestrate/include-deps.d.ts +58 -0
- package/dist/orchestrate/include-deps.d.ts.map +1 -0
- package/dist/orchestrate/include-deps.js +138 -0
- package/dist/orchestrate/include-deps.js.map +1 -0
- package/dist/orchestrate/spawn-ops.d.ts +14 -33
- package/dist/orchestrate/spawn-ops.d.ts.map +1 -1
- package/dist/orchestrate/spawn-ops.js +49 -61
- package/dist/orchestrate/spawn-ops.js.map +1 -1
- package/dist/orchestrate/worktree-complete.d.ts +15 -1
- package/dist/orchestrate/worktree-complete.d.ts.map +1 -1
- package/dist/orchestrate/worktree-complete.js +36 -3
- package/dist/orchestrate/worktree-complete.js.map +1 -1
- package/dist/release/ci-evidence.d.ts +142 -4
- package/dist/release/ci-evidence.d.ts.map +1 -1
- package/dist/release/ci-evidence.js +298 -48
- package/dist/release/ci-evidence.js.map +1 -1
- package/dist/release/open.d.ts +25 -1
- package/dist/release/open.d.ts.map +1 -1
- package/dist/release/open.js +63 -11
- package/dist/release/open.js.map +1 -1
- package/dist/resources/admission-ledger.d.ts +83 -11
- package/dist/resources/admission-ledger.d.ts.map +1 -1
- package/dist/resources/admission-ledger.js +138 -39
- package/dist/resources/admission-ledger.js.map +1 -1
- package/dist/resources/governor.js +1 -1
- package/dist/resources/governor.js.map +1 -1
- package/dist/resources/hook-footprint.d.ts +24 -0
- package/dist/resources/hook-footprint.d.ts.map +1 -1
- package/dist/resources/hook-footprint.js +38 -1
- package/dist/resources/hook-footprint.js.map +1 -1
- package/dist/resources/run-class.d.ts +38 -5
- package/dist/resources/run-class.d.ts.map +1 -1
- package/dist/resources/run-class.js +60 -11
- package/dist/resources/run-class.js.map +1 -1
- package/dist/resources/run-governed.d.ts +13 -3
- package/dist/resources/run-governed.d.ts.map +1 -1
- package/dist/resources/run-governed.js +42 -9
- package/dist/resources/run-governed.js.map +1 -1
- package/dist/resources/slot-holder.d.ts +3 -3
- package/dist/resources/slot-holder.js +3 -3
- package/dist/resources/tool-groups.d.ts +11 -7
- package/dist/resources/tool-groups.d.ts.map +1 -1
- package/dist/resources/tool-groups.js +24 -13
- package/dist/resources/tool-groups.js.map +1 -1
- package/dist/spawn/branch-integration.d.ts +281 -0
- package/dist/spawn/branch-integration.d.ts.map +1 -0
- package/dist/spawn/branch-integration.js +715 -0
- package/dist/spawn/branch-integration.js.map +1 -0
- package/dist/spawn/branch-lock.d.ts +32 -270
- package/dist/spawn/branch-lock.d.ts.map +1 -1
- package/dist/spawn/branch-lock.js +59 -770
- package/dist/spawn/branch-lock.js.map +1 -1
- package/dist/spawn/index.d.ts +3 -1
- package/dist/spawn/index.d.ts.map +1 -1
- package/dist/spawn/index.js +5 -1
- package/dist/spawn/index.js.map +1 -1
- package/dist/store/exodus/recovery.d.ts.map +1 -1
- package/dist/store/exodus/recovery.js +9 -4
- package/dist/store/exodus/recovery.js.map +1 -1
- package/dist/system/health.d.ts.map +1 -1
- package/dist/system/health.js +3 -1
- package/dist/system/health.js.map +1 -1
- package/dist/tasks/ac-coverage-gate.d.ts +18 -2
- package/dist/tasks/ac-coverage-gate.d.ts.map +1 -1
- package/dist/tasks/ac-coverage-gate.js +40 -6
- package/dist/tasks/ac-coverage-gate.js.map +1 -1
- package/dist/tasks/ac-table.d.ts.map +1 -1
- package/dist/tasks/ac-table.js +3 -10
- package/dist/tasks/ac-table.js.map +1 -1
- package/dist/tasks/affected-packages.d.ts.map +1 -1
- package/dist/tasks/affected-packages.js +54 -10
- package/dist/tasks/affected-packages.js.map +1 -1
- package/dist/tasks/affected-scope.d.ts +10 -1
- package/dist/tasks/affected-scope.d.ts.map +1 -1
- package/dist/tasks/affected-scope.js +13 -3
- package/dist/tasks/affected-scope.js.map +1 -1
- package/dist/tasks/change-set.d.ts +9 -6
- package/dist/tasks/change-set.d.ts.map +1 -1
- package/dist/tasks/change-set.js +75 -97
- package/dist/tasks/change-set.js.map +1 -1
- package/dist/tasks/command-line.d.ts.map +1 -1
- package/dist/tasks/command-line.js +6 -2
- package/dist/tasks/command-line.js.map +1 -1
- package/dist/tasks/complete.d.ts.map +1 -1
- package/dist/tasks/complete.js +7 -2
- package/dist/tasks/complete.js.map +1 -1
- package/dist/tasks/done-plan.d.ts.map +1 -1
- package/dist/tasks/done-plan.js +111 -29
- package/dist/tasks/done-plan.js.map +1 -1
- package/dist/tasks/done-record.d.ts +14 -0
- package/dist/tasks/done-record.d.ts.map +1 -1
- package/dist/tasks/done-record.js +17 -10
- package/dist/tasks/done-record.js.map +1 -1
- package/dist/tasks/evidence-tree.d.ts +33 -0
- package/dist/tasks/evidence-tree.d.ts.map +1 -0
- package/dist/tasks/evidence-tree.js +132 -0
- package/dist/tasks/evidence-tree.js.map +1 -0
- package/dist/tasks/evidence.d.ts +42 -6
- package/dist/tasks/evidence.d.ts.map +1 -1
- package/dist/tasks/evidence.js +175 -79
- package/dist/tasks/evidence.js.map +1 -1
- package/dist/tasks/gate-preview.d.ts.map +1 -1
- package/dist/tasks/gate-preview.js +3 -2
- package/dist/tasks/gate-preview.js.map +1 -1
- package/dist/tasks/gate-runner.d.ts +11 -3
- package/dist/tasks/gate-runner.d.ts.map +1 -1
- package/dist/tasks/gate-runner.js +156 -37
- package/dist/tasks/gate-runner.js.map +1 -1
- package/dist/tasks/gate-staleness.d.ts +58 -0
- package/dist/tasks/gate-staleness.d.ts.map +1 -0
- package/dist/tasks/gate-staleness.js +78 -0
- package/dist/tasks/gate-staleness.js.map +1 -0
- package/dist/tasks/heavy-tool-env.d.ts +26 -1
- package/dist/tasks/heavy-tool-env.d.ts.map +1 -1
- package/dist/tasks/heavy-tool-env.js +92 -5
- package/dist/tasks/heavy-tool-env.js.map +1 -1
- package/dist/tasks/heavy-tool-limit.d.ts +24 -0
- package/dist/tasks/heavy-tool-limit.d.ts.map +1 -1
- package/dist/tasks/heavy-tool-limit.js +44 -1
- package/dist/tasks/heavy-tool-limit.js.map +1 -1
- package/dist/tasks/show.d.ts.map +1 -1
- package/dist/tasks/show.js +43 -0
- package/dist/tasks/show.js.map +1 -1
- package/dist/tasks/test-run-binding.d.ts +48 -3
- package/dist/tasks/test-run-binding.d.ts.map +1 -1
- package/dist/tasks/test-run-binding.js +111 -3
- package/dist/tasks/test-run-binding.js.map +1 -1
- package/dist/tasks/tool-cache.d.ts +58 -7
- package/dist/tasks/tool-cache.d.ts.map +1 -1
- package/dist/tasks/tool-cache.js +216 -16
- package/dist/tasks/tool-cache.js.map +1 -1
- package/dist/tasks/tool-resolver.d.ts +13 -0
- package/dist/tasks/tool-resolver.d.ts.map +1 -1
- package/dist/tasks/tool-resolver.js +34 -0
- package/dist/tasks/tool-resolver.js.map +1 -1
- package/dist/tasks/tool-runner-guard.d.ts +6 -0
- package/dist/tasks/tool-runner-guard.d.ts.map +1 -1
- package/dist/tasks/tool-runner-guard.js.map +1 -1
- package/dist/tasks/tool-semaphore.js +1 -1
- package/dist/tasks/tool-semaphore.js.map +1 -1
- package/dist/tasks/verification-policy.d.ts +26 -3
- package/dist/tasks/verification-policy.d.ts.map +1 -1
- package/dist/tasks/verification-policy.js +76 -11
- package/dist/tasks/verification-policy.js.map +1 -1
- package/dist/validation/doctor/checks.d.ts +12 -0
- package/dist/validation/doctor/checks.d.ts.map +1 -1
- package/dist/validation/doctor/checks.js +33 -0
- package/dist/validation/doctor/checks.js.map +1 -1
- package/dist/validation/engine-ops.d.ts +6 -0
- package/dist/validation/engine-ops.d.ts.map +1 -1
- package/dist/validation/engine-ops.js +51 -1
- package/dist/validation/engine-ops.js.map +1 -1
- package/dist/worktree/force-unlock.js +2 -2
- package/dist/worktree/force-unlock.js.map +1 -1
- package/dist/worktree/list.d.ts +7 -7
- package/dist/worktree/list.d.ts.map +1 -1
- package/dist/worktree/list.js +11 -56
- package/dist/worktree/list.js.map +1 -1
- package/dist/worktree/prune.d.ts +2 -2
- package/dist/worktree/prune.d.ts.map +1 -1
- package/dist/worktree/prune.js +4 -4
- package/dist/worktree/prune.js.map +1 -1
- package/package.json +13 -13
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
* @task T12634
|
|
51
51
|
*/
|
|
52
52
|
import { execFileSync } from 'node:child_process';
|
|
53
|
-
import { readFileSync } from 'node:fs';
|
|
53
|
+
import { readdirSync, readFileSync } from 'node:fs';
|
|
54
54
|
import { join } from 'node:path';
|
|
55
55
|
import { isGitWorkTree } from '../git/work-tree.js';
|
|
56
56
|
import { checkPrTaskLinkage, isDocumentArtifact, linkedPrChange, } from '../tasks/evidence.js';
|
|
@@ -96,6 +96,51 @@ export function readCiSatisfies(projectRoot) {
|
|
|
96
96
|
const evidence = readProjectContextFile(projectRoot)?.evidence;
|
|
97
97
|
return evidence?.ciSatisfies === true;
|
|
98
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* The `evidence.ciSatisfies` remedy for a project that has GitHub workflows
|
|
101
|
+
* but has not opted in (T13096), or null when it has, or has no workflows.
|
|
102
|
+
*
|
|
103
|
+
* Without the setting, `cleo complete` cannot stand a merged PR's CI in for
|
|
104
|
+
* `testsPassed`/`qaPassed` (`ci:<pr>`), so a scoped run recorded before the
|
|
105
|
+
* merge leaves only a full local suite. The remedy is a ready-to-paste
|
|
106
|
+
* `evidence` block whose `ciChecks` are the default branch's required checks
|
|
107
|
+
* ({@link resolveRequiredWorkflowsDetailed}, read-only: nothing is cached).
|
|
108
|
+
*
|
|
109
|
+
* @param root - The project checkout (its `.cleo/project-context.json` and `.github/workflows`).
|
|
110
|
+
* @param opts - Branch-protection fetcher override (tests).
|
|
111
|
+
* @returns The remedy text, or null.
|
|
112
|
+
* @task T13096
|
|
113
|
+
*/
|
|
114
|
+
export async function ciSatisfiesRemedy(root, opts = {}) {
|
|
115
|
+
if (readCiSatisfies(root))
|
|
116
|
+
return null;
|
|
117
|
+
let workflows;
|
|
118
|
+
try {
|
|
119
|
+
workflows = readdirSync(join(root, '.github', 'workflows')).filter((f) => /\.ya?ml$/.test(f));
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
if (workflows.length === 0)
|
|
125
|
+
return null;
|
|
126
|
+
const required = await resolveRequiredWorkflowsDetailed({ storeRoot: root, executionRoot: root }, {
|
|
127
|
+
projectContext: readProjectContextFile(root),
|
|
128
|
+
readOnly: true,
|
|
129
|
+
...(opts.fetchGhBranchProtection
|
|
130
|
+
? { fetchGhBranchProtection: opts.fetchGhBranchProtection }
|
|
131
|
+
: {}),
|
|
132
|
+
});
|
|
133
|
+
const checks = required.source.tier === 'unknown' ? [] : required.workflows;
|
|
134
|
+
// ci:<pr> attests a gate only through its declared ciChecks list (T12634).
|
|
135
|
+
const listed = checks.length > 0 ? checks : ['<required check>'];
|
|
136
|
+
const block = { ciSatisfies: true, ciChecks: { tests: listed, qa: listed } };
|
|
137
|
+
return (`This project has GitHub workflows (.github/workflows) but does not set evidence.ciSatisfies, ` +
|
|
138
|
+
"so a merged PR's CI cannot stand in for testsPassed/qaPassed (ci:<pr>). Add to " +
|
|
139
|
+
`.cleo/project-context.json: "evidence": ${JSON.stringify(block)}` +
|
|
140
|
+
(checks.length > 0
|
|
141
|
+
? ` (required checks from ${describeRequiredWorkflowsSource(required.source)}; narrow ciChecks.tests/qa to the checks that run tests and QA).`
|
|
142
|
+
: `, replacing <required check> with the required checks that run tests and QA (none were found: ${describeRequiredWorkflowsSource(required.source)}).`));
|
|
143
|
+
}
|
|
99
144
|
/**
|
|
100
145
|
* The checks that attest each gate (`evidence.ciChecks`, T12634).
|
|
101
146
|
*
|
|
@@ -330,17 +375,46 @@ export function evaluateJobs(globs, checks, sha, scope, onlyEvent) {
|
|
|
330
375
|
}
|
|
331
376
|
return reasons.length > 0 ? { ok: false, reasons } : { ok: true, jobs };
|
|
332
377
|
}
|
|
333
|
-
|
|
378
|
+
/**
|
|
379
|
+
* Whether a run comes from a check's pinned app and workflow (T13017):
|
|
380
|
+
* `unknown` when the response left out an identity field the pin names (a
|
|
381
|
+
* check run whose workflow run was not listed yet), so the run is neither
|
|
382
|
+
* counted nor reported as posted by another app.
|
|
383
|
+
*/
|
|
384
|
+
function pinMatch(check, pin) {
|
|
334
385
|
if (!pin)
|
|
335
|
-
return
|
|
386
|
+
return 'match';
|
|
387
|
+
let unknown = false;
|
|
336
388
|
if (pin.app !== undefined) {
|
|
337
|
-
const
|
|
338
|
-
if (
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
389
|
+
const actual = typeof pin.app === 'number' ? check.appId : check.appSlug;
|
|
390
|
+
if (actual === undefined)
|
|
391
|
+
unknown = true;
|
|
392
|
+
else if (actual !== pin.app)
|
|
393
|
+
return 'mismatch';
|
|
394
|
+
}
|
|
395
|
+
if (pin.workflow !== undefined) {
|
|
396
|
+
if (check.workflowPath === undefined)
|
|
397
|
+
unknown = true;
|
|
398
|
+
else if (check.workflowPath !== pin.workflow)
|
|
399
|
+
return 'mismatch';
|
|
400
|
+
}
|
|
401
|
+
return unknown ? 'unknown' : 'match';
|
|
402
|
+
}
|
|
403
|
+
function pinMatches(check, pin) {
|
|
404
|
+
return pinMatch(check, pin) === 'match';
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Whether a fetch left a required check's run without the identity its pin
|
|
408
|
+
* names (T13017), so the fetch is retried before anything is judged.
|
|
409
|
+
*
|
|
410
|
+
* @param checks - Checks fetched for one commit.
|
|
411
|
+
* @param required - Required check names.
|
|
412
|
+
* @param pins - Required-name to app/workflow pin.
|
|
413
|
+
* @returns True when a required run's identity is incomplete.
|
|
414
|
+
* @task T13017
|
|
415
|
+
*/
|
|
416
|
+
export function hasIncompleteIdentity(checks, required, pins) {
|
|
417
|
+
return checks.some((c) => required.includes(c.name) && pinMatch(c, pins[c.name]) === 'unknown');
|
|
344
418
|
}
|
|
345
419
|
function describeApp(check) {
|
|
346
420
|
return check.appSlug ?? (check.appId !== undefined ? `app ${check.appId}` : 'an unknown app');
|
|
@@ -402,17 +476,20 @@ export function evaluateMergeCommitChecks(required, checks, mergeCommitSha, opts
|
|
|
402
476
|
continue;
|
|
403
477
|
}
|
|
404
478
|
}
|
|
405
|
-
const impostor = named.find((c) =>
|
|
479
|
+
const impostor = named.find((c) => pinMatch(c, pins[name]) === 'mismatch');
|
|
480
|
+
const unidentified = named.some((c) => pinMatch(c, pins[name]) === 'unknown');
|
|
406
481
|
const elsewhere = pinned.find((c) => c.headSha !== mergeCommitSha &&
|
|
407
482
|
c.headSha !== treeEquivalentSha &&
|
|
408
483
|
c.headSha !== descendantSha);
|
|
409
|
-
reasons.push(onMerge.verdict === 'missing' &&
|
|
410
|
-
? `${name}:
|
|
411
|
-
: onMerge.verdict === 'missing' &&
|
|
412
|
-
? `${name}:
|
|
413
|
-
: onMerge.verdict === 'missing'
|
|
414
|
-
? `${name}: not
|
|
415
|
-
:
|
|
484
|
+
reasons.push(onMerge.verdict === 'missing' && unidentified && pinned.length === 0
|
|
485
|
+
? `${name}: pending (GitHub has not reported its run's app or workflow yet) on merge commit ${mergeCommitSha.slice(0, 12)}`
|
|
486
|
+
: onMerge.verdict === 'missing' && impostor && pinned.length === 0
|
|
487
|
+
? `${name}: posted by ${describeApp(impostor)}${impostor.workflowPath ? ` (${impostor.workflowPath})` : ''}, not the pinned ${describePin(pins[name])}`
|
|
488
|
+
: onMerge.verdict === 'missing' && elsewhere
|
|
489
|
+
? `${name}: ran on ${elsewhere.headSha.slice(0, 12)}, not the merge commit ${mergeCommitSha.slice(0, 12)}`
|
|
490
|
+
: onMerge.verdict === 'missing'
|
|
491
|
+
? `${name}: not found on merge commit ${mergeCommitSha.slice(0, 12)}`
|
|
492
|
+
: `${name}: ${onMerge.verdict} on merge commit ${mergeCommitSha.slice(0, 12)}`);
|
|
416
493
|
}
|
|
417
494
|
return reasons.length > 0 ? { ok: false, reasons } : { ok: true, checks: matched };
|
|
418
495
|
}
|
|
@@ -588,11 +665,13 @@ function lateRun(run, nextMainCommitAt) {
|
|
|
588
665
|
* @param ancestors - First-parent ancestors of the merge, nearest first, with their checks.
|
|
589
666
|
* @param pins - Required-name to app/workflow pin.
|
|
590
667
|
* @param nextMainCommitAt - When the next default-branch commit after the merge landed, if known.
|
|
668
|
+
* @param untested - Failing jobs with no failed test, a runner cancellation or a worker crash (T14049, T14074): never the PR's, so never blamed.
|
|
591
669
|
* @returns The required names to stand in for and the inherited jobs, or why not.
|
|
592
670
|
* @task T13453
|
|
593
671
|
* @task T14026
|
|
672
|
+
* @task T14049
|
|
594
673
|
*/
|
|
595
|
-
export function inheritedOnMerge(required, checks, mergeCommitSha, ancestors, pins, nextMainCommitAt = null) {
|
|
674
|
+
export function inheritedOnMerge(required, checks, mergeCommitSha, ancestors, pins, nextMainCommitAt = null, untested = new Set()) {
|
|
596
675
|
const scope = pinnedScope(required, pins);
|
|
597
676
|
const onMerge = latestRuns(checks, mergeCommitSha, scope);
|
|
598
677
|
const pending = onMerge.find((c) => c.status !== 'completed');
|
|
@@ -602,7 +681,7 @@ export function inheritedOnMerge(required, checks, mergeCommitSha, ancestors, pi
|
|
|
602
681
|
reason: `${pending.name}: pending on merge commit ${mergeCommitSha.slice(0, 12)}`,
|
|
603
682
|
};
|
|
604
683
|
}
|
|
605
|
-
const failing = onMerge.filter((c) => !HARMLESS_CONCLUSIONS.has(c.conclusion ?? ''));
|
|
684
|
+
const failing = onMerge.filter((c) => !HARMLESS_CONCLUSIONS.has(c.conclusion ?? '') && !untested.has(c.id));
|
|
606
685
|
if (failing.length === 0)
|
|
607
686
|
return { ok: false, reason: 'no job failed on the merge commit' };
|
|
608
687
|
const inherited = [];
|
|
@@ -713,11 +792,18 @@ const RUN_DIED = /Worker exited unexpectedly|Worker terminated due to reaching m
|
|
|
713
792
|
* quarantine, a file that failed to load, or counts that do not add up
|
|
714
793
|
* (per-file `(N tests | M failed)` lines included).
|
|
715
794
|
*
|
|
795
|
+
* With `unretriedTimeouts` (T14086, later runs only), a failing test whose
|
|
796
|
+
* error is vitest's own "Test timed out in N ms" also qualifies: the budget
|
|
797
|
+
* stopped the run before its rerun-once flake rule (T13145) could judge it,
|
|
798
|
+
* so the timeout decided nothing. An assertion failure never qualifies.
|
|
799
|
+
*
|
|
716
800
|
* @param log - The job's plain-text log (ANSI colour codes allowed).
|
|
801
|
+
* @param opts - `unretriedTimeouts`: accept unretried timeouts too.
|
|
717
802
|
* @returns Whether the job failed only on the quarantine budget.
|
|
718
803
|
* @task T13809
|
|
804
|
+
* @task T14086
|
|
719
805
|
*/
|
|
720
|
-
export function isQuarantineBudgetOnlyLog(log) {
|
|
806
|
+
export function isQuarantineBudgetOnlyLog(log, opts = {}) {
|
|
721
807
|
const text = log.replace(/\u001b\[[0-9;]*m/g, '');
|
|
722
808
|
// The script's own verdict is the last heading, with its count line under it
|
|
723
809
|
// (a test's fixture output may carry the heading too).
|
|
@@ -748,9 +834,19 @@ export function isQuarantineBudgetOnlyLog(log) {
|
|
|
748
834
|
...text.slice(heading).matchAll(new RegExp(`- \`(${testPath})\` > (.+)$`, 'gm')),
|
|
749
835
|
].map((m) => ({ file: m[1] ?? '', title: normTitle(m[2] ?? '') }));
|
|
750
836
|
const failing = new Map();
|
|
837
|
+
const timedOut = new Set();
|
|
751
838
|
for (const m of run.matchAll(new RegExp(`\\bFAIL\\s{2,}(?:\\S+\\s+)?(${testPath}) > (.+)$`, 'gm'))) {
|
|
752
839
|
const entry = { file: m[1] ?? '', title: normTitle(m[2] ?? '') };
|
|
753
|
-
|
|
840
|
+
const key = `${entry.file}\u0000${entry.title}`;
|
|
841
|
+
failing.set(key, entry);
|
|
842
|
+
// The failure's own error is the first line after its FAIL header.
|
|
843
|
+
const next = run
|
|
844
|
+
.slice((m.index ?? 0) + m[0].length)
|
|
845
|
+
.split('\n')
|
|
846
|
+
.map((l) => l.replace(/^\S*Z\s/, '').trim())
|
|
847
|
+
.find((l) => l !== '');
|
|
848
|
+
if (next && /^Error: Test timed out in \d+ms\b/.test(next))
|
|
849
|
+
timedOut.add(key);
|
|
754
850
|
}
|
|
755
851
|
if (failing.size !== failedTests)
|
|
756
852
|
return false;
|
|
@@ -758,7 +854,8 @@ export function isQuarantineBudgetOnlyLog(log) {
|
|
|
758
854
|
return false;
|
|
759
855
|
if (!perFile.every((p) => [...failing.values()].some((f) => f.file === p.file)))
|
|
760
856
|
return false;
|
|
761
|
-
return [...failing.
|
|
857
|
+
return [...failing.entries()].every(([key, f]) => quarantined.some((q) => q.title === f.title && q.file.endsWith(f.file)) ||
|
|
858
|
+
(opts.unretriedTimeouts === true && timedOut.has(key)));
|
|
762
859
|
}
|
|
763
860
|
/**
|
|
764
861
|
* Whether every failure on a commit's CI is a non-test failure (T13809): each
|
|
@@ -774,10 +871,11 @@ export function isQuarantineBudgetOnlyLog(log) {
|
|
|
774
871
|
* @param pins - Required-name to app/workflow pin.
|
|
775
872
|
* @param fetchJobLog - Plain-text log of one job.
|
|
776
873
|
* @param cwd - Repository work tree for `gh`.
|
|
874
|
+
* @param opts - `unretriedTimeouts` for a later run (T14086, {@link isQuarantineBudgetOnlyLog}).
|
|
777
875
|
* @returns The quarantine-only jobs, or why the commit failed for real.
|
|
778
876
|
* @task T13809
|
|
779
877
|
*/
|
|
780
|
-
export async function nonTestFailuresOn(checks, sha, required, pins, fetchJobLog, cwd) {
|
|
878
|
+
export async function nonTestFailuresOn(checks, sha, required, pins, fetchJobLog, cwd, opts = {}) {
|
|
781
879
|
const latest = latestRuns(checks, sha, pinnedScope(required, pins));
|
|
782
880
|
const jobs = [];
|
|
783
881
|
for (const c of latest) {
|
|
@@ -790,7 +888,7 @@ export async function nonTestFailuresOn(checks, sha, required, pins, fetchJobLog
|
|
|
790
888
|
if (c.source === 'workflow-run' || required.includes(c.name))
|
|
791
889
|
continue;
|
|
792
890
|
const log = c.conclusion === 'failure' ? await fetchJobLog(c.id, cwd) : null;
|
|
793
|
-
if (log === null || !isQuarantineBudgetOnlyLog(log)) {
|
|
891
|
+
if (log === null || !isQuarantineBudgetOnlyLog(log, opts)) {
|
|
794
892
|
return {
|
|
795
893
|
ok: false,
|
|
796
894
|
reason: `${c.name}: ${c.conclusion} on ${sha.slice(0, 12)}${log === null && c.conclusion === 'failure' ? ' (its log could not be read)' : ''} — not the flaky-quarantine budget alone`,
|
|
@@ -846,11 +944,111 @@ export async function cancelledWithoutTestFailure(checks, sha, required, pins, f
|
|
|
846
944
|
return false;
|
|
847
945
|
if (runs.some((r) => r.conclusion !== 'cancelled' && r.conclusion !== 'success'))
|
|
848
946
|
return false;
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
947
|
+
return ((await everyFailedJobLog(checks, sha, required, pins, fetchJobLog, cwd, noTestFailedLog)) !==
|
|
948
|
+
null);
|
|
949
|
+
}
|
|
950
|
+
/** Whether a job log names no failed test and no dead vitest run (T13809). */
|
|
951
|
+
function noTestFailedLog(log) {
|
|
952
|
+
const text = log.replace(/\u001b\[[0-9;]*m/g, '');
|
|
953
|
+
return !/\bFAIL\s{2,}|\d+ failed/.test(text) && !RUN_DIED.test(text);
|
|
954
|
+
}
|
|
955
|
+
/** The errors GitHub prints when the runner, not the job, stopped a step (T14049). */
|
|
956
|
+
const RUNNER_CANCEL_ERROR = /##\[error\](?:The operation was canceled\.|The runner has received a shutdown signal\b.*)\s*$/;
|
|
957
|
+
/**
|
|
958
|
+
* Whether a job log shows only a runner cancellation (T14049): every
|
|
959
|
+
* `##[error]` line is GitHub's own "The operation was canceled." (or the
|
|
960
|
+
* runner shutdown notice before it), and no test failed. GitHub then reports
|
|
961
|
+
* the job, and the aggregate check that needs it, as `failure`, though
|
|
962
|
+
* nothing the PR changed failed.
|
|
963
|
+
*
|
|
964
|
+
* @param log - The job's plain-text log (ANSI colour codes allowed).
|
|
965
|
+
* @returns Whether the job failed only because its runner was cancelled.
|
|
966
|
+
* @task T14049
|
|
967
|
+
*/
|
|
968
|
+
export function isRunnerCancelOnlyLog(log) {
|
|
969
|
+
const text = log.replace(/\u001b\[[0-9;]*m/g, '');
|
|
970
|
+
const errors = text.split('\n').filter((line) => line.includes('##[error]'));
|
|
971
|
+
return (errors.some((line) => /##\[error\]The operation was canceled\./.test(line)) &&
|
|
972
|
+
errors.every((line) => RUNNER_CANCEL_ERROR.test(line)) &&
|
|
973
|
+
noTestFailedLog(text));
|
|
974
|
+
}
|
|
975
|
+
/** A vitest summary or failure line naming a failed test or file. */
|
|
976
|
+
const FAILED_TEST = /\bFAIL\s{2,}|\d+ failed/;
|
|
977
|
+
/**
|
|
978
|
+
* Whether a job log shows only an unattributable vitest worker crash
|
|
979
|
+
* (T14074): no test or file failed, every `Unhandled Error` vitest reports is
|
|
980
|
+
* its own pool's "Worker forks emitted error" (a fork that exited
|
|
981
|
+
* unexpectedly, e.g. a native addon abort), and the only `##[error]` is the
|
|
982
|
+
* step's non-zero exit. Such a run never showed a test of the change failing.
|
|
983
|
+
*
|
|
984
|
+
* @param log - The job's plain-text log (ANSI colour codes allowed).
|
|
985
|
+
* @returns Whether the job failed only on a worker crash.
|
|
986
|
+
* @task T14074
|
|
987
|
+
*/
|
|
988
|
+
export function isWorkerCrashOnlyLog(log) {
|
|
989
|
+
const text = log.replace(/\u001b\[[0-9;]*m/g, '');
|
|
990
|
+
const crashes = text.match(/Error: \[vitest-pool\]: Worker forks emitted error\./g)?.length ?? 0;
|
|
991
|
+
const unhandled = text.match(/⎯+ Unhandled Error ⎯+/g)?.length ?? 0;
|
|
992
|
+
const errors = text.split('\n').filter((line) => line.includes('##[error]'));
|
|
993
|
+
return (crashes > 0 &&
|
|
994
|
+
unhandled === crashes &&
|
|
995
|
+
/Caused by: Error: Worker exited unexpectedly/.test(text) &&
|
|
996
|
+
!FAILED_TEST.test(text) &&
|
|
997
|
+
errors.every((line) => /##\[error\]Process completed with exit code \d+\.\s*$/.test(line)));
|
|
998
|
+
}
|
|
999
|
+
/**
|
|
1000
|
+
* Why a failing job's log shows no failed test, or `null` when it may have
|
|
1001
|
+
* failed on the change: a runner cancellation ({@link isRunnerCancelOnlyLog})
|
|
1002
|
+
* or a worker crash ({@link isWorkerCrashOnlyLog}).
|
|
1003
|
+
*
|
|
1004
|
+
* @param log - The job's plain-text log.
|
|
1005
|
+
* @returns The cause, or `null`.
|
|
1006
|
+
* @task T14074
|
|
1007
|
+
*/
|
|
1008
|
+
export function untestedFailureCause(log) {
|
|
1009
|
+
if (isRunnerCancelOnlyLog(log))
|
|
1010
|
+
return 'runner-cancel';
|
|
1011
|
+
if (isWorkerCrashOnlyLog(log))
|
|
1012
|
+
return 'worker-crash';
|
|
1013
|
+
return null;
|
|
1014
|
+
}
|
|
1015
|
+
/**
|
|
1016
|
+
* The failing jobs of a pinned workflow on `sha` (the required aggregate and
|
|
1017
|
+
* workflow runs excluded) that failed without a failed test: a runner
|
|
1018
|
+
* cancellation (T14049) or a worker crash (T14074), by
|
|
1019
|
+
* {@link untestedFailureCause}. A pending job or an unreadable log is never
|
|
1020
|
+
* one, so `all` is then false.
|
|
1021
|
+
*
|
|
1022
|
+
* @param checks - Checks fetched for `sha`.
|
|
1023
|
+
* @param sha - The commit judged.
|
|
1024
|
+
* @param required - Required check names.
|
|
1025
|
+
* @param pins - Required-name to app/workflow pin.
|
|
1026
|
+
* @param fetchJobLog - Plain-text log of one job.
|
|
1027
|
+
* @param cwd - Repository work tree for `gh`.
|
|
1028
|
+
* @returns Those jobs, and whether they are every failure.
|
|
1029
|
+
* @task T14049
|
|
1030
|
+
* @task T14074
|
|
1031
|
+
*/
|
|
1032
|
+
export async function untestedJobFailures(checks, sha, required, pins, fetchJobLog, cwd) {
|
|
1033
|
+
const jobs = [];
|
|
1034
|
+
let all = true;
|
|
1035
|
+
for (const c of latestRuns(checks, sha, pinnedScope(required, pins))) {
|
|
1036
|
+
if (c.source === 'workflow-run' || required.includes(c.name))
|
|
1037
|
+
continue;
|
|
1038
|
+
if (c.status !== 'completed') {
|
|
1039
|
+
all = false;
|
|
1040
|
+
continue;
|
|
1041
|
+
}
|
|
1042
|
+
if (HARMLESS_CONCLUSIONS.has(c.conclusion ?? ''))
|
|
1043
|
+
continue;
|
|
1044
|
+
const log = c.conclusion === 'failure' ? await fetchJobLog(c.id, cwd) : null;
|
|
1045
|
+
const cause = log === null ? null : untestedFailureCause(log);
|
|
1046
|
+
if (cause)
|
|
1047
|
+
jobs.push({ name: c.name, mergeRunId: c.id, cause });
|
|
1048
|
+
else
|
|
1049
|
+
all = false;
|
|
1050
|
+
}
|
|
1051
|
+
return { jobs, ids: new Set(jobs.map((j) => j.mergeRunId)), all: all && jobs.length > 0 };
|
|
854
1052
|
}
|
|
855
1053
|
/**
|
|
856
1054
|
* Whether a job log shows a failure in a setup step before any test ran
|
|
@@ -964,16 +1162,28 @@ export async function findGreenDescendant(input) {
|
|
|
964
1162
|
});
|
|
965
1163
|
let inherited;
|
|
966
1164
|
let nonTest;
|
|
1165
|
+
let untested;
|
|
1166
|
+
let untestedIds = new Set();
|
|
1167
|
+
const unproven = () => required.filter((name) => !judgeOnSha(input.checks.filter((c) => c.name === name && pinMatches(c, pins[name])), merge).ok);
|
|
1168
|
+
if (!superseded.ok && input.fetchJobLog) {
|
|
1169
|
+
// T14049, T14074: a job that failed with no failed test (a runner
|
|
1170
|
+
// cancellation, or a vitest worker crash) decided nothing, though GitHub
|
|
1171
|
+
// reports it and the aggregate as `failure`: it is superseded, as a
|
|
1172
|
+
// cancelled run is. A job with any failed test never is.
|
|
1173
|
+
const found = await untestedJobFailures(input.checks, merge, required, pins, input.fetchJobLog, cwd);
|
|
1174
|
+
untestedIds = found.ids;
|
|
1175
|
+
if (found.all) {
|
|
1176
|
+
untested = found.jobs;
|
|
1177
|
+
superseded = { ok: true, names: unproven() };
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
967
1180
|
if (!superseded.ok && input.fetchJobLog) {
|
|
968
1181
|
// T13809: a merge-commit run red only because main's flaky quarantine was
|
|
969
1182
|
// over budget (every test passed) failed no test of the PR's.
|
|
970
1183
|
const result = await nonTestFailuresOn(input.checks, merge, required, pins, input.fetchJobLog, cwd);
|
|
971
1184
|
if (result.ok) {
|
|
972
1185
|
nonTest = result.jobs;
|
|
973
|
-
superseded = {
|
|
974
|
-
ok: true,
|
|
975
|
-
names: required.filter((name) => !judgeOnSha(input.checks.filter((c) => c.name === name && pinMatches(c, pins[name])), merge).ok),
|
|
976
|
-
};
|
|
1186
|
+
superseded = { ok: true, names: unproven() };
|
|
977
1187
|
}
|
|
978
1188
|
}
|
|
979
1189
|
if (!superseded.ok && input.firstParentOf) {
|
|
@@ -991,7 +1201,7 @@ export async function findGreenDescendant(input) {
|
|
|
991
1201
|
break;
|
|
992
1202
|
ancestors.push({ sha: at, checks: fetched.checks });
|
|
993
1203
|
}
|
|
994
|
-
const result = inheritedOnMerge(required, input.checks, merge, ancestors, pins, input.nextMainCommitAt?.(merge, input.ref, cwd) ?? null);
|
|
1204
|
+
const result = inheritedOnMerge(required, input.checks, merge, ancestors, pins, input.nextMainCommitAt?.(merge, input.ref, cwd) ?? null, untestedIds);
|
|
995
1205
|
if (result.ok) {
|
|
996
1206
|
superseded = { ok: true, names: result.names };
|
|
997
1207
|
inherited = result.inherited;
|
|
@@ -1043,6 +1253,7 @@ export async function findGreenDescendant(input) {
|
|
|
1043
1253
|
["the PR's files", input.changedPaths],
|
|
1044
1254
|
[`a CI definition (${ciPaths.join(', ')})`, ciPaths],
|
|
1045
1255
|
];
|
|
1256
|
+
const passedOver = [];
|
|
1046
1257
|
for (const sha of candidates.slice(0, CI_DESCENDANT_MAX_CANDIDATES)) {
|
|
1047
1258
|
if (sha === merge || !input.isAncestor(merge, sha, cwd))
|
|
1048
1259
|
continue;
|
|
@@ -1067,9 +1278,17 @@ export async function findGreenDescendant(input) {
|
|
|
1067
1278
|
// T13809: a later run red only for the quarantine budget, cancelled with
|
|
1068
1279
|
// no test failing, or failed before any test ran decides nothing; look on.
|
|
1069
1280
|
// Any other failure still refuses: a green run is never shopped for past a real one.
|
|
1070
|
-
|
|
1071
|
-
|
|
1281
|
+
// T14086: in such a run, a test that only timed out was never retried, so
|
|
1282
|
+
// it decided nothing either; an assertion failure still refuses.
|
|
1283
|
+
const quarantineOnly = await nonTestFailuresOn(fetched.checks, sha, required, pins, input.fetchJobLog, cwd, { unretriedTimeouts: true });
|
|
1284
|
+
if (quarantineOnly.ok) {
|
|
1285
|
+
passedOver.push({
|
|
1286
|
+
sha,
|
|
1287
|
+
jobs: quarantineOnly.jobs.map((j) => j.name),
|
|
1288
|
+
reason: 'the flaky-quarantine budget stopped the run before its flake rerun',
|
|
1289
|
+
});
|
|
1072
1290
|
continue;
|
|
1291
|
+
}
|
|
1073
1292
|
if (await cancelledWithoutTestFailure(fetched.checks, sha, required, pins, input.fetchJobLog, cwd)) {
|
|
1074
1293
|
continue;
|
|
1075
1294
|
}
|
|
@@ -1096,6 +1315,8 @@ export async function findGreenDescendant(input) {
|
|
|
1096
1315
|
...(head ? { prHeadSha: head } : {}),
|
|
1097
1316
|
...(inherited ? { inherited } : {}),
|
|
1098
1317
|
...(nonTest ? { nonTest } : {}),
|
|
1318
|
+
...(untested ? { untested } : {}),
|
|
1319
|
+
...(passedOver.length > 0 ? { passedOver } : {}),
|
|
1099
1320
|
};
|
|
1100
1321
|
}
|
|
1101
1322
|
return {
|
|
@@ -1431,6 +1652,26 @@ async function defaultFetchJobLog(jobId, cwd) {
|
|
|
1431
1652
|
return null;
|
|
1432
1653
|
}
|
|
1433
1654
|
}
|
|
1655
|
+
/**
|
|
1656
|
+
* Fetch once more when a required check's run came back without the identity
|
|
1657
|
+
* its pin names (T13017): GitHub lists a job before its workflow run, and a
|
|
1658
|
+
* run judged from that response would read as another app's. A run still
|
|
1659
|
+
* incomplete after the retry is judged pending, never as a mismatch.
|
|
1660
|
+
*
|
|
1661
|
+
* @param fetchChecks - The underlying check fetch.
|
|
1662
|
+
* @param required - Required check names.
|
|
1663
|
+
* @param pins - Required-name to app/workflow pin.
|
|
1664
|
+
* @returns The same fetch, retried once on incomplete identity.
|
|
1665
|
+
* @task T13017
|
|
1666
|
+
*/
|
|
1667
|
+
export function refetchIncompleteIdentity(fetchChecks, required, pins) {
|
|
1668
|
+
return async (sha, cwd) => {
|
|
1669
|
+
const first = await fetchChecks(sha, cwd);
|
|
1670
|
+
if (!first.ok || !hasIncompleteIdentity(first.checks, required, pins))
|
|
1671
|
+
return first;
|
|
1672
|
+
return fetchChecks(sha, cwd);
|
|
1673
|
+
};
|
|
1674
|
+
}
|
|
1434
1675
|
/**
|
|
1435
1676
|
* Drop runs a schedule started from every fetch (T13623). A nightly run that
|
|
1436
1677
|
* happens to land on a merge commit tests main at that hour, not the merge:
|
|
@@ -1755,7 +1996,7 @@ export async function resolveCiEvidenceAtom(prNumber, roots, opts = {}) {
|
|
|
1755
1996
|
];
|
|
1756
1997
|
const editedWorkflows = pinnedWorkflows.filter((w) => pr.changedPaths.includes(w));
|
|
1757
1998
|
const mainOnly = editedWorkflows.length > 0;
|
|
1758
|
-
const fetchChecks = withoutScheduledRuns(opts.fetchChecks ?? defaultFetchChecks);
|
|
1999
|
+
const fetchChecks = refetchIncompleteIdentity(withoutScheduledRuns(opts.fetchChecks ?? defaultFetchChecks), required.workflows, pins);
|
|
1759
2000
|
const fetched = await fetchChecks(pr.mergeCommitSha, roots.executionRoot);
|
|
1760
2001
|
if (!fetched.ok)
|
|
1761
2002
|
return { ok: false, reason: fetched.reason, codeName: 'E_EVIDENCE_TOOL_FAILED' };
|
|
@@ -1829,7 +2070,9 @@ export async function resolveCiEvidenceAtom(prNumber, roots, opts = {}) {
|
|
|
1829
2070
|
...(treeEqualHead ? { treeEquivalentSha: treeEqualHead } : {}),
|
|
1830
2071
|
pins,
|
|
1831
2072
|
descendantSha: found.sha,
|
|
1832
|
-
...(found.inherited || found.nonTest
|
|
2073
|
+
...(found.inherited || found.nonTest || found.untested
|
|
2074
|
+
? { inheritedFailures: true }
|
|
2075
|
+
: {}),
|
|
1833
2076
|
});
|
|
1834
2077
|
if (rejudged.ok) {
|
|
1835
2078
|
checks = withDescendant;
|
|
@@ -1840,6 +2083,8 @@ export async function resolveCiEvidenceAtom(prNumber, roots, opts = {}) {
|
|
|
1840
2083
|
...(found.prHeadSha ? { prHeadSha: found.prHeadSha } : {}),
|
|
1841
2084
|
...(found.inherited ? { inherited: found.inherited } : {}),
|
|
1842
2085
|
...(found.nonTest ? { nonTest: found.nonTest } : {}),
|
|
2086
|
+
...(found.untested ? { untested: found.untested } : {}),
|
|
2087
|
+
...(found.passedOver ? { passedOver: found.passedOver } : {}),
|
|
1843
2088
|
};
|
|
1844
2089
|
}
|
|
1845
2090
|
else {
|
|
@@ -1898,19 +2143,22 @@ export async function resolveCiEvidenceAtom(prNumber, roots, opts = {}) {
|
|
|
1898
2143
|
}
|
|
1899
2144
|
if (!judgedChecks.ok) {
|
|
1900
2145
|
const waiting = judgedChecks.reasons.some((r) => /pending|not found|missing/.test(r));
|
|
2146
|
+
const merge12 = pr.mergeCommitSha.slice(0, 12);
|
|
2147
|
+
// T14042: the decisive reason and the next step lead; the detail follows,
|
|
2148
|
+
// so a truncated refusal still says what to do.
|
|
2149
|
+
const lead = mainOnly
|
|
2150
|
+
? `PR #${prNumber} edits the pinned workflow ${editedWorkflows.join(', ')}, so only main's push CI attests it (T13174)` +
|
|
2151
|
+
(waiting
|
|
2152
|
+
? `: wait for the push run on ${merge12} (or a later main commit) to finish, then verify again. No local run is needed.`
|
|
2153
|
+
: `, and it is not green: ${judgedChecks.reasons[0]}.`)
|
|
2154
|
+
: `Required CI on PR #${prNumber}'s merge commit ${merge12} is not green: ${judgedChecks.reasons[0]}` +
|
|
2155
|
+
(waiting ? '; wait for it to finish, then verify again.' : '.');
|
|
1901
2156
|
return {
|
|
1902
2157
|
ok: false,
|
|
1903
2158
|
codeName: 'E_EVIDENCE_TESTS_FAILED',
|
|
1904
|
-
reason:
|
|
1905
|
-
`(source: ${describeRequiredWorkflowsSource(required.source)}):\n - ${judgedChecks.reasons.join('\n - ')}` +
|
|
2159
|
+
reason: `${lead}\n Required checks (source: ${describeRequiredWorkflowsSource(required.source)}):\n - ${judgedChecks.reasons.join('\n - ')}` +
|
|
1906
2160
|
(descendantReason ? `\n No later main run stands in: ${descendantReason}` : '') +
|
|
1907
|
-
(currentHeadReason ? `\n No current-HEAD run stands in: ${currentHeadReason}` : '')
|
|
1908
|
-
(mainOnly
|
|
1909
|
-
? `\n PR #${prNumber} edits the pinned workflow ${editedWorkflows.join(', ')}, so only main's push CI attests it (T13174)` +
|
|
1910
|
-
(waiting
|
|
1911
|
-
? `: wait for the push run on ${pr.mergeCommitSha.slice(0, 12)} (or a later main commit) to finish, then verify again. No local run is needed.`
|
|
1912
|
-
: '.')
|
|
1913
|
-
: ''),
|
|
2161
|
+
(currentHeadReason ? `\n No current-HEAD run stands in: ${currentHeadReason}` : ''),
|
|
1914
2162
|
};
|
|
1915
2163
|
}
|
|
1916
2164
|
// Round 2 (skipped tests): the aggregate counts skipped jobs as a pass. For a
|
|
@@ -2023,6 +2271,8 @@ export async function resolveCiEvidenceAtom(prNumber, roots, opts = {}) {
|
|
|
2023
2271
|
...(descendant.prHeadSha ? { descendantPrHeadSha: descendant.prHeadSha } : {}),
|
|
2024
2272
|
...(descendant.inherited ? { inheritedFailures: descendant.inherited } : {}),
|
|
2025
2273
|
...(descendant.nonTest ? { nonTestFailures: descendant.nonTest } : {}),
|
|
2274
|
+
...(descendant.untested ? { untestedFailures: descendant.untested } : {}),
|
|
2275
|
+
...(descendant.passedOver ? { passedOverRuns: descendant.passedOver } : {}),
|
|
2026
2276
|
}
|
|
2027
2277
|
: {}),
|
|
2028
2278
|
...(workspaceWide ? { workspaceWide } : {}),
|