@intentius/chant 0.124.28 → 0.124.30
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 +18 -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 +67 -8
- 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 +91 -10
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +73 -2
- package/dist/cli/registry.d.ts +32 -4
- 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 +49 -0
- package/dist/components/pr-pipeline.d.ts.map +1 -1
- package/dist/components/pr-pipeline.js +38 -4
- package/dist/components/pr-run.d.ts +20 -2
- package/dist/components/pr-run.d.ts.map +1 -1
- package/dist/components/pr-run.js +26 -13
- 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 +16 -0
- package/dist/components/wave-plan.d.ts.map +1 -1
- package/dist/components/wave-plan.js +36 -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-ledger.d.ts +8 -3
- package/dist/lifecycle/release-ledger.d.ts.map +1 -1
- package/dist/lifecycle/release-ledger.js +2 -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/lifecycle/run-ledger.d.ts +1 -1
- package/dist/lifecycle/run-ledger.d.ts.map +1 -1
- package/dist/lifecycle/run-ledger.js +9 -0
- package/dist/op/activities/activity-contracts.d.ts +1 -0
- package/dist/op/activities/activity-contracts.d.ts.map +1 -1
- package/dist/op/activities/activity-contracts.js +1 -0
- package/dist/op/activities/shell.d.ts +6 -0
- package/dist/op/activities/shell.d.ts.map +1 -1
- package/dist/op/activities/shell.js +1 -1
- package/dist/op/local-executor.d.ts +23 -1
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/local-executor.js +9 -3
- package/dist/op/runtime.d.ts +27 -0
- package/dist/op/runtime.d.ts.map +1 -1
- package/dist/op/runtime.js +20 -0
- package/dist/plan-summary.d.ts.map +1 -1
- package/dist/plan-summary.js +8 -2
- package/dist/pr-forge.d.ts +23 -0
- package/dist/pr-forge.d.ts.map +1 -1
- package/dist/pr-forge.js +44 -0
- package/dist/pr-loop.d.ts +17 -0
- package/dist/pr-loop.d.ts.map +1 -1
- package/dist/pr-loop.js +30 -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 +4 -3
- package/dist/run-event-stream.d.ts.map +1 -1
- package/dist/run-event-stream.js +7 -3
- package/dist/run-event.d.ts +34 -1
- package/dist/run-event.d.ts.map +1 -1
- package/dist/run-event.js +4 -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-forge.d.ts +2 -0
- package/dist/workspace/ci-green-forge.d.ts.map +1 -1
- package/dist/workspace/ci-green.d.ts +10 -0
- package/dist/workspace/ci-green.d.ts.map +1 -1
- package/dist/workspace/ci-green.js +14 -5
- package/dist/workspace/ci-run.d.ts +222 -0
- package/dist/workspace/ci-run.d.ts.map +1 -0
- package/dist/workspace/ci-run.js +464 -0
- package/dist/workspace/ci-serve.d.ts +214 -0
- package/dist/workspace/ci-serve.d.ts.map +1 -0
- package/dist/workspace/ci-serve.js +412 -0
- 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/pr-report.schema.json +5 -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 +24 -6
- package/dist/workspace/status-gates.d.ts +8 -0
- package/dist/workspace/status-gates.d.ts.map +1 -1
- package/dist/workspace/status-stewards.d.ts +2 -0
- package/dist/workspace/status-stewards.d.ts.map +1 -1
- package/dist/workspace/status-stewards.js +1 -0
- 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 +210 -2
- 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 +30 -0
- package/src/cli/handlers/build.ts +20 -4
- package/src/cli/handlers/fan-out.test.ts +262 -1
- package/src/cli/handlers/fan-out.ts +72 -8
- 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 +144 -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 +91 -10
- package/src/cli/parse-args.ts +54 -2
- package/src/cli/registry.ts +32 -4
- 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 +67 -4
- package/src/components/pr-run.test.ts +127 -0
- package/src/components/pr-run.ts +32 -12
- 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 +45 -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-ledger.ts +9 -3
- 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/lifecycle/run-ledger.ts +7 -1
- package/src/op/activities/activity-contracts.ts +1 -0
- package/src/op/activities/shell.ts +7 -1
- package/src/op/local-executor.ts +27 -4
- package/src/op/runtime.ts +42 -0
- package/src/plan-summary.test.ts +20 -0
- package/src/plan-summary.ts +8 -1
- package/src/pr-forge.test.ts +25 -0
- package/src/pr-forge.ts +62 -0
- package/src/pr-loop.test.ts +13 -1
- package/src/pr-loop.ts +41 -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 +8 -4
- package/src/run-event.ts +38 -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-forge.ts +2 -0
- package/src/workspace/ci-green.ts +20 -5
- package/src/workspace/ci-run.test.ts +416 -0
- package/src/workspace/ci-run.ts +571 -0
- package/src/workspace/ci-serve.test.ts +268 -0
- package/src/workspace/ci-serve.ts +521 -0
- 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/pr-report.schema.json +5 -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 +24 -6
- 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-stewards.ts +3 -0
- package/src/workspace/status.schema.json +210 -2
- package/src/workspace/status.ts +254 -3
- package/src/workspace/wip.ts +11 -2
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Records written before the release model read under it (#4034, phase 2 of
|
|
3
|
+
* behold#678).
|
|
4
|
+
*
|
|
5
|
+
* `__fixtures__/older-records/older-records.json` holds ledger lines in the
|
|
6
|
+
* shapes the producers wrote before #4020, #4043, #4045 and #4046 added their
|
|
7
|
+
* fields: deploy records from `components release`, `run --components`,
|
|
8
|
+
* `promote`, `rollback`, `redeploy`, SQL Yodeler and chud-studio's release Op;
|
|
9
|
+
* run records; gate facts and resolutions; a release plan; and a change-set
|
|
10
|
+
* document composed by the code of #4022. Each entry names the producer and
|
|
11
|
+
* the source its shape was taken from.
|
|
12
|
+
*
|
|
13
|
+
* The lines are written byte for byte to the `chant/lifecycle` branch of a
|
|
14
|
+
* temp repository, then read through the readers a tool uses: the release and
|
|
15
|
+
* gate ledger parsers, `chant workspace status`, `chant release list`,
|
|
16
|
+
* `joinRelease` and `chant change-set show`. For each field the newer model
|
|
17
|
+
* adds, the test asserts that it reads as absent, never as a value chant
|
|
18
|
+
* invented, and that no reader errors or skips a line. It also asserts that a
|
|
19
|
+
* read leaves the branch where it was, since ledger lines are immutable.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { execFileSync } from "node:child_process";
|
|
23
|
+
import { readFileSync } from "node:fs";
|
|
24
|
+
import { join } from "node:path";
|
|
25
|
+
import { afterAll, describe, expect, test } from "vitest";
|
|
26
|
+
import { cleanScratch, contract, declaration, git, repo, repoFixture } from "./__fixtures__/contract-repo";
|
|
27
|
+
import { changeSetDocumentDigest, composeChangeSet, verifyChangeSetDigest, type ChangeSetDocument } from "../change-set";
|
|
28
|
+
import { changeSetFile } from "../lifecycle/change-set-store";
|
|
29
|
+
import { parseGateLedger, type GateResolutionRecord, type PendingGateRecord } from "../lifecycle/gate-ledger";
|
|
30
|
+
import { joinRelease } from "../lifecycle/release-join";
|
|
31
|
+
import { readReleaseLedger, listReleaseEnvironments } from "../lifecycle/release-ledger";
|
|
32
|
+
import { parseReleaseLedger, readReleaseRecords } from "../lifecycle/release-read";
|
|
33
|
+
import { labelsByDigest, makeRelease, parseCuts, parseRelease, type Release } from "../lifecycle/release-store";
|
|
34
|
+
import { readReleasePlan } from "../lifecycle/plan-ledger";
|
|
35
|
+
import { readRunLedger } from "../lifecycle/run-ledger";
|
|
36
|
+
import { sortedJsonReplacer } from "../utils";
|
|
37
|
+
import { loadAjv2020 } from "./ajv2020";
|
|
38
|
+
import { changeSetShowDocument } from "./change-set-show";
|
|
39
|
+
import changeSetShowSchema from "./change-set-show.schema.json";
|
|
40
|
+
import { releasesDocument } from "./releases";
|
|
41
|
+
import releasesSchema from "./releases.schema.json";
|
|
42
|
+
import { formatStatus, workspaceStatus, type StatusDocument } from "./status";
|
|
43
|
+
import { gatesInLedger } from "./status-gates";
|
|
44
|
+
import statusSchema from "./status.schema.json";
|
|
45
|
+
|
|
46
|
+
afterAll(cleanScratch);
|
|
47
|
+
|
|
48
|
+
interface Entry {
|
|
49
|
+
producer: string;
|
|
50
|
+
source: string;
|
|
51
|
+
line: Record<string, unknown>;
|
|
52
|
+
}
|
|
53
|
+
const data = JSON.parse(readFileSync(join(import.meta.dirname, "__fixtures__", "older-records", "older-records.json"), "utf8")) as {
|
|
54
|
+
files: Record<string, Entry[]>;
|
|
55
|
+
plans: { digest: string; plan: Record<string, unknown> };
|
|
56
|
+
};
|
|
57
|
+
const changeSetText = readFileSync(join(import.meta.dirname, "__fixtures__", "older-records", "change-set-984534ea9.json"), "utf8");
|
|
58
|
+
const oldChangeSet = JSON.parse(changeSetText) as ChangeSetDocument;
|
|
59
|
+
|
|
60
|
+
/** A ledger file's text, one line per entry, written with sorted keys as chant writes them. */
|
|
61
|
+
const textOf = (path: string): string => data.files[path].map((e) => JSON.stringify(e.line, sortedJsonReplacer)).join("\n") + "\n";
|
|
62
|
+
const entriesOf = (path: string): Entry[] => data.files[path];
|
|
63
|
+
|
|
64
|
+
const releaseLedgers = ["staging", "prod", "fly"] as const;
|
|
65
|
+
const NOW = "2026-09-12T00:00:00.000Z";
|
|
66
|
+
const COMMIT = "2f22765d04931a078909145ca628d2264c852d7d";
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The release a later `chant release cut` made from the commit that three of
|
|
70
|
+
* the older deploys were built from. The implicit release of #4046 names its
|
|
71
|
+
* templates with a null component; the other target names one.
|
|
72
|
+
*/
|
|
73
|
+
const release: Release = makeRelease({
|
|
74
|
+
member: null,
|
|
75
|
+
commit: COMMIT,
|
|
76
|
+
artifactDigests: [{ component: "api", kind: "image", path: "api.tar", digest: "sha256:47e0cbb98c01f4a49e3450e8686ca069f23c3c13c2d49cf50a4c3513f01b6ed3" }],
|
|
77
|
+
targets: [
|
|
78
|
+
{ env: "staging", templates: [{ component: null, path: "src/dist/ops/deploy/op.json", digest: `sha256:${"1".repeat(64)}` }], params: [] },
|
|
79
|
+
{ env: "prod", templates: [{ component: "api", path: "api/template.json", digest: `sha256:${"2".repeat(64)}` }], params: [] },
|
|
80
|
+
],
|
|
81
|
+
});
|
|
82
|
+
const cutLine = JSON.stringify({ version: 1, releaseDigest: release.releaseDigest, label: "v1.0.0", actor: "alice", timestamp: "2026-10-16T09:00:00.000Z", runOrigin: null, signature: null }, sortedJsonReplacer);
|
|
83
|
+
|
|
84
|
+
const stem = (digest: string): string => digest.replace(/:/g, "_");
|
|
85
|
+
|
|
86
|
+
/** Write `files` as the whole tree of a `chant/lifecycle` commit without touching the working tree or index. */
|
|
87
|
+
function lifecycle(root: string, files: Record<string, string>): string {
|
|
88
|
+
const index = join(root, ".git", "older-records-index");
|
|
89
|
+
const env = { ...process.env, GIT_INDEX_FILE: index };
|
|
90
|
+
const exec = (args: string[], input?: string): string =>
|
|
91
|
+
execFileSync("git", ["-c", "user.name=t", "-c", "user.email=t@example.com", "-c", "commit.gpgsign=false", ...args], {
|
|
92
|
+
cwd: root,
|
|
93
|
+
encoding: "utf-8",
|
|
94
|
+
input,
|
|
95
|
+
env,
|
|
96
|
+
stdio: [input === undefined ? "ignore" : "pipe", "pipe", "pipe"],
|
|
97
|
+
}).trim();
|
|
98
|
+
for (const [path, text] of Object.entries(files)) {
|
|
99
|
+
const blob = exec(["hash-object", "-w", "--stdin"], text);
|
|
100
|
+
exec(["update-index", "--add", "--cacheinfo", `100644,${blob},${path}`]);
|
|
101
|
+
}
|
|
102
|
+
const tree = exec(["write-tree"]);
|
|
103
|
+
const commit = exec(["commit-tree", tree, "-m", "ledger"]);
|
|
104
|
+
git(root, "update-ref", "refs/heads/chant/lifecycle", commit);
|
|
105
|
+
return commit;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The tree an older chant left on chant/lifecycle, plus one release cut afterwards. */
|
|
109
|
+
const tree = (): Record<string, string> => ({
|
|
110
|
+
...Object.fromEntries(Object.keys(data.files).map((path) => [path, textOf(path)])),
|
|
111
|
+
[`_plans/${stem(data.plans.digest)}.json`]: JSON.stringify(data.plans.plan, sortedJsonReplacer) + "\n",
|
|
112
|
+
[`_change-sets/${changeSetFile(oldChangeSet.digest)}`]: changeSetText,
|
|
113
|
+
[`_releases/${stem(release.releaseDigest)}.json`]: JSON.stringify(release, sortedJsonReplacer) + "\n",
|
|
114
|
+
"_releases/cuts.jsonl": cutLine + "\n",
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const workspace = repoFixture(() => {
|
|
118
|
+
const root = repo({
|
|
119
|
+
"chant.workspace.json": declaration([
|
|
120
|
+
{ name: "site", dir: ".", kind: "other", because: "the root project" },
|
|
121
|
+
{ name: "web", dir: "apps/web", kind: "chant" },
|
|
122
|
+
]),
|
|
123
|
+
"apps/web/chant.config.ts": "",
|
|
124
|
+
}, true);
|
|
125
|
+
lifecycle(root, tree());
|
|
126
|
+
return root;
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const tip = (root: string): string => git(root, "rev-parse", "chant/lifecycle");
|
|
130
|
+
const lifecycleFile = (root: string, path: string): string => git(root, "show", `chant/lifecycle:${path}`) + "\n";
|
|
131
|
+
const doc = (d: StatusDocument): Extract<StatusDocument, { members: unknown }> => {
|
|
132
|
+
if ("error" in d) throw new Error(`${d.error.code}: ${d.error.message}`);
|
|
133
|
+
return d;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/** The fields the newer model adds to a deploy record. */
|
|
137
|
+
const NEW_DEPLOY_FIELDS = ["releaseDigest", "admission"] as const;
|
|
138
|
+
/** The fields it adds to a gate fact or resolution. `empty` and `releaseDigest` are #4045's; `gitSha` and `runId` are #4020's. */
|
|
139
|
+
const NEW_GATE_FIELDS = ["gitSha", "runId", "releaseDigest", "empty", "seal", "carriedFrom", "checkpointDigest"] as const;
|
|
140
|
+
/** The fields it adds to a run record (#4020). */
|
|
141
|
+
const NEW_RUN_FIELDS = ["gitSha", "runOrigin"] as const;
|
|
142
|
+
|
|
143
|
+
describe("deploy records from every producer", () => {
|
|
144
|
+
test("parse with no line skipped, byte for byte as written, and carry no field the release model adds", () => {
|
|
145
|
+
for (const path of ["staging/releases.jsonl", "prod/releases.jsonl", "fly/releases.jsonl"]) {
|
|
146
|
+
const { records, malformed } = parseReleaseLedger(textOf(path).split("\n").filter(Boolean));
|
|
147
|
+
expect(malformed, path).toBe(0);
|
|
148
|
+
expect(records, path).toEqual(entriesOf(path).map((e) => e.line));
|
|
149
|
+
for (const record of records) {
|
|
150
|
+
for (const field of NEW_DEPLOY_FIELDS) expect(Object.keys(record), `${path} ${record.component}`).not.toContain(field);
|
|
151
|
+
expect(Object.keys(record)).not.toContain("flags");
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("a record names the producers the fixture claims, so each producer named in #4034 is covered", () => {
|
|
157
|
+
const producers = [...releaseLedgers.flatMap((e) => entriesOf(`${e}/releases.jsonl`))].map((e) => e.producer).join("\n");
|
|
158
|
+
for (const name of ["components release", "run --components", "components promote", "components rollback", "components redeploy", "SQL Yodeler", "chud-studio"]) {
|
|
159
|
+
expect(producers, name).toContain(name);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("read from the branch, the older fields still read as they were written", async () => {
|
|
164
|
+
const root = workspace();
|
|
165
|
+
const before = tip(root);
|
|
166
|
+
expect(await listReleaseEnvironments({ cwd: root })).toEqual(["fly", "prod", "staging"]);
|
|
167
|
+
const prod = await readReleaseLedger("prod", { cwd: root });
|
|
168
|
+
expect(prod.malformed).toBe(0);
|
|
169
|
+
const [first, promote, rollback, redeploy] = prod.records;
|
|
170
|
+
expect(first.runOrigin).toEqual({ forge: "github", repo: "acme/shop", url: "https://github.com/acme/shop/actions/runs/8399000111" });
|
|
171
|
+
expect(promote.promotedFrom).toEqual({ env: "staging", runId: "8431209934", timestamp: "2026-09-08T14:02:11.482Z" });
|
|
172
|
+
expect(rollback.restores?.runId).toBe("8399000111");
|
|
173
|
+
expect(redeploy.redeploys?.runId).toBe("local-1789074000000");
|
|
174
|
+
const staging = await readReleaseLedger("staging", { cwd: root });
|
|
175
|
+
expect(staging.records.find((r) => r.component === "ingress")?.inputDigest).toBe("sha256:bc227a21bd89c5c2e7e971e001e538da4dff21e972059a914b38595400418ca1");
|
|
176
|
+
expect(tip(root)).toBe(before);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("a port that reads the file text (a bundle without git) gives the same records", async () => {
|
|
180
|
+
const port = { read: async (env: string, file: string) => (env === "staging" && file === "releases.jsonl" ? textOf("staging/releases.jsonl") : null), list: async () => [], trees: async () => ["staging"] };
|
|
181
|
+
const { records, malformed } = await readReleaseRecords(port, "staging");
|
|
182
|
+
expect(malformed).toBe(0);
|
|
183
|
+
expect(records.map((r) => r.component)).toEqual(["api", "worker", "ingress", "schema"]);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe("gate facts and resolutions", () => {
|
|
188
|
+
test("parse with no line skipped, and none carries a field the newer model adds", () => {
|
|
189
|
+
for (const path of ["_gates/api.jsonl", "_gates/release.jsonl", "_gates/web.jsonl", "_gates/nightly.jsonl"]) {
|
|
190
|
+
const read = parseGateLedger(textOf(path));
|
|
191
|
+
expect(read.malformed, path).toBe(0);
|
|
192
|
+
expect(read.pending.length + read.resolutions.length, path).toBe(entriesOf(path).length);
|
|
193
|
+
for (const record of [...read.pending, ...read.resolutions]) {
|
|
194
|
+
for (const field of NEW_GATE_FIELDS) expect(Object.keys(record), `${path} ${field}`).not.toContain(field);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("a resolution with no kind reads as a resolution, and one with no plan stays plan-less", () => {
|
|
200
|
+
const nightly = parseGateLedger(textOf("_gates/nightly.jsonl"));
|
|
201
|
+
expect(nightly.resolutions).toHaveLength(1);
|
|
202
|
+
expect(nightly.resolutions[0].kind).toBeUndefined();
|
|
203
|
+
expect(nightly.resolutions[0].planDigest).toBeUndefined();
|
|
204
|
+
expect(nightly.pending[0].planDigest).toBeUndefined();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("an approval of a component gate before #4045 still counts for that gate, and does not count for a release gate", () => {
|
|
208
|
+
const api = textOf("_gates/api.jsonl");
|
|
209
|
+
const old = gatesInLedger("api", api, ["prod"], NOW);
|
|
210
|
+
expect(old.gates.map((g) => [g.name, g.env, g.state])).toEqual([["deploy", "prod", "approved"]]);
|
|
211
|
+
|
|
212
|
+
// A release gate's fact names the release; the older approval names none, so it is not an approval of this release.
|
|
213
|
+
const releaseFact: PendingGateRecord = { version: 1, kind: "pending", op: "release", gate: "ship", planDigest: "sha256:aa", releaseDigest: release.releaseDigest, environment: "prod", timestamp: "2026-10-16T10:00:00.000Z", expiresAt: "2026-10-23T10:00:00.000Z" };
|
|
214
|
+
const olderApproval: GateResolutionRecord = { version: 1, op: "release", gate: "ship", resolvedBy: "alex", timestamp: "2026-10-16T11:00:00.000Z", planDigest: "sha256:aa", environment: "prod" };
|
|
215
|
+
const content = [releaseFact, olderApproval].map((l) => JSON.stringify(l)).join("\n");
|
|
216
|
+
const gates = gatesInLedger("release", content, ["prod"], "2026-10-16T12:00:00.000Z").gates;
|
|
217
|
+
expect(gates).toHaveLength(1);
|
|
218
|
+
expect(gates[0].state).not.toBe("approved");
|
|
219
|
+
expect(gates[0].approvals).toEqual([]);
|
|
220
|
+
expect(gates[0].releaseDigest).toBe(release.releaseDigest);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe("run records and release plans", () => {
|
|
225
|
+
test("a run record reads with no gitSha and no runOrigin", async () => {
|
|
226
|
+
const root = workspace();
|
|
227
|
+
const { records, malformed } = await readRunLedger("fly", "release", { cwd: root });
|
|
228
|
+
expect(malformed).toBe(0);
|
|
229
|
+
expect(records.map((r) => r.status)).toEqual(["gated", "ok"]);
|
|
230
|
+
for (const record of records) for (const field of NEW_RUN_FIELDS) expect(Object.keys(record)).not.toContain(field);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("the plan chud-studio wrote reads by the digest its deploy record names", async () => {
|
|
234
|
+
const root = workspace();
|
|
235
|
+
const plan = await readReleasePlan(data.plans.digest, { cwd: root });
|
|
236
|
+
expect(plan).toEqual(data.plans.plan);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
describe("chant workspace status over the older ledgers", () => {
|
|
241
|
+
const { expectValid } = contract(statusSchema);
|
|
242
|
+
|
|
243
|
+
test("every deploy item reads releaseDigest, releaseLabel and admission as null, with the older fields as written", async () => {
|
|
244
|
+
const root = workspace();
|
|
245
|
+
const before = tip(root);
|
|
246
|
+
const status = doc(await workspaceStatus({ cwd: root, env: "prod", compareTo: "staging", now: NOW }));
|
|
247
|
+
expectValid(status);
|
|
248
|
+
const site = status.members.find((m) => m.name === "site")!;
|
|
249
|
+
const releases = site.environments.flatMap((e) => e.releases);
|
|
250
|
+
expect(releases.map((r) => `${r.component}`)).toEqual(["api", "schema/acme", "api", "ingress", "schema", "worker"]);
|
|
251
|
+
for (const r of releases) {
|
|
252
|
+
expect(r.releaseDigest, r.component).toBeNull();
|
|
253
|
+
expect(r.releaseLabel, r.component).toBeNull();
|
|
254
|
+
expect(r.admission, r.component).toBeNull();
|
|
255
|
+
}
|
|
256
|
+
const prodApi = site.environments.find((e) => e.env === "prod")!.releases.find((r) => r.component === "api")!;
|
|
257
|
+
expect(prodApi.approver).toBeNull();
|
|
258
|
+
expect(prodApi.digest).toBe("sha256:db88ccee4ed09c4324d896b8a0db6dab0091c76f579eab51e54656c40517866f");
|
|
259
|
+
const worker = site.environments.find((e) => e.env === "staging")!.releases.find((r) => r.component === "worker")!;
|
|
260
|
+
expect(worker.approver).toBe("bob");
|
|
261
|
+
expect(tip(root)).toBe(before);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("the release cut afterwards runs in no environment, because no older deploy names it", async () => {
|
|
265
|
+
const root = workspace();
|
|
266
|
+
const status = doc(await workspaceStatus({ cwd: root, env: "staging", now: NOW }));
|
|
267
|
+
expectValid(status);
|
|
268
|
+
const site = status.members.find((m) => m.name === "site")!;
|
|
269
|
+
expect(site.releaseCuts).toHaveLength(1);
|
|
270
|
+
expect(site.releaseCuts![0]).toMatchObject({ label: "v1.0.0", signature: null, commit: COMMIT });
|
|
271
|
+
expect(site.releaseCuts![0].signatureCheck.status).toBe("unsigned");
|
|
272
|
+
// The staging api deploy was built from the release's commit, and says nothing of the release.
|
|
273
|
+
expect(site.environments[0].releases.find((r) => r.component === "api")).toMatchObject({ gitSha: COMMIT, releaseDigest: null, releaseLabel: null });
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("older gates list with their state, and none shows gitSha, runId or releaseDigest", async () => {
|
|
277
|
+
const root = workspace();
|
|
278
|
+
const site = doc(await workspaceStatus({ cwd: root, env: "prod", compareTo: "staging", now: NOW })).members.find((m) => m.name === "site")!;
|
|
279
|
+
expect(site.gateLedger.malformed).toBe(0);
|
|
280
|
+
const byName = Object.fromEntries(site.gates.map((g) => [`${g.component}/${g.name}`, g]));
|
|
281
|
+
expect(Object.keys(byName).sort()).toEqual(["api/deploy", "nightly/confirm", "release/ship", "web/pr-apply"]);
|
|
282
|
+
expect(byName["api/deploy"]).toMatchObject({ env: "prod", state: "approved" });
|
|
283
|
+
expect(byName["release/ship"]).toMatchObject({ env: null, state: "approved" });
|
|
284
|
+
expect(byName["nightly/confirm"]).toMatchObject({ planDigest: null, state: "approved" });
|
|
285
|
+
expect(byName["web/pr-apply"].state).toBe("pending");
|
|
286
|
+
for (const gate of site.gates) {
|
|
287
|
+
for (const field of ["gitSha", "runId", "releaseDigest"]) expect(Object.keys(gate), `${gate.component} ${field}`).not.toContain(field);
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test("a gate gets changeSet only where a document is stored under its plan digest", async () => {
|
|
292
|
+
const root = workspace();
|
|
293
|
+
const site = doc(await workspaceStatus({ cwd: root, env: "prod", compareTo: "staging", now: NOW })).members.find((m) => m.name === "site")!;
|
|
294
|
+
const byName = Object.fromEntries(site.gates.map((g) => [`${g.component}/${g.name}`, g]));
|
|
295
|
+
expect(byName["web/pr-apply"].changeSet).toBe(oldChangeSet.digest);
|
|
296
|
+
for (const name of ["api/deploy", "release/ship", "nightly/confirm"]) expect(Object.keys(byName[name]), name).not.toContain("changeSet");
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test("the fly environment lists chud-studio's release with its plan, and no release label", async () => {
|
|
300
|
+
const root = workspace();
|
|
301
|
+
const status = doc(await workspaceStatus({ cwd: root, env: "fly", now: NOW }));
|
|
302
|
+
expectValid(status);
|
|
303
|
+
const fly = status.members.find((m) => m.name === "site")!.environments[0].releases;
|
|
304
|
+
expect(fly).toHaveLength(1);
|
|
305
|
+
expect(fly[0]).toMatchObject({ component: "app", approver: "alex", releaseDigest: null, releaseLabel: null, admission: null });
|
|
306
|
+
expect(fly[0].plan).toEqual(data.plans.plan);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test("the table prints without error and says nothing of the newer fields", async () => {
|
|
310
|
+
const root = workspace();
|
|
311
|
+
const out = formatStatus(doc(await workspaceStatus({ cwd: root, env: "prod", compareTo: "staging", now: NOW })));
|
|
312
|
+
expect(out).toContain("api");
|
|
313
|
+
expect(out).not.toContain("undefined");
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
describe("chant release list, joinRelease and chant change-set show over the older records", () => {
|
|
318
|
+
test("release list shows the release with no environment running it", async () => {
|
|
319
|
+
const root = workspace();
|
|
320
|
+
const before = tip(root);
|
|
321
|
+
const list = await releasesDocument({ cwd: root });
|
|
322
|
+
contract(releasesSchema).expectValid(list);
|
|
323
|
+
if ("error" in list) throw new Error(list.error.message);
|
|
324
|
+
expect(list.reason).toBeNull();
|
|
325
|
+
expect(list.releases).toHaveLength(1);
|
|
326
|
+
expect(list.releases[0]).toMatchObject({ releaseDigest: release.releaseDigest, label: "v1.0.0", commit: COMMIT, environments: [] });
|
|
327
|
+
expect(list.releases[0].cuts[0].signatureCheck.status).toBe("unsigned");
|
|
328
|
+
// The prod target names its component; the staging target is the implicit release of #4046, whose template has none.
|
|
329
|
+
expect(list.releases[0].targets.map((t) => t.templates[0].component)).toEqual(["api", null]);
|
|
330
|
+
expect(tip(root)).toBe(before);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
test("joining from the commit finds the release and no deploy, gate or approval", async () => {
|
|
334
|
+
const root = workspace();
|
|
335
|
+
const joined = await joinRelease({ commit: COMMIT }, { cwd: root });
|
|
336
|
+
expect(joined.releases).toHaveLength(1);
|
|
337
|
+
expect(joined.releases[0]).toMatchObject({ releaseDigest: release.releaseDigest, label: "v1.0.0", commit: COMMIT, deploys: [], gates: { pending: [], resolutions: [] } });
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test("joining from an older deploy record, with or without an admission, says it names no release", async () => {
|
|
341
|
+
const root = workspace();
|
|
342
|
+
const { records } = await readReleaseLedger("staging", { cwd: root });
|
|
343
|
+
for (const deploy of records) {
|
|
344
|
+
const joined = await joinRelease({ deploy }, { cwd: root });
|
|
345
|
+
expect(joined.releases, deploy.component).toEqual([]);
|
|
346
|
+
expect(joined.reason, deploy.component).toMatch(/names no release/);
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
test("joining from an older gate fact or resolution says it binds no release", async () => {
|
|
351
|
+
const root = workspace();
|
|
352
|
+
const { pending, resolutions } = parseGateLedger(lifecycleFile(root, "_gates/release.jsonl"));
|
|
353
|
+
for (const gate of [pending[0], resolutions[0]]) {
|
|
354
|
+
const joined = await joinRelease({ gate }, { cwd: root });
|
|
355
|
+
expect(joined.releases).toEqual([]);
|
|
356
|
+
expect(joined.reason).toMatch(/binds no release/);
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
test("change-set show reads a document composed before #4045, which has no releaseDigest and no environment", async () => {
|
|
361
|
+
const root = workspace();
|
|
362
|
+
const shown = await changeSetShowDocument({ cwd: root, digest: oldChangeSet.digest });
|
|
363
|
+
const Ajv = loadAjv2020();
|
|
364
|
+
const validate = new Ajv({ strict: false, allErrors: true }).compile(changeSetShowSchema) as ((d: unknown) => boolean) & { errors?: unknown };
|
|
365
|
+
expect(validate(shown), JSON.stringify(validate.errors)).toBe(true);
|
|
366
|
+
if ("error" in shown) throw new Error(shown.error.message);
|
|
367
|
+
expect(shown.reason).toBeNull();
|
|
368
|
+
expect(shown.changeSet).toEqual(oldChangeSet);
|
|
369
|
+
expect(Object.keys(shown.changeSet!)).not.toContain("releaseDigest");
|
|
370
|
+
expect(Object.keys(shown.changeSet!)).not.toContain("environment");
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
test("the older document still hashes to its digest, and composing the same parts today gives the same digest", () => {
|
|
374
|
+
expect(verifyChangeSetDigest(oldChangeSet)).toBe(true);
|
|
375
|
+
expect(changeSetDocumentDigest(oldChangeSet)).toBe(oldChangeSet.digest);
|
|
376
|
+
const again = composeChangeSet([]);
|
|
377
|
+
expect(Object.keys(again)).not.toContain("releaseDigest");
|
|
378
|
+
expect(Object.keys(again)).not.toContain("environment");
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
describe("cut lines and release files before the fields", () => {
|
|
383
|
+
test("a cut line with no signature key reads with signature null, and a cut with no label reads as unlabelled", () => {
|
|
384
|
+
const text = [
|
|
385
|
+
JSON.stringify({ version: 1, releaseDigest: release.releaseDigest, label: "v0.9.0", actor: "alice", timestamp: "2026-10-15T09:00:00.000Z", runOrigin: null }),
|
|
386
|
+
JSON.stringify({ version: 1, releaseDigest: `sha256:${"9".repeat(64)}`, label: null, actor: "bob", timestamp: "2026-10-15T10:00:00.000Z", runOrigin: { forge: "local" } }),
|
|
387
|
+
].join("\n");
|
|
388
|
+
const { cuts, malformed } = parseCuts(text);
|
|
389
|
+
expect(malformed).toBe(0);
|
|
390
|
+
expect(cuts.map((c) => c.signature)).toEqual([null, null]);
|
|
391
|
+
expect(labelsByDigest(cuts).get(release.releaseDigest)).toBe("v0.9.0");
|
|
392
|
+
expect(labelsByDigest(cuts).has(`sha256:${"9".repeat(64)}`)).toBe(false);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
test("a release file reads whether its templates name a component or none", () => {
|
|
396
|
+
const parsed = parseRelease(JSON.stringify(release));
|
|
397
|
+
expect(parsed?.targets.map((t) => t.templates[0].component)).toEqual(["api", null]);
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
test("the fixture covers each of the ledgers the issue names", () => {
|
|
402
|
+
expect(Object.keys(data.files).sort()).toEqual([
|
|
403
|
+
"_gates/api.jsonl",
|
|
404
|
+
"_gates/nightly.jsonl",
|
|
405
|
+
"_gates/release.jsonl",
|
|
406
|
+
"_gates/web.jsonl",
|
|
407
|
+
"fly/releases.jsonl",
|
|
408
|
+
"fly/runs__release.jsonl",
|
|
409
|
+
"prod/releases.jsonl",
|
|
410
|
+
"staging/releases.jsonl",
|
|
411
|
+
]);
|
|
412
|
+
});
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Overlap between in-flight work items (#4025), in throwaway repositories
|
|
3
|
+
* built on the flag workspace (`__fixtures__/flag-workspace.ts`): its
|
|
4
|
+
* decision kind, work kind and member `shop`.
|
|
5
|
+
*
|
|
6
|
+
* - W-002 and W-003 implement d-002, which is only decided, so both are in
|
|
7
|
+
* flight. W-001 implements d-001, which is confirmed, so it is not.
|
|
8
|
+
* - Branch `a` changes line 1 of `shop/queue.ts` for W-002. Branch `b`
|
|
9
|
+
* changes line 20 of the same file for W-003, and exists only as the
|
|
10
|
+
* remote-tracking `origin/b`. Branch `c` changes it for W-001.
|
|
11
|
+
* - `m` merges `a` and `b`.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
16
|
+
import { changeSetDocumentDigest, composeChangeSet, lifecyclePlanPart, type ChangeSetDocument } from "../change-set";
|
|
17
|
+
import { readFileSync } from "node:fs";
|
|
18
|
+
import type { ChangeSet as LifecycleChangeSet } from "../lifecycle/change-set";
|
|
19
|
+
import { storeGateChangeSet } from "../lifecycle/gate-change-set";
|
|
20
|
+
import { appendPendingGate } from "../lifecycle/gate-ledger";
|
|
21
|
+
import { clearMemberLedgerCache } from "../lifecycle/member-ledger";
|
|
22
|
+
import { cleanScratch, contract, git, repo, writeFiles } from "./__fixtures__/contract-repo";
|
|
23
|
+
import { flagWorkspace, workItem } from "./__fixtures__/flag-workspace";
|
|
24
|
+
import { attributeEntry } from "./overlap";
|
|
25
|
+
import { workspaceStatus } from "./status";
|
|
26
|
+
import intentSchema from "./intent.schema.json";
|
|
27
|
+
import statusSchema from "./status.schema.json";
|
|
28
|
+
|
|
29
|
+
const plan = JSON.parse(readFileSync(join(import.meta.dirname, "..", "__fixtures__", "change-set", "lifecycle-plan.floci.json"), "utf8")) as LifecycleChangeSet;
|
|
30
|
+
const lines = (changed: Record<number, string>) => Array.from({ length: 20 }, (_, i) => changed[i + 1] ?? `line ${i + 1}`).join("\n") + "\n";
|
|
31
|
+
|
|
32
|
+
let root: string;
|
|
33
|
+
const sha: Record<string, string> = {};
|
|
34
|
+
|
|
35
|
+
function commitOn(branch: string, from: string, files: Record<string, string>, item: string): string {
|
|
36
|
+
git(root, "checkout", "-q", "-b", branch, from);
|
|
37
|
+
writeFiles(root, files);
|
|
38
|
+
git(root, "add", "-A");
|
|
39
|
+
git(root, "commit", "-q", "-m", `work on ${item}`, "--trailer", `Chant-Record: ${item}`);
|
|
40
|
+
return git(root, "rev-parse", "HEAD");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
beforeAll(() => {
|
|
44
|
+
root = flagWorkspace({ "work/W-003-the-queue.md": workItem("W-003", ["d-002"]), "shop/queue.ts": lines({}) });
|
|
45
|
+
git(root, "branch", "-M", "main");
|
|
46
|
+
sha.a = commitOn("a", "main", { "shop/queue.ts": lines({ 1: "a" }) }, "work:W-002");
|
|
47
|
+
sha.b = commitOn("b", "main", { "shop/queue.ts": lines({ 20: "b" }), "shop/other.ts": "b\n" }, "work:W-003");
|
|
48
|
+
sha.c = commitOn("c", "main", { "shop/queue.ts": lines({ 10: "c" }) }, "work:W-001");
|
|
49
|
+
// b exists only as a remote-tracking branch: pushed by someone else and fetched.
|
|
50
|
+
git(root, "update-ref", "refs/remotes/origin/b", sha.b);
|
|
51
|
+
git(root, "checkout", "-q", "main");
|
|
52
|
+
git(root, "branch", "-q", "-D", "b");
|
|
53
|
+
git(root, "checkout", "-q", "-b", "m", "a");
|
|
54
|
+
git(root, "merge", "-q", "--no-edit", "origin/b");
|
|
55
|
+
sha.m = git(root, "rev-parse", "HEAD");
|
|
56
|
+
git(root, "checkout", "-q", "main");
|
|
57
|
+
});
|
|
58
|
+
afterAll(cleanScratch);
|
|
59
|
+
|
|
60
|
+
const status = () => workspaceStatus({ cwd: root, env: "prod", readLedger: async () => ({ records: [], malformed: 0 }), readPromotionOrder: async () => [] });
|
|
61
|
+
|
|
62
|
+
describe("attributing a change-set entry to work items (#4025)", () => {
|
|
63
|
+
const commitsOf = new Map([
|
|
64
|
+
["work:W-002", ["a1"]],
|
|
65
|
+
["work:W-003", ["b1", "b2"]],
|
|
66
|
+
]);
|
|
67
|
+
const files = new Map([
|
|
68
|
+
["a1", ["shop/queue.ts"]],
|
|
69
|
+
["b1", ["shop/other.ts"]],
|
|
70
|
+
["b2", ["shop/queue.ts"]],
|
|
71
|
+
]);
|
|
72
|
+
|
|
73
|
+
test("goes to each item with a linked commit that changed the entry's source", () => {
|
|
74
|
+
expect(attributeEntry("shop/queue.ts", commitsOf, files, null)).toEqual(["work:W-002", "work:W-003"]);
|
|
75
|
+
expect(attributeEntry("shop/other.ts", commitsOf, files, null)).toEqual(["work:W-003"]);
|
|
76
|
+
expect(attributeEntry("shop/none.ts", commitsOf, files, null)).toEqual([]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("counts only commits in the history the change set was planned from", () => {
|
|
80
|
+
expect(attributeEntry("shop/queue.ts", commitsOf, files, new Set(["a1"]))).toEqual(["work:W-002"]);
|
|
81
|
+
expect(attributeEntry("shop/queue.ts", commitsOf, files, new Set(["b1"]))).toEqual([]);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("an entry with no source goes to no item", () => {
|
|
85
|
+
expect(attributeEntry(undefined, commitsOf, files, null)).toEqual([]);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("an entry's source, like its node, stays outside the change set's digest", () => {
|
|
89
|
+
const d = composeChangeSet([lifecyclePlanPart({ member: "queue", plan })]);
|
|
90
|
+
expect(changeSetDocumentDigest({ ...d, entries: d.entries.map((e) => ({ ...e, node: "x", source: "shop/queue.ts" })) })).toBe(d.digest);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe("workspace status --json overlap (#4025)", () => {
|
|
95
|
+
test("two in-flight work items touching one file are a pair; a confirmed item and a remote-only branch are handled", async () => {
|
|
96
|
+
const doc = await status();
|
|
97
|
+
contract(statusSchema).expectValid(doc);
|
|
98
|
+
if ("error" in doc) throw new Error(JSON.stringify(doc));
|
|
99
|
+
expect(doc.overlap).not.toBeNull();
|
|
100
|
+
expect(doc.overlap!.items.map((i) => [i.item, i.state, i.commits])).toEqual([
|
|
101
|
+
["work:W-002", "in-progress", [sha.a]],
|
|
102
|
+
["work:W-003", "in-progress", [sha.b]],
|
|
103
|
+
]);
|
|
104
|
+
expect(doc.overlap!.items[1].path).toBe("work/W-003-the-queue.md");
|
|
105
|
+
// W-001's decision is confirmed, so its commit to the same file is not in flight.
|
|
106
|
+
expect(doc.overlap!.pairs).toEqual([{ items: ["work:W-002", "work:W-003"], files: ["shop/queue.ts"], addresses: [] }]);
|
|
107
|
+
expect(doc.overlap!.changeSets).toEqual([]);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("a pending gate's change set: the entry whose source both items changed is a shared address; one planned from a only is not", async () => {
|
|
111
|
+
clearMemberLedgerCache();
|
|
112
|
+
const shop = join(root, "shop");
|
|
113
|
+
const now = new Date().toISOString();
|
|
114
|
+
const later = new Date(Date.now() + 3_600_000).toISOString();
|
|
115
|
+
const store = async (d: ChangeSetDocument, at: string, op: string) => {
|
|
116
|
+
await storeGateChangeSet({ doc: d, cwd: shop, at, readGraph: async () => ({ ids: ["events", "jobs"], sources: { events: "queue.ts" } }) });
|
|
117
|
+
await appendPendingGate({ op, gate: "prod-release", planDigest: d.digest, environment: "prod", timestamp: now, expiresAt: later }, { cwd: shop });
|
|
118
|
+
};
|
|
119
|
+
const both = composeChangeSet([lifecyclePlanPart({ member: "queue", plan })]);
|
|
120
|
+
const onlyA = composeChangeSet([lifecyclePlanPart({ member: "queue-a", plan })]);
|
|
121
|
+
await store(both, sha.m, "shop");
|
|
122
|
+
await store(onlyA, sha.a, "shop-a");
|
|
123
|
+
|
|
124
|
+
const doc = await status();
|
|
125
|
+
contract(statusSchema).expectValid(doc);
|
|
126
|
+
if ("error" in doc) throw new Error(JSON.stringify(doc));
|
|
127
|
+
const gates = doc.members.find((m) => m.name === "shop")!.gates;
|
|
128
|
+
expect(gates.map((g) => g.changeSet).sort()).toEqual([both.digest, onlyA.digest].sort());
|
|
129
|
+
expect(doc.overlap!.changeSets).toEqual([both.digest, onlyA.digest].sort());
|
|
130
|
+
// Each change set has one entry with a source (events); the rest are unattributed.
|
|
131
|
+
expect(doc.overlap!.unattributed).toBe(both.entries.length - 1 + onlyA.entries.length - 1);
|
|
132
|
+
expect(doc.overlap!.pairs).toEqual([
|
|
133
|
+
{
|
|
134
|
+
items: ["work:W-002", "work:W-003"],
|
|
135
|
+
files: ["shop/queue.ts"],
|
|
136
|
+
addresses: [{ changeSet: both.digest, ledgerMember: "shop", member: "queue", address: "events", node: "shop/events", source: "shop/queue.ts" }],
|
|
137
|
+
},
|
|
138
|
+
]);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("the text view lists the pair", async () => {
|
|
142
|
+
const { formatStatus } = await import("./status");
|
|
143
|
+
const doc = await status();
|
|
144
|
+
if ("error" in doc) throw new Error(JSON.stringify(doc));
|
|
145
|
+
expect(formatStatus(doc)).toMatch(/work:W-002 and work:W-003\s+shop\/queue\.ts\s+shop\/events/);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("the intent graph document gives each work node the items it overlaps by file", async () => {
|
|
149
|
+
const { intentGraph } = await import("./intent");
|
|
150
|
+
const { doc } = await intentGraph({ cwd: root, region: "shop/queue.ts" });
|
|
151
|
+
contract(intentSchema).expectValid(doc);
|
|
152
|
+
if ("error" in doc) throw new Error(JSON.stringify(doc));
|
|
153
|
+
const work = Object.fromEntries(doc.nodes.filter((n) => n.kind === "work").map((n) => [(n as { record: string }).record, (n as { overlaps?: unknown }).overlaps]));
|
|
154
|
+
expect(work["W-002"]).toEqual([{ with: "work:W-003", files: ["shop/queue.ts"] }]);
|
|
155
|
+
expect(work["W-003"]).toEqual([{ with: "work:W-002", files: ["shop/queue.ts"] }]);
|
|
156
|
+
expect(work["W-001"]).toEqual([]);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("null when the workspace declares no work kind", async () => {
|
|
160
|
+
const plain = repo({ "chant.workspace.json": JSON.stringify({ name: "plain", schema: 1, members: [] }) }, true);
|
|
161
|
+
const doc = await workspaceStatus({ cwd: plain, env: "prod", readLedger: async () => ({ records: [], malformed: 0 }) });
|
|
162
|
+
if ("error" in doc) throw new Error(JSON.stringify(doc));
|
|
163
|
+
expect(doc.overlap).toBeNull();
|
|
164
|
+
});
|
|
165
|
+
});
|