@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,508 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Garbage collection of stored change sets, releases and run events by the
|
|
3
|
+
* retention policy (#4036). `chant lifecycle gc` runs it.
|
|
4
|
+
*
|
|
5
|
+
* What it prunes on `chant/lifecycle`, in every member's ledger at once:
|
|
6
|
+
*
|
|
7
|
+
* _change-sets/<digest>.json a stored change set (#4022)
|
|
8
|
+
* _releases/<digest>.json a release (#4026)
|
|
9
|
+
* _releases/templates/<digest>.txt template bytes no kept release names
|
|
10
|
+
*
|
|
11
|
+
* A prune is one new commit on the local branch that removes those files. It
|
|
12
|
+
* never rewrites or force-pushes history: other clones and tools read the
|
|
13
|
+
* branch, and every pruned file stays reachable in the commits before the
|
|
14
|
+
* prune. So a prune bounds the tree a reader walks at the tip, and the
|
|
15
|
+
* repository's history keeps growing by what is written. `git log -- <path>`
|
|
16
|
+
* on the branch still finds a pruned file.
|
|
17
|
+
*
|
|
18
|
+
* What it never touches: the ledgers. `_releases/cuts.jsonl`,
|
|
19
|
+
* `<env>/releases.jsonl`, `_gates/<op>.jsonl`, the run ledgers and every other
|
|
20
|
+
* `.jsonl` on the branch are append-only and are not edited (#4034), and a
|
|
21
|
+
* whole ledger file is not removed either: the latest deploy record of an
|
|
22
|
+
* environment, a gate's standing fact and a release's label all live in them,
|
|
23
|
+
* and they are small next to what they index (one line per fact). Nor does it
|
|
24
|
+
* touch snapshots, `_plans/`, `_builds/` or checkpoints, which have no policy
|
|
25
|
+
* yet. A cut line whose release was pruned stays, so its label keeps naming
|
|
26
|
+
* that digest and can't be given to another release.
|
|
27
|
+
*
|
|
28
|
+
* What is never pruned, whatever its age:
|
|
29
|
+
*
|
|
30
|
+
* 1. A release an environment runs: the release the latest deploy record of
|
|
31
|
+
* each component in each environment names, by its `releaseDigest` or the
|
|
32
|
+
* release gate resolution its admission names (`releaseOfDeploy`, #4043),
|
|
33
|
+
* and that release's templates. A promotion picks up what the step before
|
|
34
|
+
* runs (#4030), so this also keeps every release a promotion can take by
|
|
35
|
+
* default.
|
|
36
|
+
* 2. What a pending gate binds: the change set (`planDigest`) and release
|
|
37
|
+
* (`releaseDigest`) of every pending fact that has not expired.
|
|
38
|
+
* 3. A release still promotable: one whose commit has no `ci/revoked` tag and
|
|
39
|
+
* that was cut within `releases.days`, or is among the newest
|
|
40
|
+
* `releases.keep` releases of its member.
|
|
41
|
+
* 4. A change set a gate fact (an approval, an `empty` resolution or a
|
|
42
|
+
* pending fact) names together with a release that is kept.
|
|
43
|
+
*
|
|
44
|
+
* Anything else is pruned once it is older than its kind's window: a change
|
|
45
|
+
* set `changeSets.days` after it was stored or last named by a gate fact,
|
|
46
|
+
* whichever is later. A file that can't be read as a release is kept.
|
|
47
|
+
*
|
|
48
|
+
* Run events are local files under `<git-common-dir>/chant/run-events/`
|
|
49
|
+
* (`../run-event-store.ts`) and are deleted outright.
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
import { execFileSync } from "node:child_process";
|
|
53
|
+
import { RefCASConflictError, writeLedgerFiles, pushLifecycleReport, type LedgerFileRemoval } from "./git";
|
|
54
|
+
import { MEMBERS_DIR } from "./member-ledger";
|
|
55
|
+
import { CHANGE_SETS_DIR } from "./change-set-store";
|
|
56
|
+
import { CUTS_FILE, RELEASES_DIR, TEMPLATES_SUBDIR, parseCuts, parseRelease, type Release } from "./release-store";
|
|
57
|
+
import { parseGateLedger, type GateResolutionRecord, type PendingGateRecord } from "./gate-ledger";
|
|
58
|
+
import { parseReleaseLedger, releaseLedgerLines } from "./release-read";
|
|
59
|
+
import { latestPerComponent } from "./release-ledger";
|
|
60
|
+
import { releaseOfDeploy } from "./release-join";
|
|
61
|
+
import { RELEASE_GATE_OP } from "../op/gate-name";
|
|
62
|
+
import { planRunEventPrune, pruneRunEvents, type RunEventFile } from "../run-event-store";
|
|
63
|
+
import type { RetentionPolicy } from "./retention-policy";
|
|
64
|
+
|
|
65
|
+
const STATE_BRANCH = "chant/lifecycle";
|
|
66
|
+
const DAY_MS = 86_400_000;
|
|
67
|
+
|
|
68
|
+
/** What a pruned or kept file on the branch is. */
|
|
69
|
+
export type GcKind = "change-set" | "release" | "template";
|
|
70
|
+
|
|
71
|
+
/** Why a file is kept. Closed. */
|
|
72
|
+
export type GcKeepCode =
|
|
73
|
+
/** The latest deploy record of a component in an environment names the release. */
|
|
74
|
+
| "deployed"
|
|
75
|
+
/** A pending fact that has not expired binds it. */
|
|
76
|
+
| "pending-gate"
|
|
77
|
+
/** The release is among the newest `releases.keep` of its member and not revoked. */
|
|
78
|
+
| "newest"
|
|
79
|
+
/** It is within its kind's window (and, for a release, not revoked). */
|
|
80
|
+
| "within-window"
|
|
81
|
+
/** A gate fact names the change set with a release that is kept, or a kept release names the template. */
|
|
82
|
+
| "kept-release"
|
|
83
|
+
/** The file can't be read as what its directory holds, so nothing can be said about it. */
|
|
84
|
+
| "unreadable";
|
|
85
|
+
|
|
86
|
+
export interface GcKeepReason {
|
|
87
|
+
code: GcKeepCode;
|
|
88
|
+
/** The environment, gate or release the reason names, when it names one. */
|
|
89
|
+
detail?: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface GcFile {
|
|
93
|
+
kind: GcKind;
|
|
94
|
+
/** The member, its path of names joined with `/`, or null for the flat layout. */
|
|
95
|
+
member: string | null;
|
|
96
|
+
/** The full path on the branch. */
|
|
97
|
+
path: string;
|
|
98
|
+
digest: string;
|
|
99
|
+
bytes: number;
|
|
100
|
+
/** The blob sha at the tip read. */
|
|
101
|
+
sha: string;
|
|
102
|
+
/** When it was last written to the branch (the newest commit that added it), or null when history does not say. */
|
|
103
|
+
storedAt: string | null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface GcKept extends GcFile {
|
|
107
|
+
reasons: GcKeepReason[];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Files and bytes in the tip's tree. */
|
|
111
|
+
export interface TreeSize {
|
|
112
|
+
files: number;
|
|
113
|
+
bytes: number;
|
|
114
|
+
changeSets: { files: number; bytes: number };
|
|
115
|
+
releases: { files: number; bytes: number };
|
|
116
|
+
templates: { files: number; bytes: number };
|
|
117
|
+
/** Every `.jsonl` file: append-only, never pruned. */
|
|
118
|
+
ledgers: { files: number; bytes: number };
|
|
119
|
+
other: { files: number; bytes: number };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** What a prune of `chant/lifecycle` would do. */
|
|
123
|
+
export interface LifecycleGcPlan {
|
|
124
|
+
/** The branch tip read, or null when the checkout has no `chant/lifecycle`. */
|
|
125
|
+
tip: string | null;
|
|
126
|
+
remove: GcFile[];
|
|
127
|
+
keep: GcKept[];
|
|
128
|
+
before: TreeSize;
|
|
129
|
+
after: TreeSize;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export interface GcInput {
|
|
133
|
+
/** Any directory in the repository. The whole branch is read, every member at once. */
|
|
134
|
+
cwd: string;
|
|
135
|
+
policy: RetentionPolicy;
|
|
136
|
+
now: Date;
|
|
137
|
+
/** Full shas of commits with a `ci/revoked` tag: their releases are not promotable. */
|
|
138
|
+
revokedCommits?: ReadonlySet<string>;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
interface BranchFile {
|
|
142
|
+
path: string;
|
|
143
|
+
sha: string;
|
|
144
|
+
bytes: number;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function git(cwd: string, args: string[], input?: string): Buffer {
|
|
148
|
+
return execFileSync("git", args, { cwd, input, maxBuffer: 1024 * 1024 * 1024, stdio: ["pipe", "pipe", "pipe"] });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function branchTip(cwd: string): string | null {
|
|
152
|
+
try {
|
|
153
|
+
return git(cwd, ["rev-parse", "--verify", "--quiet", `refs/heads/${STATE_BRANCH}`]).toString().trim() || null;
|
|
154
|
+
} catch {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function listTree(cwd: string, tip: string): BranchFile[] {
|
|
160
|
+
const out: BranchFile[] = [];
|
|
161
|
+
for (const line of git(cwd, ["ls-tree", "-r", "-l", "--full-tree", tip]).toString().split("\n")) {
|
|
162
|
+
const m = line.match(/^\d+ blob ([0-9a-f]+)\s+(\d+)\t(.+)$/);
|
|
163
|
+
if (m) out.push({ sha: m[1], bytes: Number(m[2]), path: m[3] });
|
|
164
|
+
}
|
|
165
|
+
return out;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** The text of each blob, read in one `git cat-file --batch`. */
|
|
169
|
+
function readBlobs(cwd: string, shas: readonly string[]): Map<string, string> {
|
|
170
|
+
const out = new Map<string, string>();
|
|
171
|
+
const unique = [...new Set(shas)];
|
|
172
|
+
if (unique.length === 0) return out;
|
|
173
|
+
const buf = git(cwd, ["cat-file", "--batch"], `${unique.join("\n")}\n`);
|
|
174
|
+
let at = 0;
|
|
175
|
+
while (at < buf.length) {
|
|
176
|
+
const nl = buf.indexOf(0x0a, at);
|
|
177
|
+
if (nl < 0) break;
|
|
178
|
+
const header = buf.subarray(at, nl).toString().split(" ");
|
|
179
|
+
at = nl + 1;
|
|
180
|
+
if (header[1] === "missing" || header.length < 3) continue;
|
|
181
|
+
const size = Number(header[2]);
|
|
182
|
+
out.set(header[0], buf.subarray(at, at + size).toString("utf8"));
|
|
183
|
+
at += size + 1;
|
|
184
|
+
}
|
|
185
|
+
return out;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** When each change set and release file was last added on the branch, newest add first, in one `git log`. */
|
|
189
|
+
function storedTimes(cwd: string, tip: string): Map<string, string> {
|
|
190
|
+
const out = new Map<string, string>();
|
|
191
|
+
const text = git(cwd, [
|
|
192
|
+
"log", "--format=%x00%cI", "--name-only", "--diff-filter=A", tip, "--",
|
|
193
|
+
`:(top,glob)**/${CHANGE_SETS_DIR}/*.json`, `:(top,glob)**/${RELEASES_DIR}/*.json`,
|
|
194
|
+
]).toString();
|
|
195
|
+
for (const chunk of text.split("\0")) {
|
|
196
|
+
const lines = chunk.split("\n").filter(Boolean);
|
|
197
|
+
const when = lines.shift();
|
|
198
|
+
if (!when) continue;
|
|
199
|
+
for (const path of lines) if (!out.has(path)) out.set(path, when);
|
|
200
|
+
}
|
|
201
|
+
return out;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Split a branch path into the member prefix (`_members/<a>/_members/<b>/`) and the rest. */
|
|
205
|
+
export function splitLedgerPath(path: string): { prefix: string; member: string | null; rest: string } {
|
|
206
|
+
const segs = path.split("/");
|
|
207
|
+
const names: string[] = [];
|
|
208
|
+
let i = 0;
|
|
209
|
+
while (segs[i] === MEMBERS_DIR && i + 2 < segs.length) {
|
|
210
|
+
names.push(segs[i + 1]);
|
|
211
|
+
i += 2;
|
|
212
|
+
}
|
|
213
|
+
return { prefix: segs.slice(0, i).map((s) => `${s}/`).join(""), member: names.length > 0 ? names.join("/") : null, rest: segs.slice(i).join("/") };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* The key a digest is matched by: its hex. A plan digest written `sha256:<hex>`
|
|
218
|
+
* names the same plan as `jcs1-sha256:<hex>` (`samePlanDigest`), and a file
|
|
219
|
+
* name writes `:` as `_`, so both sides are reduced to the hex.
|
|
220
|
+
*/
|
|
221
|
+
export function digestKey(digestOrStem: string): string {
|
|
222
|
+
const hex = digestOrStem.match(/[0-9a-f]{64}$/);
|
|
223
|
+
return hex ? hex[0] : digestOrStem.replace(/:/g, "_");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const stemOf = (name: string, ext: string): string => name.slice(0, -ext.length);
|
|
227
|
+
const fileDigest = (name: string, ext: string): string => stemOf(name, ext).replace(/_/g, ":");
|
|
228
|
+
|
|
229
|
+
interface MemberLedger {
|
|
230
|
+
prefix: string;
|
|
231
|
+
member: string | null;
|
|
232
|
+
changeSets: Map<string, GcFile>;
|
|
233
|
+
releases: Map<string, GcFile>;
|
|
234
|
+
templates: Map<string, GcFile>;
|
|
235
|
+
cuts: BranchFile | null;
|
|
236
|
+
gates: BranchFile[];
|
|
237
|
+
deploys: Array<{ env: string; file: BranchFile }>;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function emptySize(): TreeSize {
|
|
241
|
+
const z = () => ({ files: 0, bytes: 0 });
|
|
242
|
+
return { files: 0, bytes: 0, changeSets: z(), releases: z(), templates: z(), ledgers: z(), other: z() };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function sizeOf(files: readonly BranchFile[], kinds: ReadonlyMap<string, GcKind>): TreeSize {
|
|
246
|
+
const s = emptySize();
|
|
247
|
+
for (const f of files) {
|
|
248
|
+
s.files++;
|
|
249
|
+
s.bytes += f.bytes;
|
|
250
|
+
const kind = kinds.get(f.path);
|
|
251
|
+
const slot = kind === "change-set" ? s.changeSets : kind === "release" ? s.releases : kind === "template" ? s.templates : f.path.endsWith(".jsonl") ? s.ledgers : s.other;
|
|
252
|
+
slot.files++;
|
|
253
|
+
slot.bytes += f.bytes;
|
|
254
|
+
}
|
|
255
|
+
return s;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Decide what a prune of `chant/lifecycle` removes and keeps, by the rules at
|
|
260
|
+
* the top of this file. Reads the branch at its local tip and writes nothing.
|
|
261
|
+
*/
|
|
262
|
+
export function planLifecycleGc(input: GcInput): LifecycleGcPlan {
|
|
263
|
+
const { cwd, policy, now } = input;
|
|
264
|
+
const revoked = input.revokedCommits ?? new Set<string>();
|
|
265
|
+
const tip = branchTip(cwd);
|
|
266
|
+
if (!tip) return { tip: null, remove: [], keep: [], before: emptySize(), after: emptySize() };
|
|
267
|
+
|
|
268
|
+
const files = listTree(cwd, tip);
|
|
269
|
+
const stored = storedTimes(cwd, tip);
|
|
270
|
+
const members = new Map<string, MemberLedger>();
|
|
271
|
+
const kinds = new Map<string, GcKind>();
|
|
272
|
+
const ledgerOf = (prefix: string, member: string | null): MemberLedger => {
|
|
273
|
+
let m = members.get(prefix);
|
|
274
|
+
if (!m) {
|
|
275
|
+
m = { prefix, member, changeSets: new Map(), releases: new Map(), templates: new Map(), cuts: null, gates: [], deploys: [] };
|
|
276
|
+
members.set(prefix, m);
|
|
277
|
+
}
|
|
278
|
+
return m;
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
for (const f of files) {
|
|
282
|
+
const { prefix, member, rest } = splitLedgerPath(f.path);
|
|
283
|
+
const segs = rest.split("/");
|
|
284
|
+
const name = segs.at(-1)!;
|
|
285
|
+
const entry = (kind: GcKind, digest: string): GcFile => {
|
|
286
|
+
kinds.set(f.path, kind);
|
|
287
|
+
return { kind, member, path: f.path, digest, bytes: f.bytes, sha: f.sha, storedAt: stored.get(f.path) ?? null };
|
|
288
|
+
};
|
|
289
|
+
if (segs.length === 2 && segs[0] === CHANGE_SETS_DIR && name.endsWith(".json")) {
|
|
290
|
+
const e = entry("change-set", fileDigest(name, ".json"));
|
|
291
|
+
ledgerOf(prefix, member).changeSets.set(digestKey(stemOf(name, ".json")), e);
|
|
292
|
+
} else if (segs.length === 2 && segs[0] === RELEASES_DIR && name.endsWith(".json")) {
|
|
293
|
+
const e = entry("release", fileDigest(name, ".json"));
|
|
294
|
+
ledgerOf(prefix, member).releases.set(digestKey(stemOf(name, ".json")), e);
|
|
295
|
+
} else if (segs.length === 3 && segs[0] === RELEASES_DIR && segs[1] === TEMPLATES_SUBDIR && name.endsWith(".txt")) {
|
|
296
|
+
const e = entry("template", fileDigest(name, ".txt"));
|
|
297
|
+
ledgerOf(prefix, member).templates.set(digestKey(stemOf(name, ".txt")), e);
|
|
298
|
+
} else if (segs.length === 2 && segs[0] === RELEASES_DIR && name === CUTS_FILE) {
|
|
299
|
+
ledgerOf(prefix, member).cuts = f;
|
|
300
|
+
} else if (segs.length === 2 && segs[0] === "_gates" && name.endsWith(".jsonl")) {
|
|
301
|
+
ledgerOf(prefix, member).gates.push(f);
|
|
302
|
+
} else if (segs.length === 2 && name === "releases.jsonl" && !segs[0].startsWith("_")) {
|
|
303
|
+
ledgerOf(prefix, member).deploys.push({ env: segs[0], file: f });
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const blobs = readBlobs(
|
|
308
|
+
cwd,
|
|
309
|
+
[...members.values()].flatMap((m) => [...m.releases.values()].map((r) => r.sha).concat(m.cuts ? [m.cuts.sha] : [], m.gates.map((g) => g.sha), m.deploys.map((d) => d.file.sha))),
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
const keep: GcKept[] = [];
|
|
313
|
+
const remove: GcFile[] = [];
|
|
314
|
+
const nowMs = now.getTime();
|
|
315
|
+
|
|
316
|
+
for (const m of members.values()) {
|
|
317
|
+
const reasons = new Map<GcFile, GcKeepReason[]>();
|
|
318
|
+
const keepFile = (f: GcFile | undefined, reason: GcKeepReason): void => {
|
|
319
|
+
if (!f) return;
|
|
320
|
+
reasons.set(f, [...(reasons.get(f) ?? []), reason]);
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
// The gate facts of every op in the member.
|
|
324
|
+
const pending: PendingGateRecord[] = [];
|
|
325
|
+
const resolutions: GateResolutionRecord[] = [];
|
|
326
|
+
let releaseGateResolutions: GateResolutionRecord[] = [];
|
|
327
|
+
for (const g of m.gates) {
|
|
328
|
+
const parsed = parseGateLedger(blobs.get(g.sha) ?? "");
|
|
329
|
+
pending.push(...parsed.pending);
|
|
330
|
+
resolutions.push(...parsed.resolutions);
|
|
331
|
+
if (g.path.endsWith(`/${RELEASE_GATE_OP}.jsonl`) || g.path === `_gates/${RELEASE_GATE_OP}.jsonl`) releaseGateResolutions = parsed.resolutions;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Releases, parsed.
|
|
335
|
+
const parsed = new Map<GcFile, Release | null>();
|
|
336
|
+
for (const r of m.releases.values()) {
|
|
337
|
+
const release = parseRelease(blobs.get(r.sha) ?? "");
|
|
338
|
+
parsed.set(r, release && digestKey(release.releaseDigest) === digestKey(r.digest) ? release : null);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Rule 1: what each environment runs.
|
|
342
|
+
for (const { env, file } of m.deploys) {
|
|
343
|
+
const { records } = parseReleaseLedger(releaseLedgerLines(blobs.get(file.sha) ?? null));
|
|
344
|
+
for (const record of latestPerComponent(records).values()) {
|
|
345
|
+
const digest = releaseOfDeploy(record, releaseGateResolutions);
|
|
346
|
+
if (digest !== undefined) keepFile(m.releases.get(digestKey(digest)), { code: "deployed", detail: `${env}/${record.component}` });
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Rule 2: what a pending gate binds, until its fact expires.
|
|
351
|
+
for (const p of pending) {
|
|
352
|
+
if (new Date(p.expiresAt).getTime() <= nowMs) continue;
|
|
353
|
+
const detail = `${p.op}/${p.gate}${p.environment ? ` in ${p.environment}` : ""}`;
|
|
354
|
+
if (p.planDigest) keepFile(m.changeSets.get(digestKey(p.planDigest)), { code: "pending-gate", detail });
|
|
355
|
+
if (p.releaseDigest) keepFile(m.releases.get(digestKey(p.releaseDigest)), { code: "pending-gate", detail });
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Rule 3: a release still promotable.
|
|
359
|
+
const { cuts } = parseCuts(m.cuts ? (blobs.get(m.cuts.sha) ?? null) : null);
|
|
360
|
+
const cutAt = new Map<string, string>();
|
|
361
|
+
for (const c of cuts) {
|
|
362
|
+
const k = digestKey(c.releaseDigest);
|
|
363
|
+
if (!cutAt.has(k) || c.timestamp > cutAt.get(k)!) cutAt.set(k, c.timestamp);
|
|
364
|
+
}
|
|
365
|
+
const releaseCutoff = nowMs - policy.releases.days * DAY_MS;
|
|
366
|
+
const promotable: Array<{ file: GcFile; at: number }> = [];
|
|
367
|
+
for (const [k, file] of m.releases) {
|
|
368
|
+
const release = parsed.get(file)!;
|
|
369
|
+
if (!release) {
|
|
370
|
+
keepFile(file, { code: "unreadable" });
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
if (revoked.has(release.commit)) continue;
|
|
374
|
+
const when = cutAt.get(k) ?? file.storedAt;
|
|
375
|
+
promotable.push({ file, at: when ? new Date(when).getTime() : Number.POSITIVE_INFINITY });
|
|
376
|
+
}
|
|
377
|
+
promotable.sort((a, b) => b.at - a.at || (a.file.path < b.file.path ? -1 : 1));
|
|
378
|
+
promotable.forEach(({ file, at }, i) => {
|
|
379
|
+
if (i < policy.releases.keep) keepFile(file, { code: "newest" });
|
|
380
|
+
if (at >= releaseCutoff) keepFile(file, { code: "within-window" });
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
const keptReleases = new Set([...m.releases.values()].filter((r) => reasons.has(r)));
|
|
384
|
+
const keptKeys = new Set([...keptReleases].map((r) => digestKey(r.digest)));
|
|
385
|
+
|
|
386
|
+
// Templates follow the releases that name them.
|
|
387
|
+
const unreadableKept = [...keptReleases].some((r) => parsed.get(r) === null);
|
|
388
|
+
for (const r of keptReleases) {
|
|
389
|
+
const release = parsed.get(r);
|
|
390
|
+
if (!release) continue;
|
|
391
|
+
for (const t of release.targets) for (const tpl of t.templates) keepFile(m.templates.get(digestKey(tpl.digest)), { code: "kept-release", detail: r.digest });
|
|
392
|
+
}
|
|
393
|
+
if (unreadableKept) for (const t of m.templates.values()) keepFile(t, { code: "unreadable", detail: "a release file of this member can't be read" });
|
|
394
|
+
|
|
395
|
+
// Rule 4, and the change-set window.
|
|
396
|
+
const lastNamed = new Map<string, number>();
|
|
397
|
+
for (const fact of [...pending, ...resolutions]) {
|
|
398
|
+
if (!fact.planDigest) continue;
|
|
399
|
+
const k = digestKey(fact.planDigest);
|
|
400
|
+
const t = new Date(fact.timestamp).getTime();
|
|
401
|
+
if (!lastNamed.has(k) || t > lastNamed.get(k)!) lastNamed.set(k, t);
|
|
402
|
+
if (fact.releaseDigest && keptKeys.has(digestKey(fact.releaseDigest))) keepFile(m.changeSets.get(k), { code: "kept-release", detail: fact.releaseDigest });
|
|
403
|
+
}
|
|
404
|
+
const changeSetCutoff = nowMs - policy.changeSets.days * DAY_MS;
|
|
405
|
+
for (const [k, file] of m.changeSets) {
|
|
406
|
+
const storedMs = file.storedAt ? new Date(file.storedAt).getTime() : Number.POSITIVE_INFINITY;
|
|
407
|
+
const last = Math.max(storedMs, lastNamed.get(k) ?? Number.NEGATIVE_INFINITY);
|
|
408
|
+
if (last >= changeSetCutoff) keepFile(file, { code: "within-window" });
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
for (const f of [...m.changeSets.values(), ...m.releases.values(), ...m.templates.values()]) {
|
|
412
|
+
const why = reasons.get(f);
|
|
413
|
+
if (why) keep.push({ ...f, reasons: dedupe(why) });
|
|
414
|
+
else remove.push(f);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const byPath = (a: GcFile, b: GcFile) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
419
|
+
remove.sort(byPath);
|
|
420
|
+
keep.sort(byPath);
|
|
421
|
+
const removed = new Set(remove.map((f) => f.path));
|
|
422
|
+
return { tip, remove, keep, before: sizeOf(files, kinds), after: sizeOf(files.filter((f) => !removed.has(f.path)), kinds) };
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function dedupe(reasons: GcKeepReason[]): GcKeepReason[] {
|
|
426
|
+
const seen = new Set<string>();
|
|
427
|
+
return reasons.filter((r) => {
|
|
428
|
+
const k = `${r.code}\0${r.detail ?? ""}`;
|
|
429
|
+
if (seen.has(k)) return false;
|
|
430
|
+
seen.add(k);
|
|
431
|
+
return true;
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/** The commit message of a prune. */
|
|
436
|
+
export function pruneMessage(remove: readonly GcFile[]): string {
|
|
437
|
+
const n = (kind: GcKind) => remove.filter((f) => f.kind === kind).length;
|
|
438
|
+
return `Retention: remove ${n("change-set")} change set(s), ${n("release")} release(s) and ${n("template")} template(s)`;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const PRUNE_ATTEMPTS = 5;
|
|
442
|
+
|
|
443
|
+
/** What {@link runLifecycleGc} did, or would do on a dry run. */
|
|
444
|
+
export interface GcReport {
|
|
445
|
+
now: string;
|
|
446
|
+
policy: RetentionPolicy;
|
|
447
|
+
dryRun: boolean;
|
|
448
|
+
lifecycle: LifecycleGcPlan & {
|
|
449
|
+
/** The prune commit, or null on a dry run or when nothing was removed. */
|
|
450
|
+
commit: string | null;
|
|
451
|
+
pushed: boolean;
|
|
452
|
+
pushWarning?: string;
|
|
453
|
+
};
|
|
454
|
+
runEvents: {
|
|
455
|
+
dir: string | null;
|
|
456
|
+
remove: RunEventFile[];
|
|
457
|
+
kept: { files: number; bytes: number };
|
|
458
|
+
/** How many files were deleted; 0 on a dry run. */
|
|
459
|
+
deleted: number;
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Plan and, unless `dryRun`, carry out a prune: one commit on the local
|
|
465
|
+
* `chant/lifecycle` that removes what the plan lets go, and the run event
|
|
466
|
+
* files it lets go deleted. The commit is planned against the tip it is
|
|
467
|
+
* written on: if the branch moved in between, the plan is made again. It is
|
|
468
|
+
* pushed only when `push` is set.
|
|
469
|
+
*/
|
|
470
|
+
export async function runLifecycleGc(input: GcInput & { dryRun: boolean; push?: boolean }): Promise<GcReport> {
|
|
471
|
+
let plan = planLifecycleGc(input);
|
|
472
|
+
let commit: string | null = null;
|
|
473
|
+
if (!input.dryRun && plan.tip && plan.remove.length > 0) {
|
|
474
|
+
for (let attempt = 1; ; attempt++) {
|
|
475
|
+
const removals: LedgerFileRemoval[] = plan.remove.map((f) => ({ path: f.path, remove: true, expectPriorSha: f.sha }));
|
|
476
|
+
try {
|
|
477
|
+
commit = await writeLedgerFiles(removals, pruneMessage(plan.remove), { cwd: input.cwd, expectTip: plan.tip! });
|
|
478
|
+
break;
|
|
479
|
+
} catch (err) {
|
|
480
|
+
if (!(err instanceof RefCASConflictError) || attempt >= PRUNE_ATTEMPTS) throw err;
|
|
481
|
+
plan = planLifecycleGc(input);
|
|
482
|
+
if (!plan.tip || plan.remove.length === 0) break;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
let pushed = false;
|
|
487
|
+
let pushWarning: string | undefined;
|
|
488
|
+
if (commit && input.push) {
|
|
489
|
+
const r = await pushLifecycleReport({ cwd: input.cwd });
|
|
490
|
+
pushed = r.pushed;
|
|
491
|
+
pushWarning = r.pushWarning;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const events = await planRunEventPrune(input.cwd, input.policy.runEvents, input.now);
|
|
495
|
+
const deleted = input.dryRun ? 0 : pruneRunEvents(events);
|
|
496
|
+
return {
|
|
497
|
+
now: input.now.toISOString(),
|
|
498
|
+
policy: input.policy,
|
|
499
|
+
dryRun: input.dryRun,
|
|
500
|
+
lifecycle: { ...plan, commit, pushed, ...(pushWarning ? { pushWarning } : {}) },
|
|
501
|
+
runEvents: {
|
|
502
|
+
dir: events.dir,
|
|
503
|
+
remove: events.remove,
|
|
504
|
+
kept: { files: events.keep.length, bytes: events.keep.reduce((n, f) => n + f.bytes, 0) },
|
|
505
|
+
deleted,
|
|
506
|
+
},
|
|
507
|
+
};
|
|
508
|
+
}
|
|
@@ -38,7 +38,7 @@ import { randomUUID } from "node:crypto";
|
|
|
38
38
|
import { sortedJsonReplacer } from "../utils";
|
|
39
39
|
import type { OpConfig } from "../op/types";
|
|
40
40
|
import type { StepRecord } from "../op/local-executor";
|
|
41
|
-
import type
|
|
41
|
+
import { opRunFailureOf, type OpRunRecord, type OpRunRecordInput, type OpRunPhaseRecord, type OpRunStepRecord } from "../op/runtime";
|
|
42
42
|
import { readBlobFromPath, readPathSha, readBlobBySha, writeBlobToPath, RefCASConflictError } from "./git";
|
|
43
43
|
|
|
44
44
|
/** Prefix folding the per-op run ledgers into one flat name under `<env>/`. See the module doc. */
|
|
@@ -215,6 +215,12 @@ export async function readRunLedger(
|
|
|
215
215
|
malformed++;
|
|
216
216
|
continue;
|
|
217
217
|
}
|
|
218
|
+
// A `failure` (#4094) that is not one is dropped and the line kept, as a gate line's wrong-typed `gitSha` is.
|
|
219
|
+
if ("failure" in parsed) {
|
|
220
|
+
const failure = opRunFailureOf(parsed.failure);
|
|
221
|
+
if (failure) parsed.failure = failure;
|
|
222
|
+
else delete parsed.failure;
|
|
223
|
+
}
|
|
218
224
|
records.push(parsed as OpRunRecord);
|
|
219
225
|
} catch {
|
|
220
226
|
malformed++;
|
|
@@ -54,6 +54,7 @@ export const shellCmdContract = activityContract(
|
|
|
54
54
|
cmd: z.string(),
|
|
55
55
|
env: z.record(z.string(), z.string()).optional(),
|
|
56
56
|
cwd: z.string().optional(),
|
|
57
|
+
inheritEnv: z.boolean().optional(),
|
|
57
58
|
okExit: z.array(z.number()).optional(),
|
|
58
59
|
gatedExit: z.number().optional(),
|
|
59
60
|
gate: z.strictObject({ op: z.string(), gate: z.string() }).optional(),
|
|
@@ -13,6 +13,12 @@ export interface ShellCmdArgs {
|
|
|
13
13
|
env?: Record<string, string>;
|
|
14
14
|
/** Working directory. Default: process.cwd(). */
|
|
15
15
|
cwd?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Whether the command sees this process's environment. Default true. With
|
|
18
|
+
* false it sees `env` alone, plus the in-flight record's variable, so a job
|
|
19
|
+
* the chant runner runs (`chant ci run`, #4094) gets a clean environment.
|
|
20
|
+
*/
|
|
21
|
+
inheritEnv?: boolean;
|
|
16
22
|
/**
|
|
17
23
|
* Exit codes that count as success. Default `[0]`.
|
|
18
24
|
*
|
|
@@ -129,7 +135,7 @@ export async function shellCmd(args: ShellCmdArgs, signal?: AbortSignal): Promis
|
|
|
129
135
|
cwd: args.cwd,
|
|
130
136
|
// In a run that keeps an in-flight record, CHANT_RUN_ACTIVITY names the
|
|
131
137
|
// file the command appends activity lines to (../run-live.ts).
|
|
132
|
-
env: { ...process.env, ...liveRunEnv(), ...args.env },
|
|
138
|
+
env: { ...(args.inheritEnv === false ? {} : process.env), ...liveRunEnv(), ...args.env },
|
|
133
139
|
maxBuffer: MAX_STDOUT_BYTES,
|
|
134
140
|
signal,
|
|
135
141
|
});
|
package/src/op/local-executor.ts
CHANGED
|
@@ -56,7 +56,7 @@ import { LiveRun, currentLiveRun, withLiveRun } from "./run-live";
|
|
|
56
56
|
import { runEnvOf } from "../lifecycle/run-ledger";
|
|
57
57
|
import { resolveCommitSha } from "../lifecycle/commit-sha";
|
|
58
58
|
import { resolveCiRun, type RunOrigin } from "../lifecycle/release-ledger";
|
|
59
|
-
import { runEventsOfStepRecord, type RunEventRuntime, type RunEventSkipReason } from "../run-event";
|
|
59
|
+
import { runEventsOfStepRecord, type RunEventKind, type RunEventRuntime, type RunEventSkipReason } from "../run-event";
|
|
60
60
|
import { openRunEventStore, teeLines } from "../run-event-store";
|
|
61
61
|
import { openRunEventStream, runEventRunOf, type RunEventSink, type RunEventStream } from "../run-event-stream";
|
|
62
62
|
import { describePlanDigest, samePlanDigest } from "../lifecycle/plan-digest";
|
|
@@ -1300,6 +1300,24 @@ export interface RunOpOptions {
|
|
|
1300
1300
|
* turn and `local` otherwise. Omitted, nothing is written.
|
|
1301
1301
|
*/
|
|
1302
1302
|
runEvents?: { write: (line: string) => void; runtime?: RunEventRuntime; runOrigin?: RunOrigin };
|
|
1303
|
+
/**
|
|
1304
|
+
* What ran the Op, when a runner says so itself (#4094): `chant ci run`
|
|
1305
|
+
* runs a pull-request job as an Op and passes `kind: "pipeline"`,
|
|
1306
|
+
* `runtime: "ci"` and `runOrigin: { forge: "chant" }`. The run's events
|
|
1307
|
+
* carry all three, kept on this host as well as written to
|
|
1308
|
+
* {@link runEvents}, and the run record carries `runOrigin` in place of
|
|
1309
|
+
* the CI run the environment names. Omitted, the run is an `op` run named
|
|
1310
|
+
* as described on {@link runEvents}.
|
|
1311
|
+
*/
|
|
1312
|
+
runner?: { kind: RunEventKind; runtime: RunEventRuntime; runOrigin: RunOrigin };
|
|
1313
|
+
/**
|
|
1314
|
+
* Says why a failed run failed (#4094), given the run's step records once
|
|
1315
|
+
* it has settled `fail`. What it returns is stored on the record as
|
|
1316
|
+
* `failure`, before the record is signed, and carried on the `run-settle`
|
|
1317
|
+
* event. `chant ci run` reads the job's pull-request report here, while
|
|
1318
|
+
* the checkout it ran in still exists.
|
|
1319
|
+
*/
|
|
1320
|
+
describeFailure?: (records: readonly StepRecord[]) => OpRunRecord["failure"];
|
|
1303
1321
|
/**
|
|
1304
1322
|
* Append the run's {@link OpRunRecord} to the run ledger
|
|
1305
1323
|
* (`../lifecycle/run-ledger.ts`) when the run settles (#2118). Opt-in on
|
|
@@ -1464,7 +1482,7 @@ async function runOpInTurn(
|
|
|
1464
1482
|
const stream = eventsWrite
|
|
1465
1483
|
? openRunEventStream(
|
|
1466
1484
|
runEventRunOf({
|
|
1467
|
-
kind: "op",
|
|
1485
|
+
kind: options.runner?.kind ?? "op",
|
|
1468
1486
|
name: config.name,
|
|
1469
1487
|
runId,
|
|
1470
1488
|
...(commit ? { gitSha: commit } : {}),
|
|
@@ -1472,6 +1490,7 @@ async function runOpInTurn(
|
|
|
1472
1490
|
steward: options.steward !== undefined,
|
|
1473
1491
|
...(options.runEvents?.runtime ? { runtime: options.runEvents.runtime } : {}),
|
|
1474
1492
|
...(options.runEvents?.runOrigin ? { runOrigin: options.runEvents.runOrigin } : {}),
|
|
1493
|
+
...(options.runner ? { runtime: options.runner.runtime, runOrigin: options.runner.runOrigin } : {}),
|
|
1475
1494
|
}),
|
|
1476
1495
|
eventsWrite,
|
|
1477
1496
|
)
|
|
@@ -1612,9 +1631,13 @@ async function runOpInTurn(
|
|
|
1612
1631
|
...(options.steward ? { steward: options.steward } : {}),
|
|
1613
1632
|
...(resumed ? { resumed } : {}),
|
|
1614
1633
|
...(commit ? { gitSha: commit } : {}),
|
|
1615
|
-
...(ciRun ? { runOrigin: ciRun.runOrigin } : {}),
|
|
1634
|
+
...(options.runner ? { runOrigin: options.runner.runOrigin } : ciRun ? { runOrigin: ciRun.runOrigin } : {}),
|
|
1616
1635
|
phaseDurations,
|
|
1617
1636
|
});
|
|
1637
|
+
if (status === "fail" && options.describeFailure) {
|
|
1638
|
+
const failure = options.describeFailure(settled);
|
|
1639
|
+
if (failure) input.failure = failure;
|
|
1640
|
+
}
|
|
1618
1641
|
let record: OpRunRecord = { version: 1, ...input, id: runId };
|
|
1619
1642
|
if (options.signRecord) {
|
|
1620
1643
|
try {
|
|
@@ -1627,7 +1650,7 @@ async function runOpInTurn(
|
|
|
1627
1650
|
}
|
|
1628
1651
|
// The stream settles as the run does, with the first failing step's error.
|
|
1629
1652
|
const firstError = status === "fail" ? settled.find((r) => r.status === "fail")?.error : undefined;
|
|
1630
|
-
stream?.settle(status === "fail" ? "failed" : status, firstError);
|
|
1653
|
+
stream?.settle(status === "fail" ? "failed" : status, firstError, input.failure);
|
|
1631
1654
|
if (!options.ledger) return record;
|
|
1632
1655
|
try {
|
|
1633
1656
|
return (await appendRunRecord(input, options.ledger)).record;
|