@intentius/chant 0.124.28 → 0.124.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-params.d.ts +34 -1
- package/dist/build-params.d.ts.map +1 -1
- package/dist/build-params.js +31 -1
- package/dist/change-set-graph.d.ts +8 -1
- package/dist/change-set-graph.d.ts.map +1 -1
- package/dist/change-set-graph.js +4 -2
- package/dist/change-set.d.ts +10 -0
- package/dist/change-set.d.ts.map +1 -1
- package/dist/change-set.js +4 -3
- package/dist/cli/build-params-cli.d.ts +7 -1
- package/dist/cli/build-params-cli.d.ts.map +1 -1
- package/dist/cli/build-params-cli.js +17 -8
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/build.js +6 -3
- package/dist/cli/handlers/approve.d.ts.map +1 -1
- package/dist/cli/handlers/approve.js +45 -0
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/build.js +15 -4
- package/dist/cli/handlers/fan-out.d.ts +9 -0
- package/dist/cli/handlers/fan-out.d.ts.map +1 -1
- package/dist/cli/handlers/fan-out.js +40 -3
- package/dist/cli/handlers/lifecycle-gc.d.ts +19 -0
- package/dist/cli/handlers/lifecycle-gc.d.ts.map +1 -0
- package/dist/cli/handlers/lifecycle-gc.js +143 -0
- package/dist/cli/handlers/pr.d.ts +11 -3
- package/dist/cli/handlers/pr.d.ts.map +1 -1
- package/dist/cli/handlers/pr.js +134 -26
- package/dist/cli/handlers/promote.d.ts +7 -0
- package/dist/cli/handlers/promote.d.ts.map +1 -1
- package/dist/cli/handlers/promote.js +210 -33
- package/dist/cli/handlers/release.d.ts +62 -5
- package/dist/cli/handlers/release.d.ts.map +1 -1
- package/dist/cli/handlers/release.js +414 -28
- package/dist/cli/handlers/run.d.ts +11 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/handlers/run.js +67 -6
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +61 -10
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +24 -2
- package/dist/cli/registry.d.ts +15 -3
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/cli/run-events.d.ts +3 -2
- package/dist/cli/run-events.d.ts.map +1 -1
- package/dist/cli/run-events.js +5 -3
- package/dist/components/auto-release.d.ts +2 -0
- package/dist/components/auto-release.d.ts.map +1 -1
- package/dist/components/auto-release.js +1 -0
- package/dist/components/capability.d.ts +9 -0
- package/dist/components/capability.d.ts.map +1 -1
- package/dist/components/driver.d.ts +8 -0
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/driver.js +35 -0
- package/dist/components/fan-out-inputs.d.ts +69 -0
- package/dist/components/fan-out-inputs.d.ts.map +1 -0
- package/dist/components/fan-out-inputs.js +169 -0
- package/dist/components/fan-out-record.d.ts +13 -0
- package/dist/components/fan-out-record.d.ts.map +1 -1
- package/dist/components/fan-out-record.js +16 -0
- package/dist/components/fan-out-run.d.ts.map +1 -1
- package/dist/components/fan-out-run.js +50 -0
- package/dist/components/fan-out.d.ts +4 -0
- package/dist/components/fan-out.d.ts.map +1 -1
- package/dist/components/pr-pipeline.d.ts +25 -0
- package/dist/components/pr-pipeline.d.ts.map +1 -1
- package/dist/components/pr-pipeline.js +22 -4
- package/dist/components/promote.d.ts +23 -0
- package/dist/components/promote.d.ts.map +1 -1
- package/dist/components/promote.js +60 -2
- package/dist/components/release-compile.d.ts +9 -0
- package/dist/components/release-compile.d.ts.map +1 -1
- package/dist/components/release-compile.js +9 -3
- package/dist/components/release-gate.d.ts +11 -1
- package/dist/components/release-gate.d.ts.map +1 -1
- package/dist/components/release-gate.js +10 -4
- package/dist/components/wave-plan.d.ts +5 -0
- package/dist/components/wave-plan.d.ts.map +1 -1
- package/dist/components/wave-plan.js +10 -2
- package/dist/config.d.ts +38 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -0
- package/dist/flag-params.d.ts +54 -0
- package/dist/flag-params.d.ts.map +1 -0
- package/dist/flag-params.js +130 -0
- package/dist/lifecycle/__fixtures__/publish-fakes.d.ts +120 -0
- package/dist/lifecycle/__fixtures__/publish-fakes.d.ts.map +1 -0
- package/dist/lifecycle/__fixtures__/publish-fakes.js +304 -0
- package/dist/lifecycle/change-set-store.d.ts +2 -1
- package/dist/lifecycle/change-set-store.d.ts.map +1 -1
- package/dist/lifecycle/change-set-store.js +2 -1
- package/dist/lifecycle/delivery-metrics.d.ts +139 -0
- package/dist/lifecycle/delivery-metrics.d.ts.map +1 -0
- package/dist/lifecycle/delivery-metrics.js +145 -0
- package/dist/lifecycle/deploy-outcome.d.ts +81 -0
- package/dist/lifecycle/deploy-outcome.d.ts.map +1 -0
- package/dist/lifecycle/deploy-outcome.js +101 -0
- package/dist/lifecycle/gate-change-set.d.ts +10 -1
- package/dist/lifecycle/gate-change-set.d.ts.map +1 -1
- package/dist/lifecycle/gate-change-set.js +35 -3
- package/dist/lifecycle/gate-ledger.d.ts +2 -3
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
- package/dist/lifecycle/git.d.ts +30 -1
- package/dist/lifecycle/git.d.ts.map +1 -1
- package/dist/lifecycle/git.js +31 -5
- package/dist/lifecycle/index.d.ts +4 -0
- package/dist/lifecycle/index.d.ts.map +1 -1
- package/dist/lifecycle/index.js +4 -0
- package/dist/lifecycle/mixed-environment.d.ts +95 -0
- package/dist/lifecycle/mixed-environment.d.ts.map +1 -0
- package/dist/lifecycle/mixed-environment.js +154 -0
- package/dist/lifecycle/publish-attachments.d.ts +79 -0
- package/dist/lifecycle/publish-attachments.d.ts.map +1 -0
- package/dist/lifecycle/publish-attachments.js +179 -0
- package/dist/lifecycle/publish-credentials.d.ts +37 -0
- package/dist/lifecycle/publish-credentials.d.ts.map +1 -0
- package/dist/lifecycle/publish-credentials.js +69 -0
- package/dist/lifecycle/publish-declaration.d.ts +97 -0
- package/dist/lifecycle/publish-declaration.d.ts.map +1 -0
- package/dist/lifecycle/publish-declaration.js +83 -0
- package/dist/lifecycle/publish-forge.d.ts +53 -0
- package/dist/lifecycle/publish-forge.d.ts.map +1 -0
- package/dist/lifecycle/publish-forge.js +298 -0
- package/dist/lifecycle/publish-npm.d.ts +61 -0
- package/dist/lifecycle/publish-npm.d.ts.map +1 -0
- package/dist/lifecycle/publish-npm.js +242 -0
- package/dist/lifecycle/publish-oci.d.ts +64 -0
- package/dist/lifecycle/publish-oci.d.ts.map +1 -0
- package/dist/lifecycle/publish-oci.js +383 -0
- package/dist/lifecycle/publish-record.d.ts +66 -0
- package/dist/lifecycle/publish-record.d.ts.map +1 -0
- package/dist/lifecycle/publish-record.js +84 -0
- package/dist/lifecycle/publish-targets.d.ts +45 -0
- package/dist/lifecycle/publish-targets.d.ts.map +1 -0
- package/dist/lifecycle/publish-targets.js +85 -0
- package/dist/lifecycle/release-cut.d.ts +48 -1
- package/dist/lifecycle/release-cut.d.ts.map +1 -1
- package/dist/lifecycle/release-cut.js +116 -23
- package/dist/lifecycle/release-join.d.ts +95 -0
- package/dist/lifecycle/release-join.d.ts.map +1 -0
- package/dist/lifecycle/release-join.js +139 -0
- package/dist/lifecycle/release-publish.d.ts +312 -0
- package/dist/lifecycle/release-publish.d.ts.map +1 -0
- package/dist/lifecycle/release-publish.js +335 -0
- package/dist/lifecycle/release-revocation.d.ts +80 -0
- package/dist/lifecycle/release-revocation.d.ts.map +1 -0
- package/dist/lifecycle/release-revocation.js +123 -0
- package/dist/lifecycle/release-store.d.ts +12 -2
- package/dist/lifecycle/release-store.d.ts.map +1 -1
- package/dist/lifecycle/release-store.js +4 -2
- package/dist/lifecycle/release-verify.d.ts +61 -0
- package/dist/lifecycle/release-verify.d.ts.map +1 -0
- package/dist/lifecycle/release-verify.js +92 -0
- package/dist/lifecycle/release-workstation.d.ts +91 -0
- package/dist/lifecycle/release-workstation.d.ts.map +1 -0
- package/dist/lifecycle/release-workstation.js +180 -0
- package/dist/lifecycle/retention-policy.d.ts +45 -0
- package/dist/lifecycle/retention-policy.d.ts.map +1 -0
- package/dist/lifecycle/retention-policy.js +43 -0
- package/dist/lifecycle/retention.d.ts +185 -0
- package/dist/lifecycle/retention.d.ts.map +1 -0
- package/dist/lifecycle/retention.js +400 -0
- package/dist/plan-summary.d.ts.map +1 -1
- package/dist/plan-summary.js +8 -2
- package/dist/pr-loop.d.ts +7 -0
- package/dist/pr-loop.d.ts.map +1 -1
- package/dist/pr-loop.js +23 -4
- package/dist/pr-report-url.d.ts +82 -0
- package/dist/pr-report-url.d.ts.map +1 -0
- package/dist/pr-report-url.js +174 -0
- package/dist/provenance.d.ts +2 -2
- package/dist/provenance.d.ts.map +1 -1
- package/dist/run-event-store.d.ts +29 -2
- package/dist/run-event-store.d.ts.map +1 -1
- package/dist/run-event-store.js +50 -3
- package/dist/run-event-stream.d.ts +2 -1
- package/dist/run-event-stream.d.ts.map +1 -1
- package/dist/run-event-stream.js +5 -1
- package/dist/run-event.d.ts +19 -1
- package/dist/run-event.d.ts.map +1 -1
- package/dist/run-event.js +2 -0
- package/dist/wave-runtime.mjs +260 -229
- package/dist/workspace/__fixtures__/flag-workspace.d.ts +31 -0
- package/dist/workspace/__fixtures__/flag-workspace.d.ts.map +1 -0
- package/dist/workspace/__fixtures__/flag-workspace.js +114 -0
- package/dist/workspace/change-set.schema.json +5 -1
- package/dist/workspace/check.schema.json +4 -2
- package/dist/workspace/checks/records.d.ts +9 -1
- package/dist/workspace/checks/records.d.ts.map +1 -1
- package/dist/workspace/checks/records.js +35 -1
- package/dist/workspace/checks.d.ts +1 -0
- package/dist/workspace/checks.d.ts.map +1 -1
- package/dist/workspace/checks.js +2 -1
- package/dist/workspace/ci-green.d.ts +8 -0
- package/dist/workspace/ci-green.d.ts.map +1 -1
- package/dist/workspace/ci-green.js +10 -3
- package/dist/workspace/declaration.d.ts +16 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.js +26 -0
- package/dist/workspace/declaration.schema.json +64 -0
- package/dist/workspace/declared-kinds.d.ts +16 -0
- package/dist/workspace/declared-kinds.d.ts.map +1 -1
- package/dist/workspace/declared-kinds.js +25 -0
- package/dist/workspace/delivery-metrics.d.ts +91 -0
- package/dist/workspace/delivery-metrics.d.ts.map +1 -0
- package/dist/workspace/delivery-metrics.js +217 -0
- package/dist/workspace/delivery-metrics.schema.json +519 -0
- package/dist/workspace/flags-read.d.ts +37 -0
- package/dist/workspace/flags-read.d.ts.map +1 -0
- package/dist/workspace/flags-read.js +103 -0
- package/dist/workspace/flags.d.ts +100 -0
- package/dist/workspace/flags.d.ts.map +1 -0
- package/dist/workspace/flags.js +98 -0
- package/dist/workspace/graph-cli.js +1 -1
- package/dist/workspace/intent-cli.d.ts +11 -2
- package/dist/workspace/intent-cli.d.ts.map +1 -1
- package/dist/workspace/intent-cli.js +54 -5
- package/dist/workspace/intent-trace.d.ts +219 -0
- package/dist/workspace/intent-trace.d.ts.map +1 -0
- package/dist/workspace/intent-trace.js +314 -0
- package/dist/workspace/intent.d.ts +30 -6
- package/dist/workspace/intent.d.ts.map +1 -1
- package/dist/workspace/intent.js +141 -21
- package/dist/workspace/intent.schema.json +702 -6
- package/dist/workspace/overlap.d.ts +146 -0
- package/dist/workspace/overlap.d.ts.map +1 -0
- package/dist/workspace/overlap.js +276 -0
- package/dist/workspace/reason-codes.d.ts +2 -0
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/reason-codes.js +3 -0
- package/dist/workspace/records.d.ts +15 -2
- package/dist/workspace/records.d.ts.map +1 -1
- package/dist/workspace/records.js +32 -0
- package/dist/workspace/records.schema.json +21 -2
- package/dist/workspace/reference-kinds/flag/flag.kind.mjs +32 -0
- package/dist/workspace/reference-kinds/flag/flag.schema.json +53 -0
- package/dist/workspace/releases.d.ts +9 -0
- package/dist/workspace/releases.d.ts.map +1 -1
- package/dist/workspace/releases.js +9 -0
- package/dist/workspace/releases.schema.json +123 -1
- package/dist/workspace/run-event.schema.json +10 -3
- package/dist/workspace/status-gates.d.ts +8 -0
- package/dist/workspace/status-gates.d.ts.map +1 -1
- package/dist/workspace/status.d.ts +78 -0
- package/dist/workspace/status.d.ts.map +1 -1
- package/dist/workspace/status.js +170 -3
- package/dist/workspace/status.schema.json +198 -1
- package/dist/workspace/wip.d.ts.map +1 -1
- package/dist/workspace/wip.js +11 -3
- package/package.json +1 -1
- package/src/build-params.test.ts +46 -0
- package/src/build-params.ts +55 -1
- package/src/change-set-graph.ts +10 -2
- package/src/change-set.ts +14 -3
- package/src/cli/build-params-cli.ts +22 -9
- package/src/cli/commands/build.ts +6 -3
- package/src/cli/handlers/approve-release.test.ts +5 -1
- package/src/cli/handlers/approve-revoked.test.ts +155 -0
- package/src/cli/handlers/approve.ts +47 -0
- package/src/cli/handlers/build.test.ts +15 -0
- package/src/cli/handlers/build.ts +17 -4
- package/src/cli/handlers/fan-out.test.ts +180 -1
- package/src/cli/handlers/fan-out.ts +38 -3
- package/src/cli/handlers/intent-trace.e2e.test.ts +201 -0
- package/src/cli/handlers/lifecycle-gc.test.ts +126 -0
- package/src/cli/handlers/lifecycle-gc.ts +146 -0
- package/src/cli/handlers/pr.test.ts +148 -1
- package/src/cli/handlers/pr.ts +139 -28
- package/src/cli/handlers/promote.test.ts +280 -2
- package/src/cli/handlers/promote.ts +210 -34
- package/src/cli/handlers/release-implicit.e2e.test.ts +12 -4
- package/src/cli/handlers/release-join.e2e.test.ts +185 -0
- package/src/cli/handlers/release-publish-targets.e2e.test.ts +295 -0
- package/src/cli/handlers/release-verify.e2e.test.ts +117 -0
- package/src/cli/handlers/release-workstation-ci.e2e.test.ts +135 -0
- package/src/cli/handlers/release.e2e.test.ts +146 -32
- package/src/cli/handlers/release.ts +415 -28
- package/src/cli/handlers/run-auto-release.test.ts +9 -0
- package/src/cli/handlers/run-release.test.ts +55 -0
- package/src/cli/handlers/run.ts +69 -5
- package/src/cli/main.test.ts +1 -0
- package/src/cli/main.ts +61 -10
- package/src/cli/parse-args.ts +19 -2
- package/src/cli/registry.ts +15 -3
- package/src/cli/run-events.ts +5 -2
- package/src/components/auto-release.test.ts +9 -0
- package/src/components/auto-release.ts +3 -0
- package/src/components/capability.ts +9 -0
- package/src/components/driver.test.ts +24 -0
- package/src/components/driver.ts +36 -0
- package/src/components/fan-out-inputs.test.ts +85 -0
- package/src/components/fan-out-inputs.ts +192 -0
- package/src/components/fan-out-record.test.ts +10 -0
- package/src/components/fan-out-record.ts +19 -0
- package/src/components/fan-out-run.ts +48 -0
- package/src/components/fan-out.ts +4 -0
- package/src/components/pr-pipeline.ts +35 -4
- package/src/components/promote.ts +68 -2
- package/src/components/release-compile.ts +17 -3
- package/src/components/release-gate.ts +18 -4
- package/src/components/wave-plan.ts +15 -2
- package/src/config.ts +8 -0
- package/src/flag-params.test.ts +144 -0
- package/src/flag-params.ts +134 -0
- package/src/lifecycle/__fixtures__/publish-fakes.ts +351 -0
- package/src/lifecycle/change-set-store.ts +2 -1
- package/src/lifecycle/delivery-metrics.ts +263 -0
- package/src/lifecycle/deploy-outcome.ts +147 -0
- package/src/lifecycle/gate-change-set.ts +37 -6
- package/src/lifecycle/gate-ledger.ts +2 -2
- package/src/lifecycle/git.ts +61 -8
- package/src/lifecycle/index.ts +4 -0
- package/src/lifecycle/mixed-environment.test.ts +97 -0
- package/src/lifecycle/mixed-environment.ts +204 -0
- package/src/lifecycle/publish-attachments.ts +196 -0
- package/src/lifecycle/publish-credentials.ts +84 -0
- package/src/lifecycle/publish-declaration.ts +95 -0
- package/src/lifecycle/publish-forge.ts +325 -0
- package/src/lifecycle/publish-npm.ts +259 -0
- package/src/lifecycle/publish-oci.ts +396 -0
- package/src/lifecycle/publish-record.ts +113 -0
- package/src/lifecycle/publish-resume.test.ts +168 -0
- package/src/lifecycle/publish-targets.test.ts +494 -0
- package/src/lifecycle/publish-targets.ts +100 -0
- package/src/lifecycle/release-cut.test.ts +74 -2
- package/src/lifecycle/release-cut.ts +158 -22
- package/src/lifecycle/release-flag.test.ts +112 -0
- package/src/lifecycle/release-join.test.ts +68 -0
- package/src/lifecycle/release-join.ts +182 -0
- package/src/lifecycle/release-publish.test.ts +225 -0
- package/src/lifecycle/release-publish.ts +568 -0
- package/src/lifecycle/release-revocation.ts +147 -0
- package/src/lifecycle/release-store.ts +15 -4
- package/src/lifecycle/release-verify.test.ts +110 -0
- package/src/lifecycle/release-verify.ts +120 -0
- package/src/lifecycle/release-workstation.test.ts +204 -0
- package/src/lifecycle/release-workstation.ts +216 -0
- package/src/lifecycle/retention-growth.e2e.test.ts +279 -0
- package/src/lifecycle/retention-policy.ts +63 -0
- package/src/lifecycle/retention.test.ts +363 -0
- package/src/lifecycle/retention.ts +508 -0
- package/src/plan-summary.test.ts +20 -0
- package/src/plan-summary.ts +8 -1
- package/src/pr-loop.ts +25 -4
- package/src/pr-report-url.test.ts +131 -0
- package/src/pr-report-url.ts +209 -0
- package/src/provenance.ts +2 -2
- package/src/run-event-store.ts +63 -3
- package/src/run-event-stream.ts +4 -1
- package/src/run-event.ts +21 -1
- package/src/workspace/__fixtures__/flag-workspace.ts +132 -0
- package/src/workspace/__fixtures__/older-records/change-set-984534ea9.json +100 -0
- package/src/workspace/__fixtures__/older-records/older-records.json +326 -0
- package/src/workspace/change-set.schema.json +5 -1
- package/src/workspace/check.schema.json +4 -2
- package/src/workspace/checks/records.ts +35 -1
- package/src/workspace/checks.ts +2 -1
- package/src/workspace/ci-green.ts +14 -3
- package/src/workspace/declaration.schema.json +64 -0
- package/src/workspace/declaration.test.ts +28 -0
- package/src/workspace/declaration.ts +38 -0
- package/src/workspace/declared-kinds.ts +40 -1
- package/src/workspace/delivery-metrics.schema.json +519 -0
- package/src/workspace/delivery-metrics.test.ts +236 -0
- package/src/workspace/delivery-metrics.ts +286 -0
- package/src/workspace/flags-read.ts +121 -0
- package/src/workspace/flags.test.ts +141 -0
- package/src/workspace/flags.ts +157 -0
- package/src/workspace/graph-cli.ts +1 -1
- package/src/workspace/intent-cli.ts +46 -5
- package/src/workspace/intent-trace.test.ts +173 -0
- package/src/workspace/intent-trace.ts +484 -0
- package/src/workspace/intent.schema.json +702 -6
- package/src/workspace/intent.ts +148 -25
- package/src/workspace/older-records.test.ts +412 -0
- package/src/workspace/overlap.test.ts +165 -0
- package/src/workspace/overlap.ts +340 -0
- package/src/workspace/read-contract.e2e.test.ts +2 -1
- package/src/workspace/reason-codes.test.ts +5 -0
- package/src/workspace/reason-codes.ts +3 -0
- package/src/workspace/record-assets.test.ts +2 -1
- package/src/workspace/records-contract.e2e.test.ts +2 -1
- package/src/workspace/records.schema.json +21 -2
- package/src/workspace/records.ts +36 -2
- package/src/workspace/reference-kinds/flag/flag.kind.mjs +32 -0
- package/src/workspace/reference-kinds/flag/flag.schema.json +53 -0
- package/src/workspace/releases.schema.json +123 -1
- package/src/workspace/releases.ts +11 -0
- package/src/workspace/run-event.schema.json +10 -3
- package/src/workspace/status-contract.test.ts +29 -0
- package/src/workspace/status-gates.ts +8 -0
- package/src/workspace/status-mixed.test.ts +152 -0
- package/src/workspace/status-revoked.test.ts +153 -0
- package/src/workspace/status.schema.json +198 -1
- package/src/workspace/status.ts +254 -3
- package/src/workspace/wip.ts +11 -2
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `chant lifecycle gc [--yes] [--push] [--json] [--verbose]` (#4036): prune
|
|
3
|
+
* stored change sets, releases and their templates from `chant/lifecycle`, and
|
|
4
|
+
* run event files from this host, by the retention policy.
|
|
5
|
+
*
|
|
6
|
+
* Without `--yes` it is a dry run: it prints what it would remove and changes
|
|
7
|
+
* nothing. With `--yes` it writes one commit to the local `chant/lifecycle`
|
|
8
|
+
* that removes those files, and deletes the run event files. It pushes only
|
|
9
|
+
* with `--push`. History is never rewritten, so a pruned file stays in the
|
|
10
|
+
* branch's earlier commits. The rules are in ../../lifecycle/retention.ts.
|
|
11
|
+
*
|
|
12
|
+
* The policy is the defaults, overridden by the `retention` block of the
|
|
13
|
+
* workspace declaration at base when the checkout is a workspace. The prune
|
|
14
|
+
* covers the whole branch, every member, from wherever in the repository it
|
|
15
|
+
* runs.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { execFileSync } from "node:child_process";
|
|
19
|
+
import { existsSync } from "node:fs";
|
|
20
|
+
import { dirname, join, resolve } from "node:path";
|
|
21
|
+
import { formatError, formatSuccess, formatWarning } from "../format";
|
|
22
|
+
import type { CommandContext } from "../registry";
|
|
23
|
+
import type { RetentionPolicy } from "../../lifecycle/retention-policy";
|
|
24
|
+
|
|
25
|
+
const USAGE = "chant lifecycle gc [--yes] [--push] [--json] [--verbose]";
|
|
26
|
+
|
|
27
|
+
function tryGit(cwd: string, args: string[]): string | null {
|
|
28
|
+
try {
|
|
29
|
+
return execFileSync("git", args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Whether a workspace declaration sits in `cwd` or a directory above it, up to the repository's top. */
|
|
36
|
+
function inWorkspace(cwd: string, top: string): boolean {
|
|
37
|
+
for (let dir = cwd; ; dir = dirname(dir)) {
|
|
38
|
+
if (existsSync(join(dir, "chant.workspace.json")) || existsSync(join(dir, "chant.workspace.jsonc"))) return true;
|
|
39
|
+
if (dir === top || dirname(dir) === dir) return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Every commit with a `ci/revoked/<sha>` tag, counted as `chant release cut` counts it. */
|
|
44
|
+
async function revokedCommits(cwd: string, top: string, green: import("../../workspace/declaration").CiGreen | null, workspace: boolean): Promise<Set<string>> {
|
|
45
|
+
if (workspace) {
|
|
46
|
+
const { readCiTags } = await import("../../workspace/ci-green");
|
|
47
|
+
const base = green
|
|
48
|
+
? (tryGit(top, ["rev-parse", "--verify", "--quiet", `refs/remotes/origin/${green.branch}^{commit}`]) ??
|
|
49
|
+
tryGit(top, ["rev-parse", "--verify", "--quiet", `refs/heads/${green.branch}^{commit}`]) ??
|
|
50
|
+
tryGit(top, ["rev-parse", "HEAD"]))
|
|
51
|
+
: null;
|
|
52
|
+
return new Set(readCiTags(top, green, base).revoked.keys());
|
|
53
|
+
}
|
|
54
|
+
// With no declaration every tag counts, as readCiTags counts them without `ci.green`.
|
|
55
|
+
const out = tryGit(cwd, ["for-each-ref", "--format=%(refname:lstrip=4)", "refs/tags/ci/revoked/"]) ?? "";
|
|
56
|
+
return new Set(out.split("\n").filter((s) => /^[0-9a-f]{40,64}$/.test(s)));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const kb = (bytes: number): string => (bytes >= 1024 * 1024 ? `${(bytes / 1024 / 1024).toFixed(1)} MB` : `${(bytes / 1024).toFixed(1)} KB`);
|
|
60
|
+
|
|
61
|
+
export async function runLifecycleGc(ctx: CommandContext): Promise<number> {
|
|
62
|
+
const { args } = ctx;
|
|
63
|
+
if (args.extraPositional) {
|
|
64
|
+
console.error(formatError({ message: `lifecycle gc takes no argument, and was given "${args.extraPositional}"`, hint: USAGE }));
|
|
65
|
+
return 1;
|
|
66
|
+
}
|
|
67
|
+
const cwd = resolve(".");
|
|
68
|
+
const top = tryGit(cwd, ["rev-parse", "--show-toplevel"]);
|
|
69
|
+
if (!top) {
|
|
70
|
+
console.error(formatError({ message: "lifecycle gc prunes the chant/lifecycle branch, and this directory is not in a git repository", hint: USAGE }));
|
|
71
|
+
return 1;
|
|
72
|
+
}
|
|
73
|
+
const dryRun = args.yes !== true;
|
|
74
|
+
|
|
75
|
+
const { resolveRetention } = await import("../../lifecycle/retention-policy");
|
|
76
|
+
let policy: RetentionPolicy = resolveRetention(null);
|
|
77
|
+
let green: import("../../workspace/declaration").CiGreen | null = null;
|
|
78
|
+
const workspace = inWorkspace(cwd, top);
|
|
79
|
+
if (workspace) {
|
|
80
|
+
const { scopeSource } = await import("../../workspace/write-scope");
|
|
81
|
+
const source = scopeSource(cwd);
|
|
82
|
+
if (!source.declaration) {
|
|
83
|
+
console.error(formatError({ message: `the workspace declaration can't be read, so its retention policy is unknown and nothing is pruned: ${source.failure?.message ?? "no declaration"}` }));
|
|
84
|
+
return 1;
|
|
85
|
+
}
|
|
86
|
+
policy = resolveRetention(source.declaration.retention);
|
|
87
|
+
green = source.declaration.ci?.green ?? null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Read the remote's facts first: a prune planned on a stale branch could
|
|
91
|
+
// remove what a newer gate binds, and a forked branch must not be pushed over.
|
|
92
|
+
const { fetchLifecycleStatus } = await import("../../lifecycle/git");
|
|
93
|
+
const fetched = await fetchLifecycleStatus({ cwd });
|
|
94
|
+
if (fetched.status === "diverged" || fetched.status === "unreachable") {
|
|
95
|
+
const why = fetched.status === "diverged" ? `the local chant/lifecycle and ${fetched.remote}'s have forked` : `${fetched.remote} did not answer: ${fetched.stderr}`;
|
|
96
|
+
if (!dryRun) {
|
|
97
|
+
console.error(formatError({ message: `nothing was pruned: ${why}`, hint: "Bring chant/lifecycle up to date with its remote, then run again." }));
|
|
98
|
+
return 1;
|
|
99
|
+
}
|
|
100
|
+
if (!args.json) console.error(formatWarning({ message: `planned on the local branch only: ${why}` }));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const { runLifecycleGc: run } = await import("../../lifecycle/retention");
|
|
104
|
+
const report = await run({
|
|
105
|
+
cwd,
|
|
106
|
+
policy,
|
|
107
|
+
now: new Date(),
|
|
108
|
+
revokedCommits: await revokedCommits(cwd, top, green, workspace),
|
|
109
|
+
dryRun,
|
|
110
|
+
push: args.push === true,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
if (args.json) {
|
|
114
|
+
console.log(JSON.stringify(report, null, 2));
|
|
115
|
+
return 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const lc = report.lifecycle;
|
|
119
|
+
const count = (list: ReadonlyArray<{ kind: string }>, kind: string) => list.filter((f) => f.kind === kind).length;
|
|
120
|
+
const bytes = lc.remove.reduce((n, f) => n + f.bytes, 0);
|
|
121
|
+
const verb = dryRun ? "would remove" : "removed";
|
|
122
|
+
if (!lc.tip) {
|
|
123
|
+
console.error("No chant/lifecycle branch: nothing stored to prune.");
|
|
124
|
+
} else {
|
|
125
|
+
console.log(
|
|
126
|
+
`chant/lifecycle at ${lc.tip.slice(0, 12)}: ${verb} ${count(lc.remove, "change-set")} change set(s), ${count(lc.remove, "release")} release(s) and ${count(lc.remove, "template")} template(s), ${kb(bytes)}; ` +
|
|
127
|
+
`keeps ${count(lc.keep, "change-set")}, ${count(lc.keep, "release")} and ${count(lc.keep, "template")}`,
|
|
128
|
+
);
|
|
129
|
+
console.log(` tree at the tip: ${lc.before.files} file(s), ${kb(lc.before.bytes)} before; ${lc.after.files} file(s), ${kb(lc.after.bytes)} after. The ledgers (${kb(lc.before.ledgers.bytes)}) are never pruned, and history keeps every removed file.`);
|
|
130
|
+
if (args.verbose) for (const f of lc.remove) console.log(` - ${f.path}`);
|
|
131
|
+
}
|
|
132
|
+
const ev = report.runEvents;
|
|
133
|
+
if (ev.dir) {
|
|
134
|
+
console.log(` run events in ${ev.dir}: ${verb} ${ev.remove.length} file(s), ${kb(ev.remove.reduce((n, f) => n + f.bytes, 0))}; keeps ${ev.kept.files}`);
|
|
135
|
+
if (args.verbose) for (const f of ev.remove) console.log(` - ${f.file}`);
|
|
136
|
+
}
|
|
137
|
+
if (dryRun) {
|
|
138
|
+
console.error("Dry run: nothing was removed. Run again with --yes to prune.");
|
|
139
|
+
} else if (lc.commit) {
|
|
140
|
+
console.error(formatSuccess(`Pruned in commit ${lc.commit.slice(0, 12)} on chant/lifecycle`));
|
|
141
|
+
if (lc.pushed) console.error(" pushed");
|
|
142
|
+
else if (lc.pushWarning) console.error(formatWarning({ message: lc.pushWarning }));
|
|
143
|
+
else console.error(" not pushed: run with --push, or push chant/lifecycle yourself");
|
|
144
|
+
}
|
|
145
|
+
return 0;
|
|
146
|
+
}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
10
10
|
import { tmpdir } from "node:os";
|
|
11
|
+
import { readFileSync } from "node:fs";
|
|
11
12
|
import { join } from "node:path";
|
|
12
13
|
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
|
13
14
|
import type { DriverComponent } from "../../components/driver";
|
|
@@ -58,6 +59,8 @@ vi.mock("../../op/gate", async () => {
|
|
|
58
59
|
});
|
|
59
60
|
|
|
60
61
|
const failing = new Set<string>();
|
|
62
|
+
/** Called as a component's apply starts, before it can fail. */
|
|
63
|
+
let onRun: (root: string) => void = () => {};
|
|
61
64
|
vi.mock("../../components/fan-out-support", async () => {
|
|
62
65
|
const { CapabilityRegistry } = await import("../../components/capability");
|
|
63
66
|
const { planFanOut } = await import("../../components/fan-out");
|
|
@@ -78,6 +81,7 @@ vi.mock("../../components/fan-out-support", async () => {
|
|
|
78
81
|
return {};
|
|
79
82
|
},
|
|
80
83
|
async run(_ctx: DeployContext, input: { root: string }) {
|
|
84
|
+
onRun(input.root);
|
|
81
85
|
if (failing.has(input.root)) throw new Error(`${input.root} failed`);
|
|
82
86
|
return { outputs: {} };
|
|
83
87
|
},
|
|
@@ -106,6 +110,7 @@ const { checkRunEventLines, stepTrail } = await import("../../workspace/__fixtur
|
|
|
106
110
|
|
|
107
111
|
let dir: string;
|
|
108
112
|
let out: string[];
|
|
113
|
+
let errors: string[];
|
|
109
114
|
|
|
110
115
|
const ctx = (a: Partial<ParsedArgs>) =>
|
|
111
116
|
({ args: { command: "components", path: "pr-apply", format: "", fix: false, watch: false, verbose: false, help: false, live: false, base: "main", env: "prod", output: dir, eventsJson: true, ...a } as ParsedArgs, plugins: [] }) as never;
|
|
@@ -113,10 +118,12 @@ const ctx = (a: Partial<ParsedArgs>) =>
|
|
|
113
118
|
beforeEach(async () => {
|
|
114
119
|
dir = await mkdtemp(join(tmpdir(), "pr-events-"));
|
|
115
120
|
out = [];
|
|
121
|
+
errors = [];
|
|
116
122
|
failing.clear();
|
|
123
|
+
onRun = () => {};
|
|
117
124
|
gateLedger = memoryGateLedgerPort();
|
|
118
125
|
vi.spyOn(process.stdout, "write").mockImplementation((chunk: string | Uint8Array) => { out.push(String(chunk)); return true; });
|
|
119
|
-
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
126
|
+
vi.spyOn(console, "error").mockImplementation((...m: unknown[]) => { errors.push(m.map(String).join(" ")); });
|
|
120
127
|
vi.spyOn(console, "log").mockImplementation(() => {});
|
|
121
128
|
});
|
|
122
129
|
|
|
@@ -182,6 +189,146 @@ describe("pr-apply --events-json", () => {
|
|
|
182
189
|
});
|
|
183
190
|
});
|
|
184
191
|
|
|
192
|
+
/** Plan pull request 12 and approve its digest, as a reviewer would. */
|
|
193
|
+
async function planAndApprove(): Promise<void> {
|
|
194
|
+
await runComponentsPrPlan(ctx({ pr: 12 }));
|
|
195
|
+
const digest = await plannedDigest();
|
|
196
|
+
gateLedger = memoryGateLedgerPort({
|
|
197
|
+
resolutions: [{ version: 1, kind: "resolution", op: "pr-12", gate: "pr-apply", resolvedBy: "github:alice", timestamp: new Date().toISOString(), planDigest: digest }],
|
|
198
|
+
});
|
|
199
|
+
out = [];
|
|
200
|
+
errors = [];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** The job log's wave and component lines, without colour. */
|
|
204
|
+
const logLines = (): string[] =>
|
|
205
|
+
errors.map((l) => l.replace(/\u001b\[[0-9;]*m/g, "")).filter((l) => /^wave \d|^ {2}\S+: /.test(l));
|
|
206
|
+
|
|
207
|
+
describe("pr-apply's job log (#4007)", () => {
|
|
208
|
+
test("without --events-json, a line as each wave starts and as each component settles, in order", async () => {
|
|
209
|
+
await planAndApprove();
|
|
210
|
+
expect(await runComponentsPrApply(ctx({ pr: 12, eventsJson: false }))).toBe(0);
|
|
211
|
+
expect(out).toEqual([]);
|
|
212
|
+
expect(logLines()).toEqual(["wave 1/2: net", " net: applied", "wave 2/2: app", " app: applied"]);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("a failed dependency: the component behind it settles blocked, naming it, in the log and the stream", async () => {
|
|
216
|
+
await planAndApprove();
|
|
217
|
+
failing.add("net");
|
|
218
|
+
expect(await runComponentsPrApply(ctx({ pr: 12 }))).toBe(1);
|
|
219
|
+
expect(logLines()).toEqual(["wave 1/2: net", " net: failed", " app: blocked by net, not run"]);
|
|
220
|
+
const events = checkRunEventLines(out);
|
|
221
|
+
const blocked = events.find((e) => e.type === "component-settle" && e.component === "app");
|
|
222
|
+
expect(blocked).toMatchObject({ type: "component-settle", wave: 2, component: "app", status: "blocked", blockedBy: "net" });
|
|
223
|
+
expect(events.some((e) => e.type === "component-start" && e.component === "app")).toBe(false);
|
|
224
|
+
expect(events.some((e) => e.type === "wave-start" && e.wave === 2)).toBe(false);
|
|
225
|
+
expect(events.at(-1)).toMatchObject({ type: "run-settle", status: "failed" });
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("a resumed apply settles what the earlier attempt applied as already-applied, before the gate", async () => {
|
|
229
|
+
await planAndApprove();
|
|
230
|
+
const resume = join(dir, "attempt.json");
|
|
231
|
+
failing.add("app");
|
|
232
|
+
expect(await runComponentsPrApply(ctx({ pr: 12, resume, eventsJson: false }))).toBe(1);
|
|
233
|
+
failing.clear();
|
|
234
|
+
out = [];
|
|
235
|
+
errors = [];
|
|
236
|
+
expect(await runComponentsPrApply(ctx({ pr: 12, resume }))).toBe(0);
|
|
237
|
+
const events = checkRunEventLines(out);
|
|
238
|
+
const settles = events.filter((e) => e.type === "component-settle").map((e) => [e.component, (e as { status: string }).status, (e as { wave: number }).wave]);
|
|
239
|
+
expect(settles).toEqual([["net", "already-applied", 1], ["app", "ok", 1]]);
|
|
240
|
+
const gate = events.findIndex((e) => e.type === "phase-start" && e.phase === "gate");
|
|
241
|
+
expect(events.findIndex((e) => e.type === "component-settle" && e.component === "net")).toBeLessThan(gate);
|
|
242
|
+
expect(logLines()).toEqual([" net: already applied in an earlier attempt", "wave 1/1: app", " app: applied"]);
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
describe("--resume carries only an unchanged input (#4081)", () => {
|
|
247
|
+
test("a component whose input moved since the attempt runs again, and says why", async () => {
|
|
248
|
+
await planAndApprove();
|
|
249
|
+
const resume = join(dir, "attempt.json");
|
|
250
|
+
failing.add("app");
|
|
251
|
+
expect(await runComponentsPrApply(ctx({ pr: 12, resume, eventsJson: false }))).toBe(1);
|
|
252
|
+
expect(JSON.parse(readFileSync(resume, "utf8")).inputs).toEqual({ net: expect.stringMatching(/^jcs1-sha256:/) });
|
|
253
|
+
failing.clear();
|
|
254
|
+
errors = [];
|
|
255
|
+
const step = ESTATE[0]!.deploy[0]!.steps[0] as Record<string, unknown>;
|
|
256
|
+
step.note = "a different change of the same shape";
|
|
257
|
+
const ran: string[] = [];
|
|
258
|
+
onRun = (name) => ran.push(name);
|
|
259
|
+
try {
|
|
260
|
+
expect(await runComponentsPrApply(ctx({ pr: 12, resume, eventsJson: false }))).toBe(0);
|
|
261
|
+
} finally {
|
|
262
|
+
delete step.note;
|
|
263
|
+
}
|
|
264
|
+
expect(ran).toEqual(["net", "app"]);
|
|
265
|
+
expect(errors.join("\n")).toContain("net: changed since the earlier attempt, so it runs again");
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test("a record without input digests does not carry", async () => {
|
|
269
|
+
await planAndApprove();
|
|
270
|
+
const resume = join(dir, "attempt.json");
|
|
271
|
+
failing.add("app");
|
|
272
|
+
expect(await runComponentsPrApply(ctx({ pr: 12, resume, eventsJson: false }))).toBe(1);
|
|
273
|
+
const { writeFileSync } = await import("node:fs");
|
|
274
|
+
const record = JSON.parse(readFileSync(resume, "utf8"));
|
|
275
|
+
delete record.inputs;
|
|
276
|
+
writeFileSync(resume, JSON.stringify(record));
|
|
277
|
+
failing.clear();
|
|
278
|
+
errors = [];
|
|
279
|
+
const ran: string[] = [];
|
|
280
|
+
onRun = (name) => ran.push(name);
|
|
281
|
+
expect(await runComponentsPrApply(ctx({ pr: 12, resume, eventsJson: false }))).toBe(0);
|
|
282
|
+
expect(ran).toEqual(["net", "app"]);
|
|
283
|
+
expect(errors.join("\n")).toMatch(/does not carry: it does not record what each component applied/);
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
describe("--report-url (#4007)", () => {
|
|
288
|
+
test("refuses a URL it can't upload to, before anything plans", async () => {
|
|
289
|
+
expect(await runComponentsPrPlan(ctx({ pr: 12, reportUrl: "https://example.invalid/reports" }))).toBe(1);
|
|
290
|
+
expect(errors.join("\n")).toMatch(/takes an s3:\/\/<bucket>\/<prefix> or file:\/\/<directory> URL/);
|
|
291
|
+
expect(out).toEqual([]);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test("file://: the report, the change set and the events land under <pr>/<commit>/, the events as each wave settles", async () => {
|
|
295
|
+
const { pathToFileURL } = await import("node:url");
|
|
296
|
+
const { readFile } = await import("node:fs/promises");
|
|
297
|
+
const reports = join(dir, "reports");
|
|
298
|
+
const reportUrl = pathToFileURL(reports).href;
|
|
299
|
+
await runComponentsPrPlan(ctx({ pr: 12, reportUrl }));
|
|
300
|
+
const at = join(reports, "12", HEAD);
|
|
301
|
+
expect(JSON.parse(await readFile(join(at, "pr-plan.json"), "utf8"))).toMatchObject({ stage: "plan", pr: 12, status: "planned" });
|
|
302
|
+
expect(checkRunEventLines((await readFile(join(at, "pr-plan.events.jsonl"), "utf8")).split(/(?<=\n)/))[0]).toMatchObject({ type: "run-start" });
|
|
303
|
+
|
|
304
|
+
await planAndApprove();
|
|
305
|
+
// What a reader of the URL sees while wave 2 applies: wave 1 settled.
|
|
306
|
+
let seenMidRun: string[] = [];
|
|
307
|
+
onRun = (root) => {
|
|
308
|
+
if (root === "app") seenMidRun = readFileSync(join(at, "pr-apply.events.jsonl"), "utf8").split(/(?<=\n)/);
|
|
309
|
+
};
|
|
310
|
+
expect(await runComponentsPrApply(ctx({ pr: 12, reportUrl }))).toBe(0);
|
|
311
|
+
const mid = seenMidRun.map((l) => JSON.parse(l) as { type: string; wave?: number; status?: string });
|
|
312
|
+
expect(mid.some((e) => e.type === "wave-settle" && e.wave === 1 && e.status === "ok")).toBe(true);
|
|
313
|
+
expect(mid.some((e) => e.type === "run-settle")).toBe(false);
|
|
314
|
+
|
|
315
|
+
const final = checkRunEventLines((await readFile(join(at, "pr-apply.events.jsonl"), "utf8")).split(/(?<=\n)/));
|
|
316
|
+
expect(final.at(-1)).toMatchObject({ type: "run-settle", status: "ok" });
|
|
317
|
+
expect(final.map((e) => e.type).join("\n")).toBe(out.map((l) => JSON.parse(l).type).join("\n"));
|
|
318
|
+
expect(JSON.parse(await readFile(join(at, "pr-apply.json"), "utf8"))).toMatchObject({ stage: "apply", status: "applied" });
|
|
319
|
+
expect(JSON.parse(await readFile(join(at, "change-set.json"), "utf8"))).toHaveProperty("digest");
|
|
320
|
+
expect(errors.join("\n")).toContain(`report : ${reportUrl}/12/${HEAD}/`);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
test("without --events-json, only the report and the change set are copied", async () => {
|
|
324
|
+
const { pathToFileURL } = await import("node:url");
|
|
325
|
+
const { readdir } = await import("node:fs/promises");
|
|
326
|
+
const reports = join(dir, "reports");
|
|
327
|
+
await runComponentsPrPlan(ctx({ pr: 12, reportUrl: pathToFileURL(reports).href, eventsJson: false }));
|
|
328
|
+
expect((await readdir(join(reports, "12", HEAD))).sort()).toEqual(["change-set.json", "pr-plan.json"]);
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
|
|
185
332
|
describe("--events-json beside --json", () => {
|
|
186
333
|
test("is refused on pr-plan and pr-apply before anything plans, since both print to stdout", async () => {
|
|
187
334
|
const errors: string[] = [];
|
package/src/cli/handlers/pr.ts
CHANGED
|
@@ -33,7 +33,15 @@
|
|
|
33
33
|
* one `run-event.schema.json` line each: the planning as a `plan` phase with
|
|
34
34
|
* a step per component, the gate's decision as a `gate` phase, and on
|
|
35
35
|
* pr-apply the apply's waves and components as `chant run --components`
|
|
36
|
-
* streams them.
|
|
36
|
+
* streams them. A component the apply does not start settles too, `blocked`
|
|
37
|
+
* behind a failed dependency or `already-applied` on a resumed apply.
|
|
38
|
+
* With or without it, pr-apply prints a line on stderr as each wave starts
|
|
39
|
+
* and as each component settles, so a job log shows the waves in order.
|
|
40
|
+
*
|
|
41
|
+
* `--report-url <s3 or file URL>` (#4007) copies `pr-<stage>.json`,
|
|
42
|
+
* `change-set.json` and, with `--events-json`, the events to
|
|
43
|
+
* `<url>/<pr>/<commit>/`, the events again as each wave and component
|
|
44
|
+
* settles (`../../pr-report-url.ts`).
|
|
37
45
|
*
|
|
38
46
|
* `--member <name>` (#3465) runs a workspace member's loop: the member's
|
|
39
47
|
* generated pipeline passes it from the member's directory. The gate is
|
|
@@ -59,11 +67,13 @@ import {
|
|
|
59
67
|
type PrPlanSet,
|
|
60
68
|
} from "../../components/pr-run";
|
|
61
69
|
import { readFanOutAttempt, writeFanOutAttempt, type FanOutAttempt, type PrApplyRecord } from "../../components/fan-out-record";
|
|
70
|
+
import { componentInputDigests, progressThatCarries } from "../../components/fan-out-inputs";
|
|
62
71
|
import { samePlanDigest } from "../../lifecycle/plan-digest";
|
|
63
72
|
import { gitGateLedgerPort, type GateCheck } from "../../op/gate";
|
|
64
73
|
import { emitGateStep, gateEventsOf, GATE_PHASE, type RunEventSink, type RunEventStream } from "../../run-event-stream";
|
|
65
|
-
import type { RunEventSettleStatus } from "../../run-event";
|
|
74
|
+
import type { RunEventBody, RunEventComponentSettle, RunEventSettleStatus } from "../../run-event";
|
|
66
75
|
import { openCliRunEvents, refusesEventsWithJson } from "../run-events";
|
|
76
|
+
import { openPrReportUpload, parseReportUrl, reportKey, type PrReportUpload, type ReportTarget } from "../../pr-report-url";
|
|
67
77
|
import {
|
|
68
78
|
describePlanChanged,
|
|
69
79
|
PR_APPLY_GATE,
|
|
@@ -262,12 +272,18 @@ function settleOf(status: PrReport["status"]): RunEventSettleStatus {
|
|
|
262
272
|
}
|
|
263
273
|
}
|
|
264
274
|
|
|
265
|
-
/** Write the report, the change set and the note
|
|
266
|
-
function writeOutputs(ctx: CommandContext, report: PrReport, note: string): void {
|
|
275
|
+
/** Write the report, the change set and the note, and hand the first two to `--report-url`'s upload. */
|
|
276
|
+
function writeOutputs(ctx: CommandContext, report: PrReport, note: string, upload?: PrReportUpload): void {
|
|
267
277
|
const dir = resolve(ctx.args.output ?? PR_REPORT_DIR);
|
|
268
278
|
mkdirSync(dir, { recursive: true });
|
|
269
|
-
|
|
270
|
-
|
|
279
|
+
const files: Array<[string, string]> = [
|
|
280
|
+
[`pr-${report.stage}.json`, JSON.stringify(report, null, 2) + "\n"],
|
|
281
|
+
["change-set.json", JSON.stringify(report.changeSet, null, 2) + "\n"],
|
|
282
|
+
];
|
|
283
|
+
for (const [name, body] of files) {
|
|
284
|
+
writeFileSync(join(dir, name), body);
|
|
285
|
+
upload?.file(name, body);
|
|
286
|
+
}
|
|
271
287
|
writeFileSync(join(dir, "pr-note.md"), note);
|
|
272
288
|
}
|
|
273
289
|
|
|
@@ -310,21 +326,32 @@ function printMembers(report: PrReport): void {
|
|
|
310
326
|
}
|
|
311
327
|
}
|
|
312
328
|
|
|
313
|
-
async function finish(ctx: CommandContext, p: Prepared, report: PrReport): Promise<void> {
|
|
329
|
+
async function finish(ctx: CommandContext, p: Prepared, report: PrReport, upload?: PrReportUpload): Promise<void> {
|
|
314
330
|
const note = noteFor(report, p.forge);
|
|
315
|
-
writeOutputs(ctx, report, note);
|
|
331
|
+
writeOutputs(ctx, report, note, upload);
|
|
316
332
|
await publish(p.forge, report, note);
|
|
317
333
|
if (ctx.args.json) console.log(JSON.stringify(report, null, 2));
|
|
318
334
|
}
|
|
319
335
|
|
|
320
|
-
/**
|
|
336
|
+
/**
|
|
337
|
+
* A stage's stream and its `--report-url` upload, held so that a stage that
|
|
338
|
+
* returns or throws before it settles still ends the stream and uploads.
|
|
339
|
+
*/
|
|
321
340
|
interface StageEvents {
|
|
322
341
|
stream?: RunEventStream;
|
|
342
|
+
/** Where `--report-url` points, read before the stage starts. */
|
|
343
|
+
target?: ReportTarget;
|
|
344
|
+
upload?: PrReportUpload;
|
|
323
345
|
}
|
|
324
346
|
|
|
325
|
-
/** Run a stage,
|
|
326
|
-
async function withStageEvents(stage: string, run: (events: StageEvents) => Promise<number>): Promise<number> {
|
|
347
|
+
/** Run a stage, end its stream as `failed` when the stage did not settle it, and finish its upload. */
|
|
348
|
+
async function withStageEvents(stage: string, ctx: CommandContext, run: (events: StageEvents) => Promise<number>): Promise<number> {
|
|
327
349
|
const events: StageEvents = {};
|
|
350
|
+
if (ctx.args.reportUrl !== undefined) {
|
|
351
|
+
const target = parseReportUrl(ctx.args.reportUrl);
|
|
352
|
+
if ("error" in target) return fail(target.error);
|
|
353
|
+
events.target = target;
|
|
354
|
+
}
|
|
328
355
|
try {
|
|
329
356
|
return await run(events);
|
|
330
357
|
} catch (err) {
|
|
@@ -332,13 +359,32 @@ async function withStageEvents(stage: string, run: (events: StageEvents) => Prom
|
|
|
332
359
|
throw err;
|
|
333
360
|
} finally {
|
|
334
361
|
events.stream?.settle("failed", `${stage} stopped before it finished`);
|
|
362
|
+
if (events.upload) {
|
|
363
|
+
await events.upload.close();
|
|
364
|
+
console.error(formatInfo(`report : ${events.upload.where}/`));
|
|
365
|
+
}
|
|
335
366
|
}
|
|
336
367
|
}
|
|
337
368
|
|
|
338
|
-
/**
|
|
369
|
+
/**
|
|
370
|
+
* Open the stage's `--report-url` upload under its pull request and commit,
|
|
371
|
+
* when the flag was given. Its events are the lines `openCliRunEvents` writes.
|
|
372
|
+
*/
|
|
373
|
+
function openUpload(events: StageEvents, stage: "plan" | "apply", pr: number | null, p: Prepared): PrReportUpload | undefined {
|
|
374
|
+
if (!events.target) return undefined;
|
|
375
|
+
events.upload = openPrReportUpload({
|
|
376
|
+
target: events.target,
|
|
377
|
+
key: reportKey(pr, p.head, p.member),
|
|
378
|
+
stage,
|
|
379
|
+
warn: (message) => console.error(formatWarning({ message })),
|
|
380
|
+
});
|
|
381
|
+
return events.upload;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** chant components pr-plan --base <ref> --pr <n> [--env <env>] [--gate <name>] [--forge <kind>] [--output <dir>] [--json] [--events-json] [--report-url <url>] */
|
|
339
385
|
export function runComponentsPrPlan(ctx: CommandContext): Promise<number> {
|
|
340
386
|
if (refusesEventsWithJson(ctx.args, "the stage's report")) return Promise.resolve(1);
|
|
341
|
-
return withStageEvents("pr-plan", (events) => prPlan(ctx, events));
|
|
387
|
+
return withStageEvents("pr-plan", ctx, (events) => prPlan(ctx, events));
|
|
342
388
|
}
|
|
343
389
|
|
|
344
390
|
async function prPlan(ctx: CommandContext, stage: StageEvents): Promise<number> {
|
|
@@ -348,8 +394,9 @@ async function prPlan(ctx: CommandContext, stage: StageEvents): Promise<number>
|
|
|
348
394
|
const p = await prepare(ctx, "plan");
|
|
349
395
|
if (typeof p === "number") return p;
|
|
350
396
|
const op = prOp(ctx.args.pr, p.member);
|
|
397
|
+
const upload = openUpload(stage, "plan", ctx.args.pr, p);
|
|
351
398
|
// `--events-json` (#4041): planning, then where the approval stands.
|
|
352
|
-
const events = await openCliRunEvents(ctx.args, { kind: "component", name: `pr-plan ${op}`, cwd: resolve(".") });
|
|
399
|
+
const events = await openCliRunEvents(ctx.args, { kind: "component", name: `pr-plan ${op}`, cwd: resolve(".") }, upload?.line);
|
|
353
400
|
stage.stream = events;
|
|
354
401
|
events?.emit({ type: "run-start", waves: p.plan.waves });
|
|
355
402
|
const set = await planAll(p, events?.emit);
|
|
@@ -381,7 +428,7 @@ async function prPlan(ctx: CommandContext, stage: StageEvents): Promise<number>
|
|
|
381
428
|
approval,
|
|
382
429
|
status,
|
|
383
430
|
});
|
|
384
|
-
await finish(ctx, p, report);
|
|
431
|
+
await finish(ctx, p, report, upload);
|
|
385
432
|
// The plan stage settles on its planning: an approval still to come is what it reports, not a stop.
|
|
386
433
|
events?.settle(settleOf(status), status === "plan-failed" ? "a member failed to plan" : undefined);
|
|
387
434
|
|
|
@@ -394,10 +441,10 @@ async function prPlan(ctx: CommandContext, stage: StageEvents): Promise<number>
|
|
|
394
441
|
return status === "plan-failed" ? 1 : 0;
|
|
395
442
|
}
|
|
396
443
|
|
|
397
|
-
/** chant components pr-apply --base <ref> [--pr <n>] [--env <env>] [--gate <name>] [--forge <kind>] [--require-review] [--output <dir>] [--json] [--events-json] */
|
|
444
|
+
/** chant components pr-apply --base <ref> [--pr <n>] [--env <env>] [--gate <name>] [--forge <kind>] [--require-review] [--output <dir>] [--json] [--events-json] [--report-url <url>] */
|
|
398
445
|
export function runComponentsPrApply(ctx: CommandContext): Promise<number> {
|
|
399
446
|
if (refusesEventsWithJson(ctx.args, "the stage's report")) return Promise.resolve(1);
|
|
400
|
-
return withStageEvents("pr-apply", (events) => prApply(ctx, events));
|
|
447
|
+
return withStageEvents("pr-apply", ctx, (events) => prApply(ctx, events));
|
|
401
448
|
}
|
|
402
449
|
|
|
403
450
|
async function prApply(ctx: CommandContext, stage: StageEvents): Promise<number> {
|
|
@@ -418,7 +465,8 @@ async function prApply(ctx: CommandContext, stage: StageEvents): Promise<number>
|
|
|
418
465
|
// The run id the release records carry, chosen before the run so that
|
|
419
466
|
// `--events-json` (#4041, #4007) names the run by it too.
|
|
420
467
|
const release = autoReleaseRun(`${op}-${Date.now()}`);
|
|
421
|
-
const
|
|
468
|
+
const upload = openUpload(stage, "apply", pr, p);
|
|
469
|
+
const events = await openCliRunEvents(args, { kind: "component", name: `pr-apply ${op}`, runId: release.runId, cwd: resolve(".") }, upload?.line);
|
|
422
470
|
stage.stream = events;
|
|
423
471
|
events?.emit({ type: "run-start", waves: p.plan.waves });
|
|
424
472
|
|
|
@@ -434,15 +482,28 @@ async function prApply(ctx: CommandContext, stage: StageEvents): Promise<number>
|
|
|
434
482
|
return fail(`--resume: could not read "${args.resume}": ${err instanceof Error ? err.message : String(err)}`);
|
|
435
483
|
}
|
|
436
484
|
}
|
|
437
|
-
|
|
485
|
+
// What each component would apply now (#4081): progress carries only where it is unchanged.
|
|
486
|
+
const inputsNow = resumePath ? componentInputDigests(p.components, { projectRoot: resolve("."), env: p.env }) : {};
|
|
487
|
+
const resumed = pr !== null && p.plan.order.length > 0 && attempt ? await tryResume(p, attempt, args.resume!, op, inputsNow) : undefined;
|
|
438
488
|
|
|
439
|
-
|
|
489
|
+
// The components an earlier attempt applied settle first (#4007), each in
|
|
490
|
+
// the wave the derivation gave it, which is the run-start's.
|
|
491
|
+
const log = applyLog();
|
|
492
|
+
if (resumed) {
|
|
493
|
+
const waveOf = new Map(p.plan.waves.flatMap((wave, i) => wave.map((name) => [name, i + 1] as const)));
|
|
494
|
+
for (const component of p.plan.order.filter((name) => resumed.completed.includes(name))) {
|
|
495
|
+
const body: RunEventComponentSettle = { type: "component-settle", wave: waveOf.get(component)!, component, status: "already-applied" };
|
|
496
|
+
events?.emit(body);
|
|
497
|
+
log(body);
|
|
498
|
+
}
|
|
499
|
+
emitResumedGate(events, op, p.gate, resumed.check);
|
|
500
|
+
}
|
|
440
501
|
const set = resumed?.set ?? (await planAll(p, events?.emit));
|
|
441
502
|
const base = { stage: "apply" as const, pr, env: p.env, ...(p.member ? { member: p.member } : {}), base: p.base, head: p.head, op, gate: p.gate, plan: p.plan, signal: p.signal, set };
|
|
442
503
|
|
|
443
504
|
const stop = async (report: PrReport, code: number, settle?: RunEventSettleStatus): Promise<number> => {
|
|
444
505
|
events?.settle(settle ?? settleOf(report.status), report.status === "plan-failed" || report.status === "failed" ? report.message ?? `pr-apply ${report.status}` : undefined);
|
|
445
|
-
await finish(ctx, p, report);
|
|
506
|
+
await finish(ctx, p, report, upload);
|
|
446
507
|
console.error((code === 0 ? formatInfo : (m: string) => formatWarning({ message: m }))(`pr-apply: ${report.status}${report.refusal ? ` (${report.refusal})` : ""}`));
|
|
447
508
|
if (report.message) console.error(report.message);
|
|
448
509
|
printMembers(report);
|
|
@@ -538,6 +599,9 @@ async function prApply(ctx: CommandContext, stage: StageEvents): Promise<number>
|
|
|
538
599
|
// one settles, so a run killed partway still knows what applied.
|
|
539
600
|
const prior = resumed ? { completed: resumed.completed, outputs: resumed.outputs } : { completed: [], outputs: {} };
|
|
540
601
|
const completedSoFar = new Set<string>(prior.completed);
|
|
602
|
+
const inputsSoFar: Record<string, string> = Object.fromEntries(
|
|
603
|
+
prior.completed.filter((name) => inputsNow[name] !== undefined).map((name) => [name, inputsNow[name]!]),
|
|
604
|
+
);
|
|
541
605
|
const failedSoFar: string[] = [];
|
|
542
606
|
const outputsSoFar: Record<string, Record<string, unknown>> = { ...prior.outputs };
|
|
543
607
|
const record: PrApplyRecord = {
|
|
@@ -557,6 +621,7 @@ async function prApply(ctx: CommandContext, stage: StageEvents): Promise<number>
|
|
|
557
621
|
completed: [...completedSoFar].sort(),
|
|
558
622
|
failed: [...failedSoFar].sort(),
|
|
559
623
|
outputs: outputsSoFar,
|
|
624
|
+
inputs: Object.fromEntries(Object.entries(inputsSoFar).sort(([a], [b]) => a.localeCompare(b))),
|
|
560
625
|
prApply: record,
|
|
561
626
|
});
|
|
562
627
|
};
|
|
@@ -569,13 +634,18 @@ async function prApply(ctx: CommandContext, stage: StageEvents): Promise<number>
|
|
|
569
634
|
env: p.env,
|
|
570
635
|
set,
|
|
571
636
|
...(resumed ? { resumed: prior } : {}),
|
|
572
|
-
// The apply is this run's waves, so the fan-out's own run framing is
|
|
573
|
-
|
|
637
|
+
// The apply is this run's waves, so the fan-out's own run framing is
|
|
638
|
+
// dropped. The log's lines go to stderr with or without --events-json.
|
|
639
|
+
onRunEvent: (body: RunEventBody) => {
|
|
640
|
+
events?.nested(body);
|
|
641
|
+
log(body);
|
|
642
|
+
},
|
|
574
643
|
...(resumePath
|
|
575
644
|
? {
|
|
576
645
|
onComponentSettled: (settled, outputs) => {
|
|
577
646
|
if (settled.status === "ok") {
|
|
578
647
|
completedSoFar.add(settled.component);
|
|
648
|
+
if (inputsNow[settled.component] !== undefined) inputsSoFar[settled.component] = inputsNow[settled.component]!;
|
|
579
649
|
if (outputs) outputsSoFar[settled.component] = outputs;
|
|
580
650
|
} else if (settled.status === "fail") {
|
|
581
651
|
failedSoFar.push(settled.component);
|
|
@@ -600,6 +670,37 @@ async function prApply(ctx: CommandContext, stage: StageEvents): Promise<number>
|
|
|
600
670
|
return code;
|
|
601
671
|
}
|
|
602
672
|
|
|
673
|
+
/** What a component's settle says in the job log. */
|
|
674
|
+
function settleWords(body: RunEventComponentSettle): string {
|
|
675
|
+
switch (body.status) {
|
|
676
|
+
case "ok":
|
|
677
|
+
return "applied";
|
|
678
|
+
case "failed":
|
|
679
|
+
return "failed";
|
|
680
|
+
case "gated":
|
|
681
|
+
return "stopped at its own gate";
|
|
682
|
+
case "waiting":
|
|
683
|
+
return "waiting on a decision";
|
|
684
|
+
case "blocked":
|
|
685
|
+
return `blocked by ${body.blockedBy ?? "a dependency"}, not run`;
|
|
686
|
+
case "already-applied":
|
|
687
|
+
return "already applied in an earlier attempt";
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* pr-apply's job log (#4007): a line on stderr as each wave starts and as
|
|
693
|
+
* each component settles, so the log shows the waves in order while they run.
|
|
694
|
+
*/
|
|
695
|
+
function applyLog(): (body: RunEventBody) => void {
|
|
696
|
+
let waves = 0;
|
|
697
|
+
return (body) => {
|
|
698
|
+
if (body.type === "run-start") waves = body.waves?.length ?? 0;
|
|
699
|
+
else if (body.type === "wave-start") console.error(formatInfo(`wave ${body.wave}${waves ? `/${waves}` : ""}: ${body.components.join(", ")}`));
|
|
700
|
+
else if (body.type === "component-settle") console.error(formatInfo(` ${body.component}: ${settleWords(body)}`));
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
|
|
603
704
|
/** A resumed apply's gate (#3464), decided against the approved digest, as the run's `gate` phase. */
|
|
604
705
|
function emitResumedGate(events: RunEventStream | undefined, op: string, gate: string, check: GateCheck): void {
|
|
605
706
|
if (!events) return;
|
|
@@ -658,8 +759,13 @@ interface Resumed {
|
|
|
658
759
|
* derivation; when its approval still stands; and when every member left is
|
|
659
760
|
* covered by the approved set. Otherwise it says why on stderr and the apply
|
|
660
761
|
* runs as it would with no record.
|
|
762
|
+
*
|
|
763
|
+
* Of the components it applied, only those whose input digest is the same
|
|
764
|
+
* now carry (#4081), as in `components fan-out --resume`. One that changed
|
|
765
|
+
* runs again, under the same approval when the approved set covers its new
|
|
766
|
+
* plan. A record without input digests carries nothing.
|
|
661
767
|
*/
|
|
662
|
-
async function tryResume(p: Prepared, attempt: FanOutAttempt, file: string, op: string): Promise<Resumed | undefined> {
|
|
768
|
+
async function tryResume(p: Prepared, attempt: FanOutAttempt, file: string, op: string, inputsNow: Record<string, string>): Promise<Resumed | undefined> {
|
|
663
769
|
const skip = (why: string): undefined => {
|
|
664
770
|
console.error(formatWarning({ message: `--resume: "${file}" does not carry: ${why}`, hint: "Every member is planned again, and the plan needs an approval of its own." }));
|
|
665
771
|
return undefined;
|
|
@@ -670,6 +776,11 @@ async function tryResume(p: Prepared, attempt: FanOutAttempt, file: string, op:
|
|
|
670
776
|
if (record.head !== p.head) return skip(`it records an apply of ${record.head.slice(0, 12)}, not ${p.head.slice(0, 12)}`);
|
|
671
777
|
if (!samePlanDigest(attempt.digest, p.plan.digest)) return skip("the change selects other components than it did");
|
|
672
778
|
if (attempt.completed.length === 0) return undefined;
|
|
779
|
+
const split = progressThatCarries(attempt, inputsNow);
|
|
780
|
+
if (split.unrecorded) return skip("it does not record what each component applied, since it was written before chant recorded component inputs");
|
|
781
|
+
for (const name of split.changed) console.error(formatInfo(`${name}: changed since the earlier attempt, so it runs again`));
|
|
782
|
+
if (split.carried.length === 0) return undefined;
|
|
783
|
+
const completed = split.carried;
|
|
673
784
|
|
|
674
785
|
let check;
|
|
675
786
|
try {
|
|
@@ -679,7 +790,7 @@ async function tryResume(p: Prepared, attempt: FanOutAttempt, file: string, op:
|
|
|
679
790
|
}
|
|
680
791
|
if (!check.satisfied) return skip(`no approval stands for the plan it applied (${record.digest}) any more`);
|
|
681
792
|
|
|
682
|
-
const outputs = Object.fromEntries(Object.entries(attempt.outputs).filter(([name]) =>
|
|
793
|
+
const outputs = Object.fromEntries(Object.entries(attempt.outputs).filter(([name]) => completed.includes(name)));
|
|
683
794
|
const resumed = await resumePrSet({
|
|
684
795
|
components: p.components,
|
|
685
796
|
plan: p.plan,
|
|
@@ -687,9 +798,9 @@ async function tryResume(p: Prepared, attempt: FanOutAttempt, file: string, op:
|
|
|
687
798
|
env: p.env,
|
|
688
799
|
seededOutputs: p.seededOutputs,
|
|
689
800
|
record,
|
|
690
|
-
completed
|
|
801
|
+
completed,
|
|
691
802
|
});
|
|
692
803
|
if (!resumed.ok) return skip(`what is left is not what was approved. ${resumed.reasons.join("; ")}`);
|
|
693
|
-
console.error(formatInfo(`resume : ${
|
|
694
|
-
return { plan: resumed.plan, set: resumed.set, check, approval: approvalOf(check), completed
|
|
804
|
+
console.error(formatInfo(`resume : ${completed.join(", ")} applied in an earlier attempt; applying ${resumed.plan.order.join(", ") || "nothing more"} under the approval of ${record.digest}`));
|
|
805
|
+
return { plan: resumed.plan, set: resumed.set, check, approval: approvalOf(check), completed, outputs };
|
|
695
806
|
}
|