@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
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `chant release` (#4026, ws-104): the release is what graduates.
|
|
3
3
|
*
|
|
4
|
-
* release cut [<label>] [--build <command>] [--param k=v]... [--params-file <file>] [--src <dir>] [-o <file>] [--lexicon <name>] [--actor <name>] [--sign [<key file>]] [--json]
|
|
5
|
-
*
|
|
4
|
+
* release cut [<label>] [--yes] [--build <command>] [--param k=v]... [--params-file <file>] [--src <dir>] [-o <file>] [--lexicon <name>] [--actor <name>] [--sign [<key file>]] [--json]
|
|
5
|
+
* a dry run of a cut of this member from the checkout's HEAD, which must be
|
|
6
|
+
* clean, on trunk and pushed, and green (#4027); `--yes` cuts, records,
|
|
7
|
+
* and tags the commit `release/<label>` locally;
|
|
6
8
|
* --sign signs the cut with the actor's ssh key (#4031); `--src`, `-o` and
|
|
7
9
|
* `--lexicon` shape the build of a project with no components (#4046)
|
|
10
|
+
* release publish <label|digest> [--yes] [--allow-unsigned] [--require-provenance] [--json]
|
|
11
|
+
* publish a cut release (#4027) to the targets release.publish declares
|
|
12
|
+
* (#4047): a dry run unless --yes; refuses a revoked one, one with no
|
|
13
|
+
* signature that verifies against .chant/allowed_signers unless
|
|
14
|
+
* --allow-unsigned (#4049), and with a target a checkout that is not
|
|
15
|
+
* the release's commit; warns with no OIDC provenance, and refuses
|
|
16
|
+
* under --require-provenance; attaches SBOMs and provenance (#4049)
|
|
17
|
+
* release verify <label|digest> [--json]
|
|
18
|
+
* check the release's signature offline against .chant/allowed_signers
|
|
19
|
+
* at the release's commit (#4049)
|
|
8
20
|
* release list [--json]
|
|
9
21
|
* every release of this member with its targets, its cuts and where it runs;
|
|
10
22
|
* `--json` prints the read contract's `releases` document
|
|
@@ -13,18 +25,21 @@
|
|
|
13
25
|
* release compile --env <env> [--build <command>] [--artifacts --component <a,b>] --json
|
|
14
26
|
* internal: what the cut runs in a child process per environment
|
|
15
27
|
*
|
|
16
|
-
* The workstation
|
|
17
|
-
*
|
|
18
|
-
*
|
|
28
|
+
* The workstation checks, the actor and the tag are in
|
|
29
|
+
* ../../lifecycle/release-workstation.ts, and the publish contract in
|
|
30
|
+
* ../../lifecycle/release-publish.ts. A declaration whose `identity.gates`
|
|
31
|
+
* names a `release` gate requires a signed cut by an actor in its class (#4031).
|
|
19
32
|
*/
|
|
20
33
|
|
|
34
|
+
import { execFileSync } from "node:child_process";
|
|
35
|
+
import { existsSync } from "node:fs";
|
|
21
36
|
import { resolve } from "node:path";
|
|
22
37
|
import { formatBold, formatError, formatSuccess, formatWarning } from "../format";
|
|
23
38
|
import { parseParamFlags } from "../build-params-cli";
|
|
24
39
|
import type { CommandContext } from "../registry";
|
|
25
40
|
|
|
26
41
|
const USAGE =
|
|
27
|
-
"chant release cut [<label>] [--build <command>] [--param <k=v>] [--params-file <file>] [--src <dir>] [-o <file>] [--lexicon <name>] [--actor <name>] [--sign [<key file>]] [--json] | release list [--json] | release export <label|digest> --env <env> [-o <dir>] [--json]";
|
|
42
|
+
"chant release cut [<label>] [--yes] [--build <command>] [--param <k=v>] [--params-file <file>] [--src <dir>] [-o <file>] [--lexicon <name>] [--actor <name>] [--sign [<key file>]] [--json] | release publish <label|digest> [--yes] [--allow-unsigned] [--require-provenance] [--json] | release verify <label|digest> [--json] | release list [--json] | release export <label|digest> --env <env> [-o <dir>] [--json]";
|
|
28
43
|
|
|
29
44
|
/** The build inputs `release cut` and `release compile` share. */
|
|
30
45
|
function compileInputs(ctx: CommandContext, params: Record<string, string> | undefined): import("../../components/release-compile").CompileInputs {
|
|
@@ -40,10 +55,54 @@ function compileInputs(ctx: CommandContext, params: Record<string, string> | und
|
|
|
40
55
|
};
|
|
41
56
|
}
|
|
42
57
|
|
|
43
|
-
|
|
44
|
-
|
|
58
|
+
/** What to run after a cut (#4027): publish from CI, or push the tag so CI publishes, or deploy the first environment. */
|
|
59
|
+
export function nextCommands(req: {
|
|
60
|
+
ref: string;
|
|
61
|
+
tag: string | null;
|
|
62
|
+
inCi: boolean;
|
|
63
|
+
firstEnv: string | null;
|
|
64
|
+
/** A project with no components deploys with `release export`, not promote (#4046). */
|
|
65
|
+
noComponents: boolean;
|
|
66
|
+
}): string[] {
|
|
67
|
+
const publish = `chant release publish ${req.ref}`;
|
|
68
|
+
const deploy = req.firstEnv === null
|
|
69
|
+
? null
|
|
70
|
+
: req.noComponents
|
|
71
|
+
? `chant release export ${req.ref} --env ${req.firstEnv}`
|
|
72
|
+
: `chant components promote --release ${req.ref} --to ${req.firstEnv}`;
|
|
73
|
+
if (req.inCi) return [publish, ...(deploy ? [deploy] : [])];
|
|
74
|
+
return [...(req.tag ? [`git push origin ${req.tag}`] : []), publish, ...(deploy ? [deploy] : [])];
|
|
45
75
|
}
|
|
46
76
|
|
|
77
|
+
/** The command line that ran, without `--json`, with `--yes` added: what a dry run says to run next. */
|
|
78
|
+
export function sameWithYes(argv: readonly string[]): string {
|
|
79
|
+
const quote = (a: string) => (/^[\w@%+=:,./-]+$/.test(a) ? a : `'${a.replace(/'/g, `'\\''`)}'`);
|
|
80
|
+
return ["chant", ...argv.filter((a) => a !== "--json" && a !== "--yes"), "--yes"].map(quote).join(" ");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function describeActorSource(source: import("../../lifecycle/release-workstation").ActorSource): string {
|
|
84
|
+
switch (source) {
|
|
85
|
+
case "flag":
|
|
86
|
+
return "--actor";
|
|
87
|
+
case "signer":
|
|
88
|
+
return "the signers file at base, for the signing key";
|
|
89
|
+
case "ci":
|
|
90
|
+
return "the CI's actor";
|
|
91
|
+
case "git":
|
|
92
|
+
return "git user.email";
|
|
93
|
+
case "user":
|
|
94
|
+
return "$USER";
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* `release cut [<label>]` (#4026, #4027). A dry run unless `--yes`: it
|
|
100
|
+
* refuses a dirty tree, a commit that is not on trunk and pushed, and one
|
|
101
|
+
* that is not green, compiles every environment, and prints what the cut
|
|
102
|
+
* would record, build and tag, and what publishes where. With `--yes` it
|
|
103
|
+
* cuts, signs with `--sign`, records the cut with its actor and run id,
|
|
104
|
+
* tags the commit locally and prints the next command.
|
|
105
|
+
*/
|
|
47
106
|
export async function runReleaseCut(ctx: CommandContext): Promise<number> {
|
|
48
107
|
const { args } = ctx;
|
|
49
108
|
const positional = args.extraPositional;
|
|
@@ -52,44 +111,193 @@ export async function runReleaseCut(ctx: CommandContext): Promise<number> {
|
|
|
52
111
|
return 1;
|
|
53
112
|
}
|
|
54
113
|
const label = positional ?? args.label ?? null;
|
|
55
|
-
const
|
|
56
|
-
if (!actor) {
|
|
57
|
-
console.error(formatError({ message: "Could not resolve --actor from the environment", hint: "Pass --actor explicitly, or set GITHUB_ACTOR / GITLAB_USER_LOGIN / USER." }));
|
|
58
|
-
return 1;
|
|
59
|
-
}
|
|
114
|
+
const dryRun = args.yes !== true;
|
|
60
115
|
const cwd = resolve(".");
|
|
61
116
|
const { spawnTargetCompiler, ReleaseCompileError } = await import("../../components/release-compile");
|
|
62
|
-
const { cutRelease, ReleaseCutError } = await import("../../lifecycle/release-cut");
|
|
117
|
+
const { cutRelease, planRelease, ReleaseCutError } = await import("../../lifecycle/release-cut");
|
|
63
118
|
const { ReleaseLabelError } = await import("../../lifecycle/release-store");
|
|
64
|
-
const { pushLifecycleReport } = await import("../../lifecycle/git");
|
|
65
|
-
const
|
|
66
|
-
const
|
|
119
|
+
const { pushLifecycleReport, getHeadCommit } = await import("../../lifecycle/git");
|
|
120
|
+
const { resolveCiRun } = await import("../../lifecycle/release-ledger");
|
|
121
|
+
const ws = await import("../../lifecycle/release-workstation");
|
|
122
|
+
const { detectProvenance, releasePublishTargets } = await import("../../lifecycle/release-publish");
|
|
123
|
+
const fail = (code: string, message: string, fix?: string): number => {
|
|
124
|
+
console.error(formatError({ message: `${message} [${code}]`, ...(fix ? { hint: fix } : {}) }));
|
|
125
|
+
return 1;
|
|
126
|
+
};
|
|
127
|
+
|
|
67
128
|
try {
|
|
68
|
-
|
|
129
|
+
// The workstation checks come first, in the order #4027 lists them; the
|
|
130
|
+
// green check and the rest are the cut's own.
|
|
131
|
+
const top = (() => {
|
|
132
|
+
try {
|
|
133
|
+
return execFileSync("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
134
|
+
} catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
})();
|
|
138
|
+
let commit: string | null = null;
|
|
139
|
+
let trunk: import("../../lifecycle/release-workstation").Trunk | null = null;
|
|
140
|
+
let signers: readonly import("../../workspace/trust/trust-files").Signer[] = [];
|
|
141
|
+
let member: string | null = null;
|
|
142
|
+
if (top) {
|
|
143
|
+
commit = await getHeadCommit({ cwd });
|
|
144
|
+
ws.checkCleanTree(top, commit);
|
|
145
|
+
const { scopeSource } = await import("../../workspace/write-scope");
|
|
146
|
+
const { releaseRuleFrom } = await import("../../workspace/identity");
|
|
147
|
+
const source = scopeSource(cwd);
|
|
148
|
+
trunk = ws.resolveTrunk(top, source.declaration?.ci?.green?.branch ?? null);
|
|
149
|
+
ws.checkOnTrunk(top, commit, trunk);
|
|
150
|
+
signers = releaseRuleFrom(source).rule.signers.signers;
|
|
151
|
+
const { resolveMemberLedger } = await import("../../lifecycle/member-ledger");
|
|
152
|
+
const ledger = await resolveMemberLedger(cwd);
|
|
153
|
+
member = ledger.members.length > 0 ? ledger.members.join("/") : null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// The signing key is resolved now, so a dry run shows a key that can't be read.
|
|
157
|
+
let publicKey: string | null = null;
|
|
158
|
+
if (args.sign !== undefined) {
|
|
159
|
+
const { resolveSigningKey, SealError } = await import("../../workspace/trust/seal");
|
|
160
|
+
try {
|
|
161
|
+
const key = resolveSigningKey(args.sign, cwd);
|
|
162
|
+
try {
|
|
163
|
+
if (!existsSync(key.file)) return fail("release-signer-refused", `no release was cut: the signing key ${key.file} does not exist`);
|
|
164
|
+
publicKey = ws.publicKeyOf(key.file);
|
|
165
|
+
} finally {
|
|
166
|
+
key.cleanup();
|
|
167
|
+
}
|
|
168
|
+
} catch (err) {
|
|
169
|
+
if (!(err instanceof SealError)) throw err;
|
|
170
|
+
return fail("release-signer-refused", `no release was cut: ${err.message}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const who = ws.resolveCutActor({ cwd, ...(args.actor ? { explicit: args.actor } : {}), publicKey, signers });
|
|
174
|
+
if (!who) {
|
|
175
|
+
console.error(formatError({ message: "Could not resolve --actor", hint: "Pass --actor explicitly, or set git's user.email, GITHUB_ACTOR, GITLAB_USER_LOGIN or USER." }));
|
|
176
|
+
return 1;
|
|
177
|
+
}
|
|
178
|
+
const actor = who.actor;
|
|
179
|
+
const tag = label !== null && commit !== null && top ? { name: ws.releaseTagName(member, label), state: ws.checkReleaseTag(top, ws.releaseTagName(member, label), commit) } : null;
|
|
180
|
+
const ci = resolveCiRun(process.env);
|
|
181
|
+
const params = parseParamFlags(args.param);
|
|
182
|
+
const compiler = spawnTargetCompiler(compileInputs(ctx, params));
|
|
183
|
+
const request = { cwd, compiler, label, actor, ...(args.sign !== undefined ? { sign: args.sign } : {}) };
|
|
184
|
+
|
|
185
|
+
if (dryRun) {
|
|
186
|
+
const plan = await planRelease(request);
|
|
187
|
+
// Under the release gate, a key the signers file at base lists for someone else is refused now, not after the build.
|
|
188
|
+
if (plan.signatureRequired && publicKey !== null && !signers.some((s) => s.key === publicKey && s.principal.trim().toLowerCase() === actor.trim().toLowerCase())) {
|
|
189
|
+
return fail(
|
|
190
|
+
"release-signer-refused",
|
|
191
|
+
`no release would be cut: the signing key is not listed for ${actor} in the signers file at base, and identity.gates.release counts only a cut its signer signs`,
|
|
192
|
+
"Pass --actor <principal> naming the key's signer, or sign with that principal's key.",
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
const ref = plan.label ?? plan.alreadyCut ?? plan.releaseDigest;
|
|
196
|
+
const provenance = detectProvenance();
|
|
197
|
+
// The targets release publish would run, as the config here declares them; their files are read at publish.
|
|
198
|
+
const publishTargets = await releasePublishTargets(cwd).then(
|
|
199
|
+
(ts) => ts.map((t) => `${t.name} to ${t.destination}`),
|
|
200
|
+
(err: unknown) => (err instanceof Error ? err : new Error(String(err))),
|
|
201
|
+
);
|
|
202
|
+
const deployRef = plan.label ?? "<label|digest>";
|
|
203
|
+
const doc = {
|
|
204
|
+
dryRun: true,
|
|
205
|
+
...plan,
|
|
206
|
+
actor,
|
|
207
|
+
actorSource: who.source,
|
|
208
|
+
run: ci ? { runId: ci.runId, runOrigin: ci.runOrigin } : { runId: null, runOrigin: { forge: "local" as const } },
|
|
209
|
+
trunk,
|
|
210
|
+
sign: args.sign !== undefined ? { key: publicKey !== null ? publicKey.split(" ")[0] : null, readable: publicKey !== null } : null,
|
|
211
|
+
tag: tag ? { name: tag.name, exists: tag.state === "present" } : null,
|
|
212
|
+
publish: { targets: Array.isArray(publishTargets) ? publishTargets : [], provenance },
|
|
213
|
+
next: [sameWithYes(process.argv.slice(2))],
|
|
214
|
+
};
|
|
215
|
+
if (args.json) {
|
|
216
|
+
console.log(JSON.stringify(doc, null, 2));
|
|
217
|
+
return 0;
|
|
218
|
+
}
|
|
219
|
+
const name = plan.label ? formatBold(plan.label) : "an unlabelled release";
|
|
220
|
+
console.log(`Dry run: cut ${name} from commit ${plan.commit.slice(0, 12)}${trunk ? ` (on ${trunk.remote}/${trunk.branch}, green)` : ""}`);
|
|
221
|
+
if (plan.alreadyCut) console.log(` already cut as ${plan.alreadyCut}: the cut would record nothing new`);
|
|
222
|
+
console.log(` actor ${actor} (from ${describeActorSource(who.source)})`);
|
|
223
|
+
console.log(` run ${ci ? `${ci.runId} (${ci.runOrigin.forge})` : "minted on this machine at the cut (local)"}`);
|
|
224
|
+
console.log(
|
|
225
|
+
` ${args.sign !== undefined ? `signed with ${publicKey ? publicKey.split(" ")[0] : "a key whose public half can't be read until it signs"}` : "not signed"}` +
|
|
226
|
+
(plan.signatureRequired ? "; identity.gates.release requires a signed cut" : ""),
|
|
227
|
+
);
|
|
228
|
+
console.log(` tag ${tag ? `${tag.name}${tag.state === "present" ? " (exists)" : ""}, made here and not pushed` : "none, since the release has no label"}`);
|
|
229
|
+
for (const t of plan.targets) {
|
|
230
|
+
console.log(` ${t.env}: ${t.templates.length} template(s)${t.params.length > 0 ? `, ${t.params.map((p) => `${p.name}=${p.value}`).join(" ")}` : ""}`);
|
|
231
|
+
}
|
|
232
|
+
for (const b of plan.build) console.log(` ${b.component}: artifacts built in ${b.env}`);
|
|
233
|
+
for (const r of plan.reused) console.log(` ${r.component}: artifacts unchanged, carried from ${r.from}`);
|
|
234
|
+
console.log(` digest ${plan.releaseDigest ?? "decided when the artifacts are built"}`);
|
|
235
|
+
console.log("What will be published where:");
|
|
236
|
+
if (plan.publishSteps.length === 0) console.log(" no component declares a publish step");
|
|
237
|
+
for (const p of plan.publishSteps) {
|
|
238
|
+
console.log(` ${p.component}: ${p.kind}${p.to ? ` to ${p.to}` : ""} in ${p.envs.join(", ")}, by that environment's deploy (chant components promote --release ${deployRef})`);
|
|
239
|
+
}
|
|
240
|
+
const publishCmd = `chant release publish ${ref ?? "<label|digest>"}`;
|
|
241
|
+
if (!Array.isArray(publishTargets)) console.log(` ${publishCmd}: the publish targets can't be read: ${publishTargets.message}`);
|
|
242
|
+
else if (publishTargets.length === 0) console.log(` ${publishCmd}: nothing, since this member declares no publish target (release.publish in chant.config.ts)`);
|
|
243
|
+
else for (const t of publishTargets) console.log(` ${publishCmd}: ${t}`);
|
|
244
|
+
if (!provenance.oidc) console.log(` a publish from here would carry no OIDC provenance: ${provenance.reason}`);
|
|
245
|
+
console.error(`Dry run: nothing was cut, signed or tagged. Run again with --yes to cut: ${doc.next[0]}`);
|
|
246
|
+
return 0;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const result = await cutRelease(request);
|
|
69
250
|
const push = await pushLifecycleReport({ cwd }).catch(() => ({ pushed: false, pushWarning: undefined }));
|
|
251
|
+
// The tag names the release's label, which a re-cut with no label reads from the earlier cut.
|
|
252
|
+
let tagged: { name: string; created: boolean } | null = null;
|
|
253
|
+
let tagWarning: string | undefined;
|
|
254
|
+
if (result.label !== null && top) {
|
|
255
|
+
const name = ws.releaseTagName(member, result.label);
|
|
256
|
+
try {
|
|
257
|
+
const state = ws.checkReleaseTag(top, name, result.release.commit);
|
|
258
|
+
if (state === "absent") ws.createReleaseTag(top, name, result.release.commit, { releaseDigest: result.release.releaseDigest, label: result.label, member });
|
|
259
|
+
tagged = { name, created: state === "absent" };
|
|
260
|
+
} catch (err) {
|
|
261
|
+
tagWarning = `the release was cut, and the commit was not tagged: ${err instanceof Error ? err.message : String(err)}`;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
const firstEnv = result.release.targets[0]?.env ?? null;
|
|
265
|
+
const next = nextCommands({
|
|
266
|
+
ref: result.label ?? result.release.releaseDigest,
|
|
267
|
+
tag: tagged?.name ?? null,
|
|
268
|
+
inCi: ci !== undefined,
|
|
269
|
+
firstEnv,
|
|
270
|
+
noComponents: result.release.targets.every((t) => t.templates.every((tpl) => tpl.component === null)) && result.release.artifactDigests.length === 0,
|
|
271
|
+
});
|
|
70
272
|
if (args.json) {
|
|
71
|
-
console.log(
|
|
273
|
+
console.log(
|
|
274
|
+
JSON.stringify(
|
|
275
|
+
{ dryRun: false, release: result.release, label: result.label, cut: result.cut, stored: result.stored, reused: result.reused, actor, actorSource: who.source, tag: tagged, next },
|
|
276
|
+
null,
|
|
277
|
+
2,
|
|
278
|
+
),
|
|
279
|
+
);
|
|
72
280
|
} else {
|
|
73
281
|
const name = result.label ? formatBold(result.label) : "unlabelled release";
|
|
74
282
|
if (result.cut) console.error(formatSuccess(`Cut ${name}: ${result.release.releaseDigest}`));
|
|
75
283
|
else console.error(formatSuccess(`Already cut${result.label ? ` as ${formatBold(result.label)}` : ""}: ${result.release.releaseDigest}`));
|
|
76
284
|
console.error(` commit ${result.release.commit}`);
|
|
77
|
-
if (result.cut) console.error(result.cut.signature ? `
|
|
285
|
+
if (result.cut) console.error(` ${result.cut.signature ? `signed by ${result.cut.actor}` : `not signed; actor ${result.cut.actor}`}, run ${result.cut.runId}`);
|
|
78
286
|
console.error(` artifacts ${result.release.artifactDigests.length}, targets ${result.release.targets.map((t) => `${t.env} (${t.templates.length} template${t.templates.length === 1 ? "" : "s"})`).join(", ")}`);
|
|
79
287
|
for (const r of result.reused) console.error(` ${r.component}: artifacts unchanged, carried from ${r.from}`);
|
|
288
|
+
if (tagged) console.error(` tag ${tagged.name}${tagged.created ? "" : " (already there)"}, not pushed`);
|
|
289
|
+
console.error(`Next: ${next[0]}`);
|
|
290
|
+
for (const n of next.slice(1)) console.error(` or: ${n}`);
|
|
80
291
|
console.log(result.release.releaseDigest);
|
|
81
292
|
}
|
|
82
293
|
if (push.pushWarning) console.error(formatWarning({ message: push.pushWarning }));
|
|
294
|
+
if (tagWarning) console.error(formatWarning({ message: tagWarning }));
|
|
83
295
|
return 0;
|
|
84
296
|
} catch (err) {
|
|
85
|
-
if (err instanceof ReleaseCutError) {
|
|
86
|
-
|
|
87
|
-
return 1;
|
|
88
|
-
}
|
|
89
|
-
if (err instanceof ReleaseLabelError) {
|
|
90
|
-
console.error(formatError({ message: `${err.message} [${err.code}]` }));
|
|
91
|
-
return 1;
|
|
297
|
+
if (err instanceof ws.WorkstationCheckError || err instanceof ReleaseCutError) {
|
|
298
|
+
return fail(err.code, err.message, err.fix);
|
|
92
299
|
}
|
|
300
|
+
if (err instanceof ReleaseLabelError) return fail(err.code, err.message);
|
|
93
301
|
if (err instanceof ReleaseCompileError) {
|
|
94
302
|
console.error(formatError({ message: `no release was cut: ${err.message}` }));
|
|
95
303
|
return 1;
|
|
@@ -98,6 +306,180 @@ export async function runReleaseCut(ctx: CommandContext): Promise<number> {
|
|
|
98
306
|
}
|
|
99
307
|
}
|
|
100
308
|
|
|
309
|
+
/**
|
|
310
|
+
* `release publish <label|digest> [--yes]` (#4027, #4047): publish a cut
|
|
311
|
+
* release to the targets the member declares under `release.publish` in
|
|
312
|
+
* its `chant.config.ts`. It refuses a release no cut names, one cut from a
|
|
313
|
+
* revoked commit, one with no signature that verifies against the signers
|
|
314
|
+
* file at base unless `--allow-unsigned` (#4049; never under
|
|
315
|
+
* identity.gates.release), and, when there is a target, a checkout that is
|
|
316
|
+
* not the release's commit with a clean tree. It warns before publishing
|
|
317
|
+
* when the run has no OIDC token, so what it publishes carries no
|
|
318
|
+
* provenance, and refuses under `--require-provenance`. Each target attaches
|
|
319
|
+
* an SBOM and the release's provenance statement (#4049). Every
|
|
320
|
+
* target is planned (its files read and checked, its credentials found)
|
|
321
|
+
* before any publishes. Without `--yes` it is a dry run that prints the plan
|
|
322
|
+
* and uploads nothing. With `--yes` it stages every target, then reveals them
|
|
323
|
+
* (#4048, `runPublish`), recording each step in `_releases/publishes.jsonl`,
|
|
324
|
+
* so running it again after a failure resumes. Every line is redacted.
|
|
325
|
+
*/
|
|
326
|
+
export async function runReleasePublish(ctx: CommandContext): Promise<number> {
|
|
327
|
+
const { args } = ctx;
|
|
328
|
+
const ref = args.extraPositional;
|
|
329
|
+
if (!ref) {
|
|
330
|
+
console.error(formatError({ message: "release publish needs a release", hint: USAGE }));
|
|
331
|
+
return 1;
|
|
332
|
+
}
|
|
333
|
+
const dryRun = args.yes !== true;
|
|
334
|
+
const cwd = resolve(".");
|
|
335
|
+
const fail = (code: string, message: string, fix?: string): number => {
|
|
336
|
+
console.error(formatError({ message: `${message} [${code}]`, ...(fix ? { hint: fix } : {}) }));
|
|
337
|
+
return 1;
|
|
338
|
+
};
|
|
339
|
+
const { fetchLifecycle } = await import("../../lifecycle/git");
|
|
340
|
+
const { preparePublish, publishContext, runPublish, PublishTargetError, PublishRunError } = await import("../../lifecycle/release-publish");
|
|
341
|
+
const { publishProgress, progressOf, readPublishRecords } = await import("../../lifecycle/publish-record");
|
|
342
|
+
await fetchLifecycle({ cwd }).catch(() => false);
|
|
343
|
+
const prepared = await preparePublish({ cwd, ref, allowUnsigned: args.allowUnsigned === true, requireProvenance: args.requireProvenance === true });
|
|
344
|
+
if ("refusal" in prepared) return fail(prepared.refusal.code, prepared.refusal.message, prepared.refusal.fix);
|
|
345
|
+
const { release, label, signature, provenance, warning, signatureWarning, targets, redactor } = prepared;
|
|
346
|
+
const name = label ?? release.releaseDigest;
|
|
347
|
+
const deploySteps = release.targets.map((t) => t.env);
|
|
348
|
+
const say = (line: string) => console.error(redactor.redact(line));
|
|
349
|
+
const failed = (code: string, message: string, fix?: string) => fail(code, redactor.redact(message), fix);
|
|
350
|
+
const errorOf = (err: unknown): { code: string; message: string; fix?: string } =>
|
|
351
|
+
err instanceof PublishTargetError ? { code: err.code, message: err.message, ...(err.fix ? { fix: err.fix } : {}) } : { code: "publish-failed", message: err instanceof Error ? err.message : String(err) };
|
|
352
|
+
|
|
353
|
+
if (!args.json) {
|
|
354
|
+
say(`${dryRun ? "Dry run: publish" : "Publishing"} ${formatBold(name)} (${release.releaseDigest}) from commit ${release.commit.slice(0, 12)}`);
|
|
355
|
+
say(` ${signature.status === "verified" ? `signed by ${signature.actor}` : signature.status === "unsigned" ? "not signed" : `signature unverified: ${signature.reason}`}`);
|
|
356
|
+
}
|
|
357
|
+
// The warnings go to stderr with --json too, before any target runs.
|
|
358
|
+
if (signatureWarning) console.error(formatWarning({ message: signatureWarning }));
|
|
359
|
+
if (warning) console.error(formatWarning({ message: warning }));
|
|
360
|
+
const pctx = publishContext(prepared, { cwd, log: (line) => (args.json ? undefined : console.error(` ${line}`)) });
|
|
361
|
+
|
|
362
|
+
// Every target is planned before any publishes, so a missing file or credential stops the publish with nothing sent.
|
|
363
|
+
const progress = publishProgress(await readPublishRecords(release.releaseDigest, { cwd }).catch(() => []));
|
|
364
|
+
const planned: Array<{ kind: string; name: string; destination: string; progress: "none" | "staged" | "revealed"; items: import("../../lifecycle/release-publish").PublishItem[] }> = [];
|
|
365
|
+
for (const target of targets) {
|
|
366
|
+
try {
|
|
367
|
+
planned.push({ kind: target.kind, name: target.name, destination: target.destination, progress: progressOf(progress, target), items: await target.plan(pctx) });
|
|
368
|
+
} catch (err) {
|
|
369
|
+
const e = errorOf(err);
|
|
370
|
+
return failed(e.code, `nothing was published: ${target.name}: ${e.message}`, e.fix);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
const promoteHint = (): string | null => {
|
|
374
|
+
if (deploySteps.length === 0) return null;
|
|
375
|
+
const noComponents = release.artifactDigests.length === 0 && release.targets.every((t) => t.templates.every((tpl) => tpl.component === null));
|
|
376
|
+
return noComponents
|
|
377
|
+
? ` Each environment deploys the files the release recorded: chant release export ${name} --env ${deploySteps[0]}`
|
|
378
|
+
: ` A component's publish steps run in each environment's deploy of the release: chant components promote --release ${name} --to ${deploySteps[0]}`;
|
|
379
|
+
};
|
|
380
|
+
const base = { releaseDigest: release.releaseDigest, label, commit: release.commit, signature, provenance, warnings: [signatureWarning, warning].filter((w): w is string => w !== null) };
|
|
381
|
+
|
|
382
|
+
if (dryRun) {
|
|
383
|
+
const next = [sameWithYes(process.argv.slice(2))];
|
|
384
|
+
if (args.json) {
|
|
385
|
+
console.log(redactor.redact(JSON.stringify({ dryRun: true, ...base, targets: planned, published: [], environments: deploySteps, next }, null, 2)));
|
|
386
|
+
return 0;
|
|
387
|
+
}
|
|
388
|
+
const out = (line: string) => console.log(redactor.redact(line));
|
|
389
|
+
out("What would be published where:");
|
|
390
|
+
if (planned.length === 0) out(" nothing: this member declares no publish target (release.publish in chant.config.ts)");
|
|
391
|
+
for (const p of planned) {
|
|
392
|
+
const earlier = p.progress === "revealed" ? " (published by an earlier run; skipped)" : p.progress === "staged" ? " (staged by an earlier run; only revealed)" : "";
|
|
393
|
+
out(` ${p.name}${provenance.oidc && p.kind === "npm" ? ", with --provenance" : ""}${earlier}:`);
|
|
394
|
+
for (const i of p.items) out(` ${describeItem(i)}`);
|
|
395
|
+
}
|
|
396
|
+
say(`Dry run: nothing was published. Run again with --yes to publish: ${next[0]}`);
|
|
397
|
+
const hint = planned.length === 0 ? promoteHint() : null;
|
|
398
|
+
if (hint) say(hint);
|
|
399
|
+
return 0;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (targets.length > 0 && !args.json) say(" every target stages first, and none is visible until all have staged");
|
|
403
|
+
let run: import("../../lifecycle/release-publish").PublishRun;
|
|
404
|
+
try {
|
|
405
|
+
run = await runPublish({ cwd, release, label, targets, ctx: pctx, say: (line) => (args.json ? undefined : say(line)) });
|
|
406
|
+
} catch (err) {
|
|
407
|
+
if (err instanceof PublishRunError) {
|
|
408
|
+
const again = sameWithYes(process.argv.slice(2));
|
|
409
|
+
const where =
|
|
410
|
+
err.step === "stage"
|
|
411
|
+
? `nothing is visible yet${err.staged.length > 0 ? `; staged: ${err.staged.join(", ")}` : ""}`
|
|
412
|
+
: `revealed: ${err.revealed.length > 0 ? err.revealed.join(", ") : "none"}; staged and not yet revealed: ${err.staged.filter((t) => !err.revealed.includes(t)).join(", ")}`;
|
|
413
|
+
return failed(err.code, `${err.message}; ${where}`, `${err.fix ? `${err.fix} ` : ""}Run the same command again to resume; what went out is not published again: ${again}`);
|
|
414
|
+
}
|
|
415
|
+
const e = errorOf(err);
|
|
416
|
+
return failed(e.code, e.message, e.fix);
|
|
417
|
+
}
|
|
418
|
+
const { published } = run;
|
|
419
|
+
if (run.recordWarning && targets.length > 0) console.error(formatWarning({ message: redactor.redact(`${run.recordWarning}. A resume asks the registries what is already there.`) }));
|
|
420
|
+
if (args.json) {
|
|
421
|
+
console.log(redactor.redact(JSON.stringify({ dryRun: false, ...base, targets: planned, published, resumed: run.resumed, environments: deploySteps }, null, 2)));
|
|
422
|
+
return 0;
|
|
423
|
+
}
|
|
424
|
+
if (targets.length === 0) {
|
|
425
|
+
say("Nothing was published: this member declares no publish target (release.publish in chant.config.ts).");
|
|
426
|
+
const hint = promoteHint();
|
|
427
|
+
if (hint) say(hint);
|
|
428
|
+
} else {
|
|
429
|
+
for (const p of published) for (const i of p.items) say(` ${i.present ? "already there" : "published"}: ${describeItem(i)}`);
|
|
430
|
+
console.error(formatSuccess(`Published ${name} to ${published.length} target(s)`));
|
|
431
|
+
}
|
|
432
|
+
return 0;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/** One item a target publishes, for a line: what, where, and the SBOM or attachment it is (#4049). */
|
|
436
|
+
function describeItem(i: import("../../lifecycle/release-publish").PublishItem): string {
|
|
437
|
+
const what = i.attachment === "sbom" ? `${i.name} (SBOM)` : i.attachment === "provenance" ? `${i.name} (provenance)` : i.name;
|
|
438
|
+
return `${what} to ${i.destination}${i.sbom ? `, carrying ${i.sbom}` : ""}`;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* `release verify <label|digest> [--json]` (#4049): check, offline, that the
|
|
443
|
+
* release was signed by a key `.chant/allowed_signers` lists at the release's
|
|
444
|
+
* commit, and that its file is the digest of what it holds. Exits 0 when it
|
|
445
|
+
* verifies and 1 when it does not or can't be checked.
|
|
446
|
+
*/
|
|
447
|
+
export async function runReleaseVerify(ctx: CommandContext): Promise<number> {
|
|
448
|
+
const { args } = ctx;
|
|
449
|
+
const ref = args.extraPositional;
|
|
450
|
+
if (!ref) {
|
|
451
|
+
console.error(formatError({ message: "release verify needs a release", hint: USAGE }));
|
|
452
|
+
return 1;
|
|
453
|
+
}
|
|
454
|
+
const { verifyRelease, verificationCode } = await import("../../lifecycle/release-verify");
|
|
455
|
+
const result = await verifyRelease({ cwd: resolve("."), ref });
|
|
456
|
+
if ("error" in result) {
|
|
457
|
+
if (args.json) console.log(JSON.stringify({ ref, error: result.error }, null, 2));
|
|
458
|
+
else console.error(formatError({ message: `${result.error.message} [${result.error.code}]`, ...(result.error.fix ? { hint: result.error.fix } : {}) }));
|
|
459
|
+
return 1;
|
|
460
|
+
}
|
|
461
|
+
const code = verificationCode(result);
|
|
462
|
+
if (args.json) {
|
|
463
|
+
console.log(JSON.stringify({ ...result, ...(code ? { error: { code, message: result.reason } } : {}) }, null, 2));
|
|
464
|
+
return code ? 1 : 0;
|
|
465
|
+
}
|
|
466
|
+
const name = result.label ? `${formatBold(result.label)} (${result.releaseDigest})` : result.releaseDigest;
|
|
467
|
+
for (const c of result.cuts) {
|
|
468
|
+
console.log(` cut by ${c.actor} at ${c.timestamp}: ${c.signatureCheck.status}${c.signatureCheck.key ? ` (${c.signatureCheck.key})` : ""}`);
|
|
469
|
+
}
|
|
470
|
+
if (code) {
|
|
471
|
+
console.error(
|
|
472
|
+
formatError({
|
|
473
|
+
message: `${name} does not verify: ${result.reason} [${code}]`,
|
|
474
|
+
hint: code === "release-digest-mismatch" ? "Do not publish or deploy it; cut the release again." : `Cut it again signed with a key ${result.signers.path} lists: chant release cut ${result.label ?? "<label>"} --sign --yes`,
|
|
475
|
+
}),
|
|
476
|
+
);
|
|
477
|
+
return 1;
|
|
478
|
+
}
|
|
479
|
+
console.error(formatSuccess(`Verified ${name}: ${result.reason}`));
|
|
480
|
+
return 0;
|
|
481
|
+
}
|
|
482
|
+
|
|
101
483
|
export async function runReleaseList(ctx: CommandContext): Promise<number> {
|
|
102
484
|
const { releasesDocument } = await import("../../workspace/releases");
|
|
103
485
|
const doc = await releasesDocument({ cwd: resolve(".") });
|
|
@@ -119,8 +501,13 @@ export async function runReleaseList(ctx: CommandContext): Promise<number> {
|
|
|
119
501
|
// #4031: the newest cut's signature, as the policy at base judges it.
|
|
120
502
|
const newest = r.cuts.at(-1);
|
|
121
503
|
const signature = newest ? (newest.signatureCheck.status === "verified" ? `signed by ${newest.actor}` : newest.signatureCheck.status) : "-";
|
|
122
|
-
|
|
504
|
+
// #4033: a release cut from a revoked commit says so at the end of its line, and why on stderr.
|
|
505
|
+
console.log(`${(r.label ?? "-").padEnd(16)} ${r.releaseDigest} ${r.commit.slice(0, 12)} ${signature} ${where}${r.revoked ? " revoked" : ""}`);
|
|
123
506
|
if (newest?.signatureCheck.status === "unverified") console.error(` ${r.label ?? r.releaseDigest}: ${newest.signatureCheck.reason}`);
|
|
507
|
+
if (r.revoked) {
|
|
508
|
+
const { describeRevocation } = await import("../../lifecycle/release-revocation");
|
|
509
|
+
console.error(` ${r.label ?? r.releaseDigest}: revoked by ${r.revoked.tag}, ${describeRevocation(r.revoked)}`);
|
|
510
|
+
}
|
|
124
511
|
}
|
|
125
512
|
return 0;
|
|
126
513
|
}
|
|
@@ -46,6 +46,15 @@ describe("recordAutoReleasesForRun", () => {
|
|
|
46
46
|
expect(plain).not.toHaveProperty("admission");
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
+
test("a run under --release records the release digest on each component, one without it records none (#4043)", async () => {
|
|
50
|
+
const release = `jcs1-sha256:${"c".repeat(64)}`;
|
|
51
|
+
await recordAutoReleasesForRun([plainResult], "dev", "9001", false, { forge: "local" }, release);
|
|
52
|
+
expect(maybeRecordAutoReleaseMock.mock.calls[0][0]).toMatchObject({ releaseDigest: release });
|
|
53
|
+
maybeRecordAutoReleaseMock.mockClear();
|
|
54
|
+
await recordAutoReleasesForRun([plainResult], "dev", "9001", false, { forge: "local" });
|
|
55
|
+
expect(maybeRecordAutoReleaseMock.mock.calls[0][0]).not.toHaveProperty("releaseDigest");
|
|
56
|
+
});
|
|
57
|
+
|
|
49
58
|
test("the origin defaults to local", async () => {
|
|
50
59
|
await recordAutoReleasesForRun([plainResult], "prod", "local-1", false);
|
|
51
60
|
expect(maybeRecordAutoReleaseMock.mock.calls[0][0]).toMatchObject({ runId: "local-1", runOrigin: { forge: "local" } });
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `chant run --components --release <label|digest>` (#4043): the run builds
|
|
3
|
+
* from the checked-out commit, so the release must be cut from it and have a
|
|
4
|
+
* target for the environment. Checked before anything deploys.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, test, expect, vi } from "vitest";
|
|
7
|
+
import { makeRelease } from "../../lifecycle/release-store";
|
|
8
|
+
|
|
9
|
+
const COMMIT = "a1b2c3d4".repeat(5);
|
|
10
|
+
const release = makeRelease({
|
|
11
|
+
member: null,
|
|
12
|
+
commit: COMMIT,
|
|
13
|
+
artifactDigests: [],
|
|
14
|
+
targets: [{ env: "dev", templates: [], params: [] }],
|
|
15
|
+
});
|
|
16
|
+
let head = COMMIT;
|
|
17
|
+
|
|
18
|
+
vi.mock("../../lifecycle/git", () => ({
|
|
19
|
+
getHeadCommit: async () => head,
|
|
20
|
+
fetchLifecycle: async () => true,
|
|
21
|
+
pushLifecycle: async () => false,
|
|
22
|
+
}));
|
|
23
|
+
vi.mock("../../lifecycle/release-store", async () => {
|
|
24
|
+
const actual = await vi.importActual<typeof import("../../lifecycle/release-store")>("../../lifecycle/release-store");
|
|
25
|
+
return {
|
|
26
|
+
...actual,
|
|
27
|
+
resolveRelease: async (ref: string) => (ref === "v1" || ref === release.releaseDigest ? { release, label: "v1" } : null),
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const { resolveRunRelease } = await import("./run");
|
|
32
|
+
|
|
33
|
+
describe("resolveRunRelease", () => {
|
|
34
|
+
test("a release cut from HEAD with a target for the environment is the run's release, by label or digest", async () => {
|
|
35
|
+
head = COMMIT;
|
|
36
|
+
expect(await resolveRunRelease("v1", "dev", ".")).toMatchObject({ releaseDigest: release.releaseDigest, label: "v1" });
|
|
37
|
+
expect(await resolveRunRelease(release.releaseDigest, "dev", ".")).toMatchObject({ releaseDigest: release.releaseDigest });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("a release it can't find is refused", async () => {
|
|
41
|
+
expect(await resolveRunRelease("v9", "dev", ".")).toMatch(/no release is labelled or digested "v9"/);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("a release cut from another commit is refused, and the message says why", async () => {
|
|
45
|
+
head = "f".repeat(40);
|
|
46
|
+
const why = await resolveRunRelease("v1", "dev", ".");
|
|
47
|
+
expect(why).toMatch(/was cut from a1b2c3d4a1b2 and this checkout is at ffffffffffff/);
|
|
48
|
+
expect(why).toMatch(/promote --release/);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("a release with no target for the environment is refused", async () => {
|
|
52
|
+
head = COMMIT;
|
|
53
|
+
expect(await resolveRunRelease("v1", "prod", ".")).toMatch(/has no target for "prod"/);
|
|
54
|
+
});
|
|
55
|
+
});
|