@intentius/chant 0.124.32 → 0.124.34
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/ci-jobs.d.ts +4 -0
- package/dist/ci-jobs.d.ts.map +1 -1
- package/dist/ci-jobs.js +9 -2
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/build.js +18 -3
- package/dist/cli/handlers/lifecycle-health.d.ts +16 -0
- package/dist/cli/handlers/lifecycle-health.d.ts.map +1 -0
- package/dist/cli/handlers/lifecycle-health.js +150 -0
- package/dist/cli/handlers/lifecycle.d.ts +26 -0
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.js +1 -1
- package/dist/cli/handlers/pr.d.ts +2 -0
- package/dist/cli/handlers/pr.d.ts.map +1 -1
- package/dist/cli/handlers/pr.js +124 -2
- package/dist/cli/handlers/promote.d.ts.map +1 -1
- package/dist/cli/handlers/promote.js +277 -74
- package/dist/cli/handlers/release.d.ts +16 -1
- package/dist/cli/handlers/release.d.ts.map +1 -1
- package/dist/cli/handlers/release.js +83 -11
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +99 -22
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +30 -1
- package/dist/cli/registry.d.ts +21 -5
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/components/deploy-inputs.d.ts +91 -0
- package/dist/components/deploy-inputs.d.ts.map +1 -0
- package/dist/components/deploy-inputs.js +184 -0
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/driver.js +9 -0
- package/dist/components/gate-plan.d.ts +8 -0
- package/dist/components/gate-plan.d.ts.map +1 -1
- package/dist/components/gate-plan.js +6 -0
- package/dist/components/pr-pipeline.d.ts +33 -10
- package/dist/components/pr-pipeline.d.ts.map +1 -1
- package/dist/components/pr-pipeline.js +42 -11
- package/dist/components/pr-run.d.ts +1 -1
- package/dist/components/pr-run.d.ts.map +1 -1
- package/dist/components/promote-rounds.d.ts +102 -0
- package/dist/components/promote-rounds.d.ts.map +1 -0
- package/dist/components/promote-rounds.js +174 -0
- package/dist/components/release-compile.d.ts +37 -2
- package/dist/components/release-compile.d.ts.map +1 -1
- package/dist/components/release-compile.js +64 -8
- package/dist/components/release-gate.d.ts +30 -0
- package/dist/components/release-gate.d.ts.map +1 -1
- package/dist/components/release-gate.js +92 -42
- package/dist/components/release-promote.d.ts +17 -3
- package/dist/components/release-promote.d.ts.map +1 -1
- package/dist/components/release-promote.js +79 -18
- package/dist/components/verbs/bom-writer.d.ts +1 -1
- package/dist/components/verbs/bom-writer.d.ts.map +1 -1
- package/dist/components/verbs/lockfile-sbom-generator.d.ts +20 -0
- package/dist/components/verbs/lockfile-sbom-generator.d.ts.map +1 -1
- package/dist/components/verbs/lockfile-sbom-generator.js +41 -4
- package/dist/components/wave-plan.d.ts +10 -0
- package/dist/components/wave-plan.d.ts.map +1 -1
- package/dist/components/wave-plan.js +32 -4
- package/dist/graph-declared.d.ts +4 -0
- package/dist/graph-declared.d.ts.map +1 -1
- package/dist/graph-declared.js +11 -1
- package/dist/graph-detail.d.ts.map +1 -1
- package/dist/graph-detail.js +32 -7
- package/dist/health.d.ts +110 -0
- package/dist/health.d.ts.map +1 -0
- package/dist/health.js +105 -0
- package/dist/health.schema.json +191 -0
- package/dist/lexicon.d.ts +27 -0
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/merge-record.d.ts +104 -0
- package/dist/lifecycle/merge-record.d.ts.map +1 -0
- package/dist/lifecycle/merge-record.js +102 -0
- package/dist/lifecycle/publish-attachments.d.ts +8 -5
- package/dist/lifecycle/publish-attachments.d.ts.map +1 -1
- package/dist/lifecycle/publish-attachments.js +27 -13
- package/dist/lifecycle/publish-npm.d.ts +3 -2
- package/dist/lifecycle/publish-npm.d.ts.map +1 -1
- package/dist/lifecycle/publish-npm.js +4 -3
- package/dist/lifecycle/release-at.d.ts +40 -0
- package/dist/lifecycle/release-at.d.ts.map +1 -0
- package/dist/lifecycle/release-at.js +83 -0
- package/dist/lifecycle/release-cut.d.ts.map +1 -1
- package/dist/lifecycle/release-cut.js +23 -12
- package/dist/lifecycle/release-publish.d.ts +28 -12
- package/dist/lifecycle/release-publish.d.ts.map +1 -1
- package/dist/lifecycle/release-publish.js +29 -17
- package/dist/lifecycle/release-store.d.ts +25 -2
- package/dist/lifecycle/release-store.d.ts.map +1 -1
- package/dist/lifecycle/release-store.js +40 -13
- package/dist/lifecycle/run-ledger.d.ts.map +1 -1
- package/dist/lifecycle/run-ledger.js +1 -0
- package/dist/lint/rules/op/index.d.ts +2 -1
- package/dist/lint/rules/op/index.d.ts.map +1 -1
- package/dist/lint/rules/op/index.js +4 -2
- package/dist/lint/rules/op/ops018-shell-image-digest.d.ts +10 -0
- package/dist/lint/rules/op/ops018-shell-image-digest.d.ts.map +1 -0
- package/dist/lint/rules/op/ops018-shell-image-digest.js +38 -0
- package/dist/op/activities/activity-contracts.d.ts +5 -0
- package/dist/op/activities/activity-contracts.d.ts.map +1 -1
- package/dist/op/activities/activity-contracts.js +3 -0
- package/dist/op/activities/shell.d.ts +38 -0
- package/dist/op/activities/shell.d.ts.map +1 -1
- package/dist/op/activities/shell.js +89 -3
- package/dist/op/builders.d.ts +7 -0
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/builders.js +7 -0
- package/dist/op/runtime.d.ts +2 -0
- package/dist/op/runtime.d.ts.map +1 -1
- package/dist/pr-loop.d.ts +14 -3
- package/dist/pr-loop.d.ts.map +1 -1
- package/dist/pr-loop.js +15 -5
- package/dist/pr-report-url.d.ts +2 -2
- package/dist/pr-report-url.d.ts.map +1 -1
- package/dist/runner-start.d.ts +179 -0
- package/dist/runner-start.d.ts.map +1 -0
- package/dist/runner-start.js +277 -0
- package/dist/wave-runtime.mjs +236 -229
- package/dist/workspace/__fixtures__/contract-repo.d.ts.map +1 -1
- package/dist/workspace/__fixtures__/contract-repo.js +13 -2
- package/dist/workspace/check.schema.json +88 -2
- package/dist/workspace/checks/ci-workflow.d.ts +17 -0
- package/dist/workspace/checks/ci-workflow.d.ts.map +1 -0
- package/dist/workspace/checks/ci-workflow.js +123 -0
- package/dist/workspace/checks/security.d.ts +50 -0
- package/dist/workspace/checks/security.d.ts.map +1 -0
- package/dist/workspace/checks/security.js +305 -0
- package/dist/workspace/checks.d.ts +2 -0
- package/dist/workspace/checks.d.ts.map +1 -1
- package/dist/workspace/checks.js +8 -0
- package/dist/workspace/ci-builds.d.ts +163 -0
- package/dist/workspace/ci-builds.d.ts.map +1 -0
- package/dist/workspace/ci-builds.js +315 -0
- package/dist/workspace/ci-green-cli.d.ts +2 -1
- package/dist/workspace/ci-green-cli.d.ts.map +1 -1
- package/dist/workspace/ci-green-cli.js +25 -5
- package/dist/workspace/ci-green-forge.d.ts +12 -0
- package/dist/workspace/ci-green-forge.d.ts.map +1 -1
- package/dist/workspace/ci-green-forge.js +12 -0
- package/dist/workspace/ci-green-workflow.d.ts +55 -1
- package/dist/workspace/ci-green-workflow.d.ts.map +1 -1
- package/dist/workspace/ci-green-workflow.js +128 -1
- package/dist/workspace/ci-green.d.ts +89 -4
- package/dist/workspace/ci-green.d.ts.map +1 -1
- package/dist/workspace/ci-green.js +310 -23
- package/dist/workspace/ci-init.d.ts +100 -0
- package/dist/workspace/ci-init.d.ts.map +1 -0
- package/dist/workspace/ci-init.js +434 -0
- package/dist/workspace/ci-last-green.schema.json +49 -0
- package/dist/workspace/ci-list.schema.json +35 -1
- package/dist/workspace/ci-run-index.d.ts +1 -1
- package/dist/workspace/ci-run-index.d.ts.map +1 -1
- package/dist/workspace/ci-run-index.schema.json +1 -1
- package/dist/workspace/ci-run.d.ts +12 -6
- package/dist/workspace/ci-run.d.ts.map +1 -1
- package/dist/workspace/ci-run.js +72 -16
- package/dist/workspace/ci-runner-start.d.ts +77 -0
- package/dist/workspace/ci-runner-start.d.ts.map +1 -0
- package/dist/workspace/ci-runner-start.js +185 -0
- package/dist/workspace/ci-serve.d.ts +76 -7
- package/dist/workspace/ci-serve.d.ts.map +1 -1
- package/dist/workspace/ci-serve.js +153 -19
- package/dist/workspace/ci-workflow.d.ts +91 -0
- package/dist/workspace/ci-workflow.d.ts.map +1 -0
- package/dist/workspace/ci-workflow.js +33 -0
- package/dist/workspace/declaration.d.ts +35 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.js +12 -2
- package/dist/workspace/declaration.schema.json +51 -0
- package/dist/workspace/init.js +2 -2
- package/dist/workspace/ls.d.ts +2 -0
- package/dist/workspace/ls.d.ts.map +1 -1
- package/dist/workspace/ls.js +1 -1
- package/dist/workspace/ls.schema.json +2 -1
- package/dist/workspace/member-pipeline.d.ts +1 -0
- package/dist/workspace/member-pipeline.d.ts.map +1 -1
- package/dist/workspace/member-pipeline.js +2 -0
- package/dist/workspace/merge-record.schema.json +49 -0
- package/dist/workspace/pr-report.schema.json +3 -3
- package/dist/workspace/reason-codes.d.ts +26 -0
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/reason-codes.js +28 -0
- package/dist/workspace/release-candidates.d.ts +129 -0
- package/dist/workspace/release-candidates.d.ts.map +1 -0
- package/dist/workspace/release-candidates.js +233 -0
- package/dist/workspace/release-candidates.schema.json +353 -0
- package/dist/workspace/release-workflow.d.ts +31 -0
- package/dist/workspace/release-workflow.d.ts.map +1 -0
- package/dist/workspace/release-workflow.js +150 -0
- package/dist/workspace/security.d.ts +40 -0
- package/dist/workspace/security.d.ts.map +1 -0
- package/dist/workspace/security.js +99 -0
- package/package.json +8 -1
- package/src/__fixtures__/pr-loop/pr-plan.note.golden.md +1 -1
- package/src/ci-jobs.ts +11 -2
- package/src/cli/handlers/build.test.ts +36 -0
- package/src/cli/handlers/build.ts +18 -3
- package/src/cli/handlers/lifecycle-health.test.ts +123 -0
- package/src/cli/handlers/lifecycle-health.ts +157 -0
- package/src/cli/handlers/lifecycle.ts +1 -1
- package/src/cli/handlers/pr-deploy-inputs.test.ts +209 -0
- package/src/cli/handlers/pr-record.test.ts +201 -0
- package/src/cli/handlers/pr.test.ts +22 -0
- package/src/cli/handlers/pr.ts +127 -4
- package/src/cli/handlers/promote.test.ts +17 -0
- package/src/cli/handlers/promote.ts +296 -84
- package/src/cli/handlers/release-cut-at.e2e.test.ts +115 -0
- package/src/cli/handlers/release-publish-targets.e2e.test.ts +2 -2
- package/src/cli/handlers/release-verify.e2e.test.ts +12 -10
- package/src/cli/handlers/release.e2e.test.ts +19 -28
- package/src/cli/handlers/release.ts +81 -11
- package/src/cli/main.ts +99 -22
- package/src/cli/parse-args.ts +22 -1
- package/src/cli/registry.ts +21 -5
- package/src/components/deploy-inputs.test.ts +108 -0
- package/src/components/deploy-inputs.ts +225 -0
- package/src/components/driver.ts +11 -0
- package/src/components/gate-plan.ts +9 -0
- package/src/components/pr-pipeline.ts +56 -15
- package/src/components/pr-run.ts +1 -1
- package/src/components/promote-rounds.test.ts +220 -0
- package/src/components/promote-rounds.ts +232 -0
- package/src/components/release-compile.test.ts +34 -0
- package/src/components/release-compile.ts +89 -8
- package/src/components/release-gate.test.ts +47 -0
- package/src/components/release-gate.ts +111 -46
- package/src/components/release-promote.test.ts +55 -2
- package/src/components/release-promote.ts +93 -23
- package/src/components/verbs/bom-writer.ts +1 -1
- package/src/components/verbs/lockfile-sbom-generator.test.ts +36 -0
- package/src/components/verbs/lockfile-sbom-generator.ts +43 -5
- package/src/components/wave-plan.ts +32 -4
- package/src/graph-declared-exports.test.ts +40 -0
- package/src/graph-declared.ts +9 -1
- package/src/graph-detail.test.ts +57 -0
- package/src/graph-detail.ts +33 -7
- package/src/health.schema.json +191 -0
- package/src/health.test.ts +62 -0
- package/src/health.ts +178 -0
- package/src/lexicon.ts +41 -1
- package/src/lifecycle/merge-record.test.ts +111 -0
- package/src/lifecycle/merge-record.ts +154 -0
- package/src/lifecycle/publish-attachments.ts +25 -13
- package/src/lifecycle/publish-npm.ts +4 -3
- package/src/lifecycle/publish-targets.test.ts +34 -4
- package/src/lifecycle/release-at.test.ts +48 -0
- package/src/lifecycle/release-at.ts +100 -0
- package/src/lifecycle/release-cut.test.ts +41 -0
- package/src/lifecycle/release-cut.ts +22 -11
- package/src/lifecycle/release-publish.test.ts +43 -13
- package/src/lifecycle/release-publish.ts +38 -18
- package/src/lifecycle/release-store.ts +57 -13
- package/src/lifecycle/run-ledger.test.ts +13 -0
- package/src/lifecycle/run-ledger.ts +1 -0
- package/src/lint/rules/op/index.ts +4 -2
- package/src/lint/rules/op/ops018-shell-image-digest.test.ts +60 -0
- package/src/lint/rules/op/ops018-shell-image-digest.ts +48 -0
- package/src/op/activities/activity-contracts.ts +3 -0
- package/src/op/activities/shell-container.e2e.test.ts +67 -0
- package/src/op/activities/shell.test.ts +44 -1
- package/src/op/activities/shell.ts +110 -3
- package/src/op/builders.ts +7 -0
- package/src/op/runtime.ts +2 -0
- package/src/op/step-console.test.ts +4 -2
- package/src/pr-loop.test.ts +29 -0
- package/src/pr-loop.ts +26 -7
- package/src/pr-report-url.ts +2 -2
- package/src/runner-start-bundle.test.ts +29 -0
- package/src/runner-start.test.ts +179 -0
- package/src/runner-start.ts +329 -0
- package/src/workspace/__fixtures__/contract-repo.ts +11 -2
- package/src/workspace/check-contract.test.ts +4 -2
- package/src/workspace/check.schema.json +88 -2
- package/src/workspace/checks/ci-workflow.test.ts +150 -0
- package/src/workspace/checks/ci-workflow.ts +129 -0
- package/src/workspace/checks/security.test.ts +138 -0
- package/src/workspace/checks/security.ts +305 -0
- package/src/workspace/checks.ts +8 -0
- package/src/workspace/ci-builds.e2e.test.ts +208 -0
- package/src/workspace/ci-builds.test.ts +174 -0
- package/src/workspace/ci-builds.ts +385 -0
- package/src/workspace/ci-green-builds.test.ts +180 -0
- package/src/workspace/ci-green-cli.test.ts +54 -0
- package/src/workspace/ci-green-cli.ts +26 -5
- package/src/workspace/ci-green-forge.ts +24 -0
- package/src/workspace/ci-green-workflow.test.ts +84 -1
- package/src/workspace/ci-green-workflow.ts +139 -2
- package/src/workspace/ci-green.test.ts +5 -2
- package/src/workspace/ci-green.ts +359 -27
- package/src/workspace/ci-init.test.ts +159 -0
- package/src/workspace/ci-init.ts +445 -0
- package/src/workspace/ci-last-green.schema.json +49 -0
- package/src/workspace/ci-list.schema.json +35 -1
- package/src/workspace/ci-report-url.test.ts +6 -1
- package/src/workspace/ci-run-index.schema.json +1 -1
- package/src/workspace/ci-run-index.ts +1 -1
- package/src/workspace/ci-run.test.ts +24 -7
- package/src/workspace/ci-run.ts +78 -16
- package/src/workspace/ci-runner-start.test.ts +151 -0
- package/src/workspace/ci-runner-start.ts +193 -0
- package/src/workspace/ci-serve-builds.test.ts +86 -0
- package/src/workspace/ci-serve.test.ts +59 -0
- package/src/workspace/ci-serve.ts +177 -22
- package/src/workspace/ci-workflow.ts +71 -0
- package/src/workspace/declaration.schema.json +51 -0
- package/src/workspace/declaration.test.ts +17 -0
- package/src/workspace/declaration.ts +53 -3
- package/src/workspace/init.ts +2 -2
- package/src/workspace/ls.schema.json +2 -1
- package/src/workspace/ls.ts +3 -2
- package/src/workspace/member-pipeline.ts +2 -1
- package/src/workspace/merge-record.schema.json +49 -0
- package/src/workspace/pr-report.schema.json +3 -3
- package/src/workspace/reason-codes.test.ts +4 -0
- package/src/workspace/reason-codes.ts +28 -0
- package/src/workspace/release-candidates.schema.json +353 -0
- package/src/workspace/release-candidates.test.ts +106 -0
- package/src/workspace/release-candidates.ts +315 -0
- package/src/workspace/release-workflow.ts +167 -0
- package/src/workspace/security.test.ts +70 -0
- package/src/workspace/security.ts +118 -0
|
@@ -59,7 +59,10 @@ import { readDeclaration, readerVersion, WORKSPACE_ERROR_CODES, WorkspaceReadErr
|
|
|
59
59
|
import type { CheckRun, CiForge, OpRunRef } from "./ci-green-forge";
|
|
60
60
|
import type { OpRunRecord } from "../op/runtime";
|
|
61
61
|
import { readRunLedger } from "../lifecycle/run-ledger";
|
|
62
|
-
import { fetchLifecycleStatus, listFilesInDir, listLedgerTrees } from "../lifecycle/git";
|
|
62
|
+
import { fetchLifecycleStatus, listFilesInDir, listLedgerTrees, pushLifecycleStatus, writeLedgerFiles } from "../lifecycle/git";
|
|
63
|
+
import { ledgerRecordingOff } from "../lifecycle/no-record";
|
|
64
|
+
import { agedOut, buildRequestAnnotation, CI_BUILD_TAG_PREFIX, planBuilds, readBuildRequests, readCiRecords, recordOf, recordPath, requestedBuild, sameRecord, type BuildRequest, type CiRecord } from "./ci-builds";
|
|
65
|
+
import { dispatchableWorkflows, readWorkflowSources } from "./ci-green-workflow";
|
|
63
66
|
import { signOpRunRecord, verifyOpRunStatement } from "./trust/op-run-statement";
|
|
64
67
|
import type { ReasonCode } from "./reason-codes";
|
|
65
68
|
import { gitTop } from "./tree";
|
|
@@ -94,7 +97,15 @@ export class CiGreenError extends Error {
|
|
|
94
97
|
|
|
95
98
|
// ── Judging check runs ───────────────────────────────────────────────────────
|
|
96
99
|
|
|
97
|
-
|
|
100
|
+
/**
|
|
101
|
+
* `unbuilt` (#4169): the phase's runs were cancelled, or, for a phase that
|
|
102
|
+
* builds only the newest commit, a newer commit's run superseded this one's.
|
|
103
|
+
* Neither is a failure, and neither is a pass.
|
|
104
|
+
*/
|
|
105
|
+
export type Verdict = "pass" | "fail" | "pending" | "unbuilt";
|
|
106
|
+
|
|
107
|
+
/** Conclusions that say a run was stopped, or never started, rather than that it failed (#4169). */
|
|
108
|
+
const UNBUILT_CONCLUSIONS = new Set(["cancelled", "stale"]);
|
|
98
109
|
|
|
99
110
|
/** Whether a check run's name matches a pattern: the whole name, `*` standing for any run of characters. */
|
|
100
111
|
export function matchesCheckRun(pattern: string, name: string): boolean {
|
|
@@ -132,12 +143,27 @@ export interface PhaseVerdict {
|
|
|
132
143
|
runs: RunVerdict[];
|
|
133
144
|
/** Patterns no check run matches yet, which leave the phase undecided. */
|
|
134
145
|
unmatched: string[];
|
|
146
|
+
/**
|
|
147
|
+
* Why an `unbuilt` or held `pending` phase is so (#4169): its runs were
|
|
148
|
+
* `cancelled`, a newer commit's run `superseded` it, its retry or a
|
|
149
|
+
* requested build is still to come (`retrying`, `requested`), or a
|
|
150
|
+
* request no runner served (`request-unserved`).
|
|
151
|
+
*/
|
|
152
|
+
reason?: PhaseReason;
|
|
153
|
+
/** For a superseded phase, the newer commit whose run covers it (#4169). */
|
|
154
|
+
coveredBy?: string;
|
|
155
|
+
/** The commit a bisect found broke this phase, on that commit and the commits after it (#4169). */
|
|
156
|
+
culprit?: string;
|
|
135
157
|
}
|
|
136
158
|
|
|
159
|
+
/** Why a phase is `unbuilt`, or held `pending` (#4169). */
|
|
160
|
+
export type PhaseReason = "cancelled" | "superseded" | "retrying" | "requested" | "request-unserved";
|
|
161
|
+
|
|
137
162
|
function runVerdict(phase: CiPhase, r: CheckRun): Verdict {
|
|
138
163
|
if (r.status !== "completed") return "pending";
|
|
139
164
|
if (r.conclusion === "success") return "pass";
|
|
140
165
|
if (r.conclusion === "skipped") return phase.skipped === "pass" ? "pass" : "fail";
|
|
166
|
+
if (r.conclusion !== null && UNBUILT_CONCLUSIONS.has(r.conclusion)) return "unbuilt";
|
|
141
167
|
return "fail";
|
|
142
168
|
}
|
|
143
169
|
|
|
@@ -153,12 +179,15 @@ export function evaluatePhase(phase: CiPhase, latest: readonly CheckRun[]): Phas
|
|
|
153
179
|
runs.push({ name: r.name, id: r.id, status: r.status, conclusion: r.conclusion, url: r.url, verdict: runVerdict(phase, r), ...(r.opRun ? { opRun: r.opRun } : {}), ...(r.failureCode ? { failureCode: r.failureCode } : {}) });
|
|
154
180
|
}
|
|
155
181
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
182
|
+
return { phase: phase.name, ...phaseVerdictOf(runs, unmatched), runs, unmatched };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** A phase's verdict from its runs: a failure wins, then a run still going or a pattern no run matches, then a cancelled run. */
|
|
186
|
+
export function phaseVerdictOf(runs: readonly RunVerdict[], unmatched: readonly string[]): { verdict: Verdict; reason?: PhaseReason } {
|
|
187
|
+
if (runs.some((r) => r.verdict === "fail")) return { verdict: "fail" };
|
|
188
|
+
if (runs.some((r) => r.verdict === "pending") || unmatched.length > 0) return { verdict: "pending" };
|
|
189
|
+
if (runs.some((r) => r.verdict === "unbuilt")) return { verdict: "unbuilt", reason: "cancelled" };
|
|
190
|
+
return { verdict: "pass" };
|
|
162
191
|
}
|
|
163
192
|
|
|
164
193
|
export interface CommitVerdict {
|
|
@@ -172,10 +201,19 @@ export interface CommitVerdict {
|
|
|
172
201
|
/** Judge a commit: it fails when a required phase fails, and passes when every one passes. */
|
|
173
202
|
export function evaluateCommit(green: CiGreen, runs: readonly CheckRun[]): CommitVerdict {
|
|
174
203
|
const latest = latestAttempts(runs);
|
|
175
|
-
|
|
204
|
+
return commitVerdictOf(green.require.map((name) => evaluatePhase(green.phases.find((p) => p.name === name)!, latest)));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* A commit's verdict from its required phases: it fails when one fails, is
|
|
209
|
+
* pending while one is, is unbuilt when one is unbuilt (#4169), and passes
|
|
210
|
+
* when every one passes.
|
|
211
|
+
*/
|
|
212
|
+
export function commitVerdictOf(phases: PhaseVerdict[]): CommitVerdict {
|
|
176
213
|
const failed = phases.find((p) => p.verdict === "fail");
|
|
177
214
|
if (failed) return { verdict: "fail", phases, failure: { phase: failed.phase, run: failed.runs.find((r) => r.verdict === "fail")! } };
|
|
178
|
-
|
|
215
|
+
const verdict: Verdict = phases.some((p) => p.verdict === "pending") ? "pending" : phases.some((p) => p.verdict === "unbuilt") ? "unbuilt" : "pass";
|
|
216
|
+
return { verdict, phases, failure: null };
|
|
179
217
|
}
|
|
180
218
|
|
|
181
219
|
// ── The tick ─────────────────────────────────────────────────────────────────
|
|
@@ -505,6 +543,14 @@ export interface TickResult {
|
|
|
505
543
|
commits: TickCommit[];
|
|
506
544
|
/** The tags made and pushed, or that would be under `dryRun`. */
|
|
507
545
|
made: string[];
|
|
546
|
+
/** The builds asked for (#4169), as `ci/build/<sha>` tags made and pushed, or that would be under `dryRun`. */
|
|
547
|
+
requested: BuildRequest[];
|
|
548
|
+
/** The failed runs asked to run again through the forge (#4169). */
|
|
549
|
+
retried: { sha: string; run: string }[];
|
|
550
|
+
/** The `_ci/<sha>.json` records written (#4169). Empty under `dryRun`, or when recording is off. */
|
|
551
|
+
recorded: string[];
|
|
552
|
+
/** What the tick could not do and carried on without: a retry, a request, a record. */
|
|
553
|
+
warnings: string[];
|
|
508
554
|
/** The runner the tags were signed as (#4031), or null when they carry no statement. */
|
|
509
555
|
signer: string | null;
|
|
510
556
|
/** Where the phase results came from (#4044). */
|
|
@@ -575,22 +621,29 @@ export async function ciTick(req: TickRequest): Promise<TickResult> {
|
|
|
575
621
|
const signer = req.runnerKey !== undefined ? ciTagSigner(top, base, req.runnerKey) : undefined;
|
|
576
622
|
const links = await commitLinks(req.cwd, top, tracking, commits.map((c) => c.sha));
|
|
577
623
|
const tags = readCiTags(top, green, base);
|
|
624
|
+
const warnings: string[] = [];
|
|
578
625
|
let runs: OpRunSource | undefined;
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
626
|
+
// The runs and the records (#4169) live on chant/lifecycle, which a CI checkout may not have yet.
|
|
627
|
+
const fetched = await fetchLifecycleStatus({ cwd: top });
|
|
628
|
+
const ledgerOk = fetched.status !== "diverged" && fetched.status !== "unreachable";
|
|
629
|
+
if (!ledgerOk) {
|
|
630
|
+
const why = `could not fetch chant/lifecycle from ${fetched.remote}: ${fetched.status === "diverged" ? "the local branch and the remote's have diverged" : fetched.stderr}`;
|
|
631
|
+
if (green.source === "runs") throw new Error(why.replace("chant/lifecycle", "chant/lifecycle, where the Op runs are recorded,"));
|
|
632
|
+
warnings.push(`${why}; no _ci records were read or written`);
|
|
586
633
|
}
|
|
634
|
+
if (green.source === "runs") runs = await readOpRunSource(req.cwd, top, base, green.signedSince);
|
|
635
|
+
const runsBy = new Map<string, CheckRun[]>();
|
|
587
636
|
const planned = await planTick({
|
|
588
637
|
green,
|
|
589
638
|
commits,
|
|
590
639
|
greenTags: new Set(tags.green.keys()),
|
|
591
640
|
revokedTags: new Set(tags.revoked.keys()),
|
|
592
641
|
ignored: tags.ignored,
|
|
593
|
-
checkRuns: async (sha) =>
|
|
642
|
+
checkRuns: async (sha) => {
|
|
643
|
+
const got = runs ? runs.checkRuns(sha) : await forge!.checkRuns(sha);
|
|
644
|
+
runsBy.set(sha, got);
|
|
645
|
+
return got;
|
|
646
|
+
},
|
|
594
647
|
now,
|
|
595
648
|
signer,
|
|
596
649
|
});
|
|
@@ -601,6 +654,10 @@ export async function ciTick(req: TickRequest): Promise<TickResult> {
|
|
|
601
654
|
if (ignored && ignored.length > 0) c.ignoredRuns = ignored;
|
|
602
655
|
}
|
|
603
656
|
}
|
|
657
|
+
const records = ledgerOk ? readCiRecords(top) : new Map<string, CiRecord>();
|
|
658
|
+
const requests = readBuildRequests(top);
|
|
659
|
+
const retryVia = green.source === "runs" ? "request" : forge?.rerun ? "forge" : null;
|
|
660
|
+
const builds = planBuilds({ green, commits: planned, runs: runsBy, requests, records, now, retryVia });
|
|
604
661
|
const due = planned.filter((c) => c.made !== null);
|
|
605
662
|
if (!req.dryRun && due.length > 0) {
|
|
606
663
|
const ident = taggerConfig(top);
|
|
@@ -614,7 +671,227 @@ export async function ciTick(req: TickRequest): Promise<TickResult> {
|
|
|
614
671
|
throw new Error(`could not push ${due.map((c) => c.made!.tag).join(", ")} to ${remote}: ${cause ? `${cause}\n` : ""}${stderr}`);
|
|
615
672
|
}
|
|
616
673
|
}
|
|
617
|
-
|
|
674
|
+
const retried = builds.retries.map((r) => ({ sha: r.sha, run: r.run.name }));
|
|
675
|
+
let recorded: string[] = [];
|
|
676
|
+
if (!req.dryRun) {
|
|
677
|
+
const asked = await askForBuilds({ top, remote, green, forge, builds, requests, records, planned, warnings });
|
|
678
|
+
recorded = ledgerOk && !ledgerRecordingOff() ? await writeRecords({ top, green, planned, records, builds, requests: asked.live, dispatched: asked.dispatched, now, warnings }) : [];
|
|
679
|
+
}
|
|
680
|
+
return {
|
|
681
|
+
branch: green.branch,
|
|
682
|
+
window: green.window,
|
|
683
|
+
ref: tracking,
|
|
684
|
+
commits: planned,
|
|
685
|
+
made: due.map((c) => c.made!.tag),
|
|
686
|
+
requested: builds.requests,
|
|
687
|
+
retried,
|
|
688
|
+
recorded,
|
|
689
|
+
warnings,
|
|
690
|
+
signer: signer?.principal ?? null,
|
|
691
|
+
source: green.source,
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Make what the plan asks for (#4169): push each new request tag and start
|
|
697
|
+
* the workflows holding its phases at it on GitHub, start any older request
|
|
698
|
+
* no tick has started yet, run the failed jobs again, and delete the tags of
|
|
699
|
+
* requests that are done. Each step that fails is a warning, never the
|
|
700
|
+
* tick's failure: the green tags are already pushed.
|
|
701
|
+
*/
|
|
702
|
+
async function askForBuilds(input: {
|
|
703
|
+
top: string;
|
|
704
|
+
remote: string;
|
|
705
|
+
green: CiGreen;
|
|
706
|
+
forge: CiForge | undefined;
|
|
707
|
+
builds: ReturnType<typeof planBuilds>;
|
|
708
|
+
requests: Map<string, BuildRequest>;
|
|
709
|
+
records: Map<string, CiRecord>;
|
|
710
|
+
planned: TickCommit[];
|
|
711
|
+
warnings: string[];
|
|
712
|
+
}): Promise<{ live: Map<string, BuildRequest>; dispatched: Set<string> }> {
|
|
713
|
+
const { top, remote, green, forge, builds, warnings } = input;
|
|
714
|
+
const live = new Map(input.requests);
|
|
715
|
+
for (const tag of builds.resolved) live.delete(tag.slice(CI_BUILD_TAG_PREFIX.length));
|
|
716
|
+
if (builds.requests.length > 0) {
|
|
717
|
+
const ident = taggerConfig(top);
|
|
718
|
+
const made: BuildRequest[] = [];
|
|
719
|
+
for (const r of builds.requests) {
|
|
720
|
+
if (tryGit(top, [...ident, "tag", "-a", "-m", buildRequestAnnotation(r), r.tag, r.commit]) === null) warnings.push(`could not make ${r.tag}`);
|
|
721
|
+
else made.push(r);
|
|
722
|
+
}
|
|
723
|
+
if (made.length > 0) {
|
|
724
|
+
try {
|
|
725
|
+
git(top, ["push", "--quiet", remote, ...made.map((r) => `refs/tags/${r.tag}`)]);
|
|
726
|
+
for (const r of made) live.set(r.commit, r);
|
|
727
|
+
} catch (err) {
|
|
728
|
+
for (const r of made) tryGit(top, ["tag", "-d", r.tag]);
|
|
729
|
+
const stderr = stderrOf(err);
|
|
730
|
+
warnings.push(`could not push ${made.map((r) => r.tag).join(", ")} to ${remote}: ${pushRefusalCause(stderr) ?? stderr}`);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
const dispatched = new Set<string>();
|
|
735
|
+
if (green.source === "checks" && forge?.dispatch) {
|
|
736
|
+
const sources = readWorkflowSources(top, "");
|
|
737
|
+
for (const r of live.values()) {
|
|
738
|
+
if (input.records.get(r.commit)?.request?.dispatched && input.records.get(r.commit)?.request?.tag === r.tag) continue;
|
|
739
|
+
// A run already going at the commit was started for it, by the tick before or by `ci run --at`.
|
|
740
|
+
const c = input.planned.find((x) => x.sha === r.commit);
|
|
741
|
+
if (c?.verdict?.phases.some((p) => r.phases.includes(p.phase) && p.runs.some((x) => x.status !== "completed"))) {
|
|
742
|
+
dispatched.add(r.commit);
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
const { dispatchable, without } = dispatchableWorkflows(green, r.phases, sources);
|
|
746
|
+
for (const w of without) warnings.push(`${w} holds a run of ${r.phases.join(", ")} and has no workflow_dispatch trigger, so ${r.tag} can't start it; regenerate it with chant ci workflow`);
|
|
747
|
+
let ok = dispatchable.length > 0;
|
|
748
|
+
for (const w of dispatchable) {
|
|
749
|
+
try {
|
|
750
|
+
await forge.dispatch(w, r.tag);
|
|
751
|
+
} catch (err) {
|
|
752
|
+
ok = false;
|
|
753
|
+
warnings.push(`could not start ${w} at ${r.tag}: ${err instanceof Error ? err.message : String(err)}`);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
if (ok) dispatched.add(r.commit);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
if (forge?.rerun) {
|
|
760
|
+
for (const r of builds.retries) {
|
|
761
|
+
try {
|
|
762
|
+
await forge.rerun(r.run);
|
|
763
|
+
} catch (err) {
|
|
764
|
+
warnings.push(`could not run ${r.run.name} at ${r.sha.slice(0, 12)} again, so its failure counts once the retry's hour is up: ${err instanceof Error ? err.message : String(err)}`);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
if (builds.resolved.length > 0) {
|
|
769
|
+
for (const tag of builds.resolved) tryGit(top, ["tag", "-d", tag]);
|
|
770
|
+
if (tryGit(top, ["push", "--quiet", remote, ...builds.resolved.map((t) => `:refs/tags/${t}`)]) === null) {
|
|
771
|
+
warnings.push(`could not delete ${builds.resolved.join(", ")} on ${remote}; a done request asks for nothing, so they can stay`);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
return { live, dispatched };
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* Write each judged commit's record, and rewrite a record whose commit left
|
|
779
|
+
* the window still pending as unbuilt (#4169), in one commit on
|
|
780
|
+
* `chant/lifecycle`, and push it. Only records whose content changed are
|
|
781
|
+
* written. A push another writer beat is tried once more on the new tip.
|
|
782
|
+
*/
|
|
783
|
+
async function writeRecords(input: {
|
|
784
|
+
top: string;
|
|
785
|
+
green: CiGreen;
|
|
786
|
+
planned: TickCommit[];
|
|
787
|
+
records: Map<string, CiRecord>;
|
|
788
|
+
builds: ReturnType<typeof planBuilds>;
|
|
789
|
+
requests: Map<string, BuildRequest>;
|
|
790
|
+
dispatched: Set<string>;
|
|
791
|
+
now: Date;
|
|
792
|
+
warnings: string[];
|
|
793
|
+
}): Promise<string[]> {
|
|
794
|
+
const { top, green, planned, records, builds, requests, now, warnings } = input;
|
|
795
|
+
const writes: { path: string; content: string }[] = [];
|
|
796
|
+
const inWindow = new Set(planned.map((c) => c.sha));
|
|
797
|
+
for (const c of planned) {
|
|
798
|
+
const previous = records.get(c.sha);
|
|
799
|
+
let record = recordOf({ green, commit: c, previous, retries: builds.retries, requests, now });
|
|
800
|
+
if (record.request) {
|
|
801
|
+
const started = input.dispatched.has(c.sha) || (previous?.request?.tag === record.request.tag && previous.request.dispatched === true);
|
|
802
|
+
if (started) record = { ...record, request: { ...record.request, dispatched: true } };
|
|
803
|
+
}
|
|
804
|
+
if (!sameRecord(previous, record)) writes.push({ path: recordPath(c.sha), content: `${JSON.stringify(record, null, 2)}\n` });
|
|
805
|
+
}
|
|
806
|
+
for (const [sha, r] of records) {
|
|
807
|
+
if (inWindow.has(sha) || r.state !== "pending") continue;
|
|
808
|
+
writes.push({ path: recordPath(sha), content: `${JSON.stringify(agedOut(r, now), null, 2)}\n` });
|
|
809
|
+
}
|
|
810
|
+
if (writes.length === 0) return [];
|
|
811
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
812
|
+
try {
|
|
813
|
+
await writeLedgerFiles(writes, `ci: ${writes.length} commit record${writes.length === 1 ? "" : "s"}`, { cwd: top });
|
|
814
|
+
} catch (err) {
|
|
815
|
+
warnings.push(`could not write the _ci records: ${err instanceof Error ? err.message : String(err)}`);
|
|
816
|
+
return [];
|
|
817
|
+
}
|
|
818
|
+
const pushed = await pushLifecycleStatus({ cwd: top });
|
|
819
|
+
if (pushed.status === "pushed" || pushed.status === "no-remote" || pushed.status === "not-recorded") return writes.map((w) => w.path);
|
|
820
|
+
if (pushed.status !== "stale" || attempt === 1) {
|
|
821
|
+
warnings.push(`could not push the _ci records: ${pushed.stderr}`);
|
|
822
|
+
return [];
|
|
823
|
+
}
|
|
824
|
+
// Another writer pushed first: drop this commit, follow the remote, and write again on its tip.
|
|
825
|
+
tryGit(top, ["update-ref", "refs/heads/chant/lifecycle", "refs/heads/chant/lifecycle^"]);
|
|
826
|
+
const again = await fetchLifecycleStatus({ cwd: top });
|
|
827
|
+
if (again.status === "diverged" || again.status === "unreachable") {
|
|
828
|
+
warnings.push(`could not push the _ci records: chant/lifecycle moved on the remote and could not be fetched again`);
|
|
829
|
+
return [];
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
return [];
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// ── A build on demand (#4169) ────────────────────────────────────────────────
|
|
836
|
+
|
|
837
|
+
export interface RequestBuildResult {
|
|
838
|
+
request: BuildRequest;
|
|
839
|
+
/** Whether a request tag for the commit was already there, in which case nothing was made. */
|
|
840
|
+
existed: boolean;
|
|
841
|
+
/** The workflows started at the tag now, on GitHub. Empty when the next tick or `ci serve` starts it. */
|
|
842
|
+
dispatched: string[];
|
|
843
|
+
warnings: string[];
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/**
|
|
847
|
+
* `chant ci run --at <sha>`: ask for every required phase to be built at a
|
|
848
|
+
* commit of the branch, by pushing `ci/build/<sha>`. Given a forge that can
|
|
849
|
+
* start workflows, and with the source check runs, it starts the workflows
|
|
850
|
+
* holding the phases at the tag now; otherwise the next tick does, or `ci
|
|
851
|
+
* serve --build-command` for chant runs. Once the phases pass, the tick tags
|
|
852
|
+
* the commit green like any other, so it can be cut.
|
|
853
|
+
*/
|
|
854
|
+
export async function requestBuild(req: { cwd: string; at: string; remote?: string; forge?: CiForge; now?: Date }): Promise<RequestBuildResult> {
|
|
855
|
+
const read = readGreen(req.cwd);
|
|
856
|
+
const { green, top } = read;
|
|
857
|
+
const remote = req.remote ?? "origin";
|
|
858
|
+
const tracking = `refs/remotes/${remote}/${green.branch}`;
|
|
859
|
+
try {
|
|
860
|
+
git(top, ["fetch", "--quiet", "--no-tags", remote, `+refs/heads/${green.branch}:${tracking}`, `+refs/tags/${CI_BUILD_TAG_PREFIX}*:refs/tags/${CI_BUILD_TAG_PREFIX}*`]);
|
|
861
|
+
} catch (err) {
|
|
862
|
+
throw new Error(`could not fetch ${green.branch} and the build requests from ${remote}: ${stderrOf(err)}`);
|
|
863
|
+
}
|
|
864
|
+
if (req.at.startsWith("-") || tryGit(top, ["rev-parse", "--verify", "--quiet", `${req.at}^{commit}`]) === null) throw new WorkspaceReadError("revision-unknown", `--at ${req.at} names no commit in this repository`);
|
|
865
|
+
const sha = git(top, ["rev-parse", `${req.at}^{commit}`]);
|
|
866
|
+
const onBranch = git(top, ["rev-list", "--first-parent", tracking]).split("\n").includes(sha);
|
|
867
|
+
if (!onBranch) throw new Error(`${sha.slice(0, 12)} is not a first-parent commit of ${remote}/${green.branch}, and only those are judged green`);
|
|
868
|
+
const existing = readBuildRequests(top).get(sha);
|
|
869
|
+
const request = existing ?? requestedBuild(green, sha, green.require, req.now ?? new Date());
|
|
870
|
+
const warnings: string[] = [];
|
|
871
|
+
if (!existing) {
|
|
872
|
+
git(top, [...taggerConfig(top), "tag", "-a", "-m", buildRequestAnnotation(request), request.tag, sha]);
|
|
873
|
+
try {
|
|
874
|
+
git(top, ["push", "--quiet", remote, `refs/tags/${request.tag}`]);
|
|
875
|
+
} catch (err) {
|
|
876
|
+
tryGit(top, ["tag", "-d", request.tag]);
|
|
877
|
+
const stderr = stderrOf(err);
|
|
878
|
+
throw new Error(`could not push ${request.tag} to ${remote}: ${pushRefusalCause(stderr) ?? stderr}`);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
const dispatched: string[] = [];
|
|
882
|
+
if (green.source === "checks" && req.forge?.dispatch) {
|
|
883
|
+
const { dispatchable, without } = dispatchableWorkflows(green, request.phases, readWorkflowSources(top, ""));
|
|
884
|
+
for (const w of without) warnings.push(`${w} holds a run of a required phase and has no workflow_dispatch trigger, so it can't be started at ${request.tag}`);
|
|
885
|
+
for (const w of dispatchable) {
|
|
886
|
+
try {
|
|
887
|
+
await req.forge.dispatch(w, request.tag);
|
|
888
|
+
dispatched.push(w);
|
|
889
|
+
} catch (err) {
|
|
890
|
+
warnings.push(`could not start ${w} at ${request.tag}: ${err instanceof Error ? err.message : String(err)}; the next tick tries again`);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
return { request, existed: existing !== undefined, dispatched, warnings };
|
|
618
895
|
}
|
|
619
896
|
|
|
620
897
|
// ── Chant runs as the source (#4044) ─────────────────────────────────────────
|
|
@@ -766,8 +1043,18 @@ export function formatTick(r: TickResult, dryRun = false): string {
|
|
|
766
1043
|
lines.push(` ${short} revoked`);
|
|
767
1044
|
} else {
|
|
768
1045
|
const v = c.verdict!;
|
|
769
|
-
const
|
|
770
|
-
const
|
|
1046
|
+
const why = (p: PhaseVerdict) => `${p.phase}${p.reason ? ` (${p.reason}${p.coveredBy ? `, covered by ${p.coveredBy.slice(0, 12)}` : ""})` : ""}`;
|
|
1047
|
+
const waiting = v.phases.filter((p) => p.verdict === "pending").map(why);
|
|
1048
|
+
const unbuilt = v.phases.filter((p) => p.verdict === "unbuilt").map(why);
|
|
1049
|
+
const culprit = v.phases.find((p) => p.culprit)?.culprit;
|
|
1050
|
+
const what =
|
|
1051
|
+
c.before === "green"
|
|
1052
|
+
? "green"
|
|
1053
|
+
: v.verdict === "fail"
|
|
1054
|
+
? `failed: phase ${v.failure!.phase}, ${v.failure!.run.name} ${v.failure!.run.conclusion}${culprit ? `; culprit ${culprit.slice(0, 12)}` : ""}`
|
|
1055
|
+
: v.verdict === "unbuilt"
|
|
1056
|
+
? `unbuilt: ${unbuilt.join(", ")}`
|
|
1057
|
+
: `undecided: ${waiting.join(", ")}`;
|
|
771
1058
|
lines.push(` ${short} ${what}`);
|
|
772
1059
|
}
|
|
773
1060
|
const why = whyWorkUnknown(c.work);
|
|
@@ -777,6 +1064,10 @@ export function formatTick(r: TickResult, dryRun = false): string {
|
|
|
777
1064
|
if (c.blocked) lines.push(` did not make ${c.blocked}: an ignored tag holds the name. Delete it to let the tick tag the commit: git push origin :refs/tags/${c.blocked}`);
|
|
778
1065
|
}
|
|
779
1066
|
if (r.signer && r.made.length > 0) lines.push(`signed as ${r.signer}`);
|
|
1067
|
+
for (const q of r.requested) lines.push(`${dryRun ? "would ask" : "asked"} for ${q.phases.join(", ")} at ${q.commit.slice(0, 12)} (${q.reason}): ${q.tag}`);
|
|
1068
|
+
for (const x of r.retried) lines.push(`${dryRun ? "would run" : "ran"} ${x.run} at ${x.sha.slice(0, 12)} again`);
|
|
1069
|
+
if (r.recorded.length > 0) lines.push(`recorded ${r.recorded.length} commit${r.recorded.length === 1 ? "" : "s"} on chant/lifecycle`);
|
|
1070
|
+
for (const w of r.warnings) lines.push(`warning: ${w}`);
|
|
780
1071
|
return lines.join("\n");
|
|
781
1072
|
}
|
|
782
1073
|
|
|
@@ -802,9 +1093,14 @@ export type LastGreenDocument =
|
|
|
802
1093
|
commit: { sha: string; tag: string; tagged: string | null; committed: string } | null;
|
|
803
1094
|
/** Tags on the commits walked, down to the one found, that were ignored for want of a valid statement (#4031). Newest first. */
|
|
804
1095
|
ignored: IgnoredCiTag[];
|
|
1096
|
+
/** The commits walked past above the one found, newest first, each with its record (#4169). At most {@link LAST_GREEN_NEWER}. */
|
|
1097
|
+
newer: { sha: string; record: CiRecordSummary | null }[];
|
|
805
1098
|
}
|
|
806
1099
|
| { $schema: string; contract: number; chant: string; error: { code: CiGreenErrorCode; message: string } };
|
|
807
1100
|
|
|
1101
|
+
/** How many commits above the green one `ci last-green` reports with their records. */
|
|
1102
|
+
export const LAST_GREEN_NEWER = 50;
|
|
1103
|
+
|
|
808
1104
|
/**
|
|
809
1105
|
* `chant ci last-green`: the newest first-parent commit of the branch with a
|
|
810
1106
|
* green tag and no revoked tag. It reads local refs and tags and never
|
|
@@ -826,15 +1122,20 @@ export function lastGreen(req: { cwd: string; remote?: string; from?: string }):
|
|
|
826
1122
|
const tags = readCiTags(top, green, git(top, ["rev-parse", `${branchRef}^{commit}`]));
|
|
827
1123
|
let commit: Extract<LastGreenDocument, { branch: string }>["commit"] = null;
|
|
828
1124
|
const ignored: IgnoredCiTag[] = [];
|
|
829
|
-
|
|
1125
|
+
const newer: { sha: string; record: CiRecordSummary | null }[] = [];
|
|
1126
|
+
const records = readCiRecords(top);
|
|
1127
|
+
if (tags.green.size > 0 || tags.ignored.size > 0 || records.size > 0) {
|
|
830
1128
|
for (const sha of git(top, ["rev-list", "--first-parent", ref]).split("\n")) {
|
|
831
1129
|
ignored.push(...(tags.ignored.get(sha) ?? []));
|
|
832
|
-
if (!tags.green.has(sha) || tags.revoked.has(sha))
|
|
1130
|
+
if (!tags.green.has(sha) || tags.revoked.has(sha)) {
|
|
1131
|
+
if (newer.length < LAST_GREEN_NEWER) newer.push({ sha, record: recordSummary(green, records.get(sha)) });
|
|
1132
|
+
continue;
|
|
1133
|
+
}
|
|
833
1134
|
commit = { sha, tag: `${CI_GREEN_TAG_PREFIX}${sha}`, tagged: tags.green.get(sha)!.tagged, committed: git(top, ["show", "-s", "--format=%cI", sha]) };
|
|
834
1135
|
break;
|
|
835
1136
|
}
|
|
836
1137
|
}
|
|
837
|
-
return { ...head, workspace, branch: green.branch, ref, commit, ignored };
|
|
1138
|
+
return { ...head, workspace, branch: green.branch, ref, commit, ignored, newer: commit ? newer : [] };
|
|
838
1139
|
} catch (err) {
|
|
839
1140
|
if (err instanceof CiGreenError || err instanceof WorkspaceReadError) return { ...head, error: { code: err.code, message: err instanceof WorkspaceReadError ? err.describe() : err.message } };
|
|
840
1141
|
throw err;
|
|
@@ -852,10 +1153,11 @@ export type CiCommitState = "green" | "revoked" | "undecided";
|
|
|
852
1153
|
/**
|
|
853
1154
|
* One required phase's result on a commit. `pass` and `fail` are what a tag
|
|
854
1155
|
* recorded or a live read found, `pending` is a live read with runs still
|
|
855
|
-
* going or not yet reported,
|
|
856
|
-
* and the
|
|
1156
|
+
* going or not yet reported, `unbuilt` (#4169) is a live read whose runs
|
|
1157
|
+
* were cancelled, and `unrecorded` means no tag holds the result and the
|
|
1158
|
+
* forge was not asked.
|
|
857
1159
|
*/
|
|
858
|
-
export type CiPhaseResult = "pass" | "fail" | "pending" | "unrecorded";
|
|
1160
|
+
export type CiPhaseResult = "pass" | "fail" | "pending" | "unbuilt" | "unrecorded";
|
|
859
1161
|
|
|
860
1162
|
export interface CiListRun {
|
|
861
1163
|
name: string;
|
|
@@ -896,6 +1198,27 @@ export interface CiListCommit {
|
|
|
896
1198
|
work: CommitLink | null;
|
|
897
1199
|
/** The Op runs on the commit that do not count, each for want of a valid statement (#4044). Present only when the source is chant runs. */
|
|
898
1200
|
ignoredRuns?: IgnoredOpRun[];
|
|
1201
|
+
/** What the commit's `_ci/<sha>.json` record says (#4169), or null when it has none. */
|
|
1202
|
+
record: CiRecordSummary | null;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
/** A commit's record (#4169), as `ci list` and `ci last-green` show it: its state, and the first phase's reason, covering commit and culprit. */
|
|
1206
|
+
export interface CiRecordSummary {
|
|
1207
|
+
state: CiRecord["state"];
|
|
1208
|
+
reason?: string;
|
|
1209
|
+
coveredBy?: string;
|
|
1210
|
+
culprit?: string;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
/** The summary of a record, from its required phases in `require` order. */
|
|
1214
|
+
export function recordSummary(green: CiGreen, r: CiRecord | undefined): CiRecordSummary | null {
|
|
1215
|
+
if (!r) return null;
|
|
1216
|
+
const phases = green.require.map((name) => r.phases[name]).filter((p) => p !== undefined);
|
|
1217
|
+
const first = <K extends "reason" | "coveredBy" | "culprit">(k: K) => phases.find((p) => p[k] !== undefined)?.[k];
|
|
1218
|
+
const reason = first("reason");
|
|
1219
|
+
const coveredBy = first("coveredBy");
|
|
1220
|
+
const culprit = first("culprit");
|
|
1221
|
+
return { state: r.state, ...(reason ? { reason } : {}), ...(coveredBy ? { coveredBy } : {}), ...(culprit ? { culprit } : {}) };
|
|
899
1222
|
}
|
|
900
1223
|
|
|
901
1224
|
export type CiListDocument =
|
|
@@ -1002,6 +1325,7 @@ export async function ciList(req: ListRequest): Promise<CiListDocument> {
|
|
|
1002
1325
|
const source = req.source ?? green.source;
|
|
1003
1326
|
if (source === "runs" && req.forge) throw new Error("--forge reads check runs, and the source is chant runs (ci.green.source or --source runs); drop one");
|
|
1004
1327
|
const runs = source === "runs" ? await readOpRunSource(req.cwd, top, base, green.signedSince) : undefined;
|
|
1328
|
+
const records = readCiRecords(top);
|
|
1005
1329
|
const out: CiListCommit[] = [];
|
|
1006
1330
|
for (const c of commits) {
|
|
1007
1331
|
const g = tags.green.get(c.sha);
|
|
@@ -1026,6 +1350,7 @@ export async function ciList(req: ListRequest): Promise<CiListDocument> {
|
|
|
1026
1350
|
work: links?.get(c.sha) ?? null,
|
|
1027
1351
|
ignored: tags.ignored.get(c.sha) ?? [],
|
|
1028
1352
|
...(runs ? { ignoredRuns: runs.ignored.get(c.sha) ?? [] } : {}),
|
|
1353
|
+
record: recordSummary(green, records.get(c.sha)),
|
|
1029
1354
|
});
|
|
1030
1355
|
}
|
|
1031
1356
|
return { ...head, workspace, branch: green.branch, ref, window: req.since === undefined ? green.window : null, since: req.since ?? null, require: [...green.require], commits: out };
|
|
@@ -1035,6 +1360,12 @@ export async function ciList(req: ListRequest): Promise<CiListDocument> {
|
|
|
1035
1360
|
}
|
|
1036
1361
|
}
|
|
1037
1362
|
|
|
1363
|
+
/** A record summary in words, for `ci list` and the tick. */
|
|
1364
|
+
export function describeRecord(r: CiRecordSummary): string {
|
|
1365
|
+
const parts = [r.state, ...(r.reason ? [r.reason] : []), ...(r.coveredBy ? [`covered by ${r.coveredBy.slice(0, 12)}`] : []), ...(r.culprit ? [`culprit ${r.culprit.slice(0, 12)}`] : [])];
|
|
1366
|
+
return parts.join(", ");
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1038
1369
|
/** The text `chant ci list` prints: one line per commit. */
|
|
1039
1370
|
export function formatList(doc: Extract<CiListDocument, { branch: string }>): string {
|
|
1040
1371
|
const lines = [`${doc.branch}, ${doc.since ? `commits after ${doc.since}` : `commits within ${doc.window}`}: ${doc.commits.length}`];
|
|
@@ -1042,6 +1373,7 @@ export function formatList(doc: Extract<CiListDocument, { branch: string }>): st
|
|
|
1042
1373
|
const phases = c.phases.map((p) => `${p.phase}=${p.result}`).join(" ");
|
|
1043
1374
|
const why = whyWorkUnknown(c.work);
|
|
1044
1375
|
lines.push(` ${c.sha.slice(0, 12)} ${c.state.padEnd(9)} ${phases}${why ? `; why unknown: ${why}` : ""}`);
|
|
1376
|
+
if (c.record && c.state === "undecided") lines.push(` recorded ${describeRecord(c.record)}`);
|
|
1045
1377
|
for (const t of c.ignored) lines.push(` ignored ${t.tag} (${t.status}): ${t.reason}`);
|
|
1046
1378
|
for (const r of c.ignoredRuns ?? []) lines.push(` ignored run ${r.run} of ${r.op} in ${r.env} (${r.status}): ${r.reason}`);
|
|
1047
1379
|
}
|