@intentius/chant 0.124.23 → 0.124.25
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/ci-jobs.d.ts +41 -1
- package/dist/ci-jobs.d.ts.map +1 -1
- package/dist/ci-jobs.js +48 -1
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/build.js +14 -0
- package/dist/cli/handlers/approve.d.ts.map +1 -1
- package/dist/cli/handlers/approve.js +9 -0
- package/dist/cli/handlers/emulator.d.ts +13 -2
- package/dist/cli/handlers/emulator.d.ts.map +1 -1
- package/dist/cli/handlers/emulator.js +63 -3
- package/dist/cli/handlers/promote.d.ts.map +1 -1
- package/dist/cli/handlers/promote.js +7 -1
- package/dist/cli/handlers/run-wave.d.ts.map +1 -1
- package/dist/cli/handlers/run-wave.js +5 -4
- package/dist/cli/handlers/run.d.ts +4 -3
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/handlers/run.js +73 -9
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +38 -11
- package/dist/cli/mcp/workspace-tools.d.ts +3 -3
- package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
- package/dist/cli/mcp/workspace-tools.js +27 -4
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +31 -0
- package/dist/cli/registry.d.ts +12 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/components/cli-support.d.ts +45 -6
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/cli-support.js +97 -22
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/driver.js +25 -10
- package/dist/effect-receipt.d.ts +13 -0
- package/dist/effect-receipt.d.ts.map +1 -1
- package/dist/effect-receipt.js +19 -0
- package/dist/graph-detail.d.ts.map +1 -1
- package/dist/graph-detail.js +24 -2
- package/dist/lexicon-module.d.ts +7 -1
- package/dist/lexicon-module.d.ts.map +1 -1
- package/dist/lexicon-module.js +7 -2
- package/dist/lifecycle/git.d.ts +11 -0
- package/dist/lifecycle/git.d.ts.map +1 -1
- package/dist/lifecycle/git.js +62 -6
- package/dist/lifecycle/no-record.d.ts +59 -0
- package/dist/lifecycle/no-record.d.ts.map +1 -0
- package/dist/lifecycle/no-record.js +88 -0
- package/dist/lifecycle/receipt-plan.d.ts.map +1 -1
- package/dist/lifecycle/receipt-plan.js +4 -2
- package/dist/op/activities/shell.d.ts.map +1 -1
- package/dist/op/activities/shell.js +30 -9
- package/dist/op/emulator-lifecycle.d.ts +23 -0
- package/dist/op/emulator-lifecycle.d.ts.map +1 -1
- package/dist/op/emulator-lifecycle.js +39 -2
- package/dist/op/gate.d.ts.map +1 -1
- package/dist/op/gate.js +3 -0
- package/dist/op/index.d.ts +1 -1
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/index.js +1 -1
- package/dist/op/local-executor.d.ts +10 -0
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/local-executor.js +71 -7
- package/dist/op/op-waves-notify.d.ts +2 -0
- package/dist/op/op-waves-notify.d.ts.map +1 -1
- package/dist/op/op-waves-notify.js +9 -3
- package/dist/op/op-waves-run.d.ts +8 -2
- package/dist/op/op-waves-run.d.ts.map +1 -1
- package/dist/op/op-waves-run.js +36 -11
- package/dist/op/op-waves.d.ts +85 -1
- package/dist/op/op-waves.d.ts.map +1 -1
- package/dist/op/op-waves.js +153 -1
- package/dist/op/receipt-store.d.ts +5 -0
- package/dist/op/receipt-store.d.ts.map +1 -1
- package/dist/op/receipt-store.js +4 -3
- package/dist/op/runtimes/local.d.ts.map +1 -1
- package/dist/op/runtimes/local.js +6 -0
- package/dist/op/step-console.d.ts +65 -0
- package/dist/op/step-console.d.ts.map +1 -0
- package/dist/op/step-console.js +140 -0
- package/dist/op/step-output-ref.d.ts +26 -18
- package/dist/op/step-output-ref.d.ts.map +1 -1
- package/dist/op/step-output-ref.js +86 -63
- package/dist/point-decision.d.ts.map +1 -1
- package/dist/point-decision.js +3 -0
- package/dist/wave-runtime.mjs +239 -239
- package/dist/workspace/agent-cli.d.ts.map +1 -1
- package/dist/workspace/agent-cli.js +3 -0
- package/dist/workspace/compose-graph.d.ts +22 -3
- package/dist/workspace/compose-graph.d.ts.map +1 -1
- package/dist/workspace/compose-graph.js +18 -5
- package/dist/workspace/conformance/__writer_fixture__/answers/answer.schema.json +16 -1
- package/dist/workspace/conformance/writer.js +1 -1
- package/dist/workspace/decide.d.ts +51 -9
- package/dist/workspace/decide.d.ts.map +1 -1
- package/dist/workspace/decide.js +125 -14
- package/dist/workspace/graph.schema.json +68 -1
- package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
- package/dist/workspace/lineage-upgrade.js +20 -11
- package/dist/workspace/nested-graph.d.ts +1 -1
- package/dist/workspace/nested-graph.d.ts.map +1 -1
- package/dist/workspace/nested-graph.js +5 -1
- package/dist/workspace/point-answer.schema.json +16 -1
- package/dist/workspace/points-cli.d.ts +5 -3
- package/dist/workspace/points-cli.d.ts.map +1 -1
- package/dist/workspace/points-cli.js +16 -7
- package/dist/workspace/points-write.schema.json +39 -4
- package/dist/workspace/points.schema.json +42 -3
- package/dist/workspace/reason-codes.d.ts +2 -1
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/reason-codes.js +2 -1
- package/dist/workspace/records-cli.d.ts +12 -2
- package/dist/workspace/records-cli.d.ts.map +1 -1
- package/dist/workspace/records-cli.js +27 -7
- package/dist/workspace/records-write.d.ts.map +1 -1
- package/dist/workspace/records-write.js +2 -3
- package/dist/workspace/records.d.ts +10 -4
- package/dist/workspace/records.d.ts.map +1 -1
- package/dist/workspace/records.js +16 -3
- package/dist/workspace/records.schema.json +2 -1
- package/dist/workspace/runs.d.ts.map +1 -1
- package/dist/workspace/runs.js +4 -2
- package/dist/workspace/trust/verify-cli.d.ts.map +1 -1
- package/dist/workspace/trust/verify-cli.js +2 -1
- package/dist/workspace/trust/verify.d.ts +22 -0
- package/dist/workspace/trust/verify.d.ts.map +1 -1
- package/dist/workspace/trust/verify.js +94 -14
- package/dist/workspace/write-scope.d.ts +3 -1
- package/dist/workspace/write-scope.d.ts.map +1 -1
- package/dist/workspace/write-scope.js +11 -4
- package/package.json +1 -1
- package/src/ci-jobs.ts +58 -1
- package/src/cli/commands/build.ts +18 -0
- package/src/cli/compiled-cli.e2e.test.ts +45 -1
- package/src/cli/handlers/approve.ts +9 -0
- package/src/cli/handlers/emulator.test.ts +61 -1
- package/src/cli/handlers/emulator.ts +65 -4
- package/src/cli/handlers/promote.test.ts +7 -0
- package/src/cli/handlers/promote.ts +7 -1
- package/src/cli/handlers/run-no-record.e2e.test.ts +87 -0
- package/src/cli/handlers/run-wave.ts +5 -4
- package/src/cli/handlers/run.test.ts +126 -0
- package/src/cli/handlers/run.ts +69 -9
- package/src/cli/main.test.ts +34 -0
- package/src/cli/main.ts +38 -11
- package/src/cli/mcp/workspace-tools.e2e.test.ts +1 -1
- package/src/cli/mcp/workspace-tools.ts +27 -4
- package/src/cli/parse-args.ts +21 -0
- package/src/cli/registry.ts +12 -0
- package/src/components/cli-support.test.ts +75 -0
- package/src/components/cli-support.ts +130 -25
- package/src/components/driver.test.ts +28 -0
- package/src/components/driver.ts +23 -10
- package/src/components/generate-disabled.test.ts +56 -0
- package/src/effect-receipt.ts +22 -0
- package/src/graph-ci-edges.test.ts +50 -0
- package/src/graph-detail.ts +21 -2
- package/src/lexicon-module.ts +9 -2
- package/src/lifecycle/git.ts +61 -6
- package/src/lifecycle/no-record.test.ts +175 -0
- package/src/lifecycle/no-record.ts +108 -0
- package/src/lifecycle/receipt-plan.test.ts +9 -0
- package/src/lifecycle/receipt-plan.ts +4 -1
- package/src/op/activities/shell.ts +28 -6
- package/src/op/effect-step.test.ts +95 -1
- package/src/op/emulator-lifecycle.test.ts +24 -1
- package/src/op/emulator-lifecycle.ts +58 -2
- package/src/op/gate.ts +3 -0
- package/src/op/index.ts +1 -1
- package/src/op/local-executor.test.ts +65 -0
- package/src/op/local-executor.ts +88 -8
- package/src/op/op-waves-jobs.test.ts +213 -0
- package/src/op/op-waves-notify.ts +11 -5
- package/src/op/op-waves-run.ts +42 -11
- package/src/op/op-waves.ts +202 -2
- package/src/op/receipt-store.ts +13 -3
- package/src/op/runtimes/local.ts +6 -0
- package/src/op/step-console.test.ts +150 -0
- package/src/op/step-console.ts +155 -0
- package/src/op/step-output-ref.test.ts +68 -24
- package/src/op/step-output-ref.ts +85 -65
- package/src/point-decision-bundle.test.ts +1 -0
- package/src/point-decision.ts +3 -0
- package/src/workspace/agent-cli.ts +2 -0
- package/src/workspace/compose-graph.test.ts +55 -0
- package/src/workspace/compose-graph.ts +28 -6
- package/src/workspace/conformance/__writer_fixture__/answers/answer.schema.json +16 -1
- package/src/workspace/conformance/writer.ts +1 -1
- package/src/workspace/decide.test.ts +87 -3
- package/src/workspace/decide.ts +142 -17
- package/src/workspace/graph-contract.test.ts +26 -1
- package/src/workspace/graph.schema.json +68 -1
- package/src/workspace/lineage-upgrade.test.ts +21 -0
- package/src/workspace/lineage-upgrade.ts +19 -11
- package/src/workspace/nested-graph.test.ts +14 -0
- package/src/workspace/nested-graph.ts +7 -2
- package/src/workspace/point-answer.schema.json +16 -1
- package/src/workspace/points-cli.ts +16 -8
- package/src/workspace/points-write.schema.json +39 -4
- package/src/workspace/points.schema.json +42 -3
- package/src/workspace/reason-codes.ts +2 -1
- package/src/workspace/record-assets.test.ts +14 -0
- package/src/workspace/records-cli.ts +28 -7
- package/src/workspace/records-write-commands.e2e.test.ts +32 -0
- package/src/workspace/records-write.ts +2 -3
- package/src/workspace/records.schema.json +2 -1
- package/src/workspace/records.ts +17 -4
- package/src/workspace/runs.ts +4 -2
- package/src/workspace/trust/trust.test.ts +105 -0
- package/src/workspace/trust/verify-cli.ts +2 -1
- package/src/workspace/trust/verify.ts +106 -13
- package/src/workspace/write-scope.test.ts +18 -0
- package/src/workspace/write-scope.ts +16 -5
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
* read to the workspace root's pinned chant included (ws-021), and keeps
|
|
16
16
|
* anything a command prints away from the protocol on stdout.
|
|
17
17
|
* - The writes (`records-new`, `records-amend`, `records-review`,
|
|
18
|
-
* `records-close`, `points-answer`, `points-retract`)
|
|
19
|
-
* `chant workspace records new|amend|review|close` and
|
|
20
|
-
* answer|retract` call, and return the
|
|
18
|
+
* `records-close`, `points-answer`, `points-retract`, `points-withdraw`)
|
|
19
|
+
* call the functions `chant workspace records new|amend|review|close` and
|
|
20
|
+
* `points answer|retract|withdraw` call, and return the
|
|
21
21
|
* same JSON result. They keep every rule
|
|
22
22
|
* the CLI keeps: the kind's schema, a closed record never changing, an
|
|
23
23
|
* approved one changing only as its approval rule allows, a dissent needing
|
|
@@ -165,7 +165,7 @@ export const workspaceReadTools: ToolDefinition[] = [
|
|
|
165
165
|
{
|
|
166
166
|
name: "workspace-points",
|
|
167
167
|
description:
|
|
168
|
-
"The workspace's decision points and the questions asked of them: chant workspace points --json (points.schema.json, ws-058). A question is open while it is escalated to people, or proposed by a model and not yet confirmed; each lists any model's answer with its confidence and threshold. Returns the document unchanged. chant calls no model to answer this.",
|
|
168
|
+
"The workspace's decision points and the questions asked of them: chant workspace points --json (points.schema.json, ws-058). A question is open while it is escalated to people, or proposed by a model and not yet confirmed, and not withdrawn; each lists any model's answer with its confidence and threshold. Returns the document unchanged. chant calls no model to answer this.",
|
|
169
169
|
inputSchema: {
|
|
170
170
|
type: "object",
|
|
171
171
|
properties: {
|
|
@@ -326,6 +326,23 @@ export const workspaceWriteTools: ToolDefinition[] = [
|
|
|
326
326
|
required: ["id", "by"],
|
|
327
327
|
},
|
|
328
328
|
},
|
|
329
|
+
{
|
|
330
|
+
name: "points-withdraw",
|
|
331
|
+
description:
|
|
332
|
+
"Close an open decision point question that no longer applies, such as one about a work item its author changed, which a run asks about anew: chant workspace points withdraw (#2888). The question keeps its state, with who withdrew it, when and why; workspace-points open leaves it out, and points-answer refuses it. Asking it again opens it again. Withdrawing answers nothing, so it needs no quorum. An answered question is not withdrawn: points-retract takes the answer back. " +
|
|
333
|
+
PROTOCOL,
|
|
334
|
+
inputSchema: {
|
|
335
|
+
type: "object",
|
|
336
|
+
properties: {
|
|
337
|
+
id: { type: "string", description: "The question's id, as workspace-points lists it." },
|
|
338
|
+
by: { type: "array", items: { type: "string" }, description: "Who withdraws the question (--by)." },
|
|
339
|
+
note: { type: "string", description: "Why, such as the question that asks about the subject now (--note)." },
|
|
340
|
+
kind: { type: "string", description: "The answer kind file. Without it, the declared answer kinds." },
|
|
341
|
+
dryRun: dryRunProp,
|
|
342
|
+
},
|
|
343
|
+
required: ["id", "by"],
|
|
344
|
+
},
|
|
345
|
+
},
|
|
329
346
|
];
|
|
330
347
|
|
|
331
348
|
/** A tool call that can't be made as given: the client gets it as an error result, and nothing runs. */
|
|
@@ -581,6 +598,12 @@ export function createWorkspaceTools(options: WorkspaceToolsOptions): WorkspaceT
|
|
|
581
598
|
if (!Array.isArray(by) || by.length === 0 || !by.every((b) => typeof b === "string" && b.trim() !== "")) throw new ToolInputError("by must list each person who takes the answer back");
|
|
582
599
|
return retractAnswer({ cwd, id: str(params, "id", true)!, by: by as string[], note: str(params, "note"), kind: str(params, "kind"), dryRun: bool(params, "dryRun") });
|
|
583
600
|
},
|
|
601
|
+
"points-withdraw": async (params) => {
|
|
602
|
+
const { withdrawQuestion } = await import("../../workspace/decide");
|
|
603
|
+
const by = params.by;
|
|
604
|
+
if (!Array.isArray(by) || by.length === 0 || !by.every((b) => typeof b === "string" && b.trim() !== "")) throw new ToolInputError("by must name who withdraws the question");
|
|
605
|
+
return withdrawQuestion({ cwd, id: str(params, "id", true)!, by: by as string[], note: str(params, "note"), kind: str(params, "kind"), dryRun: bool(params, "dryRun") });
|
|
606
|
+
},
|
|
584
607
|
"records-close": async (params) => {
|
|
585
608
|
const w = await write();
|
|
586
609
|
const { closeRecord, RECORDS_CLOSE_SCHEMA_ID } = await import("../../workspace/records-close");
|
package/src/cli/parse-args.ts
CHANGED
|
@@ -54,6 +54,7 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
54
54
|
"--check",
|
|
55
55
|
"--bump",
|
|
56
56
|
"--no-release-record",
|
|
57
|
+
"--no-record",
|
|
57
58
|
"--fold",
|
|
58
59
|
"--no-fold",
|
|
59
60
|
"--no-cache",
|
|
@@ -74,6 +75,7 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
74
75
|
"--generated",
|
|
75
76
|
"--decide",
|
|
76
77
|
"--record-plans",
|
|
78
|
+
"--loopback",
|
|
77
79
|
]);
|
|
78
80
|
|
|
79
81
|
/**
|
|
@@ -261,6 +263,18 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
261
263
|
const value = args[++i];
|
|
262
264
|
if (!value || value.startsWith("-")) throw new Error("--port needs a port: --port <container-port>=<host-port>");
|
|
263
265
|
(result.port ??= []).push(value);
|
|
266
|
+
} else if (arg === "--image") {
|
|
267
|
+
result.image = args[++i];
|
|
268
|
+
if (!result.image || result.image.startsWith("-")) throw new Error("--image needs an image reference: --image <name>:<tag>");
|
|
269
|
+
} else if (arg === "--network") {
|
|
270
|
+
result.network = args[++i];
|
|
271
|
+
if (!result.network || result.network.startsWith("-")) throw new Error("--network needs a Docker network name: --network <name>");
|
|
272
|
+
} else if (arg === "--setting") {
|
|
273
|
+
const value = args[++i];
|
|
274
|
+
if (!value || value.startsWith("-")) throw new Error("--setting needs a value: --setting <KEY>=<VALUE>");
|
|
275
|
+
(result.setting ??= []).push(value);
|
|
276
|
+
} else if (arg === "--loopback") {
|
|
277
|
+
result.loopback = true;
|
|
264
278
|
} else if (arg === "--promote-to") {
|
|
265
279
|
result.promoteTo = args[++i];
|
|
266
280
|
if (!result.promoteTo || result.promoteTo.startsWith("-")) throw new Error("--promote-to needs an environment: --promote-to <env>");
|
|
@@ -333,6 +347,10 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
333
347
|
result.decide = true;
|
|
334
348
|
} else if (arg === "--record-plans") {
|
|
335
349
|
result.recordPlans = true;
|
|
350
|
+
} else if (arg === "--group") {
|
|
351
|
+
const raw = args[++i];
|
|
352
|
+
if (!raw || raw.startsWith("-")) throw new Error("--group needs a job group's name: --group <name>");
|
|
353
|
+
result.group = raw;
|
|
336
354
|
} else if (arg === "--share") {
|
|
337
355
|
const raw = args[++i];
|
|
338
356
|
const share = Number(raw);
|
|
@@ -591,6 +609,9 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
591
609
|
result.compareTo = args[++i];
|
|
592
610
|
} else if (arg === "--no-release-record") {
|
|
593
611
|
result.noReleaseRecord = true;
|
|
612
|
+
} else if (arg === "--no-record") {
|
|
613
|
+
// `chant run <op|--components> --no-record` (#3955): write nothing to chant/lifecycle and push nothing.
|
|
614
|
+
result.noRecord = true;
|
|
594
615
|
} else if (arg === "--fold") {
|
|
595
616
|
result.fold = true;
|
|
596
617
|
} else if (arg === "--no-cache") {
|
package/src/cli/registry.ts
CHANGED
|
@@ -167,6 +167,14 @@ export interface ParsedArgs {
|
|
|
167
167
|
env?: string;
|
|
168
168
|
/** `chant emulator up --port <container-port>=<host-port>`, repeatable (#3673); a bare `<host-port>` when one emulator is selected. */
|
|
169
169
|
port?: string[];
|
|
170
|
+
/** `chant emulator up --image <ref>` (#3952): the image to start, when one emulator is selected. */
|
|
171
|
+
image?: string;
|
|
172
|
+
/** `chant emulator up --network <name>` (#3952): the Docker network each started container joins. */
|
|
173
|
+
network?: string;
|
|
174
|
+
/** `chant emulator up --setting <KEY>=<VALUE>`, repeatable (#3952): an emulator setting such as `FLOCI_<NAME>`, passed as `-e`. */
|
|
175
|
+
setting?: string[];
|
|
176
|
+
/** `chant emulator up --loopback` (#3952): publish ports on 127.0.0.1 only. */
|
|
177
|
+
loopback?: boolean;
|
|
170
178
|
/** `chant build --components --generate <lexicon> --promote-to <env>` (#2575) — add a promote job to the generated pipeline. */
|
|
171
179
|
promoteTo?: string;
|
|
172
180
|
/** `chant graph --stacks` — render the cross-stack apply-ordering graph */
|
|
@@ -260,6 +268,8 @@ export interface ParsedArgs {
|
|
|
260
268
|
recordPlans?: boolean;
|
|
261
269
|
/** `chant run wave --share <i>` (#3679) — apply share `i` of a decided wide wave. */
|
|
262
270
|
share?: number;
|
|
271
|
+
/** `chant run wave --group <name>` (#3914) — apply one job group's runs of a decided wave. */
|
|
272
|
+
group?: string;
|
|
263
273
|
/** `chant components fan-out --canary <component>` (#3049), repeatable — components that form wave 1. */
|
|
264
274
|
canary?: string[];
|
|
265
275
|
/**
|
|
@@ -547,6 +557,8 @@ export interface ParsedArgs {
|
|
|
547
557
|
compareTo?: string;
|
|
548
558
|
/** `chant run --components <name> --env <env> --no-release-record` (#597) — opt out of auto-emitting a release-ledger record after a successful component deploy. Default (flag omitted): recording is ON. Also settable project-wide via `chant.config.ts`'s `release.autoRecord: false`. */
|
|
549
559
|
noReleaseRecord?: boolean;
|
|
560
|
+
/** `chant run <op>` / `chant run --components <name>` `--no-record` (#3955) — run every step but record nothing on `chant/lifecycle` and push nothing; a gate step fails. `CHANT_NO_RECORD=1` does the same for every chant process that inherits it (../../lifecycle/no-record.ts). */
|
|
561
|
+
noRecord?: boolean;
|
|
550
562
|
/** `chant run --components <name> --dump-outputs <file>` — after the run, write the accumulated cross-component/cross-stack outputs (JSON, keyed by component name) to `<file>`, for a downstream job to `--seed-outputs`. */
|
|
551
563
|
dumpOutputs?: string;
|
|
552
564
|
/** `chant run --components <name> --digest-file <file>` (#2602) — after a successful run, write one `<component>=<digest>` line per release the run recorded to `<file>`, the form `chant components promote --digest` takes, so a generated promote job promotes exactly the release its pipeline run built. Written empty when nothing was recorded. */
|
|
@@ -601,6 +601,81 @@ describe("runComponents", () => {
|
|
|
601
601
|
expect(result.error).toMatch(/cycle/i);
|
|
602
602
|
});
|
|
603
603
|
|
|
604
|
+
describe("a comma list (#3950)", () => {
|
|
605
|
+
const comp = (name: string, dependsOn: string[], extra = "") =>
|
|
606
|
+
`export const c = { name: "${name}", ${extra}dependsOn: ${JSON.stringify(dependsOn)}, deploy: [{ phase: "Apply", steps: [{ kind: "deploy-thing" }] }] };`;
|
|
607
|
+
|
|
608
|
+
beforeEach(async () => {
|
|
609
|
+
await writeFile(join(testDir, "db.component.ts"), comp("db", []));
|
|
610
|
+
await writeFile(join(testDir, "cache.component.ts"), comp("cache", []));
|
|
611
|
+
await writeFile(join(testDir, "api.component.ts"), comp("api", ["db"]));
|
|
612
|
+
await writeFile(join(testDir, "web.component.ts"), comp("web", ["api", "cache"]));
|
|
613
|
+
await writeFile(join(testDir, "worker.component.ts"), comp("worker", ["db"]));
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
test("runs the named components and their transitive dependencies in one dependency-ordered run", async () => {
|
|
617
|
+
const plans: unknown[] = [];
|
|
618
|
+
const result = await runComponents(testDir, "web", { registry: fakeRegistry() });
|
|
619
|
+
expect(result.selected).toEqual(["web"]);
|
|
620
|
+
|
|
621
|
+
const listed = await runComponents(testDir, "web,worker", { registry: fakeRegistry(), onPlan: (p) => plans.push(p) });
|
|
622
|
+
expect(listed.success).toBe(true);
|
|
623
|
+
expect([...listed.selected].sort()).toEqual(["api", "cache", "db", "web", "worker"]);
|
|
624
|
+
expect(listed.run?.waves).toEqual([["cache", "db"], ["api", "worker"], ["web"]].map((w) => expect.arrayContaining(w)));
|
|
625
|
+
const order = listed.run?.order ?? [];
|
|
626
|
+
expect(order.indexOf("db")).toBeLessThan(order.indexOf("api"));
|
|
627
|
+
expect(order.indexOf("api")).toBeLessThan(order.indexOf("web"));
|
|
628
|
+
expect(plans).toHaveLength(1);
|
|
629
|
+
expect(plans[0]).toMatchObject({ order: listed.run?.order, waves: listed.run?.waves });
|
|
630
|
+
expect([...(plans[0] as { added: string[] }).added].sort()).toEqual(["api", "cache", "db"]);
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
test("leaves out components the list does not reach", async () => {
|
|
634
|
+
const result = await runComponents(testDir, "api, api ,", { registry: fakeRegistry() });
|
|
635
|
+
expect(result.success).toBe(true);
|
|
636
|
+
expect(result.run?.order).toEqual(["db", "api"]);
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
test("refuses unknown names, naming every one, before anything runs", async () => {
|
|
640
|
+
const onPlan = vi.fn();
|
|
641
|
+
const result = await runComponents(testDir, "api,nope,also-nope", { registry: fakeRegistry(), onPlan });
|
|
642
|
+
expect(result.success).toBe(false);
|
|
643
|
+
expect(result.run).toBeUndefined();
|
|
644
|
+
expect(result.error).toContain('Components "nope", "also-nope" not found');
|
|
645
|
+
expect(result.error).toContain("Known components: api, cache, db, web, worker");
|
|
646
|
+
expect(onPlan).not.toHaveBeenCalled();
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
test("refuses a disabled name; a disabled dependency is dropped as \"all\" drops it", async () => {
|
|
650
|
+
await writeFile(join(testDir, "cache.component.ts"), comp("cache", [], "enabled: false, "));
|
|
651
|
+
const refused = await runComponents(testDir, "api,cache", { registry: fakeRegistry() });
|
|
652
|
+
expect(refused.success).toBe(false);
|
|
653
|
+
expect(refused.run).toBeUndefined();
|
|
654
|
+
expect(refused.error).toContain('Component "cache" is disabled');
|
|
655
|
+
|
|
656
|
+
const result = await runComponents(testDir, "web", { registry: fakeRegistry() });
|
|
657
|
+
expect(result.success).toBe(true);
|
|
658
|
+
const listed = await runComponents(testDir, "web,", { registry: fakeRegistry() });
|
|
659
|
+
expect(listed.success).toBe(true);
|
|
660
|
+
expect(listed.run?.order).toEqual(["db", "api", "web"]);
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
test("refuses \"all\" inside a list", async () => {
|
|
664
|
+
const result = await runComponents(testDir, "all,api", { registry: fakeRegistry() });
|
|
665
|
+
expect(result.success).toBe(false);
|
|
666
|
+
expect(result.error).toContain('"all" cannot be part of a component list');
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
test("a dependency outside the discovered set fails as it does under \"all\"", async () => {
|
|
670
|
+
await writeFile(join(testDir, "api.component.ts"), comp("api", ["shared-alb"]));
|
|
671
|
+
const onPlan = vi.fn();
|
|
672
|
+
const result = await runComponents(testDir, "api,web", { registry: fakeRegistry(), onPlan });
|
|
673
|
+
expect(result.success).toBe(false);
|
|
674
|
+
expect(result.error).toContain("shared-alb");
|
|
675
|
+
expect(onPlan).not.toHaveBeenCalled();
|
|
676
|
+
});
|
|
677
|
+
});
|
|
678
|
+
|
|
604
679
|
test("unknown component name → clear error listing known components", async () => {
|
|
605
680
|
await writeFile(
|
|
606
681
|
join(testDir, "svc.component.ts"),
|
|
@@ -228,6 +228,28 @@ export async function computeComponentGraph(
|
|
|
228
228
|
/** A generate-mode target lexicon name — any lexicon whose plugin implements `generateComponentPipeline` (gitlab, github, and forgejo today). */
|
|
229
229
|
export type GenerateLexicon = string;
|
|
230
230
|
|
|
231
|
+
/**
|
|
232
|
+
* The components an "all" run takes, in `discovered` order (chant #1522). A
|
|
233
|
+
* seam-gated component (`enabled: false`, computed from this run's own
|
|
234
|
+
* params) sits out: the run deploys the estate the parameters describe, not
|
|
235
|
+
* the units the seams turned off. A disabled dependency is satisfied
|
|
236
|
+
* vacuously, since ordering is dependsOn's only meaning, so it is also
|
|
237
|
+
* stripped from the survivors' dependsOn rather than failing wave resolution
|
|
238
|
+
* as an unknown name. `chant run --components all`, a component list's
|
|
239
|
+
* dependency closure (#3950) and the generated pipelines (#3951) all select
|
|
240
|
+
* through this.
|
|
241
|
+
*/
|
|
242
|
+
export function withoutDisabled<T extends { name: string; dependsOn?: string[] }>(
|
|
243
|
+
discovered: ReadonlyArray<{ name: string; enabled?: boolean }>,
|
|
244
|
+
components: T[],
|
|
245
|
+
): T[] {
|
|
246
|
+
const disabled = new Set(discovered.filter((c) => c.enabled === false).map((c) => c.name));
|
|
247
|
+
if (disabled.size === 0) return components;
|
|
248
|
+
return components
|
|
249
|
+
.filter((c) => !disabled.has(c.name))
|
|
250
|
+
.map((c) => (c.dependsOn?.some((d) => disabled.has(d)) ? { ...c, dependsOn: c.dependsOn.filter((d) => !disabled.has(d)) } : c));
|
|
251
|
+
}
|
|
252
|
+
|
|
231
253
|
/** Result of `chant build --components --generate <lexicon>`. */
|
|
232
254
|
export interface GenerateComponentsResult {
|
|
233
255
|
success: boolean;
|
|
@@ -301,11 +323,13 @@ export async function generateComponentsPipeline(
|
|
|
301
323
|
return { success: false, error: result.errors.map((e) => e.message).join("\n") };
|
|
302
324
|
}
|
|
303
325
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
326
|
+
// #3951: the pipeline holds the jobs `chant run --components all` would
|
|
327
|
+
// run, so a disabled component has no job and no other job needs it.
|
|
328
|
+
const discovered = [...result.components.values()].map(({ component }) => component);
|
|
329
|
+
const driverComponents = withoutDisabled(
|
|
330
|
+
discovered,
|
|
331
|
+
discovered.map((component) => ({ name: component.name, dependsOn: component.dependsOn, deploy: component.deploy })),
|
|
332
|
+
);
|
|
309
333
|
|
|
310
334
|
try {
|
|
311
335
|
const { yaml, stages, jobs, env } = plugin.generateComponentPipeline(driverComponents, options);
|
|
@@ -404,7 +428,7 @@ export interface RunComponentsOptions {
|
|
|
404
428
|
/**
|
|
405
429
|
* Opt-in structured progress observer (`chant run --components <name|all>
|
|
406
430
|
* --progress-json`, see ./run-progress.ts). Threaded straight through to
|
|
407
|
-
* `runInterpretDriver` for `
|
|
431
|
+
* `runInterpretDriver` for `"all"` and a list; for a single-component
|
|
408
432
|
* selector (which bypasses `runInterpretDriver` — see the docstring above),
|
|
409
433
|
* this function emits the same `run-start`/`wave-start`/`component-start`/
|
|
410
434
|
* …/`run-done` envelope itself, treating the one component as a run with a
|
|
@@ -430,6 +454,12 @@ export interface RunComponentsOptions {
|
|
|
430
454
|
buildParams?: BuildParamProvenance[];
|
|
431
455
|
/** Where a `gate` step's facts are read and written (#2119). Defaults to the `chant/lifecycle` orphan branch; a test passes `memoryGateLedgerPort()` from `../op/gate.ts`. */
|
|
432
456
|
gates?: GateLedgerPort;
|
|
457
|
+
/**
|
|
458
|
+
* Called once for a list selector (`a,b,c`, #3950) with the resolved run
|
|
459
|
+
* order, waves and the dependencies the list pulled in, after selection and
|
|
460
|
+
* before the first component runs. The CLI prints it.
|
|
461
|
+
*/
|
|
462
|
+
onPlan?: (plan: ComponentRunPlan) => void;
|
|
433
463
|
/**
|
|
434
464
|
* The commit the run deploys, for the workloads' `vcs.ref.head.revision`
|
|
435
465
|
* (#3061). Default: `git rev-parse HEAD` in the project, or nothing outside
|
|
@@ -438,7 +468,15 @@ export interface RunComponentsOptions {
|
|
|
438
468
|
revision?: string | null;
|
|
439
469
|
}
|
|
440
470
|
|
|
441
|
-
/**
|
|
471
|
+
/** The resolved order of a list run (#3950), reported through `RunComponentsOptions.onPlan`. */
|
|
472
|
+
export interface ComponentRunPlan {
|
|
473
|
+
order: string[];
|
|
474
|
+
waves: string[][];
|
|
475
|
+
/** Dependencies the list did not name but the run includes. */
|
|
476
|
+
added: string[];
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/** Result of `chant run --components <name|all|a,b,c>`. */
|
|
442
480
|
export interface RunComponentsResult {
|
|
443
481
|
success: boolean;
|
|
444
482
|
/** The driver's run result, when the run reached the driver (even on a failed component). */
|
|
@@ -468,6 +506,10 @@ export interface RunComponentsResult {
|
|
|
468
506
|
* - `selector === "all"`: resolves the whole set's dependency order/waves
|
|
469
507
|
* and dispatches every component through `runInterpretDriver`, in the same
|
|
470
508
|
* order `chant graph --components` reports.
|
|
509
|
+
* - a list (`a,b,c`, #3950): the named components plus everything they
|
|
510
|
+
* depend on, through the same `runInterpretDriver` path as `"all"`, so the
|
|
511
|
+
* subset runs in the order `"all"` would give it, in one process. The
|
|
512
|
+
* resolved order goes to `onPlan` before the first component starts.
|
|
471
513
|
* - a single name: dispatches just that component via `runComponentDeploy`,
|
|
472
514
|
* without requiring the rest of its `dependsOn` graph to be present in
|
|
473
515
|
* this invocation. This mirrors generate mode's per-job invocation model
|
|
@@ -483,17 +525,24 @@ export interface RunComponentsResult {
|
|
|
483
525
|
* stops the run there with `gated` set, which the CLI turns into exit code 3
|
|
484
526
|
* and the `chant approve` line.
|
|
485
527
|
*/
|
|
486
|
-
/** Result of resolving a `<name|all>` selector against discovered components. */
|
|
528
|
+
/** Result of resolving a `<name|all|a,b,c>` selector against discovered components. */
|
|
487
529
|
export interface ResolvedComponentTargets {
|
|
488
530
|
success: boolean;
|
|
489
531
|
targets: DriverComponent[];
|
|
532
|
+
/** For a list selector (#3950): the dependencies the closure added that the list did not name, in discovery order. */
|
|
533
|
+
added?: string[];
|
|
490
534
|
error?: string;
|
|
491
535
|
}
|
|
492
536
|
|
|
537
|
+
/** Whether `selector` is a comma-separated component list (#3950), as opposed to `all` or one name. */
|
|
538
|
+
export function isComponentList(selector: string): boolean {
|
|
539
|
+
return selector.includes(",");
|
|
540
|
+
}
|
|
541
|
+
|
|
493
542
|
/**
|
|
494
|
-
* Discover every component under `path` and resolve `selector` (`"all"
|
|
495
|
-
* single component name
|
|
496
|
-
* check and no dispatch — just discovery + selection, factored out of
|
|
543
|
+
* Discover every component under `path` and resolve `selector` (`"all"`, a
|
|
544
|
+
* single component name, or a comma-separated list, #3950) to the
|
|
545
|
+
* `DriverComponent`(s) to run, with no gate check and no dispatch — just discovery + selection, factored out of
|
|
497
546
|
* `runComponents` so another entrypoint can reuse the exact same selection
|
|
498
547
|
* semantics without inheriting the run itself.
|
|
499
548
|
*/
|
|
@@ -512,19 +561,9 @@ export async function resolveComponentTargets(
|
|
|
512
561
|
const discovered = [...result.components.values()].map(({ component }) => component);
|
|
513
562
|
const all = discovered.map((component) => toDriverComponent(component));
|
|
514
563
|
|
|
515
|
-
if (selector
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
// the parameters describe, not the units the seams turned off. A disabled
|
|
519
|
-
// dependency is satisfied vacuously — ordering is dependsOn's only
|
|
520
|
-
// semantics — so it is also stripped from the survivors' dependsOn,
|
|
521
|
-
// rather than failing wave resolution as an unknown name.
|
|
522
|
-
const disabled = new Set(discovered.filter((c) => c.enabled === false).map((c) => c.name));
|
|
523
|
-
const targets = all
|
|
524
|
-
.filter((c) => !disabled.has(c.name))
|
|
525
|
-
.map((c) => (c.dependsOn?.some((d) => disabled.has(d)) ? { ...c, dependsOn: c.dependsOn.filter((d) => !disabled.has(d)) } : c));
|
|
526
|
-
return { success: true, targets };
|
|
527
|
-
}
|
|
564
|
+
if (isComponentList(selector)) return resolveComponentList(selector, discovered, all);
|
|
565
|
+
|
|
566
|
+
if (selector === "all") return { success: true, targets: withoutDisabled(discovered, all) };
|
|
528
567
|
|
|
529
568
|
const found = all.find((c) => c.name === selector);
|
|
530
569
|
const disabledByName = discovered.find((c) => c.name === selector && c.enabled === false);
|
|
@@ -548,6 +587,65 @@ export async function resolveComponentTargets(
|
|
|
548
587
|
return { success: true, targets: [found] };
|
|
549
588
|
}
|
|
550
589
|
|
|
590
|
+
/**
|
|
591
|
+
* A list selector (`a,b,c`, #3950): the named components plus every component
|
|
592
|
+
* they depend on, transitively, so the list runs as one dependency-ordered
|
|
593
|
+
* set. Refused before anything runs when a name is unknown, disabled, or `all`
|
|
594
|
+
* (which already means every component). Dependencies are followed through
|
|
595
|
+
* `withoutDisabled`, the selection `"all"` uses, so a disabled *dependency*
|
|
596
|
+
* is dropped and stripped from dependsOn exactly as `"all"` drops it. A
|
|
597
|
+
* dependsOn name that matches no discovered component is left in place, so
|
|
598
|
+
* wave resolution reports it as `"all"` would.
|
|
599
|
+
*/
|
|
600
|
+
function resolveComponentList(
|
|
601
|
+
selector: string,
|
|
602
|
+
discovered: ReadonlyArray<{ name: string; enabled?: boolean }>,
|
|
603
|
+
all: DriverComponent[],
|
|
604
|
+
): ResolvedComponentTargets {
|
|
605
|
+
const names = [...new Set(selector.split(",").map((n) => n.trim()).filter((n) => n.length > 0))];
|
|
606
|
+
if (names.length === 0) {
|
|
607
|
+
return { success: false, targets: [], error: `Component list "${selector}" names no components.` };
|
|
608
|
+
}
|
|
609
|
+
if (names.includes("all")) {
|
|
610
|
+
return { success: false, targets: [], error: `"all" cannot be part of a component list; pass --components all on its own, or name the components.` };
|
|
611
|
+
}
|
|
612
|
+
// An explicit ask for a unit the parameters excluded is a mistake to
|
|
613
|
+
// surface, not to skip silently, as it is for a single name.
|
|
614
|
+
const off = names.filter((n) => discovered.some((c) => c.name === n && c.enabled === false));
|
|
615
|
+
if (off.length > 0) {
|
|
616
|
+
const one = off.length === 1;
|
|
617
|
+
return {
|
|
618
|
+
success: false,
|
|
619
|
+
targets: [],
|
|
620
|
+
error: `${one ? "Component" : "Components"} ${off.map((n) => `"${n}"`).join(", ")} ${one ? "is" : "are"} disabled under this run's parameters (enabled: false). Enable the seam ${one ? "it is" : "they are"} gated on, or leave ${one ? "it" : "them"} out of the list.`,
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
const unknown = names.filter((n) => !all.some((c) => c.name === n));
|
|
624
|
+
if (unknown.length > 0) {
|
|
625
|
+
const known = all.map((c) => c.name).sort().join(", ");
|
|
626
|
+
return {
|
|
627
|
+
success: false,
|
|
628
|
+
targets: [],
|
|
629
|
+
error: `${unknown.length === 1 ? "Component" : "Components"} ${unknown.map((n) => `"${n}"`).join(", ")} not found.${known ? ` Known components: ${known}` : " No components discovered."}`,
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
const enabled = withoutDisabled(discovered, all);
|
|
634
|
+
const byName = new Map(enabled.map((c) => [c.name, c]));
|
|
635
|
+
const included = new Set<string>();
|
|
636
|
+
const visit = (name: string): void => {
|
|
637
|
+
const component = byName.get(name);
|
|
638
|
+
if (!component || included.has(name)) return;
|
|
639
|
+
included.add(name);
|
|
640
|
+
for (const dep of component.dependsOn ?? []) visit(dep);
|
|
641
|
+
};
|
|
642
|
+
for (const name of names) visit(name);
|
|
643
|
+
|
|
644
|
+
const targets = enabled.filter((c) => included.has(c.name));
|
|
645
|
+
const named = new Set(names);
|
|
646
|
+
return { success: true, targets, added: targets.map((c) => c.name).filter((n) => !named.has(n)) };
|
|
647
|
+
}
|
|
648
|
+
|
|
551
649
|
export async function runComponents(
|
|
552
650
|
path: string,
|
|
553
651
|
selector: string,
|
|
@@ -595,7 +693,14 @@ export async function runComponents(
|
|
|
595
693
|
const releaseIdentity = () => (revision ? { revision } : {});
|
|
596
694
|
|
|
597
695
|
try {
|
|
598
|
-
if (selector === "all") {
|
|
696
|
+
if (selector === "all" || isComponentList(selector)) {
|
|
697
|
+
if (isComponentList(selector) && options.onPlan) {
|
|
698
|
+
// Resolved here, before dispatch, so the order is printed before any
|
|
699
|
+
// component runs. A cycle or unknown dependency throws the same error
|
|
700
|
+
// `runInterpretDriver` would, caught below.
|
|
701
|
+
const graph = resolveComponentGraph(resolvedTargets);
|
|
702
|
+
options.onPlan({ order: graph.order, waves: graph.waves, added: resolved.added ?? [] });
|
|
703
|
+
}
|
|
599
704
|
const run = await runInterpretDriver(resolvedTargets, registry, {
|
|
600
705
|
env,
|
|
601
706
|
releaseIdentity,
|
|
@@ -239,6 +239,34 @@ describe("runComponentDeploy — gate as fact (#2119)", () => {
|
|
|
239
239
|
return { registry, calls, rolledBack };
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
+
// #3955: a run that records nothing cannot spend an approval or leave a
|
|
243
|
+
// pending fact, so the gate fails even with an approval standing for it.
|
|
244
|
+
it("fails at the gate under CHANT_NO_RECORD, writing nothing and running nothing after it", async () => {
|
|
245
|
+
const { registry, calls } = registryWithCalls();
|
|
246
|
+
const port = memoryGateLedgerPort({
|
|
247
|
+
resolutions: [{ op: "neo4j-cluster", gate: "approve-node-1", resolvedBy: "alice", timestamp: NOW, environment: "dev" } as never],
|
|
248
|
+
});
|
|
249
|
+
process.env.CHANT_NO_RECORD = "1";
|
|
250
|
+
let result;
|
|
251
|
+
try {
|
|
252
|
+
result = await runComponentDeploy(
|
|
253
|
+
gatedComponent(), { env: "dev", component: "neo4j-cluster" }, registry, {}, undefined,
|
|
254
|
+
{ port, now: NOW },
|
|
255
|
+
);
|
|
256
|
+
} finally {
|
|
257
|
+
delete process.env.CHANT_NO_RECORD;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
expect(result.status).toBe("fail");
|
|
261
|
+
expect(port.appended).toHaveLength(0);
|
|
262
|
+
// A failure, so the component unwinds as any failed step would; nothing after the gate ran.
|
|
263
|
+
expect(calls).not.toContain("code-deploy");
|
|
264
|
+
const gate = result.records.find((r) => r.kind === "gate:approve-node-1");
|
|
265
|
+
expect(gate).toMatchObject({ status: "fail" });
|
|
266
|
+
expect(gate!.error).toContain("cannot be passed under --no-record");
|
|
267
|
+
expect(result.records.find((r) => r.kind === "code-deploy")).toMatchObject({ status: "skipped" });
|
|
268
|
+
});
|
|
269
|
+
|
|
242
270
|
it("ends gated, records the pending fact, and runs nothing after the gate", async () => {
|
|
243
271
|
const { registry, calls, rolledBack } = registryWithCalls();
|
|
244
272
|
const port = memoryGateLedgerPort();
|
package/src/components/driver.ts
CHANGED
|
@@ -50,6 +50,7 @@ import { topoSort } from "../codegen/topo-sort";
|
|
|
50
50
|
import { kahnLayers } from "./layers";
|
|
51
51
|
import { evaluateGate, gitGateLedgerPort, type GateDigestMismatch, type GateLedgerPort } from "../op/gate";
|
|
52
52
|
import { gateName } from "../op/gate-name";
|
|
53
|
+
import { GateNotRecordableError } from "../lifecycle/no-record";
|
|
53
54
|
import { componentPlanDigest } from "./gate-plan";
|
|
54
55
|
import type { PendingGateRecord } from "../lifecycle/gate-ledger";
|
|
55
56
|
import type { CapabilityRegistry, DeployContext } from "./capability";
|
|
@@ -473,17 +474,29 @@ async function runPhase(
|
|
|
473
474
|
const gateRecords: DriverStepRecord[] = [];
|
|
474
475
|
for (const gate of phaseDef.steps.filter(isGateStep)) {
|
|
475
476
|
const start = Date.now();
|
|
476
|
-
const check = await evaluateGate(gates.port, {
|
|
477
|
-
op: ctx.component,
|
|
478
|
-
gate: gateName(gate),
|
|
479
|
-
// #2574: an approval counts only for this environment and this plan.
|
|
480
|
-
environment: ctx.env,
|
|
481
|
-
...(gates.planDigest !== undefined ? { planDigest: gates.planDigest } : {}),
|
|
482
|
-
...(gate.description ? { description: gate.description } : {}),
|
|
483
|
-
...(gate.timeout ? { timeout: gate.timeout } : {}),
|
|
484
|
-
...(gates.now ? { now: gates.now } : {}),
|
|
485
|
-
});
|
|
486
477
|
const base = { component: ctx.component, phase: phaseDef.phase, kind: `gate:${gateName(gate)}` };
|
|
478
|
+
let check: Awaited<ReturnType<typeof evaluateGate>>;
|
|
479
|
+
try {
|
|
480
|
+
check = await evaluateGate(gates.port, {
|
|
481
|
+
op: ctx.component,
|
|
482
|
+
gate: gateName(gate),
|
|
483
|
+
// #2574: an approval counts only for this environment and this plan.
|
|
484
|
+
environment: ctx.env,
|
|
485
|
+
...(gates.planDigest !== undefined ? { planDigest: gates.planDigest } : {}),
|
|
486
|
+
...(gate.description ? { description: gate.description } : {}),
|
|
487
|
+
...(gate.timeout ? { timeout: gate.timeout } : {}),
|
|
488
|
+
...(gates.now ? { now: gates.now } : {}),
|
|
489
|
+
});
|
|
490
|
+
} catch (err) {
|
|
491
|
+
// #3955: a run that records nothing fails at the gate, as the Op
|
|
492
|
+
// executor's gate step does, and the component unwinds like any failure.
|
|
493
|
+
if (!(err instanceof GateNotRecordableError)) throw err;
|
|
494
|
+
gateRecords.push({ ...base, status: "fail" as const, durationMs: Date.now() - start, error: err.message });
|
|
495
|
+
for (const skipped of phaseDef.steps.filter((s): s is DriverStep | DriverPhase => !isGateStep(s))) {
|
|
496
|
+
gateRecords.push(skippedRecord(skipped));
|
|
497
|
+
}
|
|
498
|
+
throw new StepFailure(gateRecords, []);
|
|
499
|
+
}
|
|
487
500
|
if (!check.satisfied) {
|
|
488
501
|
gateRecords.push({ ...base, status: "skipped" as const, durationMs: Date.now() - start });
|
|
489
502
|
for (const skipped of phaseDef.steps.filter((s): s is DriverStep | DriverPhase => !isGateStep(s))) {
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `chant build --components --generate <forge>` leaves out a component with
|
|
3
|
+
* `enabled: false` (#3951), the same way `chant run --components all` skips it
|
|
4
|
+
* (#1522): no job for it, and no other job needs it. Runs against the real
|
|
5
|
+
* GitHub, GitLab and Forgejo plugins, not a stand-in, so each forge's own
|
|
6
|
+
* `needs` rendering is what the assertions read.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, test, expect, beforeEach, afterEach } from "vitest";
|
|
10
|
+
import { mkdir, writeFile, rm } from "node:fs/promises";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { generateComponentsPipeline, resolveComponentTargets } from "./cli-support";
|
|
14
|
+
|
|
15
|
+
describe.each(["github", "gitlab", "forgejo"])("generateComponentsPipeline %s: enabled: false (#3951)", (forge) => {
|
|
16
|
+
let testDir: string;
|
|
17
|
+
|
|
18
|
+
beforeEach(async () => {
|
|
19
|
+
testDir = join(tmpdir(), `chant-generate-disabled-${forge}-${Date.now()}-${Math.random()}`);
|
|
20
|
+
await mkdir(testDir, { recursive: true });
|
|
21
|
+
await writeFile(
|
|
22
|
+
join(testDir, "tunnel.component.ts"),
|
|
23
|
+
`export const tunnel = { name: "tunnel", enabled: false, dependsOn: [], deploy: [{ phase: "Apply", steps: [{ kind: "shell", reason: "t" }] }] };`,
|
|
24
|
+
);
|
|
25
|
+
await writeFile(
|
|
26
|
+
join(testDir, "db.component.ts"),
|
|
27
|
+
`export const db = { name: "db", dependsOn: [], deploy: [{ phase: "Apply", steps: [{ kind: "shell", reason: "t" }] }] };`,
|
|
28
|
+
);
|
|
29
|
+
await writeFile(
|
|
30
|
+
join(testDir, "api.component.ts"),
|
|
31
|
+
`export const api = { name: "api", dependsOn: ["db", "tunnel"], deploy: [{ phase: "Apply", steps: [{ kind: "shell", reason: "t" }] }] };`,
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
afterEach(async () => {
|
|
36
|
+
await rm(testDir, { recursive: true, force: true });
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("the disabled component has no job, and its dependent no longer needs it", async () => {
|
|
40
|
+
const result = await generateComponentsPipeline(testDir, forge, { env: "prod" });
|
|
41
|
+
|
|
42
|
+
expect(result.success).toBe(true);
|
|
43
|
+
expect(result.jobs?.map((j) => j.component).sort()).toEqual(["api", "db"]);
|
|
44
|
+
const api = result.jobs?.find((j) => j.component === "api");
|
|
45
|
+
const db = result.jobs?.find((j) => j.component === "db");
|
|
46
|
+
expect(api?.needs).toEqual([db?.jobName]);
|
|
47
|
+
expect(result.yaml).not.toMatch(/tunnel/);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("the pipeline holds the components an \"all\" run takes", async () => {
|
|
51
|
+
const result = await generateComponentsPipeline(testDir, forge, { env: "prod" });
|
|
52
|
+
const run = await resolveComponentTargets(testDir, "all");
|
|
53
|
+
|
|
54
|
+
expect(result.jobs?.map((j) => j.component).sort()).toEqual(run.targets.map((t) => t.name).sort());
|
|
55
|
+
});
|
|
56
|
+
});
|
package/src/effect-receipt.ts
CHANGED
|
@@ -66,6 +66,28 @@ export const EFFECT_RECEIPT_ENTITY_TYPE = "Chant::EffectReceipt";
|
|
|
66
66
|
*/
|
|
67
67
|
export const EXISTENCE_EXPECTATION = "chant.effect-receipt:exists";
|
|
68
68
|
|
|
69
|
+
const OUTPUTS_SEPARATOR = "+outputs=";
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* A receipt expectation with the digest of the step outputs its effect read
|
|
73
|
+
* appended (#3948). The effect step writes this value, so the next run fires
|
|
74
|
+
* again when one of those outputs changed, though the declared inputs did not.
|
|
75
|
+
*/
|
|
76
|
+
export function withStepOutputs(expectation: string, outputs: string): string {
|
|
77
|
+
return `${expectation}${OUTPUTS_SEPARATOR}${outputs}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The part of a stored receipt value that the declared inputs determine: the
|
|
82
|
+
* value without any step-output digest {@link withStepOutputs} appended. The
|
|
83
|
+
* plan and `receiptStaleness` compare this part, since step outputs exist only
|
|
84
|
+
* while the Op runs.
|
|
85
|
+
*/
|
|
86
|
+
export function declaredExpectation(value: string): string {
|
|
87
|
+
const at = value.indexOf(OUTPUTS_SEPARATOR);
|
|
88
|
+
return at < 0 ? value : value.slice(0, at);
|
|
89
|
+
}
|
|
90
|
+
|
|
69
91
|
/**
|
|
70
92
|
* An effect receipt declaration — a Declarable, so discovery collects it,
|
|
71
93
|
* `chant list` shows it, and (unlike a secret declaration) serialization
|