@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,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `--report-url` (#4007): the URL forms, the key, and the upload of a run's
|
|
3
|
+
* events as it goes: one upload in flight at a time, a failure that warns
|
|
4
|
+
* and never stops the run, and a last upload that carries everything. The
|
|
5
|
+
* s3 scheme runs a fake AWS CLI; nothing here reaches a network.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { pathToFileURL } from "node:url";
|
|
12
|
+
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
|
13
|
+
import { openPrReportUpload, parseReportUrl, reportKey, reportPut, type ReportPut } from "./pr-report-url";
|
|
14
|
+
|
|
15
|
+
const line = (type: string, extra: Record<string, unknown> = {}): string => JSON.stringify({ type, ...extra }) + "\n";
|
|
16
|
+
|
|
17
|
+
let dir: string;
|
|
18
|
+
beforeEach(async () => {
|
|
19
|
+
dir = await mkdtemp(join(tmpdir(), "pr-report-url-"));
|
|
20
|
+
});
|
|
21
|
+
afterEach(async () => {
|
|
22
|
+
await rm(dir, { recursive: true, force: true });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe("parseReportUrl", () => {
|
|
26
|
+
test("reads s3 and file URLs and refuses anything else", () => {
|
|
27
|
+
expect(parseReportUrl("s3://bucket/a/b/")).toEqual({ scheme: "s3", url: "s3://bucket/a/b", bucket: "bucket", prefix: "a/b" });
|
|
28
|
+
expect(parseReportUrl("s3://bucket")).toEqual({ scheme: "s3", url: "s3://bucket", bucket: "bucket", prefix: "" });
|
|
29
|
+
expect(parseReportUrl(pathToFileURL(dir).href)).toMatchObject({ scheme: "file", dir });
|
|
30
|
+
expect(parseReportUrl("https://example.invalid/x")).toHaveProperty("error");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("keys by pull request and commit, and by member after them", () => {
|
|
34
|
+
expect(reportKey(12, "abc")).toBe("12/abc");
|
|
35
|
+
expect(reportKey(null, "abc")).toBe("unknown/abc");
|
|
36
|
+
expect(reportKey(12, "abc", "api")).toBe("12/abc/api");
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe("openPrReportUpload", () => {
|
|
41
|
+
/** A put whose uploads finish only when the test says, recording what each carried. */
|
|
42
|
+
function heldPut() {
|
|
43
|
+
const calls: Array<{ key: string; body: string; finish: (err?: Error) => void }> = [];
|
|
44
|
+
let inFlight = 0;
|
|
45
|
+
let most = 0;
|
|
46
|
+
const put: ReportPut = (key, body) =>
|
|
47
|
+
new Promise<void>((resolve, reject) => {
|
|
48
|
+
inFlight++;
|
|
49
|
+
most = Math.max(most, inFlight);
|
|
50
|
+
calls.push({ key, body, finish: (err) => { inFlight--; if (err) reject(err); else resolve(); } });
|
|
51
|
+
});
|
|
52
|
+
return { put, calls, most: () => most };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
test("uploads the events on each settle, never two at once, and the settles that came meanwhile in one more", async () => {
|
|
56
|
+
const held = heldPut();
|
|
57
|
+
const upload = openPrReportUpload({ target: { scheme: "s3", url: "s3://b", bucket: "b", prefix: "" }, key: "12/abc", stage: "apply", warn: () => {}, put: held.put });
|
|
58
|
+
upload.line(line("run-start"));
|
|
59
|
+
expect(held.calls).toHaveLength(0);
|
|
60
|
+
upload.line(line("wave-start", { wave: 1 }));
|
|
61
|
+
upload.line(line("component-settle", { wave: 1 }));
|
|
62
|
+
expect(held.calls).toHaveLength(1);
|
|
63
|
+
upload.line(line("wave-settle", { wave: 1 }));
|
|
64
|
+
upload.line(line("wave-start", { wave: 2 }));
|
|
65
|
+
upload.line(line("component-settle", { wave: 2 }));
|
|
66
|
+
// Both settles wait behind the one in flight, and go up as one.
|
|
67
|
+
expect(held.calls).toHaveLength(1);
|
|
68
|
+
held.calls[0]!.finish();
|
|
69
|
+
await new Promise((r) => setImmediate(r));
|
|
70
|
+
expect(held.calls).toHaveLength(2);
|
|
71
|
+
expect(held.calls[1]!.body.split("\n").filter(Boolean)).toHaveLength(6);
|
|
72
|
+
expect(held.calls[1]!.key).toBe("12/abc/pr-apply.events.jsonl");
|
|
73
|
+
|
|
74
|
+
upload.file("pr-apply.json", "{}\n");
|
|
75
|
+
const closing = upload.close();
|
|
76
|
+
await new Promise((r) => setImmediate(r));
|
|
77
|
+
// The last upload waits for the one in flight.
|
|
78
|
+
expect(held.calls).toHaveLength(2);
|
|
79
|
+
held.calls[1]!.finish();
|
|
80
|
+
await new Promise((r) => setImmediate(r));
|
|
81
|
+
held.calls[2]!.finish();
|
|
82
|
+
await new Promise((r) => setImmediate(r));
|
|
83
|
+
held.calls[3]!.finish();
|
|
84
|
+
await closing;
|
|
85
|
+
expect(held.calls.map((c) => c.key)).toEqual(["12/abc/pr-apply.events.jsonl", "12/abc/pr-apply.events.jsonl", "12/abc/pr-apply.events.jsonl", "12/abc/pr-apply.json"]);
|
|
86
|
+
expect(held.most()).toBe(1);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("a failed upload as the run goes warns once and the run goes on; the last one is tried again", async () => {
|
|
90
|
+
const warnings: string[] = [];
|
|
91
|
+
let fail = true;
|
|
92
|
+
const put: ReportPut = async (key) => {
|
|
93
|
+
if (fail) throw new Error(`no bucket for ${key}`);
|
|
94
|
+
};
|
|
95
|
+
const upload = openPrReportUpload({ target: { scheme: "s3", url: "s3://b", bucket: "b", prefix: "" }, key: "12/abc", stage: "apply", warn: (m) => warnings.push(m), put });
|
|
96
|
+
upload.line(line("component-settle"));
|
|
97
|
+
await new Promise((r) => setImmediate(r));
|
|
98
|
+
upload.line(line("wave-settle"));
|
|
99
|
+
await new Promise((r) => setImmediate(r));
|
|
100
|
+
expect(warnings).toHaveLength(1);
|
|
101
|
+
expect(warnings[0]).toMatch(/as the run went: no bucket .*The run goes on/);
|
|
102
|
+
fail = false;
|
|
103
|
+
await upload.close();
|
|
104
|
+
expect(warnings).toHaveLength(1);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("file://: each settle replaces the events file whole, so it holds a wave's settle before the next wave starts", async () => {
|
|
108
|
+
const target = parseReportUrl(pathToFileURL(dir).href);
|
|
109
|
+
if ("error" in target) throw new Error(target.error);
|
|
110
|
+
const upload = openPrReportUpload({ target, key: "12/abc", stage: "apply", warn: () => {} });
|
|
111
|
+
upload.line(line("run-start"));
|
|
112
|
+
upload.line(line("wave-settle", { wave: 1 }));
|
|
113
|
+
await new Promise((r) => setImmediate(r));
|
|
114
|
+
const events = join(dir, "12", "abc", "pr-apply.events.jsonl");
|
|
115
|
+
expect(await readFile(events, "utf8")).toBe(line("run-start") + line("wave-settle", { wave: 1 }));
|
|
116
|
+
upload.line(line("run-settle"));
|
|
117
|
+
upload.file("change-set.json", "{}\n");
|
|
118
|
+
await upload.close();
|
|
119
|
+
expect((await readFile(events, "utf8")).split("\n").filter(Boolean)).toHaveLength(3);
|
|
120
|
+
expect(await readFile(join(dir, "12", "abc", "change-set.json"), "utf8")).toBe("{}\n");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("s3://: the AWS CLI copies each object from its stdin, with the content type, and chant hands it no credential", async () => {
|
|
124
|
+
const runs: Array<{ args: string[]; stdin: string }> = [];
|
|
125
|
+
const put = reportPut({ scheme: "s3", url: "s3://b/p", bucket: "b", prefix: "p" }, async (args, stdin) => {
|
|
126
|
+
runs.push({ args, stdin });
|
|
127
|
+
});
|
|
128
|
+
await put("12/abc/pr-plan.json", "{}\n", "application/json");
|
|
129
|
+
expect(runs).toEqual([{ args: ["s3", "cp", "-", "s3://b/p/12/abc/pr-plan.json", "--content-type", "application/json", "--only-show-errors"], stdin: "{}\n" }]);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `pr-plan --report-url` and `pr-apply --report-url` (#4007): copy a stage's
|
|
3
|
+
* report, its change set and its run events where a reader outside CI can
|
|
4
|
+
* fetch them, keyed by pull request and commit.
|
|
5
|
+
*
|
|
6
|
+
* ```
|
|
7
|
+
* <url>/<pr>/<commit>/pr-<stage>.json
|
|
8
|
+
* <url>/<pr>/<commit>/change-set.json
|
|
9
|
+
* <url>/<pr>/<commit>/pr-<stage>.events.jsonl (with --events-json)
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* `<pr>` is the pull or merge request number, or `unknown` when pr-apply
|
|
13
|
+
* found none, and `<commit>` is the full sha the stage ran on: the pull
|
|
14
|
+
* request's head for pr-plan, the pushed commit for pr-apply. A workspace
|
|
15
|
+
* member's loop (`--member`) adds `/<member>` after the commit. There is no
|
|
16
|
+
* index: a reader that knows the pull request and the commit knows the key.
|
|
17
|
+
* The events file is named for its stage because pr-plan and pr-apply run on
|
|
18
|
+
* the same commit when a pull request merges by fast-forward.
|
|
19
|
+
*
|
|
20
|
+
* The events file is uploaded again, whole, each time a wave, a component or
|
|
21
|
+
* the run settles, so a reader polling the URL sees each wave as it applies.
|
|
22
|
+
* One upload of it is in flight at a time: a settle while one is in flight
|
|
23
|
+
* asks for one more after it, with everything written by then. A failed
|
|
24
|
+
* upload is a warning and the run goes on. The last upload, after the run
|
|
25
|
+
* settles, carries the whole stream, and the report and the change set go
|
|
26
|
+
* with it.
|
|
27
|
+
*
|
|
28
|
+
* Two schemes. `file://<dir>` writes under a directory, each file through a
|
|
29
|
+
* temporary name and a rename, so a reader never sees half a file. `s3://
|
|
30
|
+
* <bucket>/<prefix>` runs the AWS CLI (`aws s3 cp - <s3 url>`, the body on
|
|
31
|
+
* its stdin), which reads its credentials, region and endpoint from the
|
|
32
|
+
* environment the job gives it, the way the aws lexicon's `s3-sync` does.
|
|
33
|
+
* chant passes it no credential and prints none: a failed upload prints the
|
|
34
|
+
* CLI's own error line.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import { spawn } from "node:child_process";
|
|
38
|
+
import { mkdirSync, renameSync, writeFileSync } from "node:fs";
|
|
39
|
+
import { dirname, isAbsolute, join } from "node:path";
|
|
40
|
+
import { fileURLToPath } from "node:url";
|
|
41
|
+
|
|
42
|
+
/** Where `--report-url` points. */
|
|
43
|
+
export type ReportTarget =
|
|
44
|
+
| { scheme: "file"; url: string; dir: string }
|
|
45
|
+
| { scheme: "s3"; url: string; bucket: string; prefix: string };
|
|
46
|
+
|
|
47
|
+
/** `--report-url` read, or why it can't be. */
|
|
48
|
+
export function parseReportUrl(raw: string): ReportTarget | { error: string } {
|
|
49
|
+
const url = raw.trim();
|
|
50
|
+
if (url.startsWith("file://")) {
|
|
51
|
+
let dir: string;
|
|
52
|
+
try {
|
|
53
|
+
dir = fileURLToPath(url);
|
|
54
|
+
} catch (err) {
|
|
55
|
+
return { error: `--report-url ${url}: ${err instanceof Error ? err.message : String(err)}` };
|
|
56
|
+
}
|
|
57
|
+
if (!isAbsolute(dir)) return { error: `--report-url ${url}: a file URL names an absolute directory` };
|
|
58
|
+
return { scheme: "file", url: url.replace(/\/+$/, ""), dir };
|
|
59
|
+
}
|
|
60
|
+
const s3 = /^s3:\/\/([^/]+)\/?(.*)$/.exec(url);
|
|
61
|
+
if (s3) {
|
|
62
|
+
const prefix = s3[2]!.replace(/\/+$/, "");
|
|
63
|
+
return { scheme: "s3", url: `s3://${s3[1]}${prefix ? `/${prefix}` : ""}`, bucket: s3[1]!, prefix };
|
|
64
|
+
}
|
|
65
|
+
return { error: `--report-url ${url}: takes an s3://<bucket>/<prefix> or file://<directory> URL` };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The key a stage's files are kept under, below the URL: `<pr>/<commit>[/<member>]`. */
|
|
69
|
+
export function reportKey(pr: number | null, commit: string, member?: string): string {
|
|
70
|
+
return [pr === null ? "unknown" : String(pr), commit, ...(member ? [member] : [])].join("/");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The events file of a stage. */
|
|
74
|
+
export const reportEventsFile = (stage: "plan" | "apply"): string => `pr-${stage}.events.jsonl`;
|
|
75
|
+
|
|
76
|
+
/** Puts one object: `key` below the URL. Rejects with a line a person can read. */
|
|
77
|
+
export type ReportPut = (key: string, body: string, contentType: string) => Promise<void>;
|
|
78
|
+
|
|
79
|
+
/** Runs the AWS CLI with `args` and `stdin`, rejecting with its error line on a non-zero exit. */
|
|
80
|
+
export type AwsCli = (args: string[], stdin: string) => Promise<void>;
|
|
81
|
+
|
|
82
|
+
const AWS_BINARY = "aws";
|
|
83
|
+
|
|
84
|
+
/** The AWS CLI on PATH. */
|
|
85
|
+
export const realAwsCli: AwsCli = (args, stdin) =>
|
|
86
|
+
new Promise((resolvePut, reject) => {
|
|
87
|
+
const child = spawn(AWS_BINARY, args, { stdio: ["pipe", "ignore", "pipe"] });
|
|
88
|
+
let stderr = "";
|
|
89
|
+
child.stderr.on("data", (chunk: Buffer) => {
|
|
90
|
+
if (stderr.length < 4096) stderr += chunk.toString("utf8");
|
|
91
|
+
});
|
|
92
|
+
child.on("error", (err: NodeJS.ErrnoException) =>
|
|
93
|
+
reject(new Error(err.code === "ENOENT" ? "the AWS CLI (aws) is not on PATH; an s3 report URL is uploaded with it" : err.message)),
|
|
94
|
+
);
|
|
95
|
+
child.on("close", (code) => {
|
|
96
|
+
if (code === 0) return resolvePut();
|
|
97
|
+
const line = stderr.trim().split("\n").filter(Boolean).at(-1) ?? "";
|
|
98
|
+
reject(new Error(`aws s3 cp exited ${code}${line ? `: ${line.slice(0, 300)}` : ""}`));
|
|
99
|
+
});
|
|
100
|
+
child.stdin.on("error", () => {
|
|
101
|
+
// The CLI exited before reading its input; its exit says why.
|
|
102
|
+
});
|
|
103
|
+
child.stdin.end(stdin);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
/** A put for `target`. `aws` replaces the AWS CLI in a test. */
|
|
107
|
+
export function reportPut(target: ReportTarget, aws: AwsCli = realAwsCli): ReportPut {
|
|
108
|
+
if (target.scheme === "file") {
|
|
109
|
+
return async (key, body) => {
|
|
110
|
+
const path = join(target.dir, ...key.split("/"));
|
|
111
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
112
|
+
const temp = `${path}.${process.pid}.tmp`;
|
|
113
|
+
writeFileSync(temp, body);
|
|
114
|
+
renameSync(temp, path);
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
return (key, body, contentType) => {
|
|
118
|
+
const object = `s3://${target.bucket}/${target.prefix ? `${target.prefix}/` : ""}${key}`;
|
|
119
|
+
return aws(["s3", "cp", "-", object, "--content-type", contentType, "--only-show-errors"], body);
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** A stage's upload: its events as they are written, and its files at the end. */
|
|
124
|
+
export interface PrReportUpload {
|
|
125
|
+
/** The folder the stage's files go to: `<url>/<key>`. */
|
|
126
|
+
readonly where: string;
|
|
127
|
+
/** One line of the stage's run-event stream. A wave, component or run settle uploads the events. */
|
|
128
|
+
line(line: string): void;
|
|
129
|
+
/** A file to upload when the stage closes, replacing one of the same name. */
|
|
130
|
+
file(name: string, body: string): void;
|
|
131
|
+
/** Wait for the upload in flight, then upload the whole stream and the files. Never throws. */
|
|
132
|
+
close(): Promise<void>;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const UPLOAD_ON = new Set(["wave-settle", "component-settle", "run-settle"]);
|
|
136
|
+
const NDJSON = "application/x-ndjson";
|
|
137
|
+
const JSON_TYPE = "application/json";
|
|
138
|
+
|
|
139
|
+
const messageOf = (err: unknown): string => (err instanceof Error ? err.message : String(err));
|
|
140
|
+
|
|
141
|
+
/** Open a stage's upload to `target` under `key`. `warn` prints a warning line. */
|
|
142
|
+
export function openPrReportUpload(input: {
|
|
143
|
+
target: ReportTarget;
|
|
144
|
+
key: string;
|
|
145
|
+
stage: "plan" | "apply";
|
|
146
|
+
warn: (message: string) => void;
|
|
147
|
+
put?: ReportPut;
|
|
148
|
+
}): PrReportUpload {
|
|
149
|
+
const put = input.put ?? reportPut(input.target);
|
|
150
|
+
const where = `${input.target.url}/${input.key}`;
|
|
151
|
+
const eventsKey = `${input.key}/${reportEventsFile(input.stage)}`;
|
|
152
|
+
const files = new Map<string, string>();
|
|
153
|
+
let events = "";
|
|
154
|
+
let inFlight: Promise<void> | undefined;
|
|
155
|
+
let again = false;
|
|
156
|
+
let warned = false;
|
|
157
|
+
|
|
158
|
+
const pushEvents = (): void => {
|
|
159
|
+
if (inFlight) {
|
|
160
|
+
again = true;
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
inFlight = put(eventsKey, events, NDJSON)
|
|
164
|
+
.catch((err) => {
|
|
165
|
+
if (warned) return;
|
|
166
|
+
warned = true;
|
|
167
|
+
input.warn(`could not upload the run's events to ${where} as the run went: ${messageOf(err)}. The run goes on, and its end tries again.`);
|
|
168
|
+
})
|
|
169
|
+
.finally(() => {
|
|
170
|
+
inFlight = undefined;
|
|
171
|
+
if (again) {
|
|
172
|
+
again = false;
|
|
173
|
+
pushEvents();
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
where,
|
|
180
|
+
line(line) {
|
|
181
|
+
events += line;
|
|
182
|
+
let type: unknown;
|
|
183
|
+
try {
|
|
184
|
+
type = (JSON.parse(line) as { type?: unknown }).type;
|
|
185
|
+
} catch {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (typeof type === "string" && UPLOAD_ON.has(type)) pushEvents();
|
|
189
|
+
},
|
|
190
|
+
file(name, body) {
|
|
191
|
+
files.set(name, body);
|
|
192
|
+
},
|
|
193
|
+
async close() {
|
|
194
|
+
again = false;
|
|
195
|
+
while (inFlight) await inFlight;
|
|
196
|
+
const uploads: Array<[string, string, string]> = [
|
|
197
|
+
...(events ? [[eventsKey, events, NDJSON] as [string, string, string]] : []),
|
|
198
|
+
...[...files].map(([name, body]): [string, string, string] => [`${input.key}/${name}`, body, JSON_TYPE]),
|
|
199
|
+
];
|
|
200
|
+
for (const [key, body, type] of uploads) {
|
|
201
|
+
try {
|
|
202
|
+
await put(key, body, type);
|
|
203
|
+
} catch (err) {
|
|
204
|
+
input.warn(`could not upload ${key.slice(input.key.length + 1)} to ${where}: ${messageOf(err)}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
}
|
package/src/provenance.ts
CHANGED
|
@@ -297,6 +297,6 @@ export interface BuildParamProvenance {
|
|
|
297
297
|
name: string;
|
|
298
298
|
/** The resolved, type-coerced value actually bound to `params.<name>` for this build. */
|
|
299
299
|
value: string | number | boolean;
|
|
300
|
-
/** Which input supplied the value. */
|
|
301
|
-
source: "cli" | "params-file" | "env" | "default"
|
|
300
|
+
/** Which input supplied the value: `flag:<name>` names the workspace flag record it came from (#4024). */
|
|
301
|
+
source: "cli" | "params-file" | "env" | "default" | `flag:${string}`;
|
|
302
302
|
}
|
package/src/run-event-store.ts
CHANGED
|
@@ -8,13 +8,14 @@
|
|
|
8
8
|
* as `--events-json` prints them. It sits beside the in-flight records
|
|
9
9
|
* (`./op/run-live.ts`), outside the working tree and off `chant/lifecycle`:
|
|
10
10
|
* the run ledger's growth is #4036's concern and these files add none to it.
|
|
11
|
-
* Unlike the in-flight record they are kept after the run settles
|
|
12
|
-
*
|
|
11
|
+
* Unlike the in-flight record they are kept after the run settles, until
|
|
12
|
+
* `chant lifecycle gc --yes` deletes the ones its retention policy lets go
|
|
13
|
+
* (#4036, {@link planRunEventPrune}).
|
|
13
14
|
*
|
|
14
15
|
* Every write is best-effort. A run whose events cannot be kept still runs.
|
|
15
16
|
*/
|
|
16
17
|
|
|
17
|
-
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
18
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
18
19
|
import { join } from "node:path";
|
|
19
20
|
import { gitCommonDir } from "./op/run-live";
|
|
20
21
|
import type { RunEvent } from "./run-event";
|
|
@@ -211,3 +212,62 @@ export async function readRunEvents(
|
|
|
211
212
|
}
|
|
212
213
|
}
|
|
213
214
|
}
|
|
215
|
+
|
|
216
|
+
/** One run's event file, as retention sees it (#4036). */
|
|
217
|
+
export interface RunEventFile {
|
|
218
|
+
file: string;
|
|
219
|
+
bytes: number;
|
|
220
|
+
/** The file's last write, ISO-8601. */
|
|
221
|
+
modified: string;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** What {@link planRunEventPrune} would delete and keep. */
|
|
225
|
+
export interface RunEventPrune {
|
|
226
|
+
/** The directory read, or null outside a checkout. */
|
|
227
|
+
dir: string | null;
|
|
228
|
+
remove: RunEventFile[];
|
|
229
|
+
keep: RunEventFile[];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Which run event files the policy lets go (#4036): every file older than
|
|
234
|
+
* `days` by its last write, apart from the newest `keep`. A run still going
|
|
235
|
+
* writes its file as it goes, so it is never older than the window. Reads
|
|
236
|
+
* only; {@link pruneRunEvents} deletes.
|
|
237
|
+
*/
|
|
238
|
+
export async function planRunEventPrune(cwd: string, policy: { days: number; keep: number }, now: Date): Promise<RunEventPrune> {
|
|
239
|
+
const dir = await runEventsDir(cwd);
|
|
240
|
+
if (!dir || !existsSync(dir)) return { dir, remove: [], keep: [] };
|
|
241
|
+
const files: Array<RunEventFile & { ms: number }> = [];
|
|
242
|
+
for (const entry of readdirSync(dir)) {
|
|
243
|
+
if (!entry.endsWith(".jsonl")) continue;
|
|
244
|
+
const file = join(dir, entry);
|
|
245
|
+
try {
|
|
246
|
+
const st = statSync(file);
|
|
247
|
+
if (!st.isFile()) continue;
|
|
248
|
+
files.push({ file, bytes: st.size, modified: new Date(st.mtimeMs).toISOString(), ms: st.mtimeMs });
|
|
249
|
+
} catch {
|
|
250
|
+
// Gone between the listing and the stat.
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
files.sort((a, b) => b.ms - a.ms || (a.file < b.file ? -1 : 1));
|
|
254
|
+
const cutoff = now.getTime() - policy.days * 86_400_000;
|
|
255
|
+
const remove: RunEventFile[] = [];
|
|
256
|
+
const keep: RunEventFile[] = [];
|
|
257
|
+
files.forEach(({ ms, ...f }, i) => (i >= policy.keep && ms < cutoff ? remove : keep).push(f));
|
|
258
|
+
return { dir, remove, keep };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Delete the files a plan lets go. Returns how many were deleted; a file already gone is not counted. */
|
|
262
|
+
export function pruneRunEvents(plan: RunEventPrune): number {
|
|
263
|
+
let n = 0;
|
|
264
|
+
for (const f of plan.remove) {
|
|
265
|
+
try {
|
|
266
|
+
rmSync(f.file);
|
|
267
|
+
n++;
|
|
268
|
+
} catch {
|
|
269
|
+
// Already gone.
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return n;
|
|
273
|
+
}
|
package/src/run-event-stream.ts
CHANGED
|
@@ -46,7 +46,8 @@ const GATE_PREFIX = "gate:";
|
|
|
46
46
|
/**
|
|
47
47
|
* A run-event body as the older `RunProgressEvent`, or undefined when that
|
|
48
48
|
* stream had no such event. It never printed a gate step, a skipped step,
|
|
49
|
-
* a phase outside a component
|
|
49
|
+
* a phase outside a component, a gate's decision or a component the run did
|
|
50
|
+
* not start (`blocked`, `already-applied`, #4007), so those map to nothing
|
|
50
51
|
* and `--progress-json` prints what it printed before #4041.
|
|
51
52
|
*/
|
|
52
53
|
export function progressOfRunEvent(body: RunEventBody): RunProgressEvent | undefined {
|
|
@@ -79,6 +80,8 @@ export function progressOfRunEvent(body: RunEventBody): RunProgressEvent | undef
|
|
|
79
80
|
? undefined
|
|
80
81
|
: { type: "phase-done", component: body.component, phase: body.phase, status: progressStatus(body.status) };
|
|
81
82
|
case "component-settle":
|
|
83
|
+
// A component the run did not start (#4007) was never in the older stream.
|
|
84
|
+
if (body.status === "blocked" || body.status === "already-applied") return undefined;
|
|
82
85
|
return { type: "component-done", wave: body.wave, component: body.component, status: progressStatus(body.status) };
|
|
83
86
|
case "wave-settle":
|
|
84
87
|
return { type: "wave-done", wave: body.wave, status: progressStatus(body.status) };
|
package/src/run-event.ts
CHANGED
|
@@ -126,11 +126,31 @@ export interface RunEventComponentStart {
|
|
|
126
126
|
component: string;
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
/**
|
|
130
|
+
* How a component settled: a {@link RunEventSettleStatus}, or one of two
|
|
131
|
+
* statuses for a component the run did not start (#4007), added within
|
|
132
|
+
* contract 1 by chant 0.124.29. `blocked`: a dependency failed or stopped at
|
|
133
|
+
* a gate, so it never ran. `already-applied`: an earlier attempt that this
|
|
134
|
+
* run resumed applied it. Neither has a `component-start` before it. A reader
|
|
135
|
+
* that knows only the four settle statuses reads either as a component that
|
|
136
|
+
* did not run in this run.
|
|
137
|
+
*/
|
|
138
|
+
export type RunEventComponentSettleStatus = RunEventSettleStatus | "blocked" | "already-applied";
|
|
139
|
+
|
|
140
|
+
/** The first chant that writes `blocked`, `already-applied` and `blockedBy` on `component-settle` (#4007). */
|
|
141
|
+
export const RUN_EVENT_COMPONENT_SKIP_FLOOR = "0.124.29";
|
|
142
|
+
|
|
129
143
|
export interface RunEventComponentSettle {
|
|
130
144
|
type: "component-settle";
|
|
145
|
+
/**
|
|
146
|
+
* 1-based, into `run-start`'s `waves`. On an `already-applied` settle it is
|
|
147
|
+
* the wave the component had in the derivation the earlier attempt ran.
|
|
148
|
+
*/
|
|
131
149
|
wave: number;
|
|
132
150
|
component: string;
|
|
133
|
-
status:
|
|
151
|
+
status: RunEventComponentSettleStatus;
|
|
152
|
+
/** With `blocked`: the failed or gated component the run reached this one from. */
|
|
153
|
+
blockedBy?: string;
|
|
134
154
|
}
|
|
135
155
|
|
|
136
156
|
/** An Op's phase, a component composition's phase, or a pipeline's stage. `component` is null outside a component run. */
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A workspace with feature flags (#4024), for the flag tests: a throwaway git
|
|
3
|
+
* repository with a decision kind, the reference workspace's work kind, and
|
|
4
|
+
* the canonical flag kind chant ships at `reference-kinds/flag`.
|
|
5
|
+
*
|
|
6
|
+
* - The decision kind closes `confirmed` and `superseded`. d-001 is
|
|
7
|
+
* confirmed, d-002 is only decided.
|
|
8
|
+
* - W-001 implements d-001, so its work is settled. W-002 implements d-002.
|
|
9
|
+
* - The flag `old-banner` hides W-001, so it is due for removal (stale).
|
|
10
|
+
* - The flag `new-checkout` hides W-002 and backs the build parameter
|
|
11
|
+
* `newCheckout` of member `shop`: on in dev, off in prod, and no value in
|
|
12
|
+
* staging.
|
|
13
|
+
* - Member `shop` has a component whose deploy declares a gate, so every
|
|
14
|
+
* environment it builds for has one. Member `blog` has a component with
|
|
15
|
+
* no gate.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { readFileSync } from "node:fs";
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
import { git, repo } from "./contract-repo";
|
|
21
|
+
|
|
22
|
+
const HERE = import.meta.dirname;
|
|
23
|
+
const REPO_ROOT = join(HERE, "..", "..", "..", "..", "..");
|
|
24
|
+
const REF = join(REPO_ROOT, "reference-workspace");
|
|
25
|
+
const ref = (path: string) => readFileSync(join(REF, path), "utf-8");
|
|
26
|
+
const canonical = (file: string) => readFileSync(join(HERE, "..", "reference-kinds", "flag", file), "utf-8");
|
|
27
|
+
|
|
28
|
+
const DECISION_KIND = `export const recordKind = {
|
|
29
|
+
name: "decision",
|
|
30
|
+
location: { dir: ".", match: "^d-[0-9]{3}-.+\\\\.md$" },
|
|
31
|
+
format: "markdown-front-matter",
|
|
32
|
+
schema: { id: "urn:test:decision:1", path: "decision.schema.json" },
|
|
33
|
+
idField: "id",
|
|
34
|
+
stateField: "state",
|
|
35
|
+
states: ["proposed", "decided", "confirmed", "superseded"],
|
|
36
|
+
closedStates: ["confirmed", "superseded"],
|
|
37
|
+
approval: { proposed: 0, decided: 1, confirmed: 2, superseded: 2 },
|
|
38
|
+
supersedes: { field: "supersedes" },
|
|
39
|
+
};
|
|
40
|
+
`;
|
|
41
|
+
|
|
42
|
+
const DECISION_SCHEMA = JSON.stringify(
|
|
43
|
+
{
|
|
44
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
45
|
+
$id: "urn:test:decision:1",
|
|
46
|
+
type: "object",
|
|
47
|
+
required: ["id", "state"],
|
|
48
|
+
properties: { id: { type: "string" }, state: { type: "string" }, supersedes: { type: "array", items: { type: "string" } } },
|
|
49
|
+
},
|
|
50
|
+
null,
|
|
51
|
+
2,
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const decision = (id: string, state: string) => `---\nid: "${id}"\nstate: "${state}"\nsupersedes: []\n---\n\n# ${id}\n`;
|
|
55
|
+
|
|
56
|
+
export function workItem(id: string, implementsIds: string[], state = "in-progress"): string {
|
|
57
|
+
const data = {
|
|
58
|
+
schema: 1,
|
|
59
|
+
id,
|
|
60
|
+
title: `Work ${id}`,
|
|
61
|
+
state,
|
|
62
|
+
implements: implementsIds,
|
|
63
|
+
needs: [],
|
|
64
|
+
constrains: ["member:shop"],
|
|
65
|
+
evidence: [],
|
|
66
|
+
owner: "alice",
|
|
67
|
+
opened_on: "2026-10-01",
|
|
68
|
+
...(state === "done" ? { closed_on: "2026-10-10" } : {}),
|
|
69
|
+
source: { kind: "workspace", member: "shop" },
|
|
70
|
+
supersedes: [],
|
|
71
|
+
};
|
|
72
|
+
return `---\n${JSON.stringify(data, null, 2)}\n---\n\n# ${id}\n`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function flagRecord(id: string, fields: { work: string; member: string; param: string; values: Record<string, string | number | boolean>; owner?: string }): string {
|
|
76
|
+
const data = { schema: 1, id, owner: fields.owner ?? "alice", work: fields.work, member: fields.member, param: fields.param, values: fields.values };
|
|
77
|
+
return `---\n${JSON.stringify(data, null, 2)}\n---\n\n# ${id}\n`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** A component file whose deploy declares a gate when `gated` is set. */
|
|
81
|
+
export function componentFile(name: string, gated: boolean): string {
|
|
82
|
+
const steps = gated ? `[{ kind: "gate", gate: "prod-release" }, { kind: "apply", template: "dist/${name}.json" }]` : `[{ kind: "apply", template: "dist/${name}.json" }]`;
|
|
83
|
+
return `export const ${name} = { name: "${name}", dependsOn: [], deploy: [{ phase: "Apply", steps: ${steps} }] };\n`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The flag workspace's files, with `overrides` laid over them (a null value removes a file). */
|
|
87
|
+
export function flagWorkspaceFiles(overrides: Record<string, string | null> = {}): Record<string, string> {
|
|
88
|
+
const files: Record<string, string | null> = {
|
|
89
|
+
"chant.workspace.json": JSON.stringify(
|
|
90
|
+
{
|
|
91
|
+
name: "flags",
|
|
92
|
+
schema: 1,
|
|
93
|
+
members: [
|
|
94
|
+
{ name: "shop", dir: "shop", kind: "chant" },
|
|
95
|
+
{ name: "blog", dir: "blog", kind: "chant" },
|
|
96
|
+
],
|
|
97
|
+
records: [{ kind: "decisions/decision.kind.mjs" }, { kind: "work/work.kind.mjs" }, { kind: "flags/flag.kind.mjs" }],
|
|
98
|
+
},
|
|
99
|
+
null,
|
|
100
|
+
2,
|
|
101
|
+
),
|
|
102
|
+
"shop/chant.config.ts": `export default { environments: ["dev", "staging", "prod"] };\n`,
|
|
103
|
+
"shop/shop.component.ts": componentFile("shop", true),
|
|
104
|
+
"blog/chant.config.ts": `export default { environments: ["dev", "prod"] };\n`,
|
|
105
|
+
"blog/blog.component.ts": componentFile("blog", false),
|
|
106
|
+
"decisions/decision.kind.mjs": DECISION_KIND,
|
|
107
|
+
"decisions/decision.schema.json": DECISION_SCHEMA,
|
|
108
|
+
"decisions/d-001-the-banner.md": decision("d-001", "confirmed"),
|
|
109
|
+
"decisions/d-002-the-checkout.md": decision("d-002", "decided"),
|
|
110
|
+
"work/work.kind.mjs": ref("work/work.kind.mjs").replace(' answers: "../answers/answer.kind.mjs",\n', ""),
|
|
111
|
+
"work/work.schema.json": ref("work/work.schema.json"),
|
|
112
|
+
"work/W-001-the-banner.md": workItem("W-001", ["d-001"], "done"),
|
|
113
|
+
"work/W-002-the-checkout.md": workItem("W-002", ["d-002"]),
|
|
114
|
+
"flags/flag.kind.mjs": canonical("flag.kind.mjs"),
|
|
115
|
+
"flags/flag.schema.json": canonical("flag.schema.json"),
|
|
116
|
+
"flags/old-banner.flag.md": flagRecord("old-banner", { work: "W-001", member: "blog", param: "banner", values: { dev: true, prod: true } }),
|
|
117
|
+
"flags/new-checkout.flag.md": flagRecord("new-checkout", { work: "W-002", member: "shop", param: "newCheckout", values: { dev: true, prod: false } }),
|
|
118
|
+
...overrides,
|
|
119
|
+
};
|
|
120
|
+
return Object.fromEntries(Object.entries(files).filter((e): e is [string, string] => e[1] !== null));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** The flag workspace in a new git repository, committed. */
|
|
124
|
+
export function flagWorkspace(overrides: Record<string, string | null> = {}): string {
|
|
125
|
+
const root = repo(flagWorkspaceFiles(overrides));
|
|
126
|
+
git(root, "config", "user.name", "t");
|
|
127
|
+
git(root, "config", "user.email", "t@example.com");
|
|
128
|
+
git(root, "config", "commit.gpgsign", "false");
|
|
129
|
+
git(root, "add", "-A");
|
|
130
|
+
git(root, "commit", "-q", "-m", "flags");
|
|
131
|
+
return root;
|
|
132
|
+
}
|