@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,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Join a commit, a release, its gates and its deploy records through the
|
|
3
|
+
* release digest (#4043, ws-104).
|
|
4
|
+
*
|
|
5
|
+
* The records already name one another. A release file holds its `commit`; a
|
|
6
|
+
* release gate's pending fact and resolution hold the `releaseDigest`; a deploy
|
|
7
|
+
* record holds the `releaseDigest` when the deploy was made from a release.
|
|
8
|
+
* From any one of the four, the others are one lookup away:
|
|
9
|
+
*
|
|
10
|
+
* commit to releases `_releases/*.json` `commit`
|
|
11
|
+
* release to gates `releaseDigest` on the records of `_gates/release.jsonl`
|
|
12
|
+
* release to deploys `releaseDigest` on the records of every `<env>/releases.jsonl`
|
|
13
|
+
*
|
|
14
|
+
* This is the one place that does it, for the readers that need the chain
|
|
15
|
+
* (the why chain of #4023, behold). Release plus a deploy record's `env`
|
|
16
|
+
* names the release target.
|
|
17
|
+
*
|
|
18
|
+
* Read-only. Reads the local `chant/lifecycle` branch; fetch it first for the
|
|
19
|
+
* remote's records. A release digest or commit that nothing recorded joins to
|
|
20
|
+
* an empty list rather than an error.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { RELEASE_GATE_OP } from "../op/gate-name";
|
|
24
|
+
import { readGateLedger, type GateResolutionRecord, type PendingGateRecord } from "./gate-ledger";
|
|
25
|
+
import { listReleaseEnvironments, readReleaseLedger, type ReleaseRecord } from "./release-ledger";
|
|
26
|
+
import { labelsByDigest, readAllReleases, readCuts, type Release, type ReleaseCut } from "./release-store";
|
|
27
|
+
import type { LedgerLocation } from "./git";
|
|
28
|
+
|
|
29
|
+
/** Where a join starts. Each names the others one lookup away. */
|
|
30
|
+
export type ReleaseJoinStart =
|
|
31
|
+
/** A full commit sha, or a prefix of at least 7 characters. Joins to every release cut from it. */
|
|
32
|
+
| { commit: string }
|
|
33
|
+
/**
|
|
34
|
+
* Several full commit shas, or prefixes of at least 7 characters. Joins to
|
|
35
|
+
* every release cut from any of them, reading the ledgers once (#4023).
|
|
36
|
+
*/
|
|
37
|
+
| { commits: readonly string[] }
|
|
38
|
+
/** A release's label or its `releaseDigest`. */
|
|
39
|
+
| { release: string }
|
|
40
|
+
/**
|
|
41
|
+
* A deploy record. It joins through its `releaseDigest`; a record with none
|
|
42
|
+
* but an admission by the release gate joins through the resolution that
|
|
43
|
+
* admission names.
|
|
44
|
+
*/
|
|
45
|
+
| { deploy: Pick<ReleaseRecord, "releaseDigest" | "admission"> }
|
|
46
|
+
/** A release gate's pending fact or resolution. */
|
|
47
|
+
| { gate: Pick<PendingGateRecord | GateResolutionRecord, "releaseDigest"> };
|
|
48
|
+
|
|
49
|
+
/** One release with everything the digest joins to it. */
|
|
50
|
+
export interface JoinedRelease {
|
|
51
|
+
releaseDigest: string;
|
|
52
|
+
/** The label of the release, or null when it was cut with none. */
|
|
53
|
+
label: string | null;
|
|
54
|
+
/** The green commit the release was cut from. */
|
|
55
|
+
commit: string;
|
|
56
|
+
release: Release;
|
|
57
|
+
/** Every cut of the release, oldest first. */
|
|
58
|
+
cuts: ReleaseCut[];
|
|
59
|
+
/** The release gate's facts for this release (`_gates/release.jsonl`), oldest first. */
|
|
60
|
+
gates: { pending: PendingGateRecord[]; resolutions: GateResolutionRecord[] };
|
|
61
|
+
/** Every deploy record that names the release, across environments, oldest first: by its `releaseDigest`, or, without one, by the release gate resolution its admission names. Each record's `env` with the release names the target. */
|
|
62
|
+
deploys: ReleaseRecord[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** What {@link joinRelease} returns. */
|
|
66
|
+
export interface ReleaseJoin {
|
|
67
|
+
/** The releases the starting point joins to: one for a release, a gate or a deploy, any number for a commit. */
|
|
68
|
+
releases: JoinedRelease[];
|
|
69
|
+
/** Why `releases` is empty, when the starting point could not be followed. Absent otherwise. */
|
|
70
|
+
reason?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The version of the shape above, for a reader that stores it. Chant 0.124.29 and newer. */
|
|
74
|
+
export const RELEASE_JOIN_VERSION = 1;
|
|
75
|
+
|
|
76
|
+
const MIN_COMMIT_PREFIX = 7;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Follow one record to the rest of its chain. Never throws for an unknown
|
|
80
|
+
* starting point; a ledger that can't be read does throw, as those readers do.
|
|
81
|
+
*/
|
|
82
|
+
export async function joinRelease(from: ReleaseJoinStart, loc: LedgerLocation = {}): Promise<ReleaseJoin> {
|
|
83
|
+
const { releases } = await readAllReleases(loc);
|
|
84
|
+
const { cuts } = await readCuts(loc);
|
|
85
|
+
const labels = labelsByDigest(cuts);
|
|
86
|
+
|
|
87
|
+
let digests: string[];
|
|
88
|
+
let reason: string | undefined;
|
|
89
|
+
if ("commit" in from) {
|
|
90
|
+
const sha = from.commit.toLowerCase();
|
|
91
|
+
if (sha.length < MIN_COMMIT_PREFIX) {
|
|
92
|
+
return { releases: [], reason: `a commit needs at least ${MIN_COMMIT_PREFIX} characters to join on` };
|
|
93
|
+
}
|
|
94
|
+
digests = releases.filter((r) => r.commit.toLowerCase().startsWith(sha)).map((r) => r.releaseDigest);
|
|
95
|
+
if (digests.length === 0) reason = `no release was cut from commit ${from.commit}`;
|
|
96
|
+
} else if ("commits" in from) {
|
|
97
|
+
const shas = from.commits.map((c) => c.toLowerCase());
|
|
98
|
+
if (shas.some((sha) => sha.length < MIN_COMMIT_PREFIX)) {
|
|
99
|
+
return { releases: [], reason: `a commit needs at least ${MIN_COMMIT_PREFIX} characters to join on` };
|
|
100
|
+
}
|
|
101
|
+
digests = releases.filter((r) => shas.some((sha) => r.commit.toLowerCase().startsWith(sha))).map((r) => r.releaseDigest);
|
|
102
|
+
if (digests.length === 0) reason = shas.length === 0 ? "no commit was given to join on" : `no release was cut from ${shas.length === 1 ? `commit ${from.commits[0]}` : `any of ${shas.length} commits`}`;
|
|
103
|
+
} else if ("release" in from) {
|
|
104
|
+
const ref = from.release;
|
|
105
|
+
const byDigest = releases.find((r) => r.releaseDigest === ref);
|
|
106
|
+
const byLabel = [...labels].find(([, label]) => label === ref)?.[0];
|
|
107
|
+
const found = byDigest?.releaseDigest ?? byLabel;
|
|
108
|
+
digests = found ? [found] : [];
|
|
109
|
+
if (digests.length === 0) reason = `no release is labelled or digested "${ref}"`;
|
|
110
|
+
} else if ("deploy" in from) {
|
|
111
|
+
const digest = from.deploy.releaseDigest ?? (await releaseOfAdmission(from.deploy.admission, loc));
|
|
112
|
+
digests = digest ? [digest] : [];
|
|
113
|
+
if (digests.length === 0) reason = "the deploy record names no release: it was not made from one";
|
|
114
|
+
} else {
|
|
115
|
+
const digest = from.gate.releaseDigest;
|
|
116
|
+
digests = digest ? [digest] : [];
|
|
117
|
+
if (digests.length === 0) reason = "the gate binds no release: it is not a release gate";
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const known = new Map(releases.map((r) => [r.releaseDigest, r]));
|
|
121
|
+
const wanted = [...new Set(digests)].filter((d) => known.has(d));
|
|
122
|
+
if (digests.length > 0 && wanted.length === 0) reason = `release ${digests[0]} is named but its file is not on this checkout's chant/lifecycle`;
|
|
123
|
+
if (wanted.length === 0) return { releases: [], ...(reason ? { reason } : {}) };
|
|
124
|
+
|
|
125
|
+
const gateLedger = await readGateLedger(RELEASE_GATE_OP, loc);
|
|
126
|
+
// A record names its release by `releaseDigest`, or, with none, through the
|
|
127
|
+
// release gate resolution its admission names (#4045), as releaseOfDeploy reads it.
|
|
128
|
+
const records: { record: ReleaseRecord; digest: string }[] = [];
|
|
129
|
+
for (const env of await listReleaseEnvironments(loc)) {
|
|
130
|
+
for (const record of (await readReleaseLedger(env, loc)).records) {
|
|
131
|
+
const digest = releaseOfDeploy(record, gateLedger.resolutions);
|
|
132
|
+
if (digest !== undefined && wanted.includes(digest)) records.push({ record, digest });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const byTime = (a: { timestamp: string }, b: { timestamp: string }): number => (a.timestamp < b.timestamp ? -1 : a.timestamp > b.timestamp ? 1 : 0);
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
releases: wanted.map((digest) => {
|
|
139
|
+
const release = known.get(digest)!;
|
|
140
|
+
return {
|
|
141
|
+
releaseDigest: digest,
|
|
142
|
+
label: labels.get(digest) ?? null,
|
|
143
|
+
commit: release.commit,
|
|
144
|
+
release,
|
|
145
|
+
cuts: cuts.filter((c) => c.releaseDigest === digest),
|
|
146
|
+
gates: {
|
|
147
|
+
pending: gateLedger.pending.filter((p) => p.releaseDigest === digest),
|
|
148
|
+
resolutions: gateLedger.resolutions.filter((r) => r.releaseDigest === digest),
|
|
149
|
+
},
|
|
150
|
+
deploys: records.filter((r) => r.digest === digest).map((r) => r.record).sort(byTime),
|
|
151
|
+
};
|
|
152
|
+
}),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The release a deploy record's admission names, when the record itself has no
|
|
158
|
+
* `releaseDigest`: the resolution it points at, in the release gate's ledger,
|
|
159
|
+
* carries one (#4045).
|
|
160
|
+
*/
|
|
161
|
+
async function releaseOfAdmission(admission: ReleaseRecord["admission"], loc: LedgerLocation): Promise<string | undefined> {
|
|
162
|
+
if (!admission || admission.op !== RELEASE_GATE_OP) return undefined;
|
|
163
|
+
const { resolutions } = await readGateLedger(RELEASE_GATE_OP, loc);
|
|
164
|
+
return releaseOfDeploy({ admission }, resolutions);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* The release a deploy record names, over the release gate's resolutions
|
|
169
|
+
* already read: its own `releaseDigest`, else the one on the resolution its
|
|
170
|
+
* admission names. Undefined when it was not made from a release. The rule
|
|
171
|
+
* {@link joinRelease} follows, for a reader that holds the ledgers already,
|
|
172
|
+
* such as retention's garbage collection (#4036).
|
|
173
|
+
*/
|
|
174
|
+
export function releaseOfDeploy(
|
|
175
|
+
record: Pick<ReleaseRecord, "releaseDigest" | "admission">,
|
|
176
|
+
releaseGateResolutions: readonly Pick<GateResolutionRecord, "gate" | "timestamp" | "releaseDigest">[],
|
|
177
|
+
): string | undefined {
|
|
178
|
+
if (record.releaseDigest !== undefined) return record.releaseDigest;
|
|
179
|
+
const admission = record.admission;
|
|
180
|
+
if (!admission || admission.op !== RELEASE_GATE_OP) return undefined;
|
|
181
|
+
return releaseGateResolutions.find((r) => r.gate === admission.gate && r.timestamp === admission.resolvedAt && r.releaseDigest !== undefined)?.releaseDigest;
|
|
182
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #4027: `chant release publish`'s contract. It refuses a release no cut
|
|
3
|
+
* names, a revoked one, and one with no verified signature (#4049; under
|
|
4
|
+
* identity.gates.release even with --allow-unsigned); it warns when the run has no OIDC token; and with no
|
|
5
|
+
* publish target declared it has nothing to publish. With a target (#4047)
|
|
6
|
+
* it reads them from release.publish and refuses a checkout that is not the
|
|
7
|
+
* release's commit with a clean tree.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { afterAll, describe, expect, test } from "vitest";
|
|
11
|
+
import { rmSync } from "node:fs";
|
|
12
|
+
import { dirname } from "node:path";
|
|
13
|
+
import { cutRelease } from "./release-cut";
|
|
14
|
+
import { detectProvenance, preparePublish, type PublishPlan, type PublishRefusal } from "./release-publish";
|
|
15
|
+
import type { ReleaseArtifact } from "./release-store";
|
|
16
|
+
import type { CompiledTarget, TargetCompiler } from "../components/release-compile";
|
|
17
|
+
import { hasSshKeygen, TestRepo, type Key } from "../workspace/trust/test-repo";
|
|
18
|
+
|
|
19
|
+
const repos: TestRepo[] = [];
|
|
20
|
+
afterAll(() => {
|
|
21
|
+
for (const r of repos) rmSync(dirname(r.dir), { recursive: true, force: true });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const compiler: TargetCompiler = {
|
|
25
|
+
async compile(env): Promise<CompiledTarget> {
|
|
26
|
+
return {
|
|
27
|
+
env,
|
|
28
|
+
params: [],
|
|
29
|
+
components: ["api"],
|
|
30
|
+
buildSteps: {},
|
|
31
|
+
buildInputs: {},
|
|
32
|
+
componentFiles: { api: "api.component.ts" },
|
|
33
|
+
templates: [{ component: "api", path: "dist/api.json", content: `{"env":"${env}"}\n` }],
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
async buildArtifacts(): Promise<ReleaseArtifact[]> {
|
|
37
|
+
return [];
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** A project, a workspace member when `declaration` is given, with alice and bob as signers and alice a releaser. */
|
|
42
|
+
function setup(declaration: Record<string, unknown> | null, opts: { signers?: boolean } = {}): { r: TestRepo; alice: Key; bob: Key; head: string } {
|
|
43
|
+
const r = new TestRepo("release-publish");
|
|
44
|
+
repos.push(r);
|
|
45
|
+
const alice = r.key("alice");
|
|
46
|
+
const bob = r.key("bob");
|
|
47
|
+
if (declaration) {
|
|
48
|
+
r.write("chant.workspace.json", JSON.stringify({ name: "acme", schema: 1, members: [{ name: "root", dir: ".", kind: "other", because: "the publish fixture" }], ...declaration }, null, 2));
|
|
49
|
+
r.write(".chant/allowed_signers", `alice ${alice.pub}\nbob ${bob.pub}\n`);
|
|
50
|
+
} else if (opts.signers) {
|
|
51
|
+
// A plain project with a signers file and no workspace declaration (#4049).
|
|
52
|
+
r.write(".chant/allowed_signers", `alice ${alice.pub}\n`);
|
|
53
|
+
}
|
|
54
|
+
r.write("chant.config.ts", `export default { environments: ["dev", "prod"] };\n`);
|
|
55
|
+
r.write("api.component.ts", "export const api = {};\n");
|
|
56
|
+
const head = r.commit("init");
|
|
57
|
+
r.git(["tag", `ci/green/${head}`]);
|
|
58
|
+
return { r, alice, bob, head };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const cut = (r: TestRepo, label: string, sign?: Key, actor = "alice") =>
|
|
62
|
+
cutRelease({ cwd: r.dir, compiler, label, actor, env: {}, now: () => new Date("2026-10-17T10:00:00.000Z"), ...(sign ? { sign: sign.file } : {}) });
|
|
63
|
+
const refusal = (p: PublishPlan | { refusal: PublishRefusal }): PublishRefusal | null => ("refusal" in p ? p.refusal : null);
|
|
64
|
+
const WORKSTATION: NodeJS.ProcessEnv = { PATH: process.env.PATH };
|
|
65
|
+
|
|
66
|
+
describe("provenance", () => {
|
|
67
|
+
test("GitHub Actions has OIDC with id-token: write, GitLab with an ID token, a workstation never", () => {
|
|
68
|
+
expect(detectProvenance({})).toEqual({ oidc: false, ci: null, reason: "this run is not in CI, and a workstation has no OIDC token" });
|
|
69
|
+
expect(detectProvenance({ GITHUB_ACTIONS: "true" })).toMatchObject({ oidc: false, ci: "github", reason: expect.stringMatching(/id-token: write/) });
|
|
70
|
+
expect(detectProvenance({ GITHUB_ACTIONS: "true", ACTIONS_ID_TOKEN_REQUEST_URL: "https://x", ACTIONS_ID_TOKEN_REQUEST_TOKEN: "t" })).toMatchObject({ oidc: true, ci: "github" });
|
|
71
|
+
expect(detectProvenance({ GITLAB_CI: "true" })).toMatchObject({ oidc: false, ci: "gitlab", reason: expect.stringMatching(/id_tokens/) });
|
|
72
|
+
expect(detectProvenance({ GITLAB_CI: "true", SIGSTORE_ID_TOKEN: "t" })).toMatchObject({ oidc: true, ci: "gitlab" });
|
|
73
|
+
// A CI variable alone is not a token.
|
|
74
|
+
expect(detectProvenance({ CI: "true", GITHUB_RUN_ID: "1" }).oidc).toBe(false);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe("chant release publish", () => {
|
|
79
|
+
test("warns on a workstation that what it publishes carries no provenance, and has no target to publish to", async () => {
|
|
80
|
+
const { r } = setup(null);
|
|
81
|
+
await cut(r, "v1");
|
|
82
|
+
const plan = await preparePublish({ cwd: r.dir, ref: "v1", env: WORKSTATION, allowUnsigned: true });
|
|
83
|
+
expect(refusal(plan)).toBeNull();
|
|
84
|
+
const p = plan as PublishPlan;
|
|
85
|
+
expect(p.label).toBe("v1");
|
|
86
|
+
expect(p.signature.status).toBe("unsigned");
|
|
87
|
+
expect(p.provenance.oidc).toBe(false);
|
|
88
|
+
expect(p.warning).toBe(
|
|
89
|
+
"this publish runs without OIDC, so what it publishes carries no provenance: this run is not in CI, and a workstation has no OIDC token. Publish from CI to get provenance.",
|
|
90
|
+
);
|
|
91
|
+
expect(p.targets).toEqual([]);
|
|
92
|
+
expect(p.releaseTag).toBe("release/v1");
|
|
93
|
+
|
|
94
|
+
const inCi = (await preparePublish({
|
|
95
|
+
cwd: r.dir,
|
|
96
|
+
ref: p.release.releaseDigest,
|
|
97
|
+
env: { GITHUB_ACTIONS: "true", ACTIONS_ID_TOKEN_REQUEST_URL: "https://x", ACTIONS_ID_TOKEN_REQUEST_TOKEN: "t" },
|
|
98
|
+
allowUnsigned: true,
|
|
99
|
+
})) as PublishPlan;
|
|
100
|
+
expect(inCi.warning).toBeNull();
|
|
101
|
+
expect(inCi.label).toBe("v1");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("refuses a release no cut names, and one cut from a revoked commit", async () => {
|
|
105
|
+
const { r, head } = setup(null);
|
|
106
|
+
await cut(r, "v1");
|
|
107
|
+
expect(refusal(await preparePublish({ cwd: r.dir, ref: "v9", env: WORKSTATION }))?.code).toBe("release-not-found");
|
|
108
|
+
r.git(["tag", `ci/revoked/${head}`]);
|
|
109
|
+
const revoked = refusal(await preparePublish({ cwd: r.dir, ref: "v1", env: WORKSTATION }));
|
|
110
|
+
expect(revoked?.code).toBe("release-revoked");
|
|
111
|
+
expect(revoked?.message).toMatch(new RegExp(`ci/revoked/${head}`));
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test.skipIf(!hasSshKeygen)("under identity.gates.release, refuses an unsigned cut and publishes a signed one", async () => {
|
|
115
|
+
const gate = { identity: { gates: { release: {} } } };
|
|
116
|
+
const { r, alice } = setup(gate);
|
|
117
|
+
// Cut before the gate applies to the cut, as an older chant or another branch could: write it through the store's own path.
|
|
118
|
+
const { appendCut, makeRelease, persistRelease } = await import("./release-store");
|
|
119
|
+
const head = r.head();
|
|
120
|
+
const unsigned = makeRelease({ member: "root", commit: head, artifactDigests: [], targets: [{ env: "dev", params: [], templates: [] }] });
|
|
121
|
+
await persistRelease(unsigned, [], { cwd: r.dir });
|
|
122
|
+
await appendCut({ releaseDigest: unsigned.releaseDigest, label: "v0", actor: "alice", timestamp: "2026-10-17T09:00:00.000Z", runOrigin: null }, { cwd: r.dir });
|
|
123
|
+
const refused = refusal(await preparePublish({ cwd: r.dir, ref: "v0", env: WORKSTATION }));
|
|
124
|
+
expect(refused?.code).toBe("release-unsigned");
|
|
125
|
+
expect(refused?.fix).toMatch(/chant release cut v0 --sign --yes/);
|
|
126
|
+
|
|
127
|
+
await cut(r, "v1", alice);
|
|
128
|
+
const signed = (await preparePublish({ cwd: r.dir, ref: "v1", env: WORKSTATION })) as PublishPlan;
|
|
129
|
+
expect(refusal(signed)).toBeNull();
|
|
130
|
+
expect(signed.signature).toMatchObject({ status: "verified", actor: "alice", key: expect.stringMatching(/^SHA256:/) });
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test.skipIf(!hasSshKeygen)("under identity.gates.release, refuses a cut whose signature does not verify for its actor", async () => {
|
|
134
|
+
const { r, bob } = setup({ identity: { gates: { release: {} } } });
|
|
135
|
+
const { appendCut, makeRelease, persistRelease } = await import("./release-store");
|
|
136
|
+
const { sealReleaseCut } = await import("../workspace/trust/seal");
|
|
137
|
+
const release = makeRelease({ member: "root", commit: r.head(), artifactDigests: [], targets: [{ env: "dev", params: [], templates: [] }] });
|
|
138
|
+
await persistRelease(release, [], { cwd: r.dir });
|
|
139
|
+
// Signed by bob's key, in alice's name.
|
|
140
|
+
const line = { releaseDigest: release.releaseDigest, label: "v2", actor: "alice", timestamp: "2026-10-17T09:00:00.000Z" };
|
|
141
|
+
const signature = sealReleaseCut(bob.file, line).signature;
|
|
142
|
+
await appendCut({ ...line, runOrigin: null, signature }, { cwd: r.dir });
|
|
143
|
+
const refused = refusal(await preparePublish({ cwd: r.dir, ref: "v2", env: WORKSTATION }));
|
|
144
|
+
expect(refused?.code).toBe("release-signature-unverified");
|
|
145
|
+
// --allow-unsigned never overrides the release gate.
|
|
146
|
+
expect(refusal(await preparePublish({ cwd: r.dir, ref: "v2", env: WORKSTATION, allowUnsigned: true }))?.code).toBe("release-signature-unverified");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("refuses an unsigned release with no release gate, and publishes it under --allow-unsigned with a warning", async () => {
|
|
150
|
+
const { r } = setup(null);
|
|
151
|
+
await cut(r, "v1");
|
|
152
|
+
const refused = refusal(await preparePublish({ cwd: r.dir, ref: "v1", env: WORKSTATION }));
|
|
153
|
+
expect(refused?.code).toBe("release-unsigned");
|
|
154
|
+
expect(refused?.message).toBe("v1 has no signed cut, and chant release publish publishes only a release signed by a key in .chant/allowed_signers");
|
|
155
|
+
expect(refused?.fix).toContain("chant release cut v1 --sign --yes");
|
|
156
|
+
expect(refused?.fix).toContain("--allow-unsigned");
|
|
157
|
+
|
|
158
|
+
const allowed = (await preparePublish({ cwd: r.dir, ref: "v1", env: WORKSTATION, allowUnsigned: true })) as PublishPlan;
|
|
159
|
+
expect(refusal(allowed)).toBeNull();
|
|
160
|
+
expect(allowed.signatureWarning).toBe("v1 has no signed cut; publishing it anyway under --allow-unsigned, so chant release verify v1 will fail for it");
|
|
161
|
+
expect(allowed.cut).toMatchObject({ actor: "alice", label: "v1", signature: null });
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test.skipIf(!hasSshKeygen)("with no release gate, publishes a release signed by a listed key and refuses one signed by a key the signers file does not list", async () => {
|
|
165
|
+
const { r, alice, bob } = setup(null, { signers: true });
|
|
166
|
+
await cut(r, "v1", alice);
|
|
167
|
+
const signed = (await preparePublish({ cwd: r.dir, ref: "v1", env: WORKSTATION })) as PublishPlan;
|
|
168
|
+
expect(refusal(signed)).toBeNull();
|
|
169
|
+
expect(signed.signature).toMatchObject({ status: "verified", actor: "alice" });
|
|
170
|
+
expect(signed.signatureWarning).toBeNull();
|
|
171
|
+
expect(signed.cut?.signature).toMatch(/^-----BEGIN SSH SIGNATURE-----/);
|
|
172
|
+
|
|
173
|
+
// bob signs in his own name, and the signers file lists only alice.
|
|
174
|
+
r.write("later.txt", "x\n");
|
|
175
|
+
const later = r.commit("later");
|
|
176
|
+
r.git(["tag", `ci/green/${later}`]);
|
|
177
|
+
await cut(r, "v2", bob, "bob");
|
|
178
|
+
const unlisted = refusal(await preparePublish({ cwd: r.dir, ref: "v2", env: WORKSTATION }));
|
|
179
|
+
expect(unlisted?.code).toBe("release-signature-unverified");
|
|
180
|
+
expect(unlisted?.message).toMatch(/bob has no key in \.chant\/allowed_signers at base/);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("--require-provenance refuses a run with no OIDC token and passes one in CI", async () => {
|
|
184
|
+
const { r } = setup(null);
|
|
185
|
+
await cut(r, "v1");
|
|
186
|
+
const refused = refusal(await preparePublish({ cwd: r.dir, ref: "v1", env: WORKSTATION, allowUnsigned: true, requireProvenance: true }));
|
|
187
|
+
expect(refused?.code).toBe("publish-provenance-missing");
|
|
188
|
+
expect(refused?.message).toMatch(/a workstation has no OIDC token/);
|
|
189
|
+
const ci = { GITHUB_ACTIONS: "true", ACTIONS_ID_TOKEN_REQUEST_URL: "https://x", ACTIONS_ID_TOKEN_REQUEST_TOKEN: "t" };
|
|
190
|
+
expect(refusal(await preparePublish({ cwd: r.dir, ref: "v1", env: ci, allowUnsigned: true, requireProvenance: true }))).toBeNull();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("reads the targets release.publish declares, and with one refuses a checkout that is not the release's commit or has changes", async () => {
|
|
194
|
+
const { r } = setup(null);
|
|
195
|
+
r.write("chant.config.ts", `export default { environments: ["dev"], release: { publish: [{ type: "npm", packages: ["pkg"] }, { type: "oci", image: "img", repository: "ghcr.io/acme/api" }] } };\n`);
|
|
196
|
+
const head = r.commit("declare targets");
|
|
197
|
+
r.git(["tag", `ci/green/${head}`]);
|
|
198
|
+
await cut(r, "v1");
|
|
199
|
+
const p = (await preparePublish({ cwd: r.dir, ref: "v1", env: WORKSTATION, allowUnsigned: true })) as PublishPlan;
|
|
200
|
+
expect(refusal(p)).toBeNull();
|
|
201
|
+
expect(p.targets.map((t) => [t.kind, t.name, t.destination])).toEqual([
|
|
202
|
+
["npm", "npm (1 package)", "npm the registry npm's config names"],
|
|
203
|
+
["oci", "oci ghcr.io/acme/api", "ghcr.io/acme/api"],
|
|
204
|
+
]);
|
|
205
|
+
expect(p.targets[1]!.credentialEnv).toEqual(["OCI_PASSWORD"]);
|
|
206
|
+
|
|
207
|
+
r.write("untracked.txt", "x\n");
|
|
208
|
+
expect(refusal(await preparePublish({ cwd: r.dir, ref: "v1", env: WORKSTATION, allowUnsigned: true }))?.code).toBe("release-worktree-dirty");
|
|
209
|
+
rmSync(`${r.dir}/untracked.txt`);
|
|
210
|
+
r.commit("later");
|
|
211
|
+
const moved = refusal(await preparePublish({ cwd: r.dir, ref: "v1", env: WORKSTATION, allowUnsigned: true }));
|
|
212
|
+
expect(moved?.code).toBe("release-head-mismatch");
|
|
213
|
+
expect(moved?.fix).toContain(`git checkout ${head}`);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("refuses a release.publish block that does not validate, before anything is read", async () => {
|
|
217
|
+
const { r } = setup(null);
|
|
218
|
+
await cut(r, "v1");
|
|
219
|
+
r.write("chant.config.ts", `export default { environments: ["dev"], release: { publish: [{ type: "npm", packages: [] }] } };\n`);
|
|
220
|
+
const refused = refusal(await preparePublish({ cwd: r.dir, ref: "v1", env: WORKSTATION, allowUnsigned: true }));
|
|
221
|
+
expect(refused?.code).toBe("publish-config-invalid");
|
|
222
|
+
expect(refused?.message).toMatch(/release\.publish\.0\.packages/);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|