@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
package/src/build-params.test.ts
CHANGED
|
@@ -194,3 +194,49 @@ describe("buildParamValues", () => {
|
|
|
194
194
|
expect(buildParamValues([])).toEqual({});
|
|
195
195
|
});
|
|
196
196
|
});
|
|
197
|
+
|
|
198
|
+
describe("resolveBuildParams — workspace flags (#4024)", () => {
|
|
199
|
+
const defs: BuildParamsConfig = { newCheckout: { type: "boolean", default: false, env: "NEW_CHECKOUT" }, tier: { type: "string", default: "light" } };
|
|
200
|
+
const flags = (value: boolean | undefined, gated = false) => ({ env: "prod", byParam: { newCheckout: { flag: "new-checkout", value } }, ...(gated ? { gated: true } : {}) });
|
|
201
|
+
|
|
202
|
+
test("a flag's value for the environment is recorded with source flag:<name>, ahead of the default", () => {
|
|
203
|
+
const r = resolveBuildParams(defs, { flags: flags(true) });
|
|
204
|
+
expect(r.errors).toEqual([]);
|
|
205
|
+
expect(r.provenance).toEqual([
|
|
206
|
+
{ name: "newCheckout", value: true, source: "flag:new-checkout" },
|
|
207
|
+
{ name: "tier", value: "light", source: "default" },
|
|
208
|
+
]);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("a flag that gives the environment no value leaves the parameter at its default", () => {
|
|
212
|
+
expect(resolveBuildParams(defs, { flags: flags(undefined) }).provenance[0]).toEqual({ name: "newCheckout", value: false, source: "default" });
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("where the environment has no gate, --param, --params-file and the env var still win over the flag", () => {
|
|
216
|
+
expect(resolveBuildParams(defs, { cli: { newCheckout: "false" }, flags: flags(true) }).provenance[0].source).toBe("cli");
|
|
217
|
+
expect(resolveBuildParams(defs, { fromFile: { newCheckout: false }, flags: flags(true) }).provenance[0].source).toBe("params-file");
|
|
218
|
+
expect(resolveBuildParams(defs, { env: { NEW_CHECKOUT: "false" }, flags: flags(true) }).provenance[0].source).toBe("env");
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("where the environment has a gate, --param, --params-file and the env var can't set a flag-backed parameter", () => {
|
|
222
|
+
const viaCli = resolveBuildParams(defs, { cli: { newCheckout: "true" }, flags: flags(false, true) });
|
|
223
|
+
expect(viaCli.errors).toEqual([
|
|
224
|
+
'build parameter "newCheckout" is backed by the flag "new-checkout", and "prod" has a gate, so --param can\'t set it; change the flag record and let the change go through the gate',
|
|
225
|
+
]);
|
|
226
|
+
expect(viaCli.provenance.map((p) => p.name)).toEqual(["tier"]);
|
|
227
|
+
expect(resolveBuildParams(defs, { fromFile: { newCheckout: true }, flags: flags(false, true) }).errors[0]).toMatch(/so --params-file can't set it/);
|
|
228
|
+
expect(resolveBuildParams(defs, { env: { NEW_CHECKOUT: "true" }, flags: flags(false, true) }).errors[0]).toMatch(/so the env var NEW_CHECKOUT can't set it/);
|
|
229
|
+
// A parameter no flag backs is not affected.
|
|
230
|
+
expect(resolveBuildParams(defs, { cli: { tier: "big" }, flags: flags(false, true) }).errors).toEqual([]);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("a flag that sets a parameter the config does not declare is an error", () => {
|
|
234
|
+
const r = resolveBuildParams({}, { flags: flags(true) });
|
|
235
|
+
expect(r.errors).toEqual([`flag "new-checkout" sets build parameter "newCheckout", which chant.config.ts's buildParams does not declare`]);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("a flag value of the wrong type, or outside the enum, fails like any other source", () => {
|
|
239
|
+
const strict: BuildParamsConfig = { newCheckout: { type: "string", enum: ["a", "b"] } };
|
|
240
|
+
expect(resolveBuildParams(strict, { flags: { env: "prod", byParam: { newCheckout: { flag: "new-checkout", value: "c" } } } }).errors[0]).toMatch(/must be one of "a", "b", got "c"/);
|
|
241
|
+
});
|
|
242
|
+
});
|
package/src/build-params.ts
CHANGED
|
@@ -73,6 +73,28 @@ export interface BuildParamsInput {
|
|
|
73
73
|
fromFile?: Record<string, unknown>;
|
|
74
74
|
/** The process environment, consulted only for a parameter that declares an `env` mapping, and only once `cli`/`fromFile` have no value for it. */
|
|
75
75
|
env?: Record<string, string | undefined>;
|
|
76
|
+
/**
|
|
77
|
+
* The workspace flags that back parameters in this build's environment
|
|
78
|
+
* (#4024), read by `./flag-params.ts`. A flag-backed parameter takes the
|
|
79
|
+
* flag's value for the environment, recorded with `source: "flag:<id>"`,
|
|
80
|
+
* ahead of its `env` mapping and its default. Absent outside a workspace
|
|
81
|
+
* and without `--env`.
|
|
82
|
+
*/
|
|
83
|
+
flags?: BuildParamFlags;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The flags that back parameters in one environment (#4024). */
|
|
87
|
+
export interface BuildParamFlags {
|
|
88
|
+
/** The environment the build is for. */
|
|
89
|
+
env: string;
|
|
90
|
+
/** Each flag-backed parameter by name: the flag's name, and its value in `env` (undefined when the flag gives it none). */
|
|
91
|
+
byParam: Record<string, { flag: string; value: BuildParamValue | undefined }>;
|
|
92
|
+
/**
|
|
93
|
+
* The environment has a gate (#4024's precedence rule): a `--param`,
|
|
94
|
+
* `--params-file` or env-var value for a flag-backed parameter is refused,
|
|
95
|
+
* so no path flips a flag around the gate.
|
|
96
|
+
*/
|
|
97
|
+
gated?: boolean;
|
|
76
98
|
}
|
|
77
99
|
|
|
78
100
|
/** Result of resolving a project's declared parameters against one build invocation's inputs. */
|
|
@@ -118,7 +140,11 @@ function coerce(name: string, raw: BuildParamValue, def: BuildParamDef, errors:
|
|
|
118
140
|
* Resolve a project's declared build-time parameters against one build's
|
|
119
141
|
* supplied inputs. Precedence per parameter, most to least specific:
|
|
120
142
|
* `cli` (`--param name=value`) > `fromFile` (`--params-file`) > the
|
|
121
|
-
* parameter's own declared `env` mapping (only if set) >
|
|
143
|
+
* parameter's own declared `env` mapping (only if set) > a workspace flag's
|
|
144
|
+
* value for the build's environment (`input.flags`, #4024) > `def.default`.
|
|
145
|
+
* When `input.flags.gated` is set, a flag-backed parameter takes none of the
|
|
146
|
+
* first three: a value from any of them is a build error, so a flag can't be
|
|
147
|
+
* flipped around the environment's gate.
|
|
122
148
|
*
|
|
123
149
|
* A parameter with no declared `default` and no value from any source is a
|
|
124
150
|
* build error, not a silently-`undefined` value — a build-time parameter
|
|
@@ -153,9 +179,26 @@ export function resolveBuildParams(defs: BuildParamsConfig | undefined, input: B
|
|
|
153
179
|
}
|
|
154
180
|
}
|
|
155
181
|
|
|
182
|
+
const flags = input.flags;
|
|
183
|
+
for (const [name, binding] of Object.entries(flags?.byParam ?? {})) {
|
|
184
|
+
if (!(name in declared)) {
|
|
185
|
+
errors.push(`flag "${binding.flag}" sets build parameter "${name}", which chant.config.ts's buildParams does not declare`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
156
189
|
for (const [name, def] of Object.entries(declared)) {
|
|
157
190
|
let raw: BuildParamValue | undefined;
|
|
158
191
|
let source: BuildParamProvenance["source"] | undefined;
|
|
192
|
+
const binding = flags?.byParam[name];
|
|
193
|
+
|
|
194
|
+
if (binding && flags?.gated) {
|
|
195
|
+
// #4024: in an environment with a gate, the flag record is the only way to set the parameter.
|
|
196
|
+
const around = name in cli ? "--param" : name in fromFile ? "--params-file" : def.env && env[def.env] !== undefined ? `the env var ${def.env}` : null;
|
|
197
|
+
if (around !== null) {
|
|
198
|
+
errors.push(flagGateRefusal(name, binding.flag, flags.env, around));
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
159
202
|
|
|
160
203
|
if (name in cli) {
|
|
161
204
|
raw = cli[name];
|
|
@@ -171,6 +214,9 @@ export function resolveBuildParams(defs: BuildParamsConfig | undefined, input: B
|
|
|
171
214
|
} else if (def.env && env[def.env] !== undefined) {
|
|
172
215
|
raw = env[def.env];
|
|
173
216
|
source = "env";
|
|
217
|
+
} else if (binding && binding.value !== undefined) {
|
|
218
|
+
raw = binding.value;
|
|
219
|
+
source = `flag:${binding.flag}`;
|
|
174
220
|
} else if (def.default !== undefined) {
|
|
175
221
|
raw = def.default;
|
|
176
222
|
source = "default";
|
|
@@ -201,6 +247,14 @@ export function resolveBuildParams(defs: BuildParamsConfig | undefined, input: B
|
|
|
201
247
|
return { provenance, errors };
|
|
202
248
|
}
|
|
203
249
|
|
|
250
|
+
/**
|
|
251
|
+
* The build error for a flag-backed parameter that `around` (`--param`,
|
|
252
|
+
* `--params-file` or an env var) set in an environment with a gate (#4024).
|
|
253
|
+
*/
|
|
254
|
+
export function flagGateRefusal(name: string, flag: string, env: string, around: string): string {
|
|
255
|
+
return `build parameter "${name}" is backed by the flag "${flag}", and "${env}" has a gate, so ${around} can't set it; change the flag record and let the change go through the gate`;
|
|
256
|
+
}
|
|
257
|
+
|
|
204
258
|
/** Project the resolved provenance records down to a plain `{ name: value }` map — what actually gets bound to `params.<name>` (see ./params.ts). */
|
|
205
259
|
export function buildParamValues(provenance: readonly BuildParamProvenance[]): Record<string, BuildParamValue> {
|
|
206
260
|
return Object.fromEntries(provenance.map((p) => [p.name, p.value]));
|
package/src/change-set-graph.ts
CHANGED
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
* {@link changeSetNodeCandidates} lists the ids an entry's address can have,
|
|
14
14
|
* by planner. {@link mapChangeSetToGraph} keeps the first that the graph
|
|
15
15
|
* holds and puts the member's name in front, as `chant workspace graph`
|
|
16
|
-
* does
|
|
16
|
+
* does, with the node's source file as `source` when the graph gives one
|
|
17
|
+
* (#4025). An entry whose address the graph does not hold gets `node: null`:
|
|
17
18
|
* a delete of a block the source no longer declares, or a warden or sql
|
|
18
19
|
* change outside the graph.
|
|
19
20
|
*
|
|
@@ -31,6 +32,12 @@ export interface ChangeSetGraphNodes {
|
|
|
31
32
|
at: string | null;
|
|
32
33
|
/** Every node id of the member's `chant graph --format ir`, without the member in front. */
|
|
33
34
|
ids: Iterable<string>;
|
|
35
|
+
/**
|
|
36
|
+
* The source file of each node that has one (#4025), by the same ids, as a
|
|
37
|
+
* path from the repository root. An entry mapped onto such a node gets it
|
|
38
|
+
* as `source`.
|
|
39
|
+
*/
|
|
40
|
+
sources?: ReadonlyMap<string, string>;
|
|
34
41
|
}
|
|
35
42
|
|
|
36
43
|
/**
|
|
@@ -126,7 +133,8 @@ export function mapChangeSetToGraph(doc: ChangeSetDocument, graph: ChangeSetGrap
|
|
|
126
133
|
...doc,
|
|
127
134
|
entries: doc.entries.map((e) => {
|
|
128
135
|
const id = changeSetNodeCandidates(e).find((c) => ids.has(c));
|
|
129
|
-
|
|
136
|
+
const source = id === undefined ? undefined : graph.sources?.get(id);
|
|
137
|
+
return { ...e, node: id === undefined ? null : `${prefix}${id}`, ...(source !== undefined ? { source } : {}) };
|
|
130
138
|
}),
|
|
131
139
|
graph: { member: graph.member, at: graph.at },
|
|
132
140
|
};
|
package/src/change-set.ts
CHANGED
|
@@ -122,6 +122,16 @@ export interface ChangeSetEntry {
|
|
|
122
122
|
* part of the digest.
|
|
123
123
|
*/
|
|
124
124
|
node?: string | null;
|
|
125
|
+
/**
|
|
126
|
+
* The source file that declares the entry's node, from the repository
|
|
127
|
+
* root (#4025): the node's `sourceLoc.file` in the member's graph IR, with
|
|
128
|
+
* the member's directory in front. Set beside `node` when a stored change
|
|
129
|
+
* set is mapped onto a graph whose node has a source location, absent
|
|
130
|
+
* otherwise. Like `node`, never part of the digest. The overlap report
|
|
131
|
+
* attributes the entry to a work item through the commits that changed
|
|
132
|
+
* this file.
|
|
133
|
+
*/
|
|
134
|
+
source?: string;
|
|
125
135
|
}
|
|
126
136
|
|
|
127
137
|
/** Something a planner could not read, so the plan says nothing about it. */
|
|
@@ -288,12 +298,13 @@ export function changeSetDocumentDigest(
|
|
|
288
298
|
.filter((m) => !provisional.has(m.member))
|
|
289
299
|
.map((m) => ({ member: m.member, status: m.status, holes: m.holes }))
|
|
290
300
|
.sort((a, b) => byString(a.member, b.member));
|
|
291
|
-
// An entry's graph node (#4022)
|
|
292
|
-
// not what the plan does, so
|
|
301
|
+
// An entry's graph node (#4022) and its source file (#4025) say where on
|
|
302
|
+
// the graph the change is, not what the plan does, so they stay outside
|
|
303
|
+
// the digest: mapping a stored
|
|
293
304
|
// document onto a graph never moves the value its gate bound.
|
|
294
305
|
const entries = doc.entries
|
|
295
306
|
.filter((e) => !provisional.has(e.member))
|
|
296
|
-
.map(({ node: _node, ...e }) => e)
|
|
307
|
+
.map(({ node: _node, source: _source, ...e }) => e)
|
|
297
308
|
.sort((a, b) => byString(entryKey(a), entryKey(b)));
|
|
298
309
|
const sideEffects = (doc.sideEffects ?? []).filter((s) => !provisional.has(s.member)).sort(bySideEffect);
|
|
299
310
|
const planned =
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from "fs";
|
|
2
2
|
import { resolve } from "path";
|
|
3
|
-
import { resolveBuildParams, type BuildParamsConfig } from "../build-params";
|
|
3
|
+
import { resolveBuildParams, type BuildParamFlags, type BuildParamsConfig } from "../build-params";
|
|
4
4
|
import type { BuildParamProvenance } from "../provenance";
|
|
5
5
|
import { formatError, formatInfo } from "./format";
|
|
6
6
|
|
|
@@ -40,6 +40,10 @@ export interface CliBuildParamsArgs {
|
|
|
40
40
|
paramsFile?: string;
|
|
41
41
|
/** `--verbose` — list every resolved parameter one per line instead of the one-line count (#1424). */
|
|
42
42
|
verbose?: boolean;
|
|
43
|
+
/** The workspace flags backing parameters in this build's environment (#4024), from `../flag-params.ts`. */
|
|
44
|
+
flags?: BuildParamFlags;
|
|
45
|
+
/** `false` to resolve without printing the summary line, for a caller that checks more before it prints ({@link echoBuildParams}). */
|
|
46
|
+
echo?: boolean;
|
|
43
47
|
}
|
|
44
48
|
|
|
45
49
|
export interface CliBuildParamsResolution {
|
|
@@ -94,6 +98,7 @@ export function resolveCliBuildParams(
|
|
|
94
98
|
cli: args.cli,
|
|
95
99
|
fromFile,
|
|
96
100
|
env: process.env,
|
|
101
|
+
...(args.flags ? { flags: args.flags } : {}),
|
|
97
102
|
});
|
|
98
103
|
for (const message of resolution.errors) {
|
|
99
104
|
errors.push(formatError({ message }));
|
|
@@ -103,21 +108,25 @@ export function resolveCliBuildParams(
|
|
|
103
108
|
return { success: false, provenance: [], errors };
|
|
104
109
|
}
|
|
105
110
|
|
|
111
|
+
if (args.echo !== false) echoBuildParams(resolution.provenance, args.verbose);
|
|
112
|
+
return { success: true, provenance: resolution.provenance, errors: [] };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Print a successful resolution: the one-line summary, or every value under `--verbose` or `CHANT_PARAM_ECHO=all` (#1424). */
|
|
116
|
+
export function echoBuildParams(provenance: readonly BuildParamProvenance[], verboseFlag?: boolean): void {
|
|
106
117
|
// #1424 — one line by default. A project with two dozen declared
|
|
107
118
|
// parameters printed two dozen grey lines on every build, ahead of the
|
|
108
119
|
// warnings the reader actually needed. The count by source still answers
|
|
109
120
|
// "did my override reach the build" (the failure mode
|
|
110
121
|
// warnIfParamsCannotReachProject exists for); --verbose names every value.
|
|
111
|
-
const verbose =
|
|
122
|
+
const verbose = verboseFlag === true || process.env.CHANT_PARAM_ECHO === "all";
|
|
112
123
|
if (verbose) {
|
|
113
|
-
for (const p of
|
|
124
|
+
for (const p of provenance) {
|
|
114
125
|
console.error(formatInfo(`[param] ${p.name} = ${JSON.stringify(p.value)} (${p.source})`));
|
|
115
126
|
}
|
|
116
|
-
} else if (
|
|
117
|
-
console.error(formatInfo(summarizeBuildParams(
|
|
127
|
+
} else if (provenance.length > 0) {
|
|
128
|
+
console.error(formatInfo(summarizeBuildParams(provenance)));
|
|
118
129
|
}
|
|
119
|
-
|
|
120
|
-
return { success: true, provenance: resolution.provenance, errors: [] };
|
|
121
130
|
}
|
|
122
131
|
|
|
123
132
|
/**
|
|
@@ -127,8 +136,12 @@ export function resolveCliBuildParams(
|
|
|
127
136
|
*/
|
|
128
137
|
export function summarizeBuildParams(provenance: readonly { source: string }[]): string {
|
|
129
138
|
const counts = new Map<string, number>();
|
|
130
|
-
|
|
131
|
-
const
|
|
139
|
+
// Every flag source (`flag:<name>`, #4024) counts as one, "from flags".
|
|
140
|
+
for (const p of provenance) {
|
|
141
|
+
const source = p.source.startsWith("flag:") ? "flags" : p.source;
|
|
142
|
+
counts.set(source, (counts.get(source) ?? 0) + 1);
|
|
143
|
+
}
|
|
144
|
+
const order = ["cli", "params-file", "env", "flags", "default"];
|
|
132
145
|
const rank = (s: string) => (order.includes(s) ? order.indexOf(s) : order.length);
|
|
133
146
|
const parts = [...counts.keys()]
|
|
134
147
|
.sort((a, b) => rank(a) - rank(b))
|
|
@@ -12,7 +12,7 @@ import { resolveTelemetryAttribution } from "../../telemetry-attribution";
|
|
|
12
12
|
import { loadOkfBundle } from "../../okf-read";
|
|
13
13
|
import { unknownEnvError } from "../../env";
|
|
14
14
|
import type { OwnershipMarker } from "../../ownership";
|
|
15
|
-
import {
|
|
15
|
+
import { resolveFlaggedBuildParams } from "../../flag-params";
|
|
16
16
|
import type { Serializer, SerializerResult } from "../../serializer";
|
|
17
17
|
import type { LexiconPlugin } from "../../lexicon";
|
|
18
18
|
import { runPostSynthChecks, type PostSynthDiagnostic } from "../../lint/post-synth";
|
|
@@ -297,11 +297,14 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
|
|
|
297
297
|
// project's own `params.ts` does before migrating to this mechanism). Also
|
|
298
298
|
// logs a one-line count on success, or every resolved value (`[param] name =
|
|
299
299
|
// value (source)`) under --verbose (#1424).
|
|
300
|
-
|
|
300
|
+
// #4024: in a workspace member, a build for an environment also reads the
|
|
301
|
+
// flag records that back its parameters, and refuses an override of one
|
|
302
|
+
// where the environment has a gate (../../flag-params.ts).
|
|
303
|
+
const paramsResolution = await resolveFlaggedBuildParams(configDir, config.buildParams, {
|
|
301
304
|
cli: options.params,
|
|
302
305
|
paramsFile: options.paramsFile,
|
|
303
306
|
verbose: options.verbose,
|
|
304
|
-
});
|
|
307
|
+
}, options.env);
|
|
305
308
|
if (!paramsResolution.success) {
|
|
306
309
|
errors.push(...paramsResolution.errors);
|
|
307
310
|
return { success: false, resourceCount: 0, fileCount: 0, errors, warnings };
|
|
@@ -11,6 +11,7 @@ import { writeFileSync } from "node:fs";
|
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
import { appendPendingGate, readGateLedger } from "../../lifecycle/gate-ledger";
|
|
13
13
|
import { resetGateOrigin } from "../../lifecycle/gate-origin";
|
|
14
|
+
import { makeRelease, persistRelease } from "../../lifecycle/release-store";
|
|
14
15
|
|
|
15
16
|
vi.mock("../../op/discover", () => ({ discoverOps: async () => ({ errors: [], ops: new Map() }) }));
|
|
16
17
|
|
|
@@ -20,7 +21,9 @@ function git(args: string[], cwd: string): void {
|
|
|
20
21
|
spawnSync("git", args, { cwd, encoding: "utf-8" });
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
/** The release the gate binds. #4033: approve reads it to check its commit isn't revoked, so it is stored. */
|
|
25
|
+
const STORED = makeRelease({ member: null, commit: "1".repeat(40), artifactDigests: [], targets: [{ env: "prod", templates: [], params: [] }] });
|
|
26
|
+
const RELEASE = STORED.releaseDigest;
|
|
24
27
|
const DOC = `jcs1-sha256:${"d".repeat(64)}`;
|
|
25
28
|
|
|
26
29
|
async function repoWithReleaseGate(dir: string): Promise<void> {
|
|
@@ -30,6 +33,7 @@ async function repoWithReleaseGate(dir: string): Promise<void> {
|
|
|
30
33
|
writeFileSync(join(dir, "README.md"), "x\n");
|
|
31
34
|
git(["add", "."], dir);
|
|
32
35
|
git(["commit", "-q", "-m", "init"], dir);
|
|
36
|
+
await persistRelease(STORED, [], { cwd: dir });
|
|
33
37
|
await appendPendingGate(
|
|
34
38
|
{
|
|
35
39
|
op: "release",
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #4033 (ws-105): when a commit is revoked, the release cut from it is
|
|
3
|
+
* revoked, and its pending release gate refuses approval. Against temp repos;
|
|
4
|
+
* nothing is pushed.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { afterEach, describe, expect, test, vi } from "vitest";
|
|
8
|
+
import { withTestDir } from "@intentius/chant-test-utils";
|
|
9
|
+
import { spawnSync } from "node:child_process";
|
|
10
|
+
import { writeFileSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { appendPendingGate, readGateLedger } from "../../lifecycle/gate-ledger";
|
|
13
|
+
import { resetGateOrigin } from "../../lifecycle/gate-origin";
|
|
14
|
+
import { appendCut, makeRelease, persistRelease, type Release } from "../../lifecycle/release-store";
|
|
15
|
+
|
|
16
|
+
vi.mock("../../op/discover", () => ({ discoverOps: async () => ({ errors: [], ops: new Map() }) }));
|
|
17
|
+
|
|
18
|
+
const { recordGateApproval } = await import("./approve");
|
|
19
|
+
|
|
20
|
+
function git(cwd: string, ...args: string[]): string {
|
|
21
|
+
const r = spawnSync("git", args, { cwd, encoding: "utf-8" });
|
|
22
|
+
if (r.status !== 0) throw new Error(`git ${args.join(" ")}: ${r.stderr}`);
|
|
23
|
+
return r.stdout.trim();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const DOC = `jcs1-sha256:${"d".repeat(64)}`;
|
|
27
|
+
const GREEN = { branch: "main", phases: { test: ["test"] }, require: ["test"] };
|
|
28
|
+
|
|
29
|
+
function declaration(dir: string, green: Record<string, unknown>): void {
|
|
30
|
+
writeFileSync(
|
|
31
|
+
join(dir, "chant.workspace.json"),
|
|
32
|
+
JSON.stringify({ name: "acme", schema: 1, members: [{ name: "root", dir: ".", kind: "other", because: "the release fixture" }], ci: { green } }, null, 2),
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** A green commit, a release cut from it as v1.0.0, and a release gate pending for it in prod. */
|
|
37
|
+
async function releaseWaitingAtGate(dir: string): Promise<{ sha: string; release: Release }> {
|
|
38
|
+
git(dir, "init", "-q", "-b", "main");
|
|
39
|
+
git(dir, "config", "user.email", "t@chant.dev");
|
|
40
|
+
git(dir, "config", "user.name", "T");
|
|
41
|
+
declaration(dir, GREEN);
|
|
42
|
+
git(dir, "add", ".");
|
|
43
|
+
git(dir, "commit", "-q", "-m", "init");
|
|
44
|
+
const sha = git(dir, "rev-parse", "HEAD");
|
|
45
|
+
git(dir, "tag", "-a", "-m", "ci: green", `ci/green/${sha}`, sha);
|
|
46
|
+
const release = makeRelease({
|
|
47
|
+
member: null,
|
|
48
|
+
commit: sha,
|
|
49
|
+
artifactDigests: [],
|
|
50
|
+
targets: [{ env: "prod", templates: [{ component: "api", path: "t.json", digest: `sha256:${"c".repeat(64)}` }], params: [] }],
|
|
51
|
+
});
|
|
52
|
+
await persistRelease(release, [], { cwd: dir });
|
|
53
|
+
await appendCut({ releaseDigest: release.releaseDigest, label: "v1.0.0", actor: "alice", timestamp: "2026-10-16T10:00:00.000Z", runOrigin: null }, { cwd: dir });
|
|
54
|
+
await appendPendingGate(
|
|
55
|
+
{
|
|
56
|
+
op: "release",
|
|
57
|
+
gate: "prod-release",
|
|
58
|
+
timestamp: "2026-10-16T11:00:00.000Z",
|
|
59
|
+
expiresAt: "2999-01-01T00:00:00.000Z",
|
|
60
|
+
planDigest: DOC,
|
|
61
|
+
environment: "prod",
|
|
62
|
+
releaseDigest: release.releaseDigest,
|
|
63
|
+
gitSha: sha,
|
|
64
|
+
},
|
|
65
|
+
{ cwd: dir },
|
|
66
|
+
);
|
|
67
|
+
return { sha, release };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** ci tick's revoked tag for a run from chant runs (#4044): the run's id is null and the Op run is named in opRun. */
|
|
71
|
+
function revoke(dir: string, sha: string): void {
|
|
72
|
+
const record = {
|
|
73
|
+
schema: 1,
|
|
74
|
+
tag: "revoked",
|
|
75
|
+
commit: sha,
|
|
76
|
+
branch: "main",
|
|
77
|
+
at: "2026-10-16T12:00:00.000Z",
|
|
78
|
+
source: "runs",
|
|
79
|
+
phase: "test",
|
|
80
|
+
run: { name: "smoke", id: null, conclusion: "failure", url: null, opRun: { id: "r-12", env: "staging", signer: "runner:ci", statement: null } },
|
|
81
|
+
};
|
|
82
|
+
git(dir, "tag", "-a", "-m", `ci: phase test failed in smoke (failure)\n\n${JSON.stringify(record, null, 2)}\n`, `ci/revoked/${sha}`, sha);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
describe("chant approve on the gate of a revoked release (#4033)", () => {
|
|
86
|
+
const cwd = process.cwd();
|
|
87
|
+
afterEach(() => {
|
|
88
|
+
process.chdir(cwd);
|
|
89
|
+
resetGateOrigin();
|
|
90
|
+
vi.restoreAllMocks();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("revoke a commit, and its release's pending gate refuses approval with release-revoked and the fix line", async () => {
|
|
94
|
+
const errors: string[] = [];
|
|
95
|
+
vi.spyOn(console, "error").mockImplementation((...m: unknown[]) => { errors.push(m.join(" ")); });
|
|
96
|
+
await withTestDir(async (dir) => {
|
|
97
|
+
const { sha } = await releaseWaitingAtGate(dir);
|
|
98
|
+
process.chdir(dir);
|
|
99
|
+
revoke(dir, sha);
|
|
100
|
+
const outcome = await recordGateApproval("release", "prod-release", { origin: "cli", actor: "alice", cwd: dir, environment: "prod" });
|
|
101
|
+
expect(outcome.ok).toBe(false);
|
|
102
|
+
const said = errors.join("\n");
|
|
103
|
+
expect(said).toContain(`release v1.0.0 was cut from commit ${sha.slice(0, 12)}, which ci/revoked/${sha} revokes (phase test failed in smoke (Op run r-12 in staging))`);
|
|
104
|
+
expect(said).toContain("[release-revoked]");
|
|
105
|
+
expect(said).toContain("chant approve release prod-release --expire");
|
|
106
|
+
expect((await readGateLedger("release", { cwd: dir })).resolutions).toEqual([]);
|
|
107
|
+
|
|
108
|
+
// Naming the plan outright doesn't get round it.
|
|
109
|
+
const named = await recordGateApproval("release", "prod-release", { origin: "cli", actor: "alice", cwd: dir, environment: "prod", plan: DOC });
|
|
110
|
+
expect(named.ok).toBe(false);
|
|
111
|
+
expect((await readGateLedger("release", { cwd: dir })).resolutions).toEqual([]);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("before the commit is revoked, the same gate takes the approval", async () => {
|
|
116
|
+
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
117
|
+
await withTestDir(async (dir) => {
|
|
118
|
+
const { release } = await releaseWaitingAtGate(dir);
|
|
119
|
+
process.chdir(dir);
|
|
120
|
+
const outcome = await recordGateApproval("release", "prod-release", { origin: "cli", actor: "alice", cwd: dir, environment: "prod" });
|
|
121
|
+
expect(outcome.ok).toBe(true);
|
|
122
|
+
expect((await readGateLedger("release", { cwd: dir })).resolutions).toEqual([expect.objectContaining({ releaseDigest: release.releaseDigest })]);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("with ci.green.signedSince, a hand-pushed revoked tag is ignored and revokes nothing", async () => {
|
|
127
|
+
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
128
|
+
await withTestDir(async (dir) => {
|
|
129
|
+
const { sha } = await releaseWaitingAtGate(dir);
|
|
130
|
+
// The release's own commit is the pivot, so a tag on it needs a statement; this one has none.
|
|
131
|
+
declaration(dir, { ...GREEN, signedSince: sha });
|
|
132
|
+
git(dir, "commit", "-q", "-am", "require signed ci tags");
|
|
133
|
+
revoke(dir, sha);
|
|
134
|
+
process.chdir(dir);
|
|
135
|
+
const outcome = await recordGateApproval("release", "prod-release", { origin: "cli", actor: "alice", cwd: dir, environment: "prod" });
|
|
136
|
+
expect(outcome.ok).toBe(true);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("a gate whose release can't be read refuses, since whether it is revoked is unknown", async () => {
|
|
141
|
+
const errors: string[] = [];
|
|
142
|
+
vi.spyOn(console, "error").mockImplementation((...m: unknown[]) => { errors.push(m.join(" ")); });
|
|
143
|
+
await withTestDir(async (dir) => {
|
|
144
|
+
await releaseWaitingAtGate(dir);
|
|
145
|
+
await appendPendingGate(
|
|
146
|
+
{ op: "release", gate: "prod-release", timestamp: "2026-10-16T13:00:00.000Z", expiresAt: "2999-01-01T00:00:00.000Z", planDigest: DOC, environment: "prod", releaseDigest: `jcs1-sha256:${"e".repeat(64)}` },
|
|
147
|
+
{ cwd: dir },
|
|
148
|
+
);
|
|
149
|
+
process.chdir(dir);
|
|
150
|
+
const outcome = await recordGateApproval("release", "prod-release", { origin: "cli", actor: "alice", cwd: dir, environment: "prod" });
|
|
151
|
+
expect(outcome.ok).toBe(false);
|
|
152
|
+
expect(errors.join("\n")).toContain("[release-unreadable]");
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
});
|
|
@@ -255,6 +255,46 @@ export type GateApprovalOutcome =
|
|
|
255
255
|
| { ok: true; record: GateResolutionRecord; pushed: boolean }
|
|
256
256
|
| { ok: false };
|
|
257
257
|
|
|
258
|
+
/**
|
|
259
|
+
* Refuse the approval of a release gate whose release is revoked (#4033,
|
|
260
|
+
* ws-105): its commit carries a `ci/revoked/<sha>` tag that counts, read from
|
|
261
|
+
* local tags as `release cut` reads them. Prints the refusal and returns true
|
|
262
|
+
* when it refuses. A release file that can't be read, or tags that can't be
|
|
263
|
+
* judged, refuse too, since whether the release is revoked is then unknown.
|
|
264
|
+
*/
|
|
265
|
+
async function refuseRevokedRelease(opName: string, gate: string, releaseDigest: string, cwd: string): Promise<boolean> {
|
|
266
|
+
const { readRelease, readCuts, labelsByDigest } = await import("../../lifecycle/release-store");
|
|
267
|
+
const { readRevokedCommits, revocationOf, revokedRefusal } = await import("../../lifecycle/release-revocation");
|
|
268
|
+
const release = await readRelease(releaseDigest, { cwd }).catch(() => null);
|
|
269
|
+
if (!release) {
|
|
270
|
+
console.error(formatError({
|
|
271
|
+
message: `Gate "${gate}" on "${opName}" binds release ${releaseDigest}, which can't be read from ${LIFECYCLE_BRANCH}, so whether it is revoked is unknown [release-unreadable]`,
|
|
272
|
+
hint: `Fetch the ledger branch (git fetch origin ${LIFECYCLE_BRANCH}:${LIFECYCLE_BRANCH}) and approve again from the member's directory.`,
|
|
273
|
+
}));
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
let revoked: Awaited<ReturnType<typeof readRevokedCommits>>;
|
|
277
|
+
try {
|
|
278
|
+
revoked = await readRevokedCommits(cwd);
|
|
279
|
+
} catch (err) {
|
|
280
|
+
console.error(formatError({
|
|
281
|
+
message: `Gate "${gate}" on "${opName}" binds release ${releaseDigest}, and the ci tags can't be judged: ${err instanceof Error ? err.message : String(err)} [declaration-invalid]`,
|
|
282
|
+
hint: "Fix ci.green in chant.workspace.json at base; chant workspace check names the problem.",
|
|
283
|
+
}));
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
const r = revocationOf(release, revoked);
|
|
287
|
+
if (!r) return false;
|
|
288
|
+
const label = labelsByDigest((await readCuts({ cwd }).catch(() => ({ cuts: [] }))).cuts).get(releaseDigest) ?? null;
|
|
289
|
+
console.error(formatError(revokedRefusal(
|
|
290
|
+
{ releaseDigest, label },
|
|
291
|
+
r,
|
|
292
|
+
`Clear the pending gate with \`chant approve ${opName} ${gate} --expire\`, then promote a release cut from a green commit: ` +
|
|
293
|
+
"`chant ci last-green` names the newest, and `chant release list` marks the revoked releases.",
|
|
294
|
+
)));
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
|
|
258
298
|
/**
|
|
259
299
|
* The ledger write behind `chant approve` — extracted so `chant run approve`
|
|
260
300
|
* (#2121) records the identical fact before waking the runtime that hosts the
|
|
@@ -400,6 +440,13 @@ export async function recordGateApproval(
|
|
|
400
440
|
// approval of another plan names none, and no release gate counts it.
|
|
401
441
|
const releaseDigest =
|
|
402
442
|
standing?.releaseDigest !== undefined && samePlanDigest(planDigest, standing.planDigest) ? standing.releaseDigest : undefined;
|
|
443
|
+
// #4033 (ws-105): a release gate whose release was cut from a revoked
|
|
444
|
+
// commit is superseded. Its promote refuses the release, so an approval of
|
|
445
|
+
// it would count for nothing; refuse it here, whatever plan is named.
|
|
446
|
+
if (standing?.releaseDigest !== undefined) {
|
|
447
|
+
const refused = await refuseRevokedRelease(opName, gate, standing.releaseDigest, opts.cwd ?? process.cwd());
|
|
448
|
+
if (refused) return { ok: false };
|
|
449
|
+
}
|
|
403
450
|
|
|
404
451
|
// chant#2384 — the gate's whole value is that its two halves have different
|
|
405
452
|
// authors. At a shell they do: a person runs, reads the plan, and approves.
|
|
@@ -133,6 +133,21 @@ describe("runBuild --components --generate (chant #1108 build-time parameters)",
|
|
|
133
133
|
vi.restoreAllMocks();
|
|
134
134
|
});
|
|
135
135
|
|
|
136
|
+
test("--pr-loop with --events-json and --report-url asks the generated jobs for both (#4007)", async () => {
|
|
137
|
+
generateComponentsPipelineMock.mockResolvedValue({ success: true, yaml: "stages: []", stages: [], jobs: [] });
|
|
138
|
+
vi.spyOn(console, "log").mockImplementation(() => {});
|
|
139
|
+
|
|
140
|
+
const exit = await runBuild({
|
|
141
|
+
args: makeArgs({ generate: "github", prLoop: true, eventsJson: true, reportUrl: "s3://reports/acme" }),
|
|
142
|
+
plugins: [],
|
|
143
|
+
serializers: [],
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
expect(exit).toBe(0);
|
|
147
|
+
expect(generateComponentsPipelineMock).toHaveBeenCalledWith(".", "github", { env: undefined, prLoop: { events: true, reportUrl: "s3://reports/acme" } }, undefined, []);
|
|
148
|
+
vi.restoreAllMocks();
|
|
149
|
+
});
|
|
150
|
+
|
|
136
151
|
test("chant.config.ts's declared buildParams resolve, log, and are forwarded to generateComponentsPipeline", async () => {
|
|
137
152
|
loadChantConfigUpwardMock.mockResolvedValue({
|
|
138
153
|
config: { buildParams: { tier: { type: "string", default: "light" } } },
|
|
@@ -8,6 +8,21 @@ import { loadChantConfigUpward, type ChantConfig } from "../../config";
|
|
|
8
8
|
import { resolveCliBuildParams, parseParamFlags } from "../build-params-cli";
|
|
9
9
|
import { findWorkspaceRoot } from "../../project-root";
|
|
10
10
|
import { resolvePromotionSource } from "../../components/promote-order";
|
|
11
|
+
import type { PrLoopPipelineOptions } from "../../components/pr-pipeline";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The `prLoop` generator option from `--pr-loop`'s flags (#3183): the gate
|
|
15
|
+
* and the branch, and with `--events-json` and `--report-url` (#4007) the
|
|
16
|
+
* flags the generated jobs pass to pr-plan and pr-apply.
|
|
17
|
+
*/
|
|
18
|
+
function prLoopOption(args: CommandContext["args"]): PrLoopPipelineOptions {
|
|
19
|
+
return {
|
|
20
|
+
...(args.gate ? { gate: args.gate } : {}),
|
|
21
|
+
...(args.branch ? { branch: args.branch } : {}),
|
|
22
|
+
...(args.eventsJson ? { events: true } : {}),
|
|
23
|
+
...(args.reportUrl ? { reportUrl: args.reportUrl } : {}),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
11
26
|
|
|
12
27
|
/**
|
|
13
28
|
* `chant build --components --generate <lexicon>` — generate mode (#563,
|
|
@@ -86,7 +101,7 @@ async function runGenerateComponents(ctx: CommandContext): Promise<number> {
|
|
|
86
101
|
params: args.param,
|
|
87
102
|
paramsFile: args.paramsFile,
|
|
88
103
|
...(args.waveGate ? { waveGate: { gate: args.waveGate, ...(args.canary?.length ? { canary: args.canary } : {}), ...(args.base ? { base: args.base } : {}) } } : {}),
|
|
89
|
-
...(args.prLoop ? { prLoop:
|
|
104
|
+
...(args.prLoop ? { prLoop: prLoopOption(args) } : {}),
|
|
90
105
|
...(args.image ? { image: args.image } : {}),
|
|
91
106
|
...(args.beforeScript?.length ? { beforeScript: args.beforeScript } : {}),
|
|
92
107
|
});
|
|
@@ -116,9 +131,7 @@ async function runGenerateComponents(ctx: CommandContext): Promise<number> {
|
|
|
116
131
|
? { gatedWaves: { gate: args.waveGate, ...(args.canary?.length ? { canary: args.canary } : {}), ...(args.base ? { base: args.base } : {}) } }
|
|
117
132
|
: {}),
|
|
118
133
|
// #3183: a plan job per pull request and an apply job per merge.
|
|
119
|
-
...(args.prLoop
|
|
120
|
-
? { prLoop: { ...(args.gate ? { gate: args.gate } : {}), ...(args.branch ? { branch: args.branch } : {}) } }
|
|
121
|
-
: {}),
|
|
134
|
+
...(args.prLoop ? { prLoop: prLoopOption(args) } : {}),
|
|
122
135
|
// #4002: the job image and the lines that install what it lacks, chant itself included.
|
|
123
136
|
...(args.image ? { image: args.image } : {}),
|
|
124
137
|
...(args.beforeScript?.length ? { beforeScript: args.beforeScript } : {}),
|