@intentius/chant 0.124.28 → 0.124.29
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-params.d.ts +34 -1
- package/dist/build-params.d.ts.map +1 -1
- package/dist/build-params.js +31 -1
- package/dist/change-set-graph.d.ts +8 -1
- package/dist/change-set-graph.d.ts.map +1 -1
- package/dist/change-set-graph.js +4 -2
- package/dist/change-set.d.ts +10 -0
- package/dist/change-set.d.ts.map +1 -1
- package/dist/change-set.js +4 -3
- package/dist/cli/build-params-cli.d.ts +7 -1
- package/dist/cli/build-params-cli.d.ts.map +1 -1
- package/dist/cli/build-params-cli.js +17 -8
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/build.js +6 -3
- package/dist/cli/handlers/approve.d.ts.map +1 -1
- package/dist/cli/handlers/approve.js +45 -0
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/build.js +15 -4
- package/dist/cli/handlers/fan-out.d.ts +9 -0
- package/dist/cli/handlers/fan-out.d.ts.map +1 -1
- package/dist/cli/handlers/fan-out.js +40 -3
- package/dist/cli/handlers/lifecycle-gc.d.ts +19 -0
- package/dist/cli/handlers/lifecycle-gc.d.ts.map +1 -0
- package/dist/cli/handlers/lifecycle-gc.js +143 -0
- package/dist/cli/handlers/pr.d.ts +11 -3
- package/dist/cli/handlers/pr.d.ts.map +1 -1
- package/dist/cli/handlers/pr.js +134 -26
- package/dist/cli/handlers/promote.d.ts +7 -0
- package/dist/cli/handlers/promote.d.ts.map +1 -1
- package/dist/cli/handlers/promote.js +210 -33
- package/dist/cli/handlers/release.d.ts +62 -5
- package/dist/cli/handlers/release.d.ts.map +1 -1
- package/dist/cli/handlers/release.js +414 -28
- package/dist/cli/handlers/run.d.ts +11 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/handlers/run.js +67 -6
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +61 -10
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +24 -2
- package/dist/cli/registry.d.ts +15 -3
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/cli/run-events.d.ts +3 -2
- package/dist/cli/run-events.d.ts.map +1 -1
- package/dist/cli/run-events.js +5 -3
- package/dist/components/auto-release.d.ts +2 -0
- package/dist/components/auto-release.d.ts.map +1 -1
- package/dist/components/auto-release.js +1 -0
- package/dist/components/capability.d.ts +9 -0
- package/dist/components/capability.d.ts.map +1 -1
- package/dist/components/driver.d.ts +8 -0
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/driver.js +35 -0
- package/dist/components/fan-out-inputs.d.ts +69 -0
- package/dist/components/fan-out-inputs.d.ts.map +1 -0
- package/dist/components/fan-out-inputs.js +169 -0
- package/dist/components/fan-out-record.d.ts +13 -0
- package/dist/components/fan-out-record.d.ts.map +1 -1
- package/dist/components/fan-out-record.js +16 -0
- package/dist/components/fan-out-run.d.ts.map +1 -1
- package/dist/components/fan-out-run.js +50 -0
- package/dist/components/fan-out.d.ts +4 -0
- package/dist/components/fan-out.d.ts.map +1 -1
- package/dist/components/pr-pipeline.d.ts +25 -0
- package/dist/components/pr-pipeline.d.ts.map +1 -1
- package/dist/components/pr-pipeline.js +22 -4
- package/dist/components/promote.d.ts +23 -0
- package/dist/components/promote.d.ts.map +1 -1
- package/dist/components/promote.js +60 -2
- package/dist/components/release-compile.d.ts +9 -0
- package/dist/components/release-compile.d.ts.map +1 -1
- package/dist/components/release-compile.js +9 -3
- package/dist/components/release-gate.d.ts +11 -1
- package/dist/components/release-gate.d.ts.map +1 -1
- package/dist/components/release-gate.js +10 -4
- package/dist/components/wave-plan.d.ts +5 -0
- package/dist/components/wave-plan.d.ts.map +1 -1
- package/dist/components/wave-plan.js +10 -2
- package/dist/config.d.ts +38 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -0
- package/dist/flag-params.d.ts +54 -0
- package/dist/flag-params.d.ts.map +1 -0
- package/dist/flag-params.js +130 -0
- package/dist/lifecycle/__fixtures__/publish-fakes.d.ts +120 -0
- package/dist/lifecycle/__fixtures__/publish-fakes.d.ts.map +1 -0
- package/dist/lifecycle/__fixtures__/publish-fakes.js +304 -0
- package/dist/lifecycle/change-set-store.d.ts +2 -1
- package/dist/lifecycle/change-set-store.d.ts.map +1 -1
- package/dist/lifecycle/change-set-store.js +2 -1
- package/dist/lifecycle/delivery-metrics.d.ts +139 -0
- package/dist/lifecycle/delivery-metrics.d.ts.map +1 -0
- package/dist/lifecycle/delivery-metrics.js +145 -0
- package/dist/lifecycle/deploy-outcome.d.ts +81 -0
- package/dist/lifecycle/deploy-outcome.d.ts.map +1 -0
- package/dist/lifecycle/deploy-outcome.js +101 -0
- package/dist/lifecycle/gate-change-set.d.ts +10 -1
- package/dist/lifecycle/gate-change-set.d.ts.map +1 -1
- package/dist/lifecycle/gate-change-set.js +35 -3
- package/dist/lifecycle/gate-ledger.d.ts +2 -3
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
- package/dist/lifecycle/git.d.ts +30 -1
- package/dist/lifecycle/git.d.ts.map +1 -1
- package/dist/lifecycle/git.js +31 -5
- package/dist/lifecycle/index.d.ts +4 -0
- package/dist/lifecycle/index.d.ts.map +1 -1
- package/dist/lifecycle/index.js +4 -0
- package/dist/lifecycle/mixed-environment.d.ts +95 -0
- package/dist/lifecycle/mixed-environment.d.ts.map +1 -0
- package/dist/lifecycle/mixed-environment.js +154 -0
- package/dist/lifecycle/publish-attachments.d.ts +79 -0
- package/dist/lifecycle/publish-attachments.d.ts.map +1 -0
- package/dist/lifecycle/publish-attachments.js +179 -0
- package/dist/lifecycle/publish-credentials.d.ts +37 -0
- package/dist/lifecycle/publish-credentials.d.ts.map +1 -0
- package/dist/lifecycle/publish-credentials.js +69 -0
- package/dist/lifecycle/publish-declaration.d.ts +97 -0
- package/dist/lifecycle/publish-declaration.d.ts.map +1 -0
- package/dist/lifecycle/publish-declaration.js +83 -0
- package/dist/lifecycle/publish-forge.d.ts +53 -0
- package/dist/lifecycle/publish-forge.d.ts.map +1 -0
- package/dist/lifecycle/publish-forge.js +298 -0
- package/dist/lifecycle/publish-npm.d.ts +61 -0
- package/dist/lifecycle/publish-npm.d.ts.map +1 -0
- package/dist/lifecycle/publish-npm.js +242 -0
- package/dist/lifecycle/publish-oci.d.ts +64 -0
- package/dist/lifecycle/publish-oci.d.ts.map +1 -0
- package/dist/lifecycle/publish-oci.js +383 -0
- package/dist/lifecycle/publish-record.d.ts +66 -0
- package/dist/lifecycle/publish-record.d.ts.map +1 -0
- package/dist/lifecycle/publish-record.js +84 -0
- package/dist/lifecycle/publish-targets.d.ts +45 -0
- package/dist/lifecycle/publish-targets.d.ts.map +1 -0
- package/dist/lifecycle/publish-targets.js +85 -0
- package/dist/lifecycle/release-cut.d.ts +48 -1
- package/dist/lifecycle/release-cut.d.ts.map +1 -1
- package/dist/lifecycle/release-cut.js +116 -23
- package/dist/lifecycle/release-join.d.ts +95 -0
- package/dist/lifecycle/release-join.d.ts.map +1 -0
- package/dist/lifecycle/release-join.js +139 -0
- package/dist/lifecycle/release-publish.d.ts +312 -0
- package/dist/lifecycle/release-publish.d.ts.map +1 -0
- package/dist/lifecycle/release-publish.js +335 -0
- package/dist/lifecycle/release-revocation.d.ts +80 -0
- package/dist/lifecycle/release-revocation.d.ts.map +1 -0
- package/dist/lifecycle/release-revocation.js +123 -0
- package/dist/lifecycle/release-store.d.ts +12 -2
- package/dist/lifecycle/release-store.d.ts.map +1 -1
- package/dist/lifecycle/release-store.js +4 -2
- package/dist/lifecycle/release-verify.d.ts +61 -0
- package/dist/lifecycle/release-verify.d.ts.map +1 -0
- package/dist/lifecycle/release-verify.js +92 -0
- package/dist/lifecycle/release-workstation.d.ts +91 -0
- package/dist/lifecycle/release-workstation.d.ts.map +1 -0
- package/dist/lifecycle/release-workstation.js +180 -0
- package/dist/lifecycle/retention-policy.d.ts +45 -0
- package/dist/lifecycle/retention-policy.d.ts.map +1 -0
- package/dist/lifecycle/retention-policy.js +43 -0
- package/dist/lifecycle/retention.d.ts +185 -0
- package/dist/lifecycle/retention.d.ts.map +1 -0
- package/dist/lifecycle/retention.js +400 -0
- package/dist/plan-summary.d.ts.map +1 -1
- package/dist/plan-summary.js +8 -2
- package/dist/pr-loop.d.ts +7 -0
- package/dist/pr-loop.d.ts.map +1 -1
- package/dist/pr-loop.js +23 -4
- package/dist/pr-report-url.d.ts +82 -0
- package/dist/pr-report-url.d.ts.map +1 -0
- package/dist/pr-report-url.js +174 -0
- package/dist/provenance.d.ts +2 -2
- package/dist/provenance.d.ts.map +1 -1
- package/dist/run-event-store.d.ts +29 -2
- package/dist/run-event-store.d.ts.map +1 -1
- package/dist/run-event-store.js +50 -3
- package/dist/run-event-stream.d.ts +2 -1
- package/dist/run-event-stream.d.ts.map +1 -1
- package/dist/run-event-stream.js +5 -1
- package/dist/run-event.d.ts +19 -1
- package/dist/run-event.d.ts.map +1 -1
- package/dist/run-event.js +2 -0
- package/dist/wave-runtime.mjs +260 -229
- package/dist/workspace/__fixtures__/flag-workspace.d.ts +31 -0
- package/dist/workspace/__fixtures__/flag-workspace.d.ts.map +1 -0
- package/dist/workspace/__fixtures__/flag-workspace.js +114 -0
- package/dist/workspace/change-set.schema.json +5 -1
- package/dist/workspace/check.schema.json +4 -2
- package/dist/workspace/checks/records.d.ts +9 -1
- package/dist/workspace/checks/records.d.ts.map +1 -1
- package/dist/workspace/checks/records.js +35 -1
- package/dist/workspace/checks.d.ts +1 -0
- package/dist/workspace/checks.d.ts.map +1 -1
- package/dist/workspace/checks.js +2 -1
- package/dist/workspace/ci-green.d.ts +8 -0
- package/dist/workspace/ci-green.d.ts.map +1 -1
- package/dist/workspace/ci-green.js +10 -3
- package/dist/workspace/declaration.d.ts +16 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.js +26 -0
- package/dist/workspace/declaration.schema.json +64 -0
- package/dist/workspace/declared-kinds.d.ts +16 -0
- package/dist/workspace/declared-kinds.d.ts.map +1 -1
- package/dist/workspace/declared-kinds.js +25 -0
- package/dist/workspace/delivery-metrics.d.ts +91 -0
- package/dist/workspace/delivery-metrics.d.ts.map +1 -0
- package/dist/workspace/delivery-metrics.js +217 -0
- package/dist/workspace/delivery-metrics.schema.json +519 -0
- package/dist/workspace/flags-read.d.ts +37 -0
- package/dist/workspace/flags-read.d.ts.map +1 -0
- package/dist/workspace/flags-read.js +103 -0
- package/dist/workspace/flags.d.ts +100 -0
- package/dist/workspace/flags.d.ts.map +1 -0
- package/dist/workspace/flags.js +98 -0
- package/dist/workspace/graph-cli.js +1 -1
- package/dist/workspace/intent-cli.d.ts +11 -2
- package/dist/workspace/intent-cli.d.ts.map +1 -1
- package/dist/workspace/intent-cli.js +54 -5
- package/dist/workspace/intent-trace.d.ts +219 -0
- package/dist/workspace/intent-trace.d.ts.map +1 -0
- package/dist/workspace/intent-trace.js +314 -0
- package/dist/workspace/intent.d.ts +30 -6
- package/dist/workspace/intent.d.ts.map +1 -1
- package/dist/workspace/intent.js +141 -21
- package/dist/workspace/intent.schema.json +702 -6
- package/dist/workspace/overlap.d.ts +146 -0
- package/dist/workspace/overlap.d.ts.map +1 -0
- package/dist/workspace/overlap.js +276 -0
- package/dist/workspace/reason-codes.d.ts +2 -0
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/reason-codes.js +3 -0
- package/dist/workspace/records.d.ts +15 -2
- package/dist/workspace/records.d.ts.map +1 -1
- package/dist/workspace/records.js +32 -0
- package/dist/workspace/records.schema.json +21 -2
- package/dist/workspace/reference-kinds/flag/flag.kind.mjs +32 -0
- package/dist/workspace/reference-kinds/flag/flag.schema.json +53 -0
- package/dist/workspace/releases.d.ts +9 -0
- package/dist/workspace/releases.d.ts.map +1 -1
- package/dist/workspace/releases.js +9 -0
- package/dist/workspace/releases.schema.json +123 -1
- package/dist/workspace/run-event.schema.json +10 -3
- package/dist/workspace/status-gates.d.ts +8 -0
- package/dist/workspace/status-gates.d.ts.map +1 -1
- package/dist/workspace/status.d.ts +78 -0
- package/dist/workspace/status.d.ts.map +1 -1
- package/dist/workspace/status.js +170 -3
- package/dist/workspace/status.schema.json +198 -1
- package/dist/workspace/wip.d.ts.map +1 -1
- package/dist/workspace/wip.js +11 -3
- package/package.json +1 -1
- package/src/build-params.test.ts +46 -0
- package/src/build-params.ts +55 -1
- package/src/change-set-graph.ts +10 -2
- package/src/change-set.ts +14 -3
- package/src/cli/build-params-cli.ts +22 -9
- package/src/cli/commands/build.ts +6 -3
- package/src/cli/handlers/approve-release.test.ts +5 -1
- package/src/cli/handlers/approve-revoked.test.ts +155 -0
- package/src/cli/handlers/approve.ts +47 -0
- package/src/cli/handlers/build.test.ts +15 -0
- package/src/cli/handlers/build.ts +17 -4
- package/src/cli/handlers/fan-out.test.ts +180 -1
- package/src/cli/handlers/fan-out.ts +38 -3
- package/src/cli/handlers/intent-trace.e2e.test.ts +201 -0
- package/src/cli/handlers/lifecycle-gc.test.ts +126 -0
- package/src/cli/handlers/lifecycle-gc.ts +146 -0
- package/src/cli/handlers/pr.test.ts +148 -1
- package/src/cli/handlers/pr.ts +139 -28
- package/src/cli/handlers/promote.test.ts +280 -2
- package/src/cli/handlers/promote.ts +210 -34
- package/src/cli/handlers/release-implicit.e2e.test.ts +12 -4
- package/src/cli/handlers/release-join.e2e.test.ts +185 -0
- package/src/cli/handlers/release-publish-targets.e2e.test.ts +295 -0
- package/src/cli/handlers/release-verify.e2e.test.ts +117 -0
- package/src/cli/handlers/release-workstation-ci.e2e.test.ts +135 -0
- package/src/cli/handlers/release.e2e.test.ts +146 -32
- package/src/cli/handlers/release.ts +415 -28
- package/src/cli/handlers/run-auto-release.test.ts +9 -0
- package/src/cli/handlers/run-release.test.ts +55 -0
- package/src/cli/handlers/run.ts +69 -5
- package/src/cli/main.test.ts +1 -0
- package/src/cli/main.ts +61 -10
- package/src/cli/parse-args.ts +19 -2
- package/src/cli/registry.ts +15 -3
- package/src/cli/run-events.ts +5 -2
- package/src/components/auto-release.test.ts +9 -0
- package/src/components/auto-release.ts +3 -0
- package/src/components/capability.ts +9 -0
- package/src/components/driver.test.ts +24 -0
- package/src/components/driver.ts +36 -0
- package/src/components/fan-out-inputs.test.ts +85 -0
- package/src/components/fan-out-inputs.ts +192 -0
- package/src/components/fan-out-record.test.ts +10 -0
- package/src/components/fan-out-record.ts +19 -0
- package/src/components/fan-out-run.ts +48 -0
- package/src/components/fan-out.ts +4 -0
- package/src/components/pr-pipeline.ts +35 -4
- package/src/components/promote.ts +68 -2
- package/src/components/release-compile.ts +17 -3
- package/src/components/release-gate.ts +18 -4
- package/src/components/wave-plan.ts +15 -2
- package/src/config.ts +8 -0
- package/src/flag-params.test.ts +144 -0
- package/src/flag-params.ts +134 -0
- package/src/lifecycle/__fixtures__/publish-fakes.ts +351 -0
- package/src/lifecycle/change-set-store.ts +2 -1
- package/src/lifecycle/delivery-metrics.ts +263 -0
- package/src/lifecycle/deploy-outcome.ts +147 -0
- package/src/lifecycle/gate-change-set.ts +37 -6
- package/src/lifecycle/gate-ledger.ts +2 -2
- package/src/lifecycle/git.ts +61 -8
- package/src/lifecycle/index.ts +4 -0
- package/src/lifecycle/mixed-environment.test.ts +97 -0
- package/src/lifecycle/mixed-environment.ts +204 -0
- package/src/lifecycle/publish-attachments.ts +196 -0
- package/src/lifecycle/publish-credentials.ts +84 -0
- package/src/lifecycle/publish-declaration.ts +95 -0
- package/src/lifecycle/publish-forge.ts +325 -0
- package/src/lifecycle/publish-npm.ts +259 -0
- package/src/lifecycle/publish-oci.ts +396 -0
- package/src/lifecycle/publish-record.ts +113 -0
- package/src/lifecycle/publish-resume.test.ts +168 -0
- package/src/lifecycle/publish-targets.test.ts +494 -0
- package/src/lifecycle/publish-targets.ts +100 -0
- package/src/lifecycle/release-cut.test.ts +74 -2
- package/src/lifecycle/release-cut.ts +158 -22
- package/src/lifecycle/release-flag.test.ts +112 -0
- package/src/lifecycle/release-join.test.ts +68 -0
- package/src/lifecycle/release-join.ts +182 -0
- package/src/lifecycle/release-publish.test.ts +225 -0
- package/src/lifecycle/release-publish.ts +568 -0
- package/src/lifecycle/release-revocation.ts +147 -0
- package/src/lifecycle/release-store.ts +15 -4
- package/src/lifecycle/release-verify.test.ts +110 -0
- package/src/lifecycle/release-verify.ts +120 -0
- package/src/lifecycle/release-workstation.test.ts +204 -0
- package/src/lifecycle/release-workstation.ts +216 -0
- package/src/lifecycle/retention-growth.e2e.test.ts +279 -0
- package/src/lifecycle/retention-policy.ts +63 -0
- package/src/lifecycle/retention.test.ts +363 -0
- package/src/lifecycle/retention.ts +508 -0
- package/src/plan-summary.test.ts +20 -0
- package/src/plan-summary.ts +8 -1
- package/src/pr-loop.ts +25 -4
- package/src/pr-report-url.test.ts +131 -0
- package/src/pr-report-url.ts +209 -0
- package/src/provenance.ts +2 -2
- package/src/run-event-store.ts +63 -3
- package/src/run-event-stream.ts +4 -1
- package/src/run-event.ts +21 -1
- package/src/workspace/__fixtures__/flag-workspace.ts +132 -0
- package/src/workspace/__fixtures__/older-records/change-set-984534ea9.json +100 -0
- package/src/workspace/__fixtures__/older-records/older-records.json +326 -0
- package/src/workspace/change-set.schema.json +5 -1
- package/src/workspace/check.schema.json +4 -2
- package/src/workspace/checks/records.ts +35 -1
- package/src/workspace/checks.ts +2 -1
- package/src/workspace/ci-green.ts +14 -3
- package/src/workspace/declaration.schema.json +64 -0
- package/src/workspace/declaration.test.ts +28 -0
- package/src/workspace/declaration.ts +38 -0
- package/src/workspace/declared-kinds.ts +40 -1
- package/src/workspace/delivery-metrics.schema.json +519 -0
- package/src/workspace/delivery-metrics.test.ts +236 -0
- package/src/workspace/delivery-metrics.ts +286 -0
- package/src/workspace/flags-read.ts +121 -0
- package/src/workspace/flags.test.ts +141 -0
- package/src/workspace/flags.ts +157 -0
- package/src/workspace/graph-cli.ts +1 -1
- package/src/workspace/intent-cli.ts +46 -5
- package/src/workspace/intent-trace.test.ts +173 -0
- package/src/workspace/intent-trace.ts +484 -0
- package/src/workspace/intent.schema.json +702 -6
- package/src/workspace/intent.ts +148 -25
- package/src/workspace/older-records.test.ts +412 -0
- package/src/workspace/overlap.test.ts +165 -0
- package/src/workspace/overlap.ts +340 -0
- package/src/workspace/read-contract.e2e.test.ts +2 -1
- package/src/workspace/reason-codes.test.ts +5 -0
- package/src/workspace/reason-codes.ts +3 -0
- package/src/workspace/record-assets.test.ts +2 -1
- package/src/workspace/records-contract.e2e.test.ts +2 -1
- package/src/workspace/records.schema.json +21 -2
- package/src/workspace/records.ts +36 -2
- package/src/workspace/reference-kinds/flag/flag.kind.mjs +32 -0
- package/src/workspace/reference-kinds/flag/flag.schema.json +53 -0
- package/src/workspace/releases.schema.json +123 -1
- package/src/workspace/releases.ts +11 -0
- package/src/workspace/run-event.schema.json +10 -3
- package/src/workspace/status-contract.test.ts +29 -0
- package/src/workspace/status-gates.ts +8 -0
- package/src/workspace/status-mixed.test.ts +152 -0
- package/src/workspace/status-revoked.test.ts +153 -0
- package/src/workspace/status.schema.json +198 -1
- package/src/workspace/status.ts +254 -3
- package/src/workspace/wip.ts +11 -2
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What each component would apply, as a digest (#4081), and which of an
|
|
3
|
+
* earlier attempt's progress holds against it.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, expect, test } from "vitest";
|
|
7
|
+
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { componentInputDigests, progressThatCarries } from "./fan-out-inputs";
|
|
11
|
+
import type { DriverComponent } from "./driver";
|
|
12
|
+
|
|
13
|
+
function project(): string {
|
|
14
|
+
const root = mkdtempSync(join(tmpdir(), "fan-out-inputs-"));
|
|
15
|
+
mkdirSync(join(root, "dist"));
|
|
16
|
+
for (const name of ["net", "cluster", "app"]) writeFileSync(join(root, "dist", `${name}.json`), `{"name":"${name}"}`);
|
|
17
|
+
return root;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const component = (name: string, dependsOn: string[] = [], extra: Record<string, unknown> = {}): DriverComponent => ({
|
|
21
|
+
name,
|
|
22
|
+
dependsOn,
|
|
23
|
+
deploy: [{ phase: "Apply", steps: [{ kind: "cfn-deploy", stack: name, template: `dist/${name}.json`, ...extra }] }],
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const estate = [component("net"), component("cluster", ["net"]), component("app", ["cluster"])];
|
|
27
|
+
|
|
28
|
+
describe("componentInputDigests", () => {
|
|
29
|
+
test("is stable for the same project, environment and files", () => {
|
|
30
|
+
const root = project();
|
|
31
|
+
expect(componentInputDigests(estate, { projectRoot: root, env: "prod" })).toEqual(
|
|
32
|
+
componentInputDigests(estate, { projectRoot: root, env: "prod" }),
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("covers the bytes of a file a step input names, and everything downstream of it", () => {
|
|
37
|
+
const root = project();
|
|
38
|
+
const before = componentInputDigests(estate, { projectRoot: root, env: "prod" });
|
|
39
|
+
writeFileSync(join(root, "dist", "cluster.json"), `{"name":"cluster","queue":true}`);
|
|
40
|
+
const after = componentInputDigests(estate, { projectRoot: root, env: "prod" });
|
|
41
|
+
expect(after.net).toBe(before.net);
|
|
42
|
+
expect(after.cluster).not.toBe(before.cluster);
|
|
43
|
+
expect(after.app).not.toBe(before.app);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("covers a directory a step names, by its files", () => {
|
|
47
|
+
const root = project();
|
|
48
|
+
const chart = [component("chart", [], { chart: "dist" })];
|
|
49
|
+
const before = componentInputDigests(chart, { projectRoot: root, env: "prod" });
|
|
50
|
+
writeFileSync(join(root, "dist", "app.json"), "{}");
|
|
51
|
+
expect(componentInputDigests(chart, { projectRoot: root, env: "prod" }).chart).not.toBe(before.chart);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("covers the environment and the authored steps", () => {
|
|
55
|
+
const root = project();
|
|
56
|
+
const prod = componentInputDigests(estate, { projectRoot: root, env: "prod" });
|
|
57
|
+
expect(componentInputDigests(estate, { projectRoot: root, env: "staging" }).net).not.toBe(prod.net);
|
|
58
|
+
const moved = [component("net", [], { inputs: { size: 2 } }), ...estate.slice(1)];
|
|
59
|
+
expect(componentInputDigests(moved, { projectRoot: root, env: "prod" }).net).not.toBe(prod.net);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("ignores a string that is not a file, a path outside the project, and the phase name", () => {
|
|
63
|
+
const root = project();
|
|
64
|
+
mkdirSync(join(root, "Apply"));
|
|
65
|
+
const before = componentInputDigests(estate, { projectRoot: root, env: "prod" });
|
|
66
|
+
writeFileSync(join(root, "Apply", "x"), "changes nothing");
|
|
67
|
+
expect(componentInputDigests(estate, { projectRoot: root, env: "prod" })).toEqual(before);
|
|
68
|
+
const outside = [component("net", [], { other: "../elsewhere.json" })];
|
|
69
|
+
expect(() => componentInputDigests(outside, { projectRoot: root, env: "prod" })).not.toThrow();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("progressThatCarries", () => {
|
|
74
|
+
test("carries a component only when its recorded input is the one it has now", () => {
|
|
75
|
+
const split = progressThatCarries(
|
|
76
|
+
{ completed: ["net", "cluster", "app"], inputs: { net: "a", cluster: "b" } },
|
|
77
|
+
{ net: "a", cluster: "B", app: "c" },
|
|
78
|
+
);
|
|
79
|
+
expect(split).toEqual({ carried: ["net"], changed: ["app", "cluster"], unrecorded: false });
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("a record with no input digests carries nothing", () => {
|
|
83
|
+
expect(progressThatCarries({ completed: ["net"] }, { net: "a" })).toEqual({ carried: [], changed: ["net"], unrecorded: true });
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What each component of a fan-out would apply, as one digest per component
|
|
3
|
+
* (#4081), and which of an earlier attempt's progress still holds against it.
|
|
4
|
+
*
|
|
5
|
+
* A fan-out's plan digest identifies the derivation: who runs, in what order,
|
|
6
|
+
* off which edges. Two different changes that touch the same components in
|
|
7
|
+
* the same shape derive the same plan, so the plan digest alone cannot say
|
|
8
|
+
* whether "cluster-a already applied" is about the change in front of us.
|
|
9
|
+
* The attempt record therefore keeps, for each component it applied, the
|
|
10
|
+
* input digest computed here, and a resumed attempt carries a component as
|
|
11
|
+
* `already-applied` only when that digest is the one the component has now.
|
|
12
|
+
*
|
|
13
|
+
* ## What a component's input digest covers
|
|
14
|
+
*
|
|
15
|
+
* - The environment. Progress against `local` says nothing about `prod`.
|
|
16
|
+
* - The composition: every phase and step as authored, `rollback` included.
|
|
17
|
+
* A `stackOutput` reference is hashed as the reference, not its value.
|
|
18
|
+
* - The bytes of every file a step input names. A string input that resolves,
|
|
19
|
+
* relative to the project root, to a file or directory inside the root is
|
|
20
|
+
* read and hashed, so `template: "dist/cluster-a.template.json"` covers the
|
|
21
|
+
* built template and a rebuild that changes it changes the digest. A
|
|
22
|
+
* directory is hashed by its files, skipping `.git`, `node_modules` and
|
|
23
|
+
* `.chant`, the last because the attempt record itself lives there.
|
|
24
|
+
* - The input digests of the components it depends on. A parameter filled
|
|
25
|
+
* from an upstream output gets its value from that upstream's apply, so a
|
|
26
|
+
* change upstream changes the digest of everything downstream of it, the
|
|
27
|
+
* same reach the fan-out's own walk has.
|
|
28
|
+
*
|
|
29
|
+
* Nothing decided during the run goes in: no run id, no time, no output an
|
|
30
|
+
* apply produced.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
34
|
+
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
35
|
+
import { computePlanDigest } from "../lifecycle/plan-digest";
|
|
36
|
+
import { sha256Hex } from "../content-digest";
|
|
37
|
+
import type { DriverComponent, DriverPhase } from "./driver";
|
|
38
|
+
|
|
39
|
+
export interface ComponentInputOptions {
|
|
40
|
+
/** The root step inputs' file paths are read relative to, the project the fan-out runs in. */
|
|
41
|
+
projectRoot: string;
|
|
42
|
+
/** The environment the fan-out applies to. */
|
|
43
|
+
env: string;
|
|
44
|
+
/** Environment config resolved ahead of the run, when the caller has any. */
|
|
45
|
+
vars?: Record<string, unknown>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const SKIPPED_DIRS = new Set([".git", "node_modules", ".chant"]);
|
|
49
|
+
|
|
50
|
+
/** Inside `root`, and not `root` itself: a bare "." names the whole project, which is no one step's input. */
|
|
51
|
+
function insideRoot(root: string, path: string): boolean {
|
|
52
|
+
const rel = relative(root, path);
|
|
53
|
+
return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Hash a file, or a directory by the relative path and bytes of each file under it. */
|
|
57
|
+
function hashPath(path: string): string | undefined {
|
|
58
|
+
let stat;
|
|
59
|
+
try {
|
|
60
|
+
stat = statSync(path);
|
|
61
|
+
} catch {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
if (stat.isFile()) return `sha256:${sha256Hex(readFileSync(path))}`;
|
|
65
|
+
if (!stat.isDirectory()) return undefined;
|
|
66
|
+
const files: Record<string, string> = {};
|
|
67
|
+
const walk = (dir: string): void => {
|
|
68
|
+
for (const entry of readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
69
|
+
if (SKIPPED_DIRS.has(entry.name)) continue;
|
|
70
|
+
const full = join(dir, entry.name);
|
|
71
|
+
if (entry.isDirectory()) walk(full);
|
|
72
|
+
else if (entry.isFile()) files[relative(path, full).split(sep).join("/")] = sha256Hex(readFileSync(full));
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
walk(path);
|
|
76
|
+
return computePlanDigest("component-input-dir", files);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Every file or directory a component's steps name, keyed by the string the step wrote. */
|
|
80
|
+
function referencedFiles(component: DriverComponent, root: string): Record<string, string> {
|
|
81
|
+
const out: Record<string, string> = {};
|
|
82
|
+
const visit = (value: unknown): void => {
|
|
83
|
+
if (typeof value === "string") {
|
|
84
|
+
if (value === "" || value.includes("\n") || value in out) return;
|
|
85
|
+
const path = resolve(root, value);
|
|
86
|
+
if (!insideRoot(root, path) || !existsSync(path)) return;
|
|
87
|
+
const digest = hashPath(path);
|
|
88
|
+
if (digest !== undefined) out[value] = digest;
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (Array.isArray(value)) value.forEach(visit);
|
|
92
|
+
else if (value && typeof value === "object") Object.values(value).forEach(visit);
|
|
93
|
+
};
|
|
94
|
+
// Only a step's inputs name files. A phase name or a step's `kind` that
|
|
95
|
+
// happens to match a directory in the project is not one.
|
|
96
|
+
const visitPhases = (phases: DriverPhase[] | undefined): void => {
|
|
97
|
+
for (const phase of phases ?? []) {
|
|
98
|
+
for (const step of phase.steps) {
|
|
99
|
+
if (isPhase(step)) {
|
|
100
|
+
visitPhases([step]);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (step.kind === "gate") continue;
|
|
104
|
+
const { kind: _kind, ...inputs } = step;
|
|
105
|
+
visit(inputs);
|
|
106
|
+
}
|
|
107
|
+
visitPhases(phase.onFailure);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
visitPhases(component.deploy);
|
|
111
|
+
visitPhases(component.rollback);
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function isPhase(step: DriverPhase["steps"][number]): step is DriverPhase {
|
|
116
|
+
return typeof (step as { phase?: unknown }).phase === "string" && Array.isArray((step as DriverPhase).steps);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* One input digest per component in `components`, keyed by name. Each folds
|
|
121
|
+
* in the digests of the components it depends on, so `components` should be
|
|
122
|
+
* the whole project: a dependency it does not contain is hashed by name only.
|
|
123
|
+
*/
|
|
124
|
+
export function componentInputDigests(
|
|
125
|
+
components: DriverComponent[],
|
|
126
|
+
options: ComponentInputOptions,
|
|
127
|
+
): Record<string, string> {
|
|
128
|
+
const root = resolve(options.projectRoot);
|
|
129
|
+
const byName = new Map(components.map((c) => [c.name, c]));
|
|
130
|
+
const digests = new Map<string, string>();
|
|
131
|
+
const visiting = new Set<string>();
|
|
132
|
+
|
|
133
|
+
const digestOf = (name: string): string | null => {
|
|
134
|
+
const known = digests.get(name);
|
|
135
|
+
if (known !== undefined) return known;
|
|
136
|
+
const component = byName.get(name);
|
|
137
|
+
// An unknown dependency or a cycle is refused by the fan-out's derivation
|
|
138
|
+
// before this runs; here it only has to not loop.
|
|
139
|
+
if (!component || visiting.has(name)) return null;
|
|
140
|
+
visiting.add(name);
|
|
141
|
+
const upstream = [...(component.dependsOn ?? [])].sort().map((dep) => ({ component: dep, digest: digestOf(dep) }));
|
|
142
|
+
visiting.delete(name);
|
|
143
|
+
const digest = computePlanDigest("component-input", {
|
|
144
|
+
environment: options.env,
|
|
145
|
+
component: name,
|
|
146
|
+
deploy: component.deploy,
|
|
147
|
+
rollback: component.rollback ?? null,
|
|
148
|
+
vars: options.vars ?? null,
|
|
149
|
+
files: referencedFiles(component, root),
|
|
150
|
+
upstream,
|
|
151
|
+
});
|
|
152
|
+
digests.set(name, digest);
|
|
153
|
+
return digest;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
for (const c of components) digestOf(c.name);
|
|
157
|
+
return Object.fromEntries([...digests.entries()].sort(([a], [b]) => a.localeCompare(b)));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** What of an earlier attempt's progress holds against the inputs the components have now. */
|
|
161
|
+
export interface CarriedProgress {
|
|
162
|
+
/** Components the attempt applied whose input is the same now. These stay `already-applied`. */
|
|
163
|
+
carried: string[];
|
|
164
|
+
/** Components the attempt applied whose input moved since. These run again. */
|
|
165
|
+
changed: string[];
|
|
166
|
+
/**
|
|
167
|
+
* True when the record has no input digests at all, as one written before
|
|
168
|
+
* #4081 has. Nothing carries from such a record, since nothing in it says
|
|
169
|
+
* what was applied.
|
|
170
|
+
*/
|
|
171
|
+
unrecorded: boolean;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Split an attempt's `completed` by whether each component's input is the
|
|
176
|
+
* same as now. A component the record holds no digest for counts as changed:
|
|
177
|
+
* a claim that it applied is only worth keeping for the input it applied.
|
|
178
|
+
*/
|
|
179
|
+
export function progressThatCarries(
|
|
180
|
+
attempt: { completed: string[]; inputs?: Record<string, string> },
|
|
181
|
+
now: Record<string, string>,
|
|
182
|
+
): CarriedProgress {
|
|
183
|
+
const recorded = attempt.inputs;
|
|
184
|
+
const carried: string[] = [];
|
|
185
|
+
const changed: string[] = [];
|
|
186
|
+
for (const name of [...attempt.completed].sort()) {
|
|
187
|
+
const then = recorded?.[name];
|
|
188
|
+
if (then !== undefined && now[name] !== undefined && then === now[name]) carried.push(name);
|
|
189
|
+
else changed.push(name);
|
|
190
|
+
}
|
|
191
|
+
return { carried, changed, unrecorded: recorded === undefined };
|
|
192
|
+
}
|
|
@@ -70,3 +70,13 @@ describe("prApply (#3464)", () => {
|
|
|
70
70
|
for (const name of ["a.json", "b.json", "c.json"]) expect(readFanOutAttempt(join(dir, name))?.prApply).toBeUndefined();
|
|
71
71
|
});
|
|
72
72
|
});
|
|
73
|
+
|
|
74
|
+
describe("inputs (#4081)", () => {
|
|
75
|
+
test("round-trips, and a record without them reads as none", () => {
|
|
76
|
+
const dir = mkdtempSync(join(tmpdir(), "fan-out-record-"));
|
|
77
|
+
writeFanOutAttempt(join(dir, "a.json"), { digest: "d", completed: ["net"], failed: [], outputs: {}, inputs: { net: "jcs1-sha256:x" } });
|
|
78
|
+
expect(readFanOutAttempt(join(dir, "a.json"))?.inputs).toEqual({ net: "jcs1-sha256:x" });
|
|
79
|
+
writeFileSync(join(dir, "b.json"), JSON.stringify({ digest: "d", completed: ["net"], failed: [], outputs: {} }));
|
|
80
|
+
expect(readFanOutAttempt(join(dir, "b.json"))?.inputs).toBeUndefined();
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -25,6 +25,14 @@
|
|
|
25
25
|
* `carried` holds what a step asked to keep for its member (#3459). It rides
|
|
26
26
|
* in the record rather than beside it because the record is the one file a
|
|
27
27
|
* per-wave CI job hands to the next.
|
|
28
|
+
*
|
|
29
|
+
* `inputs` holds, for each component in `completed`, the input digest it
|
|
30
|
+
* applied (#4081, ./fan-out-inputs.ts). The plan digest only identifies the
|
|
31
|
+
* derivation, and two different changes with the same shape derive the same
|
|
32
|
+
* one, so the digest alone would carry "cluster-a already applied" into a
|
|
33
|
+
* change that never applied it. A resumed attempt carries a component only
|
|
34
|
+
* when its input digest is the same now. A record written before #4081 has
|
|
35
|
+
* no `inputs`, and none of its progress carries.
|
|
28
36
|
*/
|
|
29
37
|
|
|
30
38
|
import { existsSync, readFileSync } from "node:fs";
|
|
@@ -68,6 +76,11 @@ export interface FanOutAttempt {
|
|
|
68
76
|
carried?: Record<string, unknown>;
|
|
69
77
|
/** A pull request's apply only (#3464). */
|
|
70
78
|
prApply?: PrApplyRecord;
|
|
79
|
+
/**
|
|
80
|
+
* The input digest each completed component applied (#4081), keyed by
|
|
81
|
+
* name. Absent on a record written before it, which then carries nothing.
|
|
82
|
+
*/
|
|
83
|
+
inputs?: Record<string, string>;
|
|
71
84
|
}
|
|
72
85
|
|
|
73
86
|
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
@@ -109,6 +122,11 @@ export function readFanOutAttempt(path: string): FanOutAttempt | undefined {
|
|
|
109
122
|
}
|
|
110
123
|
const waves = readWaveRecords(parsed.waves);
|
|
111
124
|
const prApply = readPrApplyRecord(parsed.prApply);
|
|
125
|
+
let inputs: Record<string, string> | undefined;
|
|
126
|
+
if (isRecord(parsed.inputs)) {
|
|
127
|
+
inputs = {};
|
|
128
|
+
for (const [name, value] of Object.entries(parsed.inputs)) if (typeof value === "string") inputs[name] = value;
|
|
129
|
+
}
|
|
112
130
|
return {
|
|
113
131
|
digest: typeof parsed.digest === "string" ? parsed.digest : "",
|
|
114
132
|
completed: Array.isArray(parsed.completed) ? parsed.completed.filter((n): n is string => typeof n === "string") : [],
|
|
@@ -117,6 +135,7 @@ export function readFanOutAttempt(path: string): FanOutAttempt | undefined {
|
|
|
117
135
|
...(waves.length > 0 ? { waves } : {}),
|
|
118
136
|
...(isRecord(parsed.carried) && Object.keys(parsed.carried).length > 0 ? { carried: parsed.carried } : {}),
|
|
119
137
|
...(prApply ? { prApply } : {}),
|
|
138
|
+
...(inputs ? { inputs } : {}),
|
|
120
139
|
};
|
|
121
140
|
}
|
|
122
141
|
|
|
@@ -61,6 +61,46 @@ function fanOutSink(options: FanOutRunOptions): RunEventSink | undefined {
|
|
|
61
61
|
return joinRunEventSinks(options.onRunEvent, progressSinkOf(options.onProgress));
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/** The 1-based wave of each component in `waves`. */
|
|
65
|
+
function waveNumbers(waves: string[][]): Map<string, number> {
|
|
66
|
+
const out = new Map<string, number>();
|
|
67
|
+
waves.forEach((wave, i) => wave.forEach((name) => out.set(name, i + 1)));
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The settles of components a fan-out does not start (#4007): `blocked`
|
|
73
|
+
* when a dependency failed or stopped at a gate, `already-applied` when an
|
|
74
|
+
* earlier attempt applied it. Each is said once, with no `component-start`
|
|
75
|
+
* before it, so a reader of the stream sees every component of the plan.
|
|
76
|
+
*/
|
|
77
|
+
function skipSettles(emit: RunEventSink | undefined, waveOf: Map<string, number>) {
|
|
78
|
+
const said = new Set<string>();
|
|
79
|
+
const say = (component: string, wave: number | undefined, body: { status: "blocked"; blockedBy: string } | { status: "already-applied" }): void => {
|
|
80
|
+
if (!emit || wave === undefined || said.has(component)) return;
|
|
81
|
+
said.add(component);
|
|
82
|
+
emit({ type: "component-settle", wave, component, ...body });
|
|
83
|
+
};
|
|
84
|
+
return {
|
|
85
|
+
/** Each component in `blockedBy` not yet said, in wave order then by name. */
|
|
86
|
+
blocked(blockedBy: Map<string, string>): void {
|
|
87
|
+
const order = [...blockedBy.keys()].sort((a, b) => (waveOf.get(a) ?? 0) - (waveOf.get(b) ?? 0) || a.localeCompare(b));
|
|
88
|
+
for (const component of order) say(component, waveOf.get(component), { status: "blocked", blockedBy: blockedBy.get(component)! });
|
|
89
|
+
},
|
|
90
|
+
/**
|
|
91
|
+
* What an earlier attempt left (`remainingFanOut`'s skips): the components
|
|
92
|
+
* it applied, and the ones its failures blocked. Their wave is the one
|
|
93
|
+
* they have in the derivation, `derived`, which a resumed run re-layers.
|
|
94
|
+
*/
|
|
95
|
+
carried(skipped: FanOutSkip[], derived: Map<string, number>): void {
|
|
96
|
+
for (const skip of skipped) {
|
|
97
|
+
if (skip.reason === "already-applied") say(skip.component, derived.get(skip.component), { status: "already-applied" });
|
|
98
|
+
else if (skip.reason === "blocked" && skip.blockedBy) say(skip.component, derived.get(skip.component), { status: "blocked", blockedBy: skip.blockedBy });
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
64
104
|
/**
|
|
65
105
|
* One gate per wave (#3049), each bound to its wave's set digest. The other
|
|
66
106
|
* mode beside {@link FanOutGate}: that one approves the whole set once, this
|
|
@@ -220,6 +260,8 @@ export async function runFanOut(
|
|
|
220
260
|
|
|
221
261
|
const emit = fanOutSink(options);
|
|
222
262
|
emit?.({ type: "run-start", waves: active.waves });
|
|
263
|
+
const skips = skipSettles(emit, waveNumbers(active.waves));
|
|
264
|
+
skips.carried(active.skipped, waveNumbers(plan.waves));
|
|
223
265
|
|
|
224
266
|
if (options.gate) {
|
|
225
267
|
const gate = options.gate;
|
|
@@ -320,6 +362,8 @@ export async function runFanOut(
|
|
|
320
362
|
wave: waveNum,
|
|
321
363
|
status: waveFailed ? "failed" : waveResults.some((r) => r.status === "gated") ? "gated" : "ok",
|
|
322
364
|
});
|
|
365
|
+
// What this wave's failures and gates hold back settles now, before the next wave starts.
|
|
366
|
+
skips.blocked(blockedBy);
|
|
323
367
|
}
|
|
324
368
|
|
|
325
369
|
const blocked: FanOutSkip[] = [...blockedBy.entries()]
|
|
@@ -411,6 +455,8 @@ async function runGatedWaves(
|
|
|
411
455
|
|
|
412
456
|
const emit = fanOutSink(options);
|
|
413
457
|
emit?.({ type: "run-start", waves: plan.waves });
|
|
458
|
+
const skips = skipSettles(emit, waveNumbers(plan.waves));
|
|
459
|
+
skips.carried(active.skipped, waveNumbers(plan.waves));
|
|
414
460
|
|
|
415
461
|
/** What planning one wave gave. */
|
|
416
462
|
interface Planned {
|
|
@@ -478,6 +524,7 @@ async function runGatedWaves(
|
|
|
478
524
|
results.push(planFailure(f.component, f.error ?? "plan failed"));
|
|
479
525
|
markBlocked(f.component);
|
|
480
526
|
}
|
|
527
|
+
skips.blocked(blockedBy);
|
|
481
528
|
|
|
482
529
|
const toRun = runnable.filter((name) => plansFor.has(name));
|
|
483
530
|
const record: WaveRecord = {
|
|
@@ -569,6 +616,7 @@ async function runGatedWaves(
|
|
|
569
616
|
wave: waveNum,
|
|
570
617
|
status: waveFailures.length > 0 ? "failed" : waveResults.some((r) => r.status === "gated") ? "gated" : "ok",
|
|
571
618
|
});
|
|
619
|
+
skips.blocked(blockedBy);
|
|
572
620
|
return "next";
|
|
573
621
|
},
|
|
574
622
|
});
|
|
@@ -89,6 +89,10 @@ export interface FanOutRequest {
|
|
|
89
89
|
* {@link FanOutPlan.digest} so an approval is bound to *what* would be
|
|
90
90
|
* applied and not only to who would run. Absent, the digest still identifies
|
|
91
91
|
* the derivation — the selection, the order and the edges it came from.
|
|
92
|
+
*
|
|
93
|
+
* `chant components fan-out` does not pass it: its approval stays bound to
|
|
94
|
+
* the derivation, and it binds a resumed attempt's progress to each
|
|
95
|
+
* component's input instead (./fan-out-inputs.ts, #4081).
|
|
92
96
|
*/
|
|
93
97
|
inputDigests?: Record<string, string>;
|
|
94
98
|
/**
|
|
@@ -29,6 +29,13 @@
|
|
|
29
29
|
* squash merges and rebase merges alike, and every job measures from the
|
|
30
30
|
* merge base of that commit and its own head (`resolveMergeBase`).
|
|
31
31
|
*
|
|
32
|
+
* With `events` (#4007) both commands pass `--events-json`, so the job log
|
|
33
|
+
* carries the run-event stream, and keep a copy of it under the report
|
|
34
|
+
* directory through `CHANT_RUN_EVENTS_DIR`, so the report artifact carries
|
|
35
|
+
* it too. With `reportUrl` both pass `--report-url`, which copies the report,
|
|
36
|
+
* the change set and the events where a reader outside CI can fetch them;
|
|
37
|
+
* the job's own environment brings the credentials an `s3://` URL needs.
|
|
38
|
+
*
|
|
32
39
|
* In a workspace member (#3465) both commands run in the member's directory
|
|
33
40
|
* and pass `--member <name>`, so the member's gate (`pr-<number>-<member>`),
|
|
34
41
|
* note and statuses stay apart from every other member's on the same pull
|
|
@@ -68,6 +75,13 @@ export function prLoopSetup(image: string): string[] {
|
|
|
68
75
|
return image === PR_LOOP_IMAGE ? PR_LOOP_SETUP : [];
|
|
69
76
|
}
|
|
70
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Where the jobs keep their run-event files with `events` (#4007), as
|
|
80
|
+
* `CHANT_RUN_EVENTS_DIR`: inside the report directory, so the report
|
|
81
|
+
* artifact carries them.
|
|
82
|
+
*/
|
|
83
|
+
export const PR_LOOP_EVENTS_DIR = `${PR_LOOP_REPORT_DIR}/run-events`;
|
|
84
|
+
|
|
71
85
|
/** The `prLoop` generator option (`ComponentPipelineOptions.prLoop`). */
|
|
72
86
|
export interface PrLoopPipelineOptions {
|
|
73
87
|
/** The gate the apply waits on. Default `pr-apply`. */
|
|
@@ -79,6 +93,18 @@ export interface PrLoopPipelineOptions {
|
|
|
79
93
|
branch?: string;
|
|
80
94
|
/** Count an approval only from someone who approved the pull request on the forge. Default true. */
|
|
81
95
|
requireReview?: boolean;
|
|
96
|
+
/**
|
|
97
|
+
* Stream each job's run events (#4007): `--events-json` on both commands,
|
|
98
|
+
* and `CHANT_RUN_EVENTS_DIR` set to {@link PR_LOOP_EVENTS_DIR}. Default false.
|
|
99
|
+
*/
|
|
100
|
+
events?: boolean;
|
|
101
|
+
/**
|
|
102
|
+
* Copy each job's report, change set and events to this `s3://` or
|
|
103
|
+
* `file://` URL (#4007, `--report-url`), under `<pr>/<commit>/`. It is
|
|
104
|
+
* passed in double quotes, so it may name a variable the job expands,
|
|
105
|
+
* such as `$CHANT_REPORT_URL`.
|
|
106
|
+
*/
|
|
107
|
+
reportUrl?: string;
|
|
82
108
|
/**
|
|
83
109
|
* Which GitHub-shaped forge the workflow is for. Set by the forgejo
|
|
84
110
|
* generator, which reuses the github one; a caller never needs to.
|
|
@@ -112,16 +138,21 @@ export function prLoopJobs(forge: ForgeKind, env: string, options: PrLoopPipelin
|
|
|
112
138
|
const common = [
|
|
113
139
|
"--env", env, "--gate", gate, "--output", PR_LOOP_REPORT_DIR, "--forge", forge,
|
|
114
140
|
...(member ? ["--member", member] : []),
|
|
141
|
+
...(options.events ? ["--events-json"] : []),
|
|
142
|
+
...(options.reportUrl ? ["--report-url", JSON.stringify(options.reportUrl)] : []),
|
|
115
143
|
];
|
|
116
144
|
const github = forge !== "gitlab";
|
|
117
|
-
const
|
|
145
|
+
const jobEnv: Record<string, string> = {
|
|
146
|
+
...(github ? { GITHUB_TOKEN: GITHUB_JOB_TOKEN } : {}),
|
|
147
|
+
...(options.events ? { CHANT_RUN_EVENTS_DIR: PR_LOOP_EVENTS_DIR } : {}),
|
|
148
|
+
};
|
|
118
149
|
return [
|
|
119
150
|
{
|
|
120
151
|
jobName: "plan",
|
|
121
152
|
command: ["chant", "components", "pr-plan", "--base", '"$BASE_SHA"', "--pr", '"$PR_NUMBER"', ...common].join(" "),
|
|
122
153
|
env: github
|
|
123
|
-
? { BASE_SHA: "${{ github.event.pull_request.base.sha }}", PR_NUMBER: "${{ github.event.pull_request.number }}", ...
|
|
124
|
-
: { BASE_SHA: "$CI_MERGE_REQUEST_DIFF_BASE_SHA", PR_NUMBER: "$CI_MERGE_REQUEST_IID" },
|
|
154
|
+
? { BASE_SHA: "${{ github.event.pull_request.base.sha }}", PR_NUMBER: "${{ github.event.pull_request.number }}", ...jobEnv }
|
|
155
|
+
: { BASE_SHA: "$CI_MERGE_REQUEST_DIFF_BASE_SHA", PR_NUMBER: "$CI_MERGE_REQUEST_IID", ...jobEnv },
|
|
125
156
|
},
|
|
126
157
|
{
|
|
127
158
|
jobName: "apply",
|
|
@@ -130,7 +161,7 @@ export function prLoopJobs(forge: ForgeKind, env: string, options: PrLoopPipelin
|
|
|
130
161
|
...(options.requireReview === false ? [] : ["--require-review"]),
|
|
131
162
|
"--resume", PR_APPLY_RECORD,
|
|
132
163
|
].join(" "),
|
|
133
|
-
env: github ? { BASE_SHA: "${{ github.event.before }}", ...
|
|
164
|
+
env: github ? { BASE_SHA: "${{ github.event.before }}", ...jobEnv } : { BASE_SHA: "$CI_COMMIT_BEFORE_SHA", ...jobEnv },
|
|
134
165
|
...(github ? {} : { setup: [gitlabBaseFallback(options.branch)] }),
|
|
135
166
|
},
|
|
136
167
|
];
|
|
@@ -53,6 +53,7 @@ import {
|
|
|
53
53
|
type DriverStep,
|
|
54
54
|
} from "./driver";
|
|
55
55
|
import type { GateLedgerPort } from "../op/gate";
|
|
56
|
+
import type { Release } from "../lifecycle/release-store";
|
|
56
57
|
import type { RunProgressEvent } from "./run-progress";
|
|
57
58
|
import type { RunEventSink } from "../run-event-stream";
|
|
58
59
|
|
|
@@ -249,6 +250,66 @@ export function planRollback(input: {
|
|
|
249
250
|
return { from: env, to: env, items: [{ component, digest: target.digest, source: target }], notPromoted: [] };
|
|
250
251
|
}
|
|
251
252
|
|
|
253
|
+
/**
|
|
254
|
+
* Decide what a rollback to a release restores (#4035): every component of
|
|
255
|
+
* `release`, each at the newest record in `env`'s ledger that names the
|
|
256
|
+
* release, so the digest restored is the one the environment ran from it. A
|
|
257
|
+
* component of the release this checkout does not declare is reported and
|
|
258
|
+
* left alone. One the environment never ran from the release has no recorded
|
|
259
|
+
* digest, and is refused. So is a release the environment already runs,
|
|
260
|
+
* unless one of its components is `unsettled` (a run left it failed or gated):
|
|
261
|
+
* then the rollback is what puts that component back too.
|
|
262
|
+
*/
|
|
263
|
+
export function planReleaseRollback(input: {
|
|
264
|
+
env: string;
|
|
265
|
+
records: ReleaseRecord[];
|
|
266
|
+
declared: string[];
|
|
267
|
+
release: Pick<Release, "releaseDigest" | "artifactDigests" | "targets">;
|
|
268
|
+
/** The release's label, or null; used in messages. */
|
|
269
|
+
label: string | null;
|
|
270
|
+
/** Components a run left failed or gated in `env` (../lifecycle/mixed-environment.ts). */
|
|
271
|
+
unsettled?: ReadonlySet<string>;
|
|
272
|
+
}): PromotionPlan | { error: string } {
|
|
273
|
+
const { env, records, declared, release } = input;
|
|
274
|
+
const name = input.label ?? release.releaseDigest;
|
|
275
|
+
const target = release.targets.find((t) => t.env === env);
|
|
276
|
+
const inRelease = new Set<string>();
|
|
277
|
+
for (const t of target?.templates ?? []) if (t.component !== null) inRelease.add(t.component);
|
|
278
|
+
for (const a of release.artifactDigests) inRelease.add(a.component);
|
|
279
|
+
const items: PromotionItem[] = [];
|
|
280
|
+
const notPromoted: PromotionPlan["notPromoted"] = [];
|
|
281
|
+
const neverRan: string[] = [];
|
|
282
|
+
for (const component of [...inRelease].sort()) {
|
|
283
|
+
if (!declared.includes(component)) {
|
|
284
|
+
notPromoted.push({ component, reason: `in release ${name} but not declared in this checkout` });
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
const ran = records.filter((r) => r.component === component && r.releaseDigest === release.releaseDigest);
|
|
288
|
+
if (ran.length === 0) {
|
|
289
|
+
neverRan.push(component);
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
const record = latestPerComponent(ran).get(component)!;
|
|
293
|
+
items.push({ component, digest: record.digest, source: record });
|
|
294
|
+
}
|
|
295
|
+
if (neverRan.length > 0) {
|
|
296
|
+
return {
|
|
297
|
+
error: `release ${name} never deployed ${neverRan.map((c) => `"${c}"`).join(", ")} to "${env}", so no recorded digest can be restored; ` +
|
|
298
|
+
`promote it instead: chant components promote --to ${env} --release ${name}`,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
if (items.length === 0) return { error: `release ${name} has no component declared in this checkout` };
|
|
302
|
+
const current = latestPerComponent(records);
|
|
303
|
+
const already = items.every((i) => {
|
|
304
|
+
const now = current.get(i.component);
|
|
305
|
+
return now !== undefined && now.releaseDigest === release.releaseDigest && now.digest === i.digest && !input.unsettled?.has(i.component);
|
|
306
|
+
});
|
|
307
|
+
if (already) {
|
|
308
|
+
return { error: `"${env}" already runs release ${name}; nothing to roll back. After a deploy that failed partway, chant components redeploy puts a component's release back` };
|
|
309
|
+
}
|
|
310
|
+
return { from: env, to: env, items, notPromoted };
|
|
311
|
+
}
|
|
312
|
+
|
|
252
313
|
/**
|
|
253
314
|
* Decide what a redeploy deploys: the release of `component` the ledger in
|
|
254
315
|
* `env` records as current. A deploy that fails partway records nothing, so
|
|
@@ -641,6 +702,11 @@ function refTo(record: ReleaseRecord) {
|
|
|
641
702
|
return { env: record.env, runId: record.runId, timestamp: record.timestamp };
|
|
642
703
|
}
|
|
643
704
|
|
|
705
|
+
/** A rollback and a redeploy put an earlier deploy back, so they name the release it came from (#4043). */
|
|
706
|
+
function releaseOf(source: ReleaseRecord): { releaseDigest?: string } {
|
|
707
|
+
return source.releaseDigest !== undefined ? { releaseDigest: source.releaseDigest } : {};
|
|
708
|
+
}
|
|
709
|
+
|
|
644
710
|
/**
|
|
645
711
|
* The release record a successful promotion appends to the target ledger.
|
|
646
712
|
* The digest, git sha and archive identities are the source release's own,
|
|
@@ -657,7 +723,7 @@ export function promotionRecord(item: PromotionItem, to: string, run: DeployRunI
|
|
|
657
723
|
* release.
|
|
658
724
|
*/
|
|
659
725
|
export function rollbackRecord(item: PromotionItem, env: string, run: DeployRunInfo): ReleaseRecordInput {
|
|
660
|
-
return { ...sharedRecord(item, env, run), restores: refTo(item.source) };
|
|
726
|
+
return { ...sharedRecord(item, env, run), restores: refTo(item.source), ...releaseOf(item.source) };
|
|
661
727
|
}
|
|
662
728
|
|
|
663
729
|
/**
|
|
@@ -667,5 +733,5 @@ export function rollbackRecord(item: PromotionItem, env: string, run: DeployRunI
|
|
|
667
733
|
* change, and the record says when it was last put back.
|
|
668
734
|
*/
|
|
669
735
|
export function redeployRecord(item: PromotionItem, env: string, run: DeployRunInfo): ReleaseRecordInput {
|
|
670
|
-
return { ...sharedRecord(item, env, run), redeploys: refTo(item.source) };
|
|
736
|
+
return { ...sharedRecord(item, env, run), redeploys: refTo(item.source), ...releaseOf(item.source) };
|
|
671
737
|
}
|