@intentius/chant 0.124.23 → 0.124.24
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 +17 -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/graph-detail.d.ts.map +1 -1
- package/dist/graph-detail.js +24 -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/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 +7 -2
- 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/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/point-decision.d.ts.map +1 -1
- package/dist/point-decision.js +3 -0
- package/dist/wave-runtime.mjs +238 -238
- 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/graph.schema.json +68 -1
- 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/runs.d.ts.map +1 -1
- package/dist/workspace/runs.js +4 -2
- package/package.json +1 -1
- package/src/ci-jobs.ts +58 -1
- package/src/cli/commands/build.ts +18 -0
- 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 +24 -0
- package/src/cli/main.ts +17 -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/graph-ci-edges.test.ts +50 -0
- package/src/graph-detail.ts +21 -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/op/activities/shell.ts +28 -6
- 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.ts +20 -3
- 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/runtimes/local.ts +6 -0
- package/src/op/step-console.test.ts +150 -0
- package/src/op/step-console.ts +155 -0
- package/src/point-decision-bundle.test.ts +1 -0
- package/src/point-decision.ts +3 -0
- package/src/workspace/compose-graph.test.ts +55 -0
- package/src/workspace/compose-graph.ts +28 -6
- package/src/workspace/graph-contract.test.ts +26 -1
- package/src/workspace/graph.schema.json +68 -1
- package/src/workspace/nested-graph.test.ts +14 -0
- package/src/workspace/nested-graph.ts +7 -2
- package/src/workspace/runs.ts +4 -2
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { exec, execFile } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
|
-
import { emulatorLifecycle, emulatorsOf, type EmulatorCapability } from "../../op";
|
|
3
|
+
import { checkSettings, emulatorLifecycle, emulatorsOf, type EmulatorCapability, type EmulatorUpArgs } from "../../op";
|
|
4
4
|
import type { LexiconPlugin } from "../../lexicon";
|
|
5
5
|
import { formatError, formatWarning, formatSuccess } from "../format";
|
|
6
|
-
import type { CommandContext } from "../registry";
|
|
6
|
+
import type { CommandContext, ParsedArgs } from "../registry";
|
|
7
7
|
|
|
8
8
|
const execAsync = promisify(exec);
|
|
9
9
|
const execFileAsync = promisify(execFile);
|
|
@@ -116,13 +116,60 @@ export function parsePortMappings(values: readonly string[], targets: readonly E
|
|
|
116
116
|
return ports;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
/** The `up` options other than the port (#3952), per selected emulator. */
|
|
120
|
+
type UpOptions = Pick<EmulatorUpArgs, "image" | "network" | "settings" | "loopback">;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* `--image`, `--network`, `--setting` and `--loopback` as each selected
|
|
124
|
+
* emulator's `up` options (#3952). `--image` needs exactly one emulator
|
|
125
|
+
* selected, the way a bare `--port` does. A `--setting` goes to each emulator
|
|
126
|
+
* whose settings prefix its key starts with, and a key that no selected
|
|
127
|
+
* emulator takes is refused rather than passed to a container as plain env.
|
|
128
|
+
*/
|
|
129
|
+
export function parseUpOptions(
|
|
130
|
+
args: Pick<ParsedArgs, "image" | "network" | "setting" | "loopback">,
|
|
131
|
+
targets: readonly EmulatorCapability[],
|
|
132
|
+
): Map<EmulatorCapability, UpOptions> {
|
|
133
|
+
if (args.image !== undefined && targets.length !== 1) {
|
|
134
|
+
throw new Error(`--image ${args.image} is ambiguous with ${targets.length} emulators selected; pick one with --lexicon <name>`);
|
|
135
|
+
}
|
|
136
|
+
const settings = new Map<EmulatorCapability, Record<string, string>>();
|
|
137
|
+
for (const value of args.setting ?? []) {
|
|
138
|
+
const match = /^([^=]+)=(.*)$/s.exec(value);
|
|
139
|
+
if (!match) throw new Error(`--setting ${value}: expected <KEY>=<VALUE>, e.g. --setting FLOCI_<NAME>=<value>`);
|
|
140
|
+
const [, key, val] = match as unknown as [string, string, string];
|
|
141
|
+
const takers = targets.filter((t) => t.spec.settingsPrefix && key.startsWith(t.spec.settingsPrefix));
|
|
142
|
+
if (takers.length === 0) {
|
|
143
|
+
const prefixes = targets.filter((t) => t.spec.settingsPrefix).map((t) => `${t.spec.settingsPrefix}* (${t.spec.name})`);
|
|
144
|
+
throw new Error(`--setting ${key}: no selected emulator takes it; ${prefixes.length ? `the selected ones take ${prefixes.join(", ")}` : "the selected ones take no settings"}`);
|
|
145
|
+
}
|
|
146
|
+
for (const t of takers) {
|
|
147
|
+
const own = { ...settings.get(t), [key]: val };
|
|
148
|
+
checkSettings(t.spec, own);
|
|
149
|
+
settings.set(t, own);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const out = new Map<EmulatorCapability, UpOptions>();
|
|
153
|
+
for (const t of targets) {
|
|
154
|
+
const opts: UpOptions = {};
|
|
155
|
+
if (args.image !== undefined) opts.image = args.image;
|
|
156
|
+
if (args.network !== undefined) opts.network = args.network;
|
|
157
|
+
if (args.loopback) opts.loopback = true;
|
|
158
|
+
const own = settings.get(t);
|
|
159
|
+
if (own) opts.settings = own;
|
|
160
|
+
out.set(t, opts);
|
|
161
|
+
}
|
|
162
|
+
return out;
|
|
163
|
+
}
|
|
164
|
+
|
|
119
165
|
/**
|
|
120
166
|
* `chant emulator --help` (#3673): the subcommands and flags, and for each
|
|
121
167
|
* configured lexicon's emulator the port, endpoint and sign-in a client uses.
|
|
122
168
|
*/
|
|
123
169
|
export function formatEmulatorHelp(plugins: readonly LexiconPlugin[]): string {
|
|
124
170
|
const lines = [
|
|
125
|
-
"Usage: chant emulator <up|down|status> [--lexicon <name>] [--port <container-port>=<host-port>] [--
|
|
171
|
+
"Usage: chant emulator <up|down|status> [--lexicon <name>] [--port <container-port>=<host-port>] [--image <ref>]",
|
|
172
|
+
" [--network <name>] [--setting <KEY>=<VALUE>] [--loopback] [--json]",
|
|
126
173
|
"",
|
|
127
174
|
"Boot, stop or inspect the local emulators of the project's configured lexicons.",
|
|
128
175
|
"",
|
|
@@ -137,6 +184,11 @@ export function formatEmulatorHelp(plugins: readonly LexiconPlugin[]): string {
|
|
|
137
184
|
" --lexicon <name> Act on one lexicon's emulators only.",
|
|
138
185
|
" --port <c>=<h> up: publish container port <c> on host port <h>; repeatable.",
|
|
139
186
|
" A bare <h> works when one emulator is selected.",
|
|
187
|
+
" --image <ref> up: start this image instead of the pinned default; one emulator only.",
|
|
188
|
+
" --network <name> up: join each started container to this Docker network.",
|
|
189
|
+
" --setting <K>=<V> up: pass an emulator setting as -e K=V; repeatable. The key must",
|
|
190
|
+
" start with the emulator's prefix (FLOCI_ for aws).",
|
|
191
|
+
" --loopback up: publish ports on 127.0.0.1 only, not on every interface.",
|
|
140
192
|
" --json Print the machine-readable report on stdout.",
|
|
141
193
|
"",
|
|
142
194
|
];
|
|
@@ -195,9 +247,18 @@ export async function runEmulator(ctx: CommandContext): Promise<number> {
|
|
|
195
247
|
}
|
|
196
248
|
|
|
197
249
|
let ports: Map<number, number>;
|
|
250
|
+
let upOptions: Map<EmulatorCapability, UpOptions>;
|
|
198
251
|
try {
|
|
199
252
|
if (args.port?.length && action !== "up") throw new Error("--port applies to `chant emulator up` only");
|
|
253
|
+
const upOnly = [
|
|
254
|
+
args.image !== undefined && "--image",
|
|
255
|
+
args.network !== undefined && "--network",
|
|
256
|
+
args.setting?.length && "--setting",
|
|
257
|
+
args.loopback && "--loopback",
|
|
258
|
+
].filter(Boolean);
|
|
259
|
+
if (upOnly.length && action !== "up") throw new Error(`${upOnly.join(", ")} applies to \`chant emulator up\` only`);
|
|
200
260
|
ports = parsePortMappings(args.port ?? [], targets.map((t) => t.cap));
|
|
261
|
+
upOptions = parseUpOptions(args, targets.map((t) => t.cap));
|
|
201
262
|
} catch (err) {
|
|
202
263
|
console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
|
|
203
264
|
return 1;
|
|
@@ -209,7 +270,7 @@ export async function runEmulator(ctx: CommandContext): Promise<number> {
|
|
|
209
270
|
if (action === "up") {
|
|
210
271
|
const port = ports.get(cap.spec.containerPort);
|
|
211
272
|
try {
|
|
212
|
-
const { endpoint } = await lc.up(port === undefined ? {} : { port });
|
|
273
|
+
const { endpoint } = await lc.up({ ...upOptions.get(cap), ...(port === undefined ? {} : { port }) });
|
|
213
274
|
reports.push({ lexicon, name: cap.spec.name, endpoint, env: cap.env(endpoint) });
|
|
214
275
|
} catch (err) {
|
|
215
276
|
console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
|
|
@@ -39,6 +39,7 @@ vi.mock("../../config", async () => {
|
|
|
39
39
|
});
|
|
40
40
|
|
|
41
41
|
vi.mock("../../components/cli-support", () => ({
|
|
42
|
+
isComponentList: (selector: string) => selector.includes(","),
|
|
42
43
|
resolveComponentTargets: (...args: unknown[]) => resolveTargetsMock(...args),
|
|
43
44
|
}));
|
|
44
45
|
|
|
@@ -286,6 +287,12 @@ describe("chant components rollback", () => {
|
|
|
286
287
|
expect(appendReleaseRecordMock.mock.calls[0][0]).not.toHaveProperty("promotedFrom");
|
|
287
288
|
});
|
|
288
289
|
|
|
290
|
+
test("a comma list is refused: rollback acts on one component (#3950)", async () => {
|
|
291
|
+
expect(await runComponentsRollback(rb({ component: "api,web" }))).toBe(1);
|
|
292
|
+
expect(errors.join("\n")).toMatch(/--component takes one component name, not a list/);
|
|
293
|
+
expect(ran).toEqual([]);
|
|
294
|
+
});
|
|
295
|
+
|
|
289
296
|
test("a digest the environment never recorded fails clearly", async () => {
|
|
290
297
|
expect(await runComponentsRollback(rb({ digest: "sha256:elsewhere" }))).toBe(1);
|
|
291
298
|
expect(errors.join("\n")).toMatch(/sha256:elsewhere is not recorded for "api" in "prod"/);
|
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
type ReleaseRecord,
|
|
26
26
|
type ReleaseRecordInput,
|
|
27
27
|
} from "../../lifecycle/release-ledger";
|
|
28
|
-
import { resolveComponentTargets } from "../../components/cli-support";
|
|
28
|
+
import { isComponentList, resolveComponentTargets } from "../../components/cli-support";
|
|
29
29
|
import { applyConfigDefaults } from "../../components/config-defaults";
|
|
30
30
|
import { fanOutRegistry } from "../../components/fan-out-support";
|
|
31
31
|
import { renderDriverHuman } from "../../components/driver-output";
|
|
@@ -107,6 +107,12 @@ async function loadProject(ctx: CommandContext, selector: string) {
|
|
|
107
107
|
for (const message of paramsResolution.errors) console.error(message);
|
|
108
108
|
return 1;
|
|
109
109
|
}
|
|
110
|
+
// A comma list is `chant run --components`' selector (#3950), which pulls in
|
|
111
|
+
// dependencies; promote, rollback and redeploy act on the one named component.
|
|
112
|
+
if (isComponentList(selector)) {
|
|
113
|
+
console.error(formatError({ message: `--component takes one component name, not a list ("${selector}")` }));
|
|
114
|
+
return 1;
|
|
115
|
+
}
|
|
110
116
|
const targets = await resolveComponentTargets(projectPath, selector, args.sandbox, paramsResolution.provenance);
|
|
111
117
|
if (!targets.success) {
|
|
112
118
|
console.error(formatError({ message: targets.error ?? "Could not discover components" }));
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #3955 — `chant run <op> --no-record` through the real CLI: the run executes,
|
|
3
|
+
* says at its start and end that it recorded nothing, and leaves
|
|
4
|
+
* `chant/lifecycle` where it was, both in the checkout and on origin. A plain
|
|
5
|
+
* run beside it records, so the unchanged ref is the flag's doing.
|
|
6
|
+
*/
|
|
7
|
+
import { spawnSync } from "node:child_process";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { pathToFileURL } from "node:url";
|
|
10
|
+
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
11
|
+
import { cleanScratch, commitAll, git, repo, REPO } from "../../workspace/__fixtures__/contract-repo";
|
|
12
|
+
import { pushLifecycleStatus, writeBlobToPath } from "../../lifecycle/git";
|
|
13
|
+
|
|
14
|
+
const MAIN = join(REPO, "packages/core/src/cli/main.ts");
|
|
15
|
+
const LOADER = pathToFileURL(join(REPO, "node_modules/tsx/dist/loader.mjs")).href;
|
|
16
|
+
|
|
17
|
+
/** An Op entity as `Op()` exports it, written as a literal so the fixture needs no import to resolve. */
|
|
18
|
+
function opModule(name: string, steps: unknown[]): string {
|
|
19
|
+
return `export const op = ${JSON.stringify({ entityType: "Chant::Op", props: { name, overview: name, phases: [{ name: "Main", steps }] } })};\n`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const SHELL = { kind: "activity", fn: "shellCmd", profile: "atMostOnce", args: { cmd: "echo ran" } };
|
|
23
|
+
|
|
24
|
+
let dir: string;
|
|
25
|
+
let remote: string;
|
|
26
|
+
|
|
27
|
+
beforeAll(async () => {
|
|
28
|
+
remote = repo({}, false);
|
|
29
|
+
git(remote, "config", "core.bare", "true");
|
|
30
|
+
dir = repo(
|
|
31
|
+
{
|
|
32
|
+
"chant.config.ts": "export default { lexicons: [] };\n",
|
|
33
|
+
"ops/hello.op.ts": opModule("hello", [SHELL]),
|
|
34
|
+
"ops/gated.op.ts": opModule("gated", [{ kind: "gate", gate: "approve-prod" }, SHELL]),
|
|
35
|
+
},
|
|
36
|
+
false,
|
|
37
|
+
);
|
|
38
|
+
git(dir, "config", "user.name", "Run Test");
|
|
39
|
+
git(dir, "config", "user.email", "run@example.com");
|
|
40
|
+
commitAll(dir, "c0");
|
|
41
|
+
git(dir, "remote", "add", "origin", join(remote, ".git"));
|
|
42
|
+
await writeBlobToPath("seed", "seed.json", "{}\n", "seed", { cwd: dir });
|
|
43
|
+
expect((await pushLifecycleStatus({ cwd: dir })).status).toBe("pushed");
|
|
44
|
+
});
|
|
45
|
+
afterAll(cleanScratch);
|
|
46
|
+
|
|
47
|
+
function chant(args: string[], env: Record<string, string> = {}): { status: number | null; stderr: string } {
|
|
48
|
+
const r = spawnSync(process.execPath, ["--import", LOADER, MAIN, ...args], {
|
|
49
|
+
cwd: dir,
|
|
50
|
+
encoding: "utf8",
|
|
51
|
+
env: { ...process.env, NO_COLOR: "1", TSX_DISABLE_CACHE: "1", CHANT_NO_RECORD: "", ...env },
|
|
52
|
+
});
|
|
53
|
+
return { status: r.status, stderr: r.stderr };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function ledgerRefs(): { local: string; origin: string } {
|
|
57
|
+
return { local: git(dir, "rev-parse", "refs/heads/chant/lifecycle"), origin: git(dir, "ls-remote", "origin", "refs/heads/chant/lifecycle").split(/\s/)[0]! };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
describe("chant run --no-record (#3955)", () => {
|
|
61
|
+
test("runs the Op, says so at the start and the end, and leaves the ledger ref unchanged here and on origin", () => {
|
|
62
|
+
const before = ledgerRefs();
|
|
63
|
+
const r = chant(["run", "hello", "--no-record"]);
|
|
64
|
+
expect(r.status, r.stderr).toBe(0);
|
|
65
|
+
expect(r.stderr).toContain("not recording (--no-record): nothing this run writes goes on chant/lifecycle");
|
|
66
|
+
expect(r.stderr).toMatch(/not recorded \(--no-record\): nothing went on chant\/lifecycle and nothing was pushed; 1 ledger write was dropped \(1 file\)/);
|
|
67
|
+
expect(ledgerRefs()).toEqual(before);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("CHANT_NO_RECORD=1 fails a gate without writing a pending fact", () => {
|
|
71
|
+
const before = ledgerRefs();
|
|
72
|
+
const r = chant(["run", "gated"], { CHANT_NO_RECORD: "1" });
|
|
73
|
+
expect(r.status).toBe(1);
|
|
74
|
+
expect(r.stderr).toContain('gate "approve-prod" of "gated" cannot be passed under --no-record');
|
|
75
|
+
expect(r.stderr).toContain("not recorded (CHANT_NO_RECORD=1)");
|
|
76
|
+
expect(ledgerRefs()).toEqual(before);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("a plain run of the same Op records its run", () => {
|
|
80
|
+
const before = ledgerRefs();
|
|
81
|
+
const r = chant(["run", "hello"]);
|
|
82
|
+
expect(r.status, r.stderr).toBe(0);
|
|
83
|
+
expect(r.stderr).not.toContain("not recording");
|
|
84
|
+
// The run record moves the local branch; it is pushed by the next write that pushes.
|
|
85
|
+
expect(ledgerRefs().local).not.toBe(before.local);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -29,16 +29,16 @@ export async function runOpWaveCommand(ctx: CommandContext): Promise<number> {
|
|
|
29
29
|
if (!specFile || args.wave === undefined) {
|
|
30
30
|
console.error(formatError({
|
|
31
31
|
message: "`chant run wave` needs --spec <file> and --wave <k>",
|
|
32
|
-
hint: "Run it as a generated Op waves pipeline does: chant run wave --spec <file> --wave <k> [--decide | --share <i>]",
|
|
32
|
+
hint: "Run it as a generated Op waves pipeline does: chant run wave --spec <file> --wave <k> [--decide | --share <i> | --group <name> [--share <i>]]",
|
|
33
33
|
}));
|
|
34
34
|
return 1;
|
|
35
35
|
}
|
|
36
|
-
if (args.decide && args.share !== undefined) {
|
|
37
|
-
console.error(formatError({ message:
|
|
36
|
+
if (args.decide && (args.share !== undefined || args.group !== undefined)) {
|
|
37
|
+
console.error(formatError({ message: `--decide and --${args.share !== undefined ? "share" : "group"} are two different jobs; pass one` }));
|
|
38
38
|
return 1;
|
|
39
39
|
}
|
|
40
40
|
let spec: OpWavesSpec | undefined;
|
|
41
|
-
const job = args.share !== undefined ? { share: args.share } : {};
|
|
41
|
+
const job = { ...(args.share !== undefined ? { share: args.share } : {}), ...(args.group !== undefined ? { group: args.group } : {}) };
|
|
42
42
|
try {
|
|
43
43
|
const path = resolve(specFile);
|
|
44
44
|
spec = parseOpWavesSpec(readFileSync(path, "utf-8"), `--spec ${specFile}`);
|
|
@@ -48,6 +48,7 @@ export async function runOpWaveCommand(ctx: CommandContext): Promise<number> {
|
|
|
48
48
|
wave: args.wave,
|
|
49
49
|
...(args.decide ? { decide: true } : {}),
|
|
50
50
|
...(args.share !== undefined ? { share: args.share } : {}),
|
|
51
|
+
...(args.group !== undefined ? { group: args.group } : {}),
|
|
51
52
|
...(args.base ? { base: args.base } : {}),
|
|
52
53
|
});
|
|
53
54
|
if (args.json) console.log(JSON.stringify(result, null, 2));
|
|
@@ -61,6 +61,7 @@ vi.mock("node:fs", async () => {
|
|
|
61
61
|
};
|
|
62
62
|
});
|
|
63
63
|
vi.mock("../../components/cli-support", () => ({
|
|
64
|
+
isComponentList: (selector: string) => selector.includes(","),
|
|
64
65
|
runComponents: (...args: unknown[]) => runComponentsMock(...args),
|
|
65
66
|
listComponents: (...args: unknown[]) => listComponentsMock(...args),
|
|
66
67
|
}));
|
|
@@ -1277,6 +1278,25 @@ describe("runOp dispatcher: --components routes to runOpComponents", () => {
|
|
|
1277
1278
|
vi.restoreAllMocks();
|
|
1278
1279
|
});
|
|
1279
1280
|
|
|
1281
|
+
test("a list selector prints the resolved order and the added dependencies before running (#3950)", async () => {
|
|
1282
|
+
discoverOpsMock.mockReset();
|
|
1283
|
+
const stderr = makeStderrSpy();
|
|
1284
|
+
let printedBeforeRun = "";
|
|
1285
|
+
runComponentsMock.mockImplementation(async (_path: string, _selector: string, options: { onPlan?: (plan: unknown) => void }) => {
|
|
1286
|
+
options.onPlan?.({ order: ["db", "api", "web"], waves: [["db"], ["api", "web"]], added: ["db"] });
|
|
1287
|
+
printedBeforeRun = stderr.join("\n");
|
|
1288
|
+
return { success: true, selected: ["db", "api", "web"], run: { order: ["db", "api", "web"], waves: [["db"], ["api", "web"]], results: [], ok: true, status: "ok" } };
|
|
1289
|
+
});
|
|
1290
|
+
|
|
1291
|
+
const exit = await runOp({ args: makeArgs({ path: "api,web", components: true }), plugins: [], serializers: [] });
|
|
1292
|
+
|
|
1293
|
+
expect(exit).toBe(0);
|
|
1294
|
+
expect(runComponentsMock).toHaveBeenCalledWith(expect.any(String), "api,web", expect.objectContaining({ onPlan: expect.any(Function) }));
|
|
1295
|
+
expect(printedBeforeRun).toContain("run order: db, then api + web");
|
|
1296
|
+
expect(printedBeforeRun).toContain("added as dependencies: db");
|
|
1297
|
+
vi.restoreAllMocks();
|
|
1298
|
+
});
|
|
1299
|
+
|
|
1280
1300
|
// chant #1116 — the component driver never checked --report, so it was
|
|
1281
1301
|
// silently ignored and the command fell through to a real dispatch.
|
|
1282
1302
|
// Hard-error instead, before runComponents is ever reached.
|
|
@@ -1929,3 +1949,109 @@ describe("runOp: --fail-on-drift (#3675)", () => {
|
|
|
1929
1949
|
expect(await runOp({ args: makeArgs({ path: "watch" }), plugins: [], serializers: [] })).toBe(2);
|
|
1930
1950
|
});
|
|
1931
1951
|
});
|
|
1952
|
+
|
|
1953
|
+
/**
|
|
1954
|
+
* chant #3955 — `--no-record` runs every step and records nothing on
|
|
1955
|
+
* `chant/lifecycle`. The run says so before it starts and after it ends, a
|
|
1956
|
+
* gate fails rather than recording a pending fact nobody could approve, and
|
|
1957
|
+
* a runtime that would record elsewhere is refused.
|
|
1958
|
+
*/
|
|
1959
|
+
describe("runOp --no-record (#3955)", () => {
|
|
1960
|
+
const START = "not recording (--no-record)";
|
|
1961
|
+
const END = "not recorded (--no-record): nothing went on chant/lifecycle and nothing was pushed";
|
|
1962
|
+
|
|
1963
|
+
beforeEach(() => {
|
|
1964
|
+
discoverOpsMock.mockReset();
|
|
1965
|
+
loadChantConfigMock.mockReset().mockResolvedValue({ config: {} });
|
|
1966
|
+
loadPluginsMock.mockReset().mockResolvedValue([]);
|
|
1967
|
+
runComponentsMock.mockReset();
|
|
1968
|
+
maybeRecordAutoReleaseMock.mockReset();
|
|
1969
|
+
maybePersistBuildManifestMock.mockReset().mockResolvedValue({ persisted: false, reason: "no-manifest" });
|
|
1970
|
+
gateLedger = memoryGateLedgerPort();
|
|
1971
|
+
delete process.env.CHANT_NO_RECORD;
|
|
1972
|
+
});
|
|
1973
|
+
|
|
1974
|
+
afterEach(() => {
|
|
1975
|
+
delete process.env.CHANT_NO_RECORD;
|
|
1976
|
+
vi.restoreAllMocks();
|
|
1977
|
+
});
|
|
1978
|
+
|
|
1979
|
+
test("an Op runs, the run says at its start and end that nothing was recorded, and the flag does not outlive it", async () => {
|
|
1980
|
+
let during: string | undefined;
|
|
1981
|
+
discoverOpsMock.mockImplementation(async () => {
|
|
1982
|
+
during = process.env.CHANT_NO_RECORD;
|
|
1983
|
+
return { ops: new Map([localOp("hello", [{ kind: "activity", fn: "shellCmd", profile: "atMostOnce", args: { cmd: "true" } }])]), errors: [] };
|
|
1984
|
+
});
|
|
1985
|
+
vi.spyOn(process.stderr, "write").mockImplementation(() => true);
|
|
1986
|
+
const stderr = makeStderrSpy();
|
|
1987
|
+
|
|
1988
|
+
const exit = await runOp({ args: makeArgs({ path: "hello", noRecord: true }), plugins: [], serializers: [] });
|
|
1989
|
+
|
|
1990
|
+
expect(exit).toBe(0);
|
|
1991
|
+
expect(during).toBe("1");
|
|
1992
|
+
expect(process.env.CHANT_NO_RECORD).toBeUndefined();
|
|
1993
|
+
expect(stderr[0]).toContain(START);
|
|
1994
|
+
expect(stderr.at(-1)).toContain(END);
|
|
1995
|
+
});
|
|
1996
|
+
|
|
1997
|
+
test("a gate fails the run and leaves no pending fact, and the end line still prints", async () => {
|
|
1998
|
+
discoverOpsMock.mockResolvedValue({ ops: new Map([localOp("gated", [{ kind: "gate", gate: "approve-prod" }])]), errors: [] });
|
|
1999
|
+
vi.spyOn(process.stderr, "write").mockImplementation(() => true);
|
|
2000
|
+
const stderr = makeStderrSpy();
|
|
2001
|
+
|
|
2002
|
+
const exit = await runOp({ args: makeArgs({ path: "gated", noRecord: true, gatedExit: 0 }), plugins: [], serializers: [] });
|
|
2003
|
+
|
|
2004
|
+
// A failure, not a gated stop: --gated-exit cannot turn it green.
|
|
2005
|
+
expect(exit).toBe(1);
|
|
2006
|
+
expect(gateLedger.appended).toHaveLength(0);
|
|
2007
|
+
expect(stderr.at(-1)).toContain(END);
|
|
2008
|
+
});
|
|
2009
|
+
|
|
2010
|
+
test("CHANT_NO_RECORD does the same without the flag, names itself, and stays set", async () => {
|
|
2011
|
+
process.env.CHANT_NO_RECORD = "1";
|
|
2012
|
+
discoverOpsMock.mockResolvedValue({ ops: new Map([localOp("hello", [])]), errors: [] });
|
|
2013
|
+
vi.spyOn(process.stderr, "write").mockImplementation(() => true);
|
|
2014
|
+
const stderr = makeStderrSpy();
|
|
2015
|
+
|
|
2016
|
+
const exit = await runOp({ args: makeArgs({ path: "hello" }), plugins: [], serializers: [] });
|
|
2017
|
+
|
|
2018
|
+
expect(exit).toBe(0);
|
|
2019
|
+
expect(stderr[0]).toContain("not recording (CHANT_NO_RECORD=1)");
|
|
2020
|
+
expect(process.env.CHANT_NO_RECORD).toBe("1");
|
|
2021
|
+
});
|
|
2022
|
+
|
|
2023
|
+
test("a runtime other than local is refused before the run starts", async () => {
|
|
2024
|
+
const start = vi.fn();
|
|
2025
|
+
loadChantConfigMock.mockResolvedValue({ config: { lexicons: ["stub"] } });
|
|
2026
|
+
discoverOpsMock.mockResolvedValue({ ops: new Map([makeOp("hello")]), errors: [] });
|
|
2027
|
+
const stderr = makeStderrSpy();
|
|
2028
|
+
|
|
2029
|
+
const exit = await runOp({
|
|
2030
|
+
args: makeArgs({ path: "hello", on: "stub", noRecord: true }),
|
|
2031
|
+
plugins: [{ name: "stub", opRuntime: { name: "stub", start } } as never], serializers: [],
|
|
2032
|
+
});
|
|
2033
|
+
|
|
2034
|
+
expect(exit).toBe(1);
|
|
2035
|
+
expect(start).not.toHaveBeenCalled();
|
|
2036
|
+
expect(stderr.join("\n")).toContain('--no-record runs on the local runtime; the "stub" runtime records the run where it executes it');
|
|
2037
|
+
});
|
|
2038
|
+
|
|
2039
|
+
test("--components records no release and persists no manifest", async () => {
|
|
2040
|
+
runComponentsMock.mockResolvedValue({
|
|
2041
|
+
success: true,
|
|
2042
|
+
selected: ["svc"],
|
|
2043
|
+
run: { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, status: "ok", records: [] }], ok: true, status: "ok" },
|
|
2044
|
+
});
|
|
2045
|
+
maybeRecordAutoReleaseMock.mockResolvedValue({ recorded: false, reason: "opted-out" });
|
|
2046
|
+
vi.spyOn(process.stderr, "write").mockImplementation(() => true);
|
|
2047
|
+
const stderr = makeStderrSpy();
|
|
2048
|
+
|
|
2049
|
+
const exit = await runOpComponents({ args: makeArgs({ path: "svc", noRecord: true }), plugins: [], serializers: [] });
|
|
2050
|
+
|
|
2051
|
+
expect(exit).toBe(0);
|
|
2052
|
+
expect(maybeRecordAutoReleaseMock.mock.calls[0][1]).toMatchObject({ disabled: true });
|
|
2053
|
+
expect(maybePersistBuildManifestMock.mock.calls[0][1]).toMatchObject({ disabled: true });
|
|
2054
|
+
expect(stderr[0]).toContain(START);
|
|
2055
|
+
expect(stderr.at(-1)).toContain(END);
|
|
2056
|
+
});
|
|
2057
|
+
});
|
package/src/cli/handlers/run.ts
CHANGED
|
@@ -23,11 +23,13 @@ import { acquireStewardTurn, STEWARD_TURN_WAIT_MS } from "../../op/operator";
|
|
|
23
23
|
import { releaseLease, currentHolderId, type AcquireLeaseResult } from "../../lifecycle/lease";
|
|
24
24
|
import { StaleLockError } from "../../lifecycle/git";
|
|
25
25
|
import { LIFECYCLE_LOCAL_NOTE, isLifecycleLocalNote } from "../../lifecycle/local-note";
|
|
26
|
+
import { ledgerRecordingOff, noRecordEndNote, noRecordStartNote, resetNoRecordLedger, NO_RECORD_ENV } from "../../lifecycle/no-record";
|
|
26
27
|
import { renderDriverHuman, renderDriverJson } from "../../components/driver-output";
|
|
27
28
|
import { ndjsonProgressSink } from "../../components/run-progress";
|
|
28
29
|
import { maybeRecordAutoRelease } from "../../components/auto-release";
|
|
29
30
|
import { maybePersistBuildManifest } from "../../components/manifest-persistence";
|
|
30
31
|
import type { DriverComponentResult } from "../../components/driver";
|
|
32
|
+
import { isComponentList, type ComponentRunPlan } from "../../components/cli-support";
|
|
31
33
|
import type { ReleaseRecord } from "../../lifecycle/release-ledger";
|
|
32
34
|
import { runOpGenerate } from "./run-generate";
|
|
33
35
|
|
|
@@ -873,12 +875,13 @@ export async function recordAutoReleasesForRun(
|
|
|
873
875
|
return recorded;
|
|
874
876
|
}
|
|
875
877
|
|
|
876
|
-
// ── chant run --components <name|all>
|
|
878
|
+
// ── chant run --components <name|all|a,b,c> ──────────────────────────────────
|
|
877
879
|
|
|
878
880
|
/**
|
|
879
|
-
* `chant run --components <name|all> [--env <env>]` (#585) — the
|
|
880
|
-
* driver's CLI entrypoint, and since #2116 the only component run
|
|
881
|
-
* is. `args.path` is the component name (or `all
|
|
881
|
+
* `chant run --components <name|all|a,b,c> [--env <env>]` (#585) — the
|
|
882
|
+
* interpret driver's CLI entrypoint, and since #2116 the only component run
|
|
883
|
+
* path there is. `args.path` is the component name (or `all`, or a comma list
|
|
884
|
+
* that also runs what the named components depend on, #3950), matching `chant run
|
|
882
885
|
* <name>`'s Op-dispatch convention exactly (`args.path` is the Op name there
|
|
883
886
|
* too) rather than a project directory — components are always discovered from
|
|
884
887
|
* the current working directory, the same way Op discovery (`discoverOps()`)
|
|
@@ -909,11 +912,52 @@ export async function recordAutoReleasesForRun(
|
|
|
909
912
|
* auto-release check below used to apply itself) and reused for that check,
|
|
910
913
|
* rather than loaded a second time.
|
|
911
914
|
*/
|
|
912
|
-
export
|
|
915
|
+
export function runOpComponents(ctx: CommandContext): Promise<number> {
|
|
916
|
+
return withRecordingChoice(ctx, () => runComponentSelection(ctx));
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/**
|
|
920
|
+
* Run `run` with recording off when `--no-record` or `CHANT_NO_RECORD` asks
|
|
921
|
+
* for it (#3955), and say so before it starts and after it ends, whatever
|
|
922
|
+
* the outcome. The flag sets `CHANT_NO_RECORD` for the length of the run, so
|
|
923
|
+
* the ledger seam (`../../lifecycle/git.ts`) and every chant process a step
|
|
924
|
+
* starts see the same choice. A run with recording on is untouched.
|
|
925
|
+
*/
|
|
926
|
+
async function withRecordingChoice(ctx: CommandContext, run: () => Promise<number>): Promise<number> {
|
|
927
|
+
const flagged = ctx.args.noRecord === true;
|
|
928
|
+
if (!flagged && !ledgerRecordingOff()) return run();
|
|
929
|
+
const source = flagged ? "--no-record" : `${NO_RECORD_ENV}=${process.env[NO_RECORD_ENV]}`;
|
|
930
|
+
const prior = process.env[NO_RECORD_ENV];
|
|
931
|
+
if (flagged) process.env[NO_RECORD_ENV] = "1";
|
|
932
|
+
resetNoRecordLedger();
|
|
933
|
+
console.error(formatWarning({ message: noRecordStartNote(source) }));
|
|
934
|
+
try {
|
|
935
|
+
return await run();
|
|
936
|
+
} finally {
|
|
937
|
+
console.error(formatWarning({ message: noRecordEndNote(source) }));
|
|
938
|
+
resetNoRecordLedger();
|
|
939
|
+
if (flagged) {
|
|
940
|
+
if (prior === undefined) delete process.env[NO_RECORD_ENV];
|
|
941
|
+
else process.env[NO_RECORD_ENV] = prior;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
/** A run that records nothing stays in this process (#3955): a runtime that runs it elsewhere would record there. */
|
|
947
|
+
function refusesNoRecordOn(runtime: Pick<OpRuntimeProvider, "name">): boolean {
|
|
948
|
+
if (!ledgerRecordingOff() || runtime.name === "local") return false;
|
|
949
|
+
console.error(formatError({
|
|
950
|
+
message: `--no-record runs on the local runtime; the "${runtime.name}" runtime records the run where it executes it`,
|
|
951
|
+
hint: `Omit --on to run it here without recording, or drop --no-record (and ${NO_RECORD_ENV}).`,
|
|
952
|
+
}));
|
|
953
|
+
return true;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
async function runComponentSelection(ctx: CommandContext): Promise<number> {
|
|
913
957
|
const selector = ctx.args.path;
|
|
914
958
|
if (!selector || selector === ".") {
|
|
915
959
|
console.error(formatError({
|
|
916
|
-
message: "Component name is required: chant run --components <name|all>",
|
|
960
|
+
message: "Component name is required: chant run --components <name|all|a,b,c>",
|
|
917
961
|
hint: "Run `chant list --components` to see available components.",
|
|
918
962
|
}));
|
|
919
963
|
return 1;
|
|
@@ -936,6 +980,7 @@ export async function runOpComponents(ctx: CommandContext): Promise<number> {
|
|
|
936
980
|
|
|
937
981
|
const runtime = await resolveOpRuntime(ctx);
|
|
938
982
|
if (!runtime) return 1;
|
|
983
|
+
if (refusesNoRecordOn(runtime)) return 1;
|
|
939
984
|
if (!runtime.runComponents) {
|
|
940
985
|
console.error(formatError({
|
|
941
986
|
message: `--components is not supported on the "${runtime.name}" runtime`,
|
|
@@ -966,12 +1011,22 @@ export async function runOpComponents(ctx: CommandContext): Promise<number> {
|
|
|
966
1011
|
// `undefined` and every `onProgress?.(...)` call in the driver is a no-op —
|
|
967
1012
|
// behavior is byte-for-byte unchanged from before this flag existed.
|
|
968
1013
|
const onProgress = ctx.args.progressJson ? ndjsonProgressSink() : undefined;
|
|
1014
|
+
// A list (#3950) pulls in what its components depend on; say which ones and
|
|
1015
|
+
// in what order before the first one runs. stderr, so --json and
|
|
1016
|
+
// --progress-json keep stdout to themselves.
|
|
1017
|
+
const onPlan = isComponentList(selector)
|
|
1018
|
+
? (plan: ComponentRunPlan) => {
|
|
1019
|
+
console.error(formatInfo(`run order: ${plan.waves.map((wave) => wave.join(" + ")).join(", then ")}`));
|
|
1020
|
+
if (plan.added.length > 0) console.error(formatInfo(`added as dependencies: ${plan.added.join(", ")}`));
|
|
1021
|
+
}
|
|
1022
|
+
: undefined;
|
|
969
1023
|
const result = await runtime.runComponents(projectPath, selector, {
|
|
970
1024
|
env: ctx.args.env,
|
|
971
1025
|
componentOutputs: seededOutputs,
|
|
972
1026
|
onProgress,
|
|
973
1027
|
sandbox: ctx.args.sandbox,
|
|
974
1028
|
buildParams: paramsResolution.provenance,
|
|
1029
|
+
...(onPlan ? { onPlan } : {}),
|
|
975
1030
|
});
|
|
976
1031
|
|
|
977
1032
|
// Dump the accumulated outputs for a downstream job to seed from. Written
|
|
@@ -1037,7 +1092,8 @@ export async function runOpComponents(ctx: CommandContext): Promise<number> {
|
|
|
1037
1092
|
}
|
|
1038
1093
|
|
|
1039
1094
|
if (result.success && result.run) {
|
|
1040
|
-
|
|
1095
|
+
// #3955: a run that records nothing has no release to report either.
|
|
1096
|
+
const disabled = resolveAutoReleaseDisabled(config, ctx.args.noReleaseRecord) || ledgerRecordingOff();
|
|
1041
1097
|
const recorded = await recordAutoReleasesForRun(result.run.results, env, `local-${Date.now()}`, disabled);
|
|
1042
1098
|
// `--digest-file` (#2602): the releases this run recorded, one
|
|
1043
1099
|
// `<component>=<digest>` line each, the form `chant components promote
|
|
@@ -1065,7 +1121,11 @@ export async function runOpComponents(ctx: CommandContext): Promise<number> {
|
|
|
1065
1121
|
* what they were before the seam existed; a runtime that reports only a state
|
|
1066
1122
|
* gets the terser render instead.
|
|
1067
1123
|
*/
|
|
1068
|
-
export
|
|
1124
|
+
export function runOpOnRuntime(ctx: CommandContext): Promise<number> {
|
|
1125
|
+
return withRecordingChoice(ctx, () => runOpOnResolvedRuntime(ctx));
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
async function runOpOnResolvedRuntime(ctx: CommandContext): Promise<number> {
|
|
1069
1129
|
const opName = ctx.args.path;
|
|
1070
1130
|
if (!opName || opName === ".") {
|
|
1071
1131
|
console.error(formatError({
|
|
@@ -1108,7 +1168,7 @@ export async function runOpOnRuntime(ctx: CommandContext): Promise<number> {
|
|
|
1108
1168
|
}
|
|
1109
1169
|
|
|
1110
1170
|
const runtime = await resolveOpRuntime(ctx);
|
|
1111
|
-
if (!runtime) {
|
|
1171
|
+
if (!runtime || refusesNoRecordOn(runtime)) {
|
|
1112
1172
|
await stewardGate?.release?.();
|
|
1113
1173
|
return 1;
|
|
1114
1174
|
}
|
package/src/cli/main.test.ts
CHANGED
|
@@ -361,6 +361,22 @@ describe("parseArgs", () => {
|
|
|
361
361
|
|
|
362
362
|
// ── --no-release-record (#597) ───────────────────────────────────────────
|
|
363
363
|
|
|
364
|
+
test("emulator up --image/--network/--setting/--loopback parse, and a missing value is refused (#3952)", () => {
|
|
365
|
+
const a = parseArgs([
|
|
366
|
+
"emulator", "up", "--image", "floci/floci:1.6.0", "--network", "loom",
|
|
367
|
+
"--setting", "FLOCI_A=1", "--setting", "FLOCI_B=2", "--loopback",
|
|
368
|
+
]);
|
|
369
|
+
expect(a.image).toBe("floci/floci:1.6.0");
|
|
370
|
+
expect(a.network).toBe("loom");
|
|
371
|
+
expect(a.setting).toEqual(["FLOCI_A=1", "FLOCI_B=2"]);
|
|
372
|
+
expect(a.loopback).toBe(true);
|
|
373
|
+
expect(parseArgs(["emulator", "up", "--setting=FLOCI_A=1"]).setting).toEqual(["FLOCI_A=1"]);
|
|
374
|
+
expect(() => parseArgs(["emulator", "up", "--image"])).toThrow(/--image needs an image reference/);
|
|
375
|
+
expect(() => parseArgs(["emulator", "up", "--network", "--json"])).toThrow(/--network needs a Docker network name/);
|
|
376
|
+
expect(() => parseArgs(["emulator", "up", "--setting"])).toThrow(/--setting needs a value/);
|
|
377
|
+
expect(() => parseArgs(["emulator", "up", "--loopback=yes"])).toThrow();
|
|
378
|
+
});
|
|
379
|
+
|
|
364
380
|
test("--promote-to <env> sets promoteTo, and a missing value is refused (#2575)", () => {
|
|
365
381
|
expect(parseArgs(["build", "--components", "--generate", "github", "--promote-to", "prod"]).promoteTo).toBe("prod");
|
|
366
382
|
expect(() => parseArgs(["build", "--components", "--generate", "github", "--promote-to"])).toThrow(/--promote-to needs an environment/);
|
|
@@ -390,6 +406,14 @@ describe("parseArgs", () => {
|
|
|
390
406
|
expect(result.noReleaseRecord).toBeUndefined();
|
|
391
407
|
});
|
|
392
408
|
|
|
409
|
+
test("parses --no-record for run <op> and run --components, apart from --no-release-record (#3955)", () => {
|
|
410
|
+
expect(parseArgs(["run", "smoke", "--no-record"])).toMatchObject({ path: "smoke", noRecord: true });
|
|
411
|
+
const components = parseArgs(["run", "--components", "svc", "--no-record"]);
|
|
412
|
+
expect(components).toMatchObject({ components: true, noRecord: true });
|
|
413
|
+
expect(components.noReleaseRecord).toBeUndefined();
|
|
414
|
+
expect(parseArgs(["run", "smoke"]).noRecord).toBeUndefined();
|
|
415
|
+
});
|
|
416
|
+
|
|
393
417
|
// ── --progress-json (M3, behold roadmap) ─────────────────────────────────
|
|
394
418
|
|
|
395
419
|
test("parses --progress-json for run --components", () => {
|