@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,363 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retention's garbage collection (#4036) against temp repos: each never-prune
|
|
3
|
+
* rule, the window, the prune commit and what it leaves alone.
|
|
4
|
+
*
|
|
5
|
+
* Records are written now, and the GC is planned at `NOW`, a month later, so
|
|
6
|
+
* what a test writes is old unless a timestamp it sets says otherwise. They
|
|
7
|
+
* are written as the stores write them, in one commit per test to keep it
|
|
8
|
+
* fast; the stores' own writers are tested beside them.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, test, expect, beforeEach, afterEach } from "vitest";
|
|
12
|
+
import { withTestDir } from "@intentius/chant-test-utils";
|
|
13
|
+
import { spawnSync } from "node:child_process";
|
|
14
|
+
import { mkdirSync, mkdtempSync, readdirSync, rmSync, utimesSync, writeFileSync } from "node:fs";
|
|
15
|
+
import { tmpdir } from "node:os";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import { contentDigest } from "../content-digest";
|
|
18
|
+
import { makeRelease, type Release } from "./release-store";
|
|
19
|
+
import type { ReleaseRecord } from "./release-ledger";
|
|
20
|
+
import type { GateResolutionInput, PendingGateInput } from "./gate-ledger";
|
|
21
|
+
import { writeLedgerFiles, RefCASConflictError } from "./git";
|
|
22
|
+
import { digestKey, planLifecycleGc, runLifecycleGc, splitLedgerPath, type GcInput } from "./retention";
|
|
23
|
+
import { DEFAULT_RETENTION, resolveRetention, type RetentionPolicy } from "./retention-policy";
|
|
24
|
+
import { planRunEventPrune, pruneRunEvents, RUN_EVENTS_DIR_ENV } from "../run-event-store";
|
|
25
|
+
|
|
26
|
+
const DAY = 86_400_000;
|
|
27
|
+
const NOW = new Date(Date.now() + 30 * DAY);
|
|
28
|
+
const recent = (daysBefore = 1) => new Date(NOW.getTime() - daysBefore * DAY).toISOString();
|
|
29
|
+
const old = new Date(Date.now() - DAY).toISOString();
|
|
30
|
+
|
|
31
|
+
/** Keep nothing by count, so only the rules and the window keep anything. */
|
|
32
|
+
const STRICT: RetentionPolicy = { changeSets: { days: 7, keep: 0 }, releases: { days: 7, keep: 0 }, runEvents: { days: 7, keep: 0 } };
|
|
33
|
+
|
|
34
|
+
function git(dir: string, ...args: string[]): string {
|
|
35
|
+
const r = spawnSync("git", args, { cwd: dir, encoding: "utf-8" });
|
|
36
|
+
if (r.status !== 0) throw new Error(`git ${args.join(" ")}: ${r.stderr}`);
|
|
37
|
+
return r.stdout.trim();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function initRepo(dir: string): void {
|
|
41
|
+
git(dir, "init", "-q", "-b", "main");
|
|
42
|
+
git(dir, "config", "user.email", "t@chant.dev");
|
|
43
|
+
git(dir, "config", "user.name", "T");
|
|
44
|
+
writeFileSync(join(dir, "README.md"), "x\n");
|
|
45
|
+
git(dir, "add", ".");
|
|
46
|
+
git(dir, "commit", "-q", "-m", "init");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Files to write, flushed in one commit before a plan reads the branch. */
|
|
50
|
+
const buffered = new Map<string, string>();
|
|
51
|
+
const append = (path: string, line: object) => buffered.set(path, `${buffered.get(path) ?? ""}${JSON.stringify(line)}\n`);
|
|
52
|
+
async function flush(dir: string): Promise<void> {
|
|
53
|
+
if (buffered.size === 0) return;
|
|
54
|
+
const files = [...buffered].map(([path, content]) => ({ path, content }));
|
|
55
|
+
buffered.clear();
|
|
56
|
+
await writeLedgerFiles(files, "fixture", { cwd: dir });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let n = 0;
|
|
60
|
+
/** A release with one template, stored and cut, as `persistRelease` and `appendCut` write it. */
|
|
61
|
+
function release(_dir: string, opts: { template?: string; cutAt?: string; commit?: string } = {}): Release {
|
|
62
|
+
const tpl = opts.template ?? `kind: Deployment # ${++n}\n`;
|
|
63
|
+
const digest = contentDigest(tpl);
|
|
64
|
+
const r = makeRelease({
|
|
65
|
+
member: null,
|
|
66
|
+
commit: opts.commit ?? `${String(++n).padStart(4, "0")}${"a".repeat(36)}`,
|
|
67
|
+
artifactDigests: [{ component: "api", kind: "image", path: "api.tar", digest: `sha256:${String(n).padStart(64, "0")}` }],
|
|
68
|
+
targets: [{ env: "prod", templates: [{ component: "api", path: "api.yaml", digest }], params: [] }],
|
|
69
|
+
});
|
|
70
|
+
buffered.set(releasePath(r), `${JSON.stringify(r)}\n`);
|
|
71
|
+
buffered.set(templatePath(r), tpl);
|
|
72
|
+
append("_releases/cuts.jsonl", { version: 1, releaseDigest: r.releaseDigest, label: null, actor: "a", timestamp: opts.cutAt ?? old, runOrigin: null, signature: null });
|
|
73
|
+
return r;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const appendReleaseRecord = (record: Omit<ReleaseRecord, "version">, _opts?: unknown) => append(`${record.env}/releases.jsonl`, { version: 1, ...record });
|
|
77
|
+
const appendGateResolution = (r: GateResolutionInput, _opts?: unknown) => append(`_gates/${r.op}.jsonl`, { version: 1, kind: "resolution", ...r });
|
|
78
|
+
const appendPendingGate = (p: PendingGateInput, _opts?: unknown) => append(`_gates/${p.op}.jsonl`, { version: 1, kind: "pending", ...p });
|
|
79
|
+
|
|
80
|
+
const changeSetDigest = (i: number) => `jcs1-sha256:${i.toString(16).padStart(64, "0")}`;
|
|
81
|
+
function storeChangeSet(_dir: string, digest: string, prefix = ""): string {
|
|
82
|
+
const path = `${prefix}_change-sets/${digest.replace(/:/g, "_")}.json`;
|
|
83
|
+
buffered.set(path, `${JSON.stringify({ digest })}\n`);
|
|
84
|
+
return path;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function gcPlan(i: GcInput): Promise<ReturnType<typeof planLifecycleGc>> {
|
|
88
|
+
await flush(i.cwd);
|
|
89
|
+
return planLifecycleGc(i);
|
|
90
|
+
}
|
|
91
|
+
async function gcRun(i: Parameters<typeof runLifecycleGc>[0]): ReturnType<typeof runLifecycleGc> {
|
|
92
|
+
await flush(i.cwd);
|
|
93
|
+
return runLifecycleGc(i);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const input = (dir: string, policy: RetentionPolicy = STRICT, extra: Partial<GcInput> = {}): GcInput => ({ cwd: dir, policy, now: NOW, ...extra });
|
|
97
|
+
const removed = (plan: ReturnType<typeof planLifecycleGc>) => plan.remove.map((f) => f.path);
|
|
98
|
+
const kept = (plan: ReturnType<typeof planLifecycleGc>, path: string) => plan.keep.find((f) => f.path === path)?.reasons.map((r) => r.code);
|
|
99
|
+
const releasePath = (r: Release, prefix = "") => `${prefix}_releases/${r.releaseDigest.replace(/:/g, "_")}.json`;
|
|
100
|
+
const templatePath = (r: Release, prefix = "") => `${prefix}_releases/templates/${r.targets[0].templates[0].digest.replace(/:/g, "_")}.txt`;
|
|
101
|
+
|
|
102
|
+
let eventsDir: string;
|
|
103
|
+
let priorEventsDir: string | undefined;
|
|
104
|
+
beforeEach(() => {
|
|
105
|
+
// GC deletes run event files: point it at a directory of this test's own.
|
|
106
|
+
priorEventsDir = process.env[RUN_EVENTS_DIR_ENV];
|
|
107
|
+
eventsDir = mkdtempSync(join(tmpdir(), "chant-gc-events-"));
|
|
108
|
+
process.env[RUN_EVENTS_DIR_ENV] = eventsDir;
|
|
109
|
+
});
|
|
110
|
+
afterEach(() => {
|
|
111
|
+
if (priorEventsDir === undefined) delete process.env[RUN_EVENTS_DIR_ENV];
|
|
112
|
+
else process.env[RUN_EVENTS_DIR_ENV] = priorEventsDir;
|
|
113
|
+
rmSync(eventsDir, { recursive: true, force: true });
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe("never-prune rules", () => {
|
|
117
|
+
test("a release an environment runs is kept with its templates, by releaseDigest or by the admission's resolution", async () => {
|
|
118
|
+
await withTestDir(async (dir) => {
|
|
119
|
+
initRepo(dir);
|
|
120
|
+
const running = await release(dir);
|
|
121
|
+
const viaAdmission = await release(dir);
|
|
122
|
+
const superseded = await release(dir);
|
|
123
|
+
const unused = await release(dir);
|
|
124
|
+
const rec = (component: string, timestamp: string, extra: object) =>
|
|
125
|
+
appendReleaseRecord({ component, env: "prod", digest: "sha256:1", gitSha: "c".repeat(40), runId: `r-${timestamp}`, timestamp, actor: "ci", ...extra }, { cwd: dir });
|
|
126
|
+
// api ran `superseded`, then `running`: only the latest record counts.
|
|
127
|
+
await rec("api", "2026-10-01T00:00:00.000Z", { releaseDigest: superseded.releaseDigest });
|
|
128
|
+
await rec("api", "2026-10-02T00:00:00.000Z", { releaseDigest: running.releaseDigest });
|
|
129
|
+
// web's latest record names no release, and its admission's release gate resolution does.
|
|
130
|
+
await appendGateResolution({ op: "release", gate: "ship", resolvedBy: "bob", timestamp: "2026-10-03T00:00:00.000Z", planDigest: changeSetDigest(1), releaseDigest: viaAdmission.releaseDigest, environment: "prod" }, { cwd: dir });
|
|
131
|
+
await rec("web", "2026-10-03T00:00:01.000Z", { admission: { op: "release", gate: "ship", resolvedAt: "2026-10-03T00:00:00.000Z" } });
|
|
132
|
+
|
|
133
|
+
const plan = await gcPlan(input(dir));
|
|
134
|
+
expect(kept(plan, releasePath(running))).toEqual(["deployed"]);
|
|
135
|
+
expect(kept(plan, templatePath(running))).toEqual(["kept-release"]);
|
|
136
|
+
expect(kept(plan, releasePath(viaAdmission))).toEqual(["deployed"]);
|
|
137
|
+
expect(removed(plan)).toEqual(expect.arrayContaining([releasePath(superseded), templatePath(superseded), releasePath(unused), templatePath(unused)]));
|
|
138
|
+
expect(plan.remove).toHaveLength(4);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("what a pending gate binds is kept until its fact expires", async () => {
|
|
143
|
+
await withTestDir(async (dir) => {
|
|
144
|
+
initRepo(dir);
|
|
145
|
+
const bound = await release(dir);
|
|
146
|
+
const lapsed = await release(dir);
|
|
147
|
+
const live = await storeChangeSet(dir, changeSetDigest(10));
|
|
148
|
+
const expired = await storeChangeSet(dir, changeSetDigest(11));
|
|
149
|
+
await appendPendingGate({ op: "pr-12", gate: "review", timestamp: old, expiresAt: new Date(NOW.getTime() + DAY).toISOString(), planDigest: changeSetDigest(10) }, { cwd: dir });
|
|
150
|
+
await appendPendingGate({ op: "release", gate: "ship", timestamp: old, expiresAt: new Date(NOW.getTime() + DAY).toISOString(), planDigest: changeSetDigest(12), releaseDigest: bound.releaseDigest, environment: "prod" }, { cwd: dir });
|
|
151
|
+
// Expired a day before NOW: binds nothing, and its fact is too old to keep the change set in the window.
|
|
152
|
+
await appendPendingGate({ op: "pr-13", gate: "review", timestamp: old, expiresAt: recent(1), planDigest: changeSetDigest(11) }, { cwd: dir });
|
|
153
|
+
await appendPendingGate({ op: "release", gate: "ship", timestamp: old, expiresAt: recent(1), releaseDigest: lapsed.releaseDigest, environment: "staging" }, { cwd: dir });
|
|
154
|
+
|
|
155
|
+
const plan = await gcPlan(input(dir));
|
|
156
|
+
expect(kept(plan, live)).toEqual(["pending-gate"]);
|
|
157
|
+
expect(kept(plan, releasePath(bound))).toEqual(["pending-gate"]);
|
|
158
|
+
expect(kept(plan, templatePath(bound))).toEqual(["kept-release"]);
|
|
159
|
+
expect(removed(plan)).toEqual(expect.arrayContaining([expired, releasePath(lapsed)]));
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("a release is promotable within the window or among the newest of its member, and never when its commit is revoked", async () => {
|
|
164
|
+
await withTestDir(async (dir) => {
|
|
165
|
+
initRepo(dir);
|
|
166
|
+
const oldest = await release(dir, { cutAt: "2026-01-01T00:00:00.000Z" });
|
|
167
|
+
const older = await release(dir, { cutAt: "2026-01-02T00:00:00.000Z" });
|
|
168
|
+
const newer = await release(dir, { cutAt: "2026-01-03T00:00:00.000Z" });
|
|
169
|
+
const fresh = await release(dir, { cutAt: recent(2) });
|
|
170
|
+
const revokedFresh = await release(dir, { cutAt: recent(1), commit: "f".repeat(40) });
|
|
171
|
+
const revokedRunning = await release(dir, { commit: "e".repeat(40) });
|
|
172
|
+
await appendReleaseRecord({ component: "api", env: "prod", digest: "sha256:1", gitSha: "e".repeat(40), runId: "r1", timestamp: old, actor: "ci", releaseDigest: revokedRunning.releaseDigest }, { cwd: dir });
|
|
173
|
+
|
|
174
|
+
const policy = { ...STRICT, releases: { days: 7, keep: 2 } };
|
|
175
|
+
const plan = await gcPlan(input(dir, policy, { revokedCommits: new Set(["f".repeat(40), "e".repeat(40)]) }));
|
|
176
|
+
// The newest two promotable are `fresh` and `newer`; revoked ones don't count toward keep.
|
|
177
|
+
expect(kept(plan, releasePath(fresh))).toEqual(["newest", "within-window"]);
|
|
178
|
+
expect(kept(plan, releasePath(newer))).toEqual(["newest"]);
|
|
179
|
+
expect(kept(plan, releasePath(revokedRunning))).toEqual(["deployed"]);
|
|
180
|
+
expect(removed(plan)).toEqual(expect.arrayContaining([releasePath(oldest), releasePath(older), releasePath(revokedFresh)]));
|
|
181
|
+
expect(plan.remove.filter((f) => f.kind === "release")).toHaveLength(3);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("a change set an approval or empty resolution names is kept while its release is kept", async () => {
|
|
186
|
+
await withTestDir(async (dir) => {
|
|
187
|
+
initRepo(dir);
|
|
188
|
+
const keptRelease = await release(dir, { cutAt: recent(1) });
|
|
189
|
+
const prunedRelease = await release(dir);
|
|
190
|
+
const approved = await storeChangeSet(dir, changeSetDigest(20));
|
|
191
|
+
const empty = await storeChangeSet(dir, changeSetDigest(21));
|
|
192
|
+
const orphaned = await storeChangeSet(dir, changeSetDigest(22));
|
|
193
|
+
const res = (planDigest: string, releaseDigest: string, extra: object = {}) =>
|
|
194
|
+
appendGateResolution({ op: "release", gate: "ship", resolvedBy: "bob", timestamp: old, planDigest, releaseDigest, environment: "prod", ...extra }, { cwd: dir });
|
|
195
|
+
await res(changeSetDigest(20), keptRelease.releaseDigest);
|
|
196
|
+
await res(changeSetDigest(21), keptRelease.releaseDigest, { empty: true });
|
|
197
|
+
await res(changeSetDigest(22), prunedRelease.releaseDigest);
|
|
198
|
+
|
|
199
|
+
const plan = await gcPlan(input(dir));
|
|
200
|
+
expect(kept(plan, approved)).toEqual(["kept-release"]);
|
|
201
|
+
expect(kept(plan, empty)).toEqual(["kept-release"]);
|
|
202
|
+
expect(removed(plan)).toEqual(expect.arrayContaining([orphaned, releasePath(prunedRelease)]));
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("a template two releases share stays while either is kept", async () => {
|
|
207
|
+
await withTestDir(async (dir) => {
|
|
208
|
+
initRepo(dir);
|
|
209
|
+
const shared = "kind: Service\n";
|
|
210
|
+
const a = await release(dir, { template: shared, cutAt: recent(1) });
|
|
211
|
+
const b = await release(dir, { template: shared });
|
|
212
|
+
const plan = await gcPlan(input(dir));
|
|
213
|
+
expect(removed(plan)).toEqual([releasePath(b)]);
|
|
214
|
+
expect(kept(plan, templatePath(a))).toEqual(["kept-release"]);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("a release file that can't be read is kept, and so is every template of its member", async () => {
|
|
219
|
+
await withTestDir(async (dir) => {
|
|
220
|
+
initRepo(dir);
|
|
221
|
+
const r = await release(dir);
|
|
222
|
+
await writeLedgerFiles([{ path: "_releases/jcs1-sha256_" + "9".repeat(64) + ".json", content: "{", expectPriorSha: null }], "broken", { cwd: dir });
|
|
223
|
+
const plan = await gcPlan(input(dir));
|
|
224
|
+
expect(removed(plan)).toEqual([releasePath(r)]);
|
|
225
|
+
expect(kept(plan, templatePath(r))).toEqual(["unreadable"]);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe("the window", () => {
|
|
231
|
+
test("a change set stays for changeSets.days after it was stored or last named by a gate fact", async () => {
|
|
232
|
+
await withTestDir(async (dir) => {
|
|
233
|
+
initRepo(dir);
|
|
234
|
+
const stale = await storeChangeSet(dir, changeSetDigest(30));
|
|
235
|
+
const named = await storeChangeSet(dir, changeSetDigest(31));
|
|
236
|
+
await appendGateResolution({ op: "pr-31", gate: "review", resolvedBy: "bob", timestamp: recent(3), planDigest: changeSetDigest(31) }, { cwd: dir });
|
|
237
|
+
expect(removed(await gcPlan(input(dir)))).toEqual([stale]);
|
|
238
|
+
expect(kept(await gcPlan(input(dir)), named)).toEqual(["within-window"]);
|
|
239
|
+
// Planned today, both were stored within the window.
|
|
240
|
+
expect((await gcPlan({ ...input(dir), now: new Date() })).remove).toEqual([]);
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test("a digest written sha256:<hex> matches the file stored as jcs1-sha256_<hex>", () => {
|
|
245
|
+
expect(digestKey("sha256:" + "a".repeat(64))).toBe(digestKey("jcs1-sha256_" + "a".repeat(64)));
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
describe("the prune", () => {
|
|
250
|
+
test("members are read under their own prefix", async () => {
|
|
251
|
+
await withTestDir(async (dir) => {
|
|
252
|
+
initRepo(dir);
|
|
253
|
+
expect(splitLedgerPath("_members/api/_members/db/_releases/x.json")).toEqual({ prefix: "_members/api/_members/db/", member: "api/db", rest: "_releases/x.json" });
|
|
254
|
+
const prefix = "_members/api/";
|
|
255
|
+
const cs = await storeChangeSet(dir, changeSetDigest(40), prefix);
|
|
256
|
+
// The flat layout's pending fact for the same digest binds nothing in the member.
|
|
257
|
+
await appendPendingGate({ op: "pr-40", gate: "review", timestamp: old, expiresAt: new Date(NOW.getTime() + DAY).toISOString(), planDigest: changeSetDigest(40) }, { cwd: dir });
|
|
258
|
+
const plan = await gcPlan(input(dir));
|
|
259
|
+
expect(plan.remove.map((f) => [f.path, f.member])).toEqual([[cs, "api"]]);
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test("a prune is one new commit on the old tip that removes files; ledgers and history are untouched, and nothing is pushed", async () => {
|
|
264
|
+
await withTestDir(async (dir) => {
|
|
265
|
+
initRepo(dir);
|
|
266
|
+
const remote = mkdtempSync(join(tmpdir(), "chant-gc-remote-"));
|
|
267
|
+
try {
|
|
268
|
+
git(remote, "init", "-q", "--bare");
|
|
269
|
+
git(dir, "remote", "add", "origin", remote);
|
|
270
|
+
const r = await release(dir);
|
|
271
|
+
await appendReleaseRecord({ component: "api", env: "prod", digest: "sha256:1", gitSha: "c".repeat(40), runId: "r1", timestamp: old, actor: "ci" }, { cwd: dir });
|
|
272
|
+
const cs = await storeChangeSet(dir, changeSetDigest(50));
|
|
273
|
+
await flush(dir);
|
|
274
|
+
git(dir, "push", "-q", "origin", "chant/lifecycle");
|
|
275
|
+
const before = git(dir, "rev-parse", "chant/lifecycle");
|
|
276
|
+
const ledgers = () => git(dir, "ls-tree", "-r", "chant/lifecycle").split("\n").filter((l) => l.endsWith(".jsonl")).join("\n");
|
|
277
|
+
const ledgersBefore = ledgers();
|
|
278
|
+
|
|
279
|
+
const dry = await gcRun({ ...input(dir), dryRun: true });
|
|
280
|
+
expect(dry.lifecycle.commit).toBeNull();
|
|
281
|
+
expect(git(dir, "rev-parse", "chant/lifecycle")).toBe(before);
|
|
282
|
+
|
|
283
|
+
const report = await gcRun({ ...input(dir), dryRun: false });
|
|
284
|
+
const after = git(dir, "rev-parse", "chant/lifecycle");
|
|
285
|
+
expect(report.lifecycle.commit).toBe(after);
|
|
286
|
+
expect(git(dir, "rev-parse", `${after}^`)).toBe(before);
|
|
287
|
+
expect(git(dir, "log", "-1", "--format=%s", after)).toBe("Retention: remove 1 change set(s), 1 release(s) and 1 template(s)");
|
|
288
|
+
expect(git(dir, "ls-tree", "-r", "--name-only", "chant/lifecycle").split("\n").sort()).toEqual(["_releases/cuts.jsonl", "prod/releases.jsonl"]);
|
|
289
|
+
expect(ledgers()).toBe(ledgersBefore);
|
|
290
|
+
// History still holds every removed file.
|
|
291
|
+
expect(git(dir, "cat-file", "-p", `${before}:${cs}`)).toContain(changeSetDigest(50));
|
|
292
|
+
expect(git(dir, "cat-file", "-p", `${before}:${releasePath(r)}`)).toContain(r.releaseDigest);
|
|
293
|
+
expect(report.lifecycle.pushed).toBe(false);
|
|
294
|
+
expect(git(remote, "rev-parse", "chant/lifecycle")).toBe(before);
|
|
295
|
+
expect(report.lifecycle.after.ledgers).toEqual(report.lifecycle.before.ledgers);
|
|
296
|
+
expect(report.lifecycle.after.files).toBe(2);
|
|
297
|
+
|
|
298
|
+
// A second run finds nothing, and writes nothing.
|
|
299
|
+
const again = await gcRun({ ...input(dir), dryRun: false });
|
|
300
|
+
expect(again.lifecycle.remove).toEqual([]);
|
|
301
|
+
expect(again.lifecycle.commit).toBeNull();
|
|
302
|
+
|
|
303
|
+
// --push sends it as a fast-forward.
|
|
304
|
+
await storeChangeSet(dir, changeSetDigest(51));
|
|
305
|
+
const pushed = await gcRun({ ...input(dir), dryRun: false, push: true });
|
|
306
|
+
expect(pushed.lifecycle.pushed).toBe(true);
|
|
307
|
+
expect(git(remote, "rev-parse", "chant/lifecycle")).toBe(pushed.lifecycle.commit);
|
|
308
|
+
expect(git(remote, "merge-base", "--is-ancestor", before, "chant/lifecycle")).toBe("");
|
|
309
|
+
} finally {
|
|
310
|
+
rmSync(remote, { recursive: true, force: true });
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test("a removal against a moved tip is a conflict, and an emptied directory leaves the tree", async () => {
|
|
316
|
+
await withTestDir(async (dir) => {
|
|
317
|
+
initRepo(dir);
|
|
318
|
+
const cs = await storeChangeSet(dir, changeSetDigest(60));
|
|
319
|
+
await flush(dir);
|
|
320
|
+
const tip = git(dir, "rev-parse", "chant/lifecycle");
|
|
321
|
+
const sha = git(dir, "rev-parse", `chant/lifecycle:${cs}`);
|
|
322
|
+
await storeChangeSet(dir, changeSetDigest(61));
|
|
323
|
+
await flush(dir);
|
|
324
|
+
await expect(writeLedgerFiles([{ path: cs, remove: true, expectPriorSha: sha }], "gc", { cwd: dir, expectTip: tip })).rejects.toBeInstanceOf(RefCASConflictError);
|
|
325
|
+
const other = `_change-sets/${changeSetDigest(61).replace(/:/g, "_")}.json`;
|
|
326
|
+
await writeLedgerFiles([{ path: cs, remove: true, expectPriorSha: sha }, { path: other, remove: true }, { path: "_gates/x.jsonl", content: "{}\n" }], "gc", { cwd: dir });
|
|
327
|
+
expect(git(dir, "ls-tree", "-r", "--name-only", "chant/lifecycle")).toBe("_gates/x.jsonl");
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
describe("run events", () => {
|
|
333
|
+
test("files older than the window go, apart from the newest keep", async () => {
|
|
334
|
+
const at = (name: string, daysAgo: number) => {
|
|
335
|
+
const file = join(eventsDir, name);
|
|
336
|
+
writeFileSync(file, '{"runId":"x"}\n');
|
|
337
|
+
const t = (NOW.getTime() - daysAgo * DAY) / 1000;
|
|
338
|
+
utimesSync(file, t, t);
|
|
339
|
+
};
|
|
340
|
+
mkdirSync(eventsDir, { recursive: true });
|
|
341
|
+
at("a.jsonl", 1);
|
|
342
|
+
at("b.jsonl", 20);
|
|
343
|
+
at("c.jsonl", 30);
|
|
344
|
+
at("d.jsonl", 40);
|
|
345
|
+
writeFileSync(join(eventsDir, "notes.txt"), "not an event file\n");
|
|
346
|
+
const plan = await planRunEventPrune(eventsDir, { days: 7, keep: 2 }, NOW);
|
|
347
|
+
expect(plan.keep.map((f) => f.file.split("/").pop())).toEqual(["a.jsonl", "b.jsonl"]);
|
|
348
|
+
expect(plan.remove.map((f) => f.file.split("/").pop())).toEqual(["c.jsonl", "d.jsonl"]);
|
|
349
|
+
expect(pruneRunEvents(plan)).toBe(2);
|
|
350
|
+
expect(readdirSync(eventsDir).sort()).toEqual(["a.jsonl", "b.jsonl", "notes.txt"]);
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
describe("the policy", () => {
|
|
355
|
+
test("an override replaces only the fields it sets; changeSets takes no keep", () => {
|
|
356
|
+
expect(resolveRetention(null)).toEqual(DEFAULT_RETENTION);
|
|
357
|
+
expect(resolveRetention({ releases: { keep: 3 }, changeSets: { days: 30, keep: 9 } })).toEqual({
|
|
358
|
+
...DEFAULT_RETENTION,
|
|
359
|
+
changeSets: { days: 30, keep: 0 },
|
|
360
|
+
releases: { days: DEFAULT_RETENTION.releases.days, keep: 3 },
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
});
|