@intentius/chant 0.22.0 → 0.24.0
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/cli/build-params-cli.d.ts +55 -0
- package/dist/cli/build-params-cli.d.ts.map +1 -0
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts +22 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/lsp/server.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/plugins.d.ts +8 -0
- package/dist/cli/plugins.d.ts.map +1 -1
- package/dist/components/cli-support.d.ts +33 -2
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/discover.d.ts +28 -0
- package/dist/components/discover.d.ts.map +1 -1
- package/dist/config.d.ts +18 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/discovery/fold-import.d.ts +38 -8
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/discovery/index.d.ts +15 -4
- package/dist/discovery/index.d.ts.map +1 -1
- package/dist/fold/subset.d.ts +16 -2
- package/dist/fold/subset.d.ts.map +1 -1
- package/dist/lint/config.d.ts +1 -12
- package/dist/lint/config.d.ts.map +1 -1
- package/dist/lint/engine.d.ts +11 -1
- package/dist/lint/engine.d.ts.map +1 -1
- package/dist/lint/rule.d.ts +14 -0
- package/dist/lint/rule.d.ts.map +1 -1
- package/dist/project-root.d.ts +51 -0
- package/dist/project-root.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/cli/build-params-cli.test.ts +139 -0
- package/src/cli/build-params-cli.ts +107 -0
- package/src/cli/commands/build.ts +44 -41
- package/src/cli/commands/lint.test.ts +74 -0
- package/src/cli/commands/lint.ts +33 -9
- package/src/cli/handlers/build.test.ts +149 -0
- package/src/cli/handlers/build.ts +28 -8
- package/src/cli/handlers/run.test.ts +191 -5
- package/src/cli/handlers/run.ts +61 -8
- package/src/cli/lsp/server.ts +7 -2
- package/src/cli/main.test.ts +23 -0
- package/src/cli/main.ts +17 -3
- package/src/cli/plugins.ts +10 -2
- package/src/components/cli-support.test.ts +221 -3
- package/src/components/cli-support.ts +37 -6
- package/src/components/discover.test.ts +63 -1
- package/src/components/discover.ts +42 -0
- package/src/config.ts +23 -0
- package/src/discovery/fold-import.ts +202 -20
- package/src/discovery/index.test.ts +131 -0
- package/src/discovery/index.ts +38 -8
- package/src/discovery/sandbox/fold-boundary.test.ts +254 -0
- package/src/fold/subset.test.ts +28 -14
- package/src/fold/subset.ts +16 -2
- package/src/lint/config.test.ts +9 -4
- package/src/lint/config.ts +7 -23
- package/src/lint/engine.ts +12 -0
- package/src/lint/policy.ts +5 -5
- package/src/lint/rule.ts +14 -0
- package/src/lint/rules/evl001-non-literal-expression.test.ts +39 -0
- package/src/lint/rules/evl001-non-literal-expression.ts +1 -1
- package/src/project-root.test.ts +105 -0
- package/src/project-root.ts +78 -0
package/src/cli/handlers/run.ts
CHANGED
|
@@ -2,12 +2,13 @@ import { resolve, join, dirname } from "node:path";
|
|
|
2
2
|
import { existsSync, writeFileSync, mkdirSync, readFileSync } from "node:fs";
|
|
3
3
|
import { createConnection } from "node:net";
|
|
4
4
|
import { spawn as spawnChild, type ChildProcess } from "node:child_process";
|
|
5
|
-
import { loadChantConfig, resolveAutoReleaseDisabled } from "../../config";
|
|
5
|
+
import { loadChantConfig, resolveAutoReleaseDisabled, type ChantConfig } from "../../config";
|
|
6
6
|
import { discoverOps } from "../../op/discover";
|
|
7
7
|
import { loadActivities, loadProfiles } from "../../op/activity-registry";
|
|
8
8
|
import { runOpLocally, findGate, LocalGateUnsupportedError, OpRunFailure } from "../../op/local-executor";
|
|
9
9
|
import { renderHuman, renderJson } from "../../op/local-output";
|
|
10
10
|
import { formatError, formatWarning, formatSuccess, formatBold, formatInfo } from "../format";
|
|
11
|
+
import { resolveCliBuildParams, parseParamFlags } from "../build-params-cli";
|
|
11
12
|
import type { CommandContext } from "../registry";
|
|
12
13
|
import {
|
|
13
14
|
loadTemporalClient,
|
|
@@ -27,6 +28,7 @@ import { applyConfigDefaults } from "../../components/config-defaults";
|
|
|
27
28
|
import { maybeRecordAutoRelease, extractRunDigestFromPhaseOutputs } from "../../components/auto-release";
|
|
28
29
|
import { maybePersistBuildManifest, extractRunManifestFromPhaseOutputs } from "../../components/manifest-persistence";
|
|
29
30
|
import type { DriverComponentResult } from "../../components/driver";
|
|
31
|
+
import type { BuildParamProvenance } from "../../provenance";
|
|
30
32
|
|
|
31
33
|
function kebabToCamel(s: string): string {
|
|
32
34
|
return s.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase());
|
|
@@ -546,8 +548,23 @@ function renderProgress(opName: string, history: WorkflowHistoryRaw): void {
|
|
|
546
548
|
* (`../../components/driver.ts`) rather than a `*.op.ts` Op. Checked first,
|
|
547
549
|
* mirroring `runGraph`'s `if (ctx.args.components) return
|
|
548
550
|
* runComponentGraph(ctx)` branch (../handlers/graph.ts).
|
|
551
|
+
*
|
|
552
|
+
* chant #1116 — `--report` with `--components` is checked and hard-errored
|
|
553
|
+
* before that dispatch. There is no preview/dry-run mode for the component
|
|
554
|
+
* driver: unlike the Op path (where `--report` reads a past Temporal run),
|
|
555
|
+
* `runOpComponents` has never read `ctx.args.report` at all, so the flag was
|
|
556
|
+
* silently ignored and the command fell through to a real dispatch — observed
|
|
557
|
+
* live reaching an actual cloud shell-out. Erroring here is the safe minimum
|
|
558
|
+
* called out on the issue; a real preview is future work.
|
|
549
559
|
*/
|
|
550
560
|
export async function runOp(ctx: CommandContext): Promise<number> {
|
|
561
|
+
if (ctx.args.components && ctx.args.report) {
|
|
562
|
+
console.error(formatError({
|
|
563
|
+
message: "--report is not supported with --components",
|
|
564
|
+
hint: "No preview/dry-run mode exists yet for the component driver (see chant#1116). Omit --report.",
|
|
565
|
+
}));
|
|
566
|
+
return 1;
|
|
567
|
+
}
|
|
551
568
|
if (ctx.args.components) return runOpComponents(ctx);
|
|
552
569
|
if (ctx.args.local && ctx.args.temporal) {
|
|
553
570
|
console.error(formatError({
|
|
@@ -669,6 +686,18 @@ async function recordAutoReleasesForRun(
|
|
|
669
686
|
* Opt out with `--no-release-record` or `chant.config.ts`'s
|
|
670
687
|
* `release.autoRecord: false`; the default is ON. A failed run never reaches
|
|
671
688
|
* this step, so it writes nothing.
|
|
689
|
+
*
|
|
690
|
+
* chant #1108 — resolves this invocation's declared build-time parameters
|
|
691
|
+
* (`chant.config.ts`'s `buildParams`, against `--param`/`--params-file`/a
|
|
692
|
+
* declared `env` mapping) the exact same way `chant build` does
|
|
693
|
+
* (`resolveCliBuildParams`, shared with `buildCommand`), BEFORE either the
|
|
694
|
+
* local or `--temporal` path discovers/imports any `*.component.ts` file.
|
|
695
|
+
* Before this, `params.*` (`@intentius/chant/params`) was always `{}` under
|
|
696
|
+
* this command, no matter what a component's `chant.config.ts` declared or a
|
|
697
|
+
* CI job's environment supplied — see chant #1108. `chant.config.ts` is
|
|
698
|
+
* loaded once here (with the same defensive fallback the post-run
|
|
699
|
+
* auto-release check below used to apply itself) and reused for that check,
|
|
700
|
+
* rather than loaded a second time.
|
|
672
701
|
*/
|
|
673
702
|
export async function runOpComponents(ctx: CommandContext): Promise<number> {
|
|
674
703
|
const selector = ctx.args.path;
|
|
@@ -680,7 +709,18 @@ export async function runOpComponents(ctx: CommandContext): Promise<number> {
|
|
|
680
709
|
return 1;
|
|
681
710
|
}
|
|
682
711
|
|
|
683
|
-
|
|
712
|
+
const projectPath = resolve(".");
|
|
713
|
+
const { config } = await loadChantConfig(projectPath).catch(() => ({ config: {} as ChantConfig }));
|
|
714
|
+
const paramsResolution = resolveCliBuildParams(config.buildParams, {
|
|
715
|
+
cli: parseParamFlags(ctx.args.param),
|
|
716
|
+
paramsFile: ctx.args.paramsFile,
|
|
717
|
+
});
|
|
718
|
+
if (!paramsResolution.success) {
|
|
719
|
+
for (const message of paramsResolution.errors) console.error(message);
|
|
720
|
+
return 1;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
if (ctx.args.temporal) return runComponentTemporal(ctx, selector, config, paramsResolution.provenance);
|
|
684
724
|
|
|
685
725
|
const env = ctx.args.env ?? "local";
|
|
686
726
|
// Seed cross-component/cross-stack outputs from upstream jobs' dumped files
|
|
@@ -704,11 +744,12 @@ export async function runOpComponents(ctx: CommandContext): Promise<number> {
|
|
|
704
744
|
// `undefined` and every `onProgress?.(...)` call in the driver is a no-op —
|
|
705
745
|
// behavior is byte-for-byte unchanged from before this flag existed.
|
|
706
746
|
const onProgress = ctx.args.progressJson ? ndjsonProgressSink() : undefined;
|
|
707
|
-
const result = await runComponents(
|
|
747
|
+
const result = await runComponents(projectPath, selector, {
|
|
708
748
|
env: ctx.args.env,
|
|
709
749
|
componentOutputs: seededOutputs,
|
|
710
750
|
onProgress,
|
|
711
751
|
sandbox: ctx.args.sandbox,
|
|
752
|
+
buildParams: paramsResolution.provenance,
|
|
712
753
|
});
|
|
713
754
|
|
|
714
755
|
// Dump the accumulated outputs for a downstream job to seed from. Written
|
|
@@ -739,7 +780,6 @@ export async function runOpComponents(ctx: CommandContext): Promise<number> {
|
|
|
739
780
|
}
|
|
740
781
|
|
|
741
782
|
if (result.success && result.run) {
|
|
742
|
-
const { config } = await loadChantConfig(resolve(".")).catch(() => ({ config: {} }));
|
|
743
783
|
const disabled = resolveAutoReleaseDisabled(config, ctx.args.noReleaseRecord);
|
|
744
784
|
await recordAutoReleasesForRun(result.run.results, env, `local-${Date.now()}`, disabled);
|
|
745
785
|
}
|
|
@@ -771,8 +811,19 @@ function componentWorkflowId(componentName: string): string {
|
|
|
771
811
|
* crash and clearable via `chant run signal <name> <signal> --components
|
|
772
812
|
* --temporal` (`runOpSignal` above, extended for components alongside this
|
|
773
813
|
* issue).
|
|
814
|
+
*
|
|
815
|
+
* `config`/`buildParams` are resolved once by the caller (`runOpComponents`,
|
|
816
|
+
* chant #1108) BEFORE this function runs, so `resolveComponentTargets`
|
|
817
|
+
* below — which discovers/imports the target `*.component.ts` file — sees
|
|
818
|
+
* `params.*` (`@intentius/chant/params`) already populated, the same
|
|
819
|
+
* guarantee the local executor path gets.
|
|
774
820
|
*/
|
|
775
|
-
async function runComponentTemporal(
|
|
821
|
+
async function runComponentTemporal(
|
|
822
|
+
ctx: CommandContext,
|
|
823
|
+
selector: string,
|
|
824
|
+
config: ChantConfig,
|
|
825
|
+
buildParams: BuildParamProvenance[],
|
|
826
|
+
): Promise<number> {
|
|
776
827
|
if (selector === "all") {
|
|
777
828
|
console.error(formatError({
|
|
778
829
|
message: "`chant run --components all --temporal` is not supported",
|
|
@@ -782,15 +833,17 @@ async function runComponentTemporal(ctx: CommandContext, selector: string): Prom
|
|
|
782
833
|
}
|
|
783
834
|
|
|
784
835
|
const projectPath = resolve(".");
|
|
785
|
-
const resolved = await resolveComponentTargets(projectPath, selector, ctx.args.sandbox);
|
|
836
|
+
const resolved = await resolveComponentTargets(projectPath, selector, ctx.args.sandbox, buildParams);
|
|
786
837
|
if (!resolved.success || resolved.targets.length === 0) {
|
|
787
838
|
console.error(formatError({ message: resolved.error ?? `Component "${selector}" not found` }));
|
|
788
839
|
return 1;
|
|
789
840
|
}
|
|
790
841
|
const component = resolved.targets[0];
|
|
791
842
|
|
|
792
|
-
//
|
|
793
|
-
|
|
843
|
+
// Config + profile (mirrors runOpTemporal). `config` was already loaded by
|
|
844
|
+
// the caller to resolve build-time parameters (chant #1108); reused here
|
|
845
|
+
// rather than loading chant.config.ts a second time.
|
|
846
|
+
const chantConfig = config;
|
|
794
847
|
let profile;
|
|
795
848
|
try {
|
|
796
849
|
profile = resolveProfile(chantConfig as Record<string, unknown>, ctx.args.profile);
|
package/src/cli/lsp/server.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { LexiconPlugin } from "../../lexicon";
|
|
1
|
+
import type { LexiconPlugin, IntrinsicDef } from "../../lexicon";
|
|
2
2
|
import type { CompletionContext, HoverContext, CodeActionContext } from "../../lsp/types";
|
|
3
3
|
import { computeCapabilities } from "./capabilities";
|
|
4
4
|
import { toLspDiagnostics } from "./diagnostics";
|
|
@@ -356,13 +356,18 @@ export class LspServer {
|
|
|
356
356
|
try {
|
|
357
357
|
const { runLint } = await import("../../lint/engine");
|
|
358
358
|
const rules = [];
|
|
359
|
+
// chant #1106 — the same active plugins' registered intrinsics, so
|
|
360
|
+
// EVL001 answers exactly like `fold()` does for a registered,
|
|
361
|
+
// opted-in call instead of flagging every call as a violation.
|
|
362
|
+
const intrinsics: IntrinsicDef[] = [];
|
|
359
363
|
for (const plugin of this.plugins) {
|
|
360
364
|
rules.push(...(plugin.lintRules?.() ?? []));
|
|
365
|
+
intrinsics.push(...(plugin.intrinsics?.() ?? []));
|
|
361
366
|
}
|
|
362
367
|
|
|
363
368
|
if (rules.length === 0) return [];
|
|
364
369
|
|
|
365
|
-
const { diagnostics } = await runLint([filePath], rules);
|
|
370
|
+
const { diagnostics } = await runLint([filePath], rules, undefined, intrinsics);
|
|
366
371
|
return toLspDiagnostics(diagnostics);
|
|
367
372
|
} catch {
|
|
368
373
|
return [];
|
package/src/cli/main.test.ts
CHANGED
|
@@ -270,6 +270,29 @@ describe("parseArgs", () => {
|
|
|
270
270
|
const result = parseArgs(["build", "src", "--params-file", "./params.json"]);
|
|
271
271
|
expect(result.paramsFile).toBe("./params.json");
|
|
272
272
|
});
|
|
273
|
+
|
|
274
|
+
// ── --param=name=value hard error (chant #1118) ──────────────────────────
|
|
275
|
+
// The joined `--flag=value` form is not supported anywhere in this parser
|
|
276
|
+
// (see "ignores unknown flags" above) — a dropped --param can silently
|
|
277
|
+
// change what a build measures/deploys, so this form is rejected loudly
|
|
278
|
+
// instead of silently accepted as a no-op.
|
|
279
|
+
|
|
280
|
+
test("--param=name=value throws instead of silently dropping", () => {
|
|
281
|
+
expect(() => parseArgs(["build", "src", "--param=tier=production"])).toThrow(/--param=tier=production/);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test("--param=name=value error names the working form", () => {
|
|
285
|
+
expect(() => parseArgs(["build", "src", "--param=tier=production"])).toThrow(/--param name=value/);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test("--param= (empty value) also throws", () => {
|
|
289
|
+
expect(() => parseArgs(["build", "src", "--param="])).toThrow(/--param=/);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test("plain --param name=value is unaffected", () => {
|
|
293
|
+
const result = parseArgs(["build", "src", "--param", "tier=production"]);
|
|
294
|
+
expect(result.param).toEqual(["tier=production"]);
|
|
295
|
+
});
|
|
273
296
|
});
|
|
274
297
|
|
|
275
298
|
// ── resolveCommand tests ──────────────────────────────────────────
|
package/src/cli/main.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { isEntryPoint } from "./is-entry-point";
|
|
|
5
5
|
import { formatSuccess, formatError } from "./format";
|
|
6
6
|
import { loadPlugins, resolveProjectLexicons } from "./plugins";
|
|
7
7
|
import { resolveCommand, type CommandDef, type ParsedArgs } from "./registry";
|
|
8
|
-
import {
|
|
8
|
+
import { loadChantConfigUpward } from "../config";
|
|
9
9
|
import { ENV_VAR, unknownEnvError } from "../env";
|
|
10
10
|
import { initRuntime } from "../runtime-adapter";
|
|
11
11
|
import { runBuild } from "./handlers/build";
|
|
@@ -224,6 +224,16 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
224
224
|
result.sandbox = true;
|
|
225
225
|
} else if (arg === "--param") {
|
|
226
226
|
(result.param ??= []).push(args[++i]);
|
|
227
|
+
} else if (arg.startsWith("--param=")) {
|
|
228
|
+
// chant #1118 — this parser never supports an `--flag=value` joined
|
|
229
|
+
// form for any value-taking flag (every branch above is an exact `===`
|
|
230
|
+
// match, so a joined token falls through unrecognized and is silently
|
|
231
|
+
// dropped — see the "ignores unknown flags" case below). `--param
|
|
232
|
+
// name=value` (space-separated) is the only accepted form. Rather than
|
|
233
|
+
// teach the parser joined forms generally, `--param=name=value` is
|
|
234
|
+
// called out as a hard error instead of a silent no-op: a dropped
|
|
235
|
+
// `--param` can silently change what a build measures/deploys.
|
|
236
|
+
throw new Error(`${arg} is not supported. Use --param name=value (space-separated) instead.`);
|
|
227
237
|
} else if (arg === "--params-file") {
|
|
228
238
|
result.paramsFile = args[++i];
|
|
229
239
|
} else if (!arg.startsWith("-")) {
|
|
@@ -588,11 +598,15 @@ async function main(): Promise<void> {
|
|
|
588
598
|
// chant.config itself may branch on the env. (#505)
|
|
589
599
|
if (args.env) process.env[ENV_VAR] = args.env;
|
|
590
600
|
|
|
591
|
-
// Initialize runtime adapter early — before plugins or commands run
|
|
601
|
+
// Initialize runtime adapter early — before plugins or commands run.
|
|
602
|
+
// chant #1117 — walks up from `args.path` to the project root: for a
|
|
603
|
+
// subdirectory build/command (`chant build src/<stack> --env prod`) the
|
|
604
|
+
// declared `environments` almost always live in the root `chant.config.ts`,
|
|
605
|
+
// not `args.path` itself.
|
|
592
606
|
const projectPath0 = resolve(args.path === "." ? "." : args.path);
|
|
593
607
|
let loadedConfig;
|
|
594
608
|
try {
|
|
595
|
-
loadedConfig = await
|
|
609
|
+
loadedConfig = await loadChantConfigUpward(projectPath0);
|
|
596
610
|
initRuntime();
|
|
597
611
|
} catch {
|
|
598
612
|
// Config may not exist yet (e.g. `chant init`)
|
package/src/cli/plugins.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isLexiconPlugin, type LexiconPlugin } from "../lexicon";
|
|
2
|
-
import {
|
|
2
|
+
import { loadChantConfigUpward } from "../config";
|
|
3
3
|
import { findInfraFiles, detectLexicons } from "../index";
|
|
4
4
|
import { checkConflicts } from "./conflict-check";
|
|
5
5
|
|
|
@@ -96,9 +96,17 @@ export async function loadPlugins(lexiconNames: string[]): Promise<LexiconPlugin
|
|
|
96
96
|
* that never needed live module exports to begin with. This is strictly
|
|
97
97
|
* better than routing the detection through the sandbox: it removes the
|
|
98
98
|
* execution entirely rather than containing it, at no bundling/spawn cost.
|
|
99
|
+
*
|
|
100
|
+
* chant #1117 — walks up from `projectPath` to the project root
|
|
101
|
+
* (`loadChantConfigUpward`) rather than reading `projectPath` alone: `chant
|
|
102
|
+
* build src/<stack>` calls this with the stack's own subdirectory, and a
|
|
103
|
+
* project that declares `lexicons` only in its root `chant.config.ts` (never
|
|
104
|
+
* detectable by `detectLexicons()`'s import scan alone, e.g. a lexicon that's
|
|
105
|
+
* loaded but never imported by name in that particular stack's files) would
|
|
106
|
+
* otherwise silently miss it.
|
|
99
107
|
*/
|
|
100
108
|
export async function resolveProjectLexicons(projectPath: string): Promise<string[]> {
|
|
101
|
-
const { config } = await
|
|
109
|
+
const { config } = await loadChantConfigUpward(projectPath);
|
|
102
110
|
|
|
103
111
|
if (config.lexicons && config.lexicons.length > 0) {
|
|
104
112
|
return config.lexicons;
|
|
@@ -8,16 +8,45 @@
|
|
|
8
8
|
* rather than going through the CLI arg-parsing/handler dispatch layer.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { describe, test, expect, beforeEach, afterEach } from "vitest";
|
|
11
|
+
import { describe, test, expect, beforeEach, afterEach, vi } from "vitest";
|
|
12
12
|
import { mkdir, writeFile, rm } from "node:fs/promises";
|
|
13
|
-
import { join } from "node:path";
|
|
13
|
+
import { join, dirname, resolve as resolvePath } from "node:path";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
14
15
|
import { tmpdir } from "node:os";
|
|
15
16
|
import Ajv2020 from "ajv/dist/2020";
|
|
16
17
|
import componentSchema from "./component.schema.json";
|
|
17
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
listComponents,
|
|
20
|
+
describeComponent,
|
|
21
|
+
computeComponentGraph,
|
|
22
|
+
runComponents,
|
|
23
|
+
findComponentGate,
|
|
24
|
+
resolveComponentTargets,
|
|
25
|
+
generateComponentsPipeline,
|
|
26
|
+
} from "./cli-support";
|
|
18
27
|
import { CapabilityRegistry, type DeployContext } from "./capability";
|
|
19
28
|
import type { DriverComponent, RunProgressEvent } from "./driver";
|
|
20
29
|
|
|
30
|
+
// A minimal stand-in for the real gitlab lexicon plugin, satisfying
|
|
31
|
+
// `isLexiconPlugin` (../lexicon.ts) — used only by the `generateComponentsPipeline`
|
|
32
|
+
// buildParams test below, so that test doesn't depend on how `@intentius/
|
|
33
|
+
// chant-lexicon-gitlab`'s own real module resolves under the test runner.
|
|
34
|
+
vi.mock("@intentius/chant-lexicon-gitlab", () => ({
|
|
35
|
+
gitlab: {
|
|
36
|
+
name: "gitlab",
|
|
37
|
+
serializer: { name: "gitlab", rulePrefix: "GL", serialize: () => "" },
|
|
38
|
+
generate: () => {},
|
|
39
|
+
validate: () => [],
|
|
40
|
+
coverage: () => ({ total: 0, covered: 0 }),
|
|
41
|
+
package: () => "gitlab",
|
|
42
|
+
generateComponentPipeline: (components: DriverComponent[]) => ({
|
|
43
|
+
yaml: components.map((c) => c.name).join(","),
|
|
44
|
+
stages: ["wave-1"],
|
|
45
|
+
jobs: components.map((c) => ({ jobName: c.name, component: c.name, stage: "wave-1", needs: [] })),
|
|
46
|
+
}),
|
|
47
|
+
},
|
|
48
|
+
}));
|
|
49
|
+
|
|
21
50
|
describe("listComponents", () => {
|
|
22
51
|
let testDir: string;
|
|
23
52
|
|
|
@@ -578,6 +607,195 @@ describe("runComponents", () => {
|
|
|
578
607
|
});
|
|
579
608
|
});
|
|
580
609
|
|
|
610
|
+
// ── build-time parameters (chant #1108) ──────────────────────────────────────
|
|
611
|
+
//
|
|
612
|
+
// Before #1108, `discoverComponents`'s callers here never resolved
|
|
613
|
+
// `chant.config.ts`'s declared `buildParams` at all, so a `*.component.ts`
|
|
614
|
+
// file reading `params.<name>` (`@intentius/chant/params`) always saw `{}` —
|
|
615
|
+
// the exact probe from chant#1108's reproduction. These tests lock in that
|
|
616
|
+
// `runComponents`/`resolveComponentTargets`/`generateComponentsPipeline` now
|
|
617
|
+
// forward an already-resolved `buildParams` into discovery, so a component's
|
|
618
|
+
// `params.<name>` read reflects it before any step dispatches.
|
|
619
|
+
|
|
620
|
+
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
621
|
+
const paramsModulePath = resolvePath(thisDir, "../params");
|
|
622
|
+
|
|
623
|
+
describe("runComponents — buildParams (chant #1108)", () => {
|
|
624
|
+
let testDir: string;
|
|
625
|
+
|
|
626
|
+
beforeEach(async () => {
|
|
627
|
+
testDir = join(tmpdir(), `chant-run-components-buildparams-test-${Date.now()}-${Math.random()}`);
|
|
628
|
+
await mkdir(testDir, { recursive: true });
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
afterEach(async () => {
|
|
632
|
+
await rm(testDir, { recursive: true, force: true });
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
test("the previously-{} case: a component's params.<name> read resolves when buildParams is passed", async () => {
|
|
636
|
+
await writeFile(
|
|
637
|
+
join(testDir, "svc.component.ts"),
|
|
638
|
+
`
|
|
639
|
+
import { params } from ${JSON.stringify(paramsModulePath)};
|
|
640
|
+
export const svc = {
|
|
641
|
+
name: "svc",
|
|
642
|
+
dependsOn: [],
|
|
643
|
+
deploy: [{ phase: "Apply", steps: [{ kind: "deploy-thing", tier: params.tier }] }],
|
|
644
|
+
};
|
|
645
|
+
`,
|
|
646
|
+
);
|
|
647
|
+
|
|
648
|
+
const capability = fakeCapability("deploy-thing");
|
|
649
|
+
const registry = new CapabilityRegistry();
|
|
650
|
+
registry.register(capability);
|
|
651
|
+
|
|
652
|
+
const result = await runComponents(testDir, "svc", {
|
|
653
|
+
registry,
|
|
654
|
+
buildParams: [{ name: "tier", value: "production", source: "cli" }],
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
expect(result.success).toBe(true);
|
|
658
|
+
expect(capability.calls[0]?.input).toMatchObject({ tier: "production" });
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
test("without buildParams, params.<name> is undefined (the bug this issue fixes, still true for a caller that resolves none)", async () => {
|
|
662
|
+
await writeFile(
|
|
663
|
+
join(testDir, "svc.component.ts"),
|
|
664
|
+
`
|
|
665
|
+
import { params } from ${JSON.stringify(paramsModulePath)};
|
|
666
|
+
export const svc = {
|
|
667
|
+
name: "svc",
|
|
668
|
+
dependsOn: [],
|
|
669
|
+
deploy: [{ phase: "Apply", steps: [{ kind: "deploy-thing", tier: params.tier ?? "unset" }] }],
|
|
670
|
+
};
|
|
671
|
+
`,
|
|
672
|
+
);
|
|
673
|
+
|
|
674
|
+
const capability = fakeCapability("deploy-thing");
|
|
675
|
+
const registry = new CapabilityRegistry();
|
|
676
|
+
registry.register(capability);
|
|
677
|
+
|
|
678
|
+
const result = await runComponents(testDir, "svc", { registry });
|
|
679
|
+
|
|
680
|
+
expect(result.success).toBe(true);
|
|
681
|
+
expect(capability.calls[0]?.input).toMatchObject({ tier: "unset" });
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
test("RunComponentsResult.buildParams echoes back the resolved provenance that was passed in", async () => {
|
|
685
|
+
await writeFile(
|
|
686
|
+
join(testDir, "svc.component.ts"),
|
|
687
|
+
`export const svc = { name: "svc", dependsOn: [], deploy: [{ phase: "Apply", steps: [{ kind: "deploy-thing" }] }] };`,
|
|
688
|
+
);
|
|
689
|
+
|
|
690
|
+
const registry = fakeRegistry();
|
|
691
|
+
const result = await runComponents(testDir, "svc", {
|
|
692
|
+
registry,
|
|
693
|
+
buildParams: [{ name: "tier", value: "production", source: "cli" }],
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
expect(result.buildParams).toEqual([{ name: "tier", value: "production", source: "cli" }]);
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
test("selector 'all' also forwards buildParams into discovery", async () => {
|
|
700
|
+
await writeFile(
|
|
701
|
+
join(testDir, "svc.component.ts"),
|
|
702
|
+
`
|
|
703
|
+
import { params } from ${JSON.stringify(paramsModulePath)};
|
|
704
|
+
export const svc = { name: "svc", dependsOn: [], deploy: [{ phase: "Apply", steps: [{ kind: "deploy-thing", tier: params.tier }] }] };
|
|
705
|
+
`,
|
|
706
|
+
);
|
|
707
|
+
|
|
708
|
+
const capability = fakeCapability("deploy-thing");
|
|
709
|
+
const registry = new CapabilityRegistry();
|
|
710
|
+
registry.register(capability);
|
|
711
|
+
|
|
712
|
+
const result = await runComponents(testDir, "all", {
|
|
713
|
+
registry,
|
|
714
|
+
buildParams: [{ name: "tier", value: "staging", source: "env" }],
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
expect(result.success).toBe(true);
|
|
718
|
+
expect(capability.calls[0]?.input).toMatchObject({ tier: "staging" });
|
|
719
|
+
});
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
describe("resolveComponentTargets — buildParams (chant #1108)", () => {
|
|
723
|
+
let testDir: string;
|
|
724
|
+
|
|
725
|
+
beforeEach(async () => {
|
|
726
|
+
testDir = join(tmpdir(), `chant-resolve-targets-buildparams-test-${Date.now()}-${Math.random()}`);
|
|
727
|
+
await mkdir(testDir, { recursive: true });
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
afterEach(async () => {
|
|
731
|
+
await rm(testDir, { recursive: true, force: true });
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
test("forwards buildParams into discovery — the durable (--temporal) path's entrypoint", async () => {
|
|
735
|
+
await writeFile(
|
|
736
|
+
join(testDir, "svc.component.ts"),
|
|
737
|
+
`
|
|
738
|
+
import { params } from ${JSON.stringify(paramsModulePath)};
|
|
739
|
+
export const svc = {
|
|
740
|
+
name: "svc",
|
|
741
|
+
dependsOn: [],
|
|
742
|
+
deploy: [{ phase: "Apply", steps: [{ kind: "cfn-deploy", stack: params.env }] }],
|
|
743
|
+
};
|
|
744
|
+
`,
|
|
745
|
+
);
|
|
746
|
+
|
|
747
|
+
const result = await resolveComponentTargets(testDir, "svc", undefined, [
|
|
748
|
+
{ name: "env", value: "prod-a", source: "env" },
|
|
749
|
+
]);
|
|
750
|
+
|
|
751
|
+
expect(result.success).toBe(true);
|
|
752
|
+
expect((result.targets[0].deploy[0].steps[0] as { stack?: unknown }).stack).toBe("prod-a");
|
|
753
|
+
});
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
describe("generateComponentsPipeline — buildParams (chant #1108)", () => {
|
|
757
|
+
let testDir: string;
|
|
758
|
+
|
|
759
|
+
beforeEach(async () => {
|
|
760
|
+
testDir = join(tmpdir(), `chant-generate-components-buildparams-test-${Date.now()}-${Math.random()}`);
|
|
761
|
+
await mkdir(testDir, { recursive: true });
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
afterEach(async () => {
|
|
765
|
+
await rm(testDir, { recursive: true, force: true });
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
test("forwards buildParams into discovery before synthesizing the pipeline", async () => {
|
|
769
|
+
await writeFile(
|
|
770
|
+
join(testDir, "svc.component.ts"),
|
|
771
|
+
`
|
|
772
|
+
import { params } from ${JSON.stringify(paramsModulePath)};
|
|
773
|
+
export const svc = {
|
|
774
|
+
name: String(params.name),
|
|
775
|
+
dependsOn: [],
|
|
776
|
+
deploy: [{ phase: "Apply", steps: [{ kind: "shell" }] }],
|
|
777
|
+
};
|
|
778
|
+
`,
|
|
779
|
+
);
|
|
780
|
+
|
|
781
|
+
// Uses the mocked "gitlab" plugin above (see the `vi.mock` at the top of
|
|
782
|
+
// this file) — proves the component name `discoverComponents` resolved
|
|
783
|
+
// (via `params.name`) made it all the way through to the synthesized
|
|
784
|
+
// pipeline, not just to `discover()`'s own return value.
|
|
785
|
+
const result = await generateComponentsPipeline(
|
|
786
|
+
testDir,
|
|
787
|
+
"gitlab",
|
|
788
|
+
undefined,
|
|
789
|
+
undefined,
|
|
790
|
+
[{ name: "name", value: "named-from-params", source: "cli" }],
|
|
791
|
+
);
|
|
792
|
+
|
|
793
|
+
expect(result.success).toBe(true);
|
|
794
|
+
expect(result.jobs?.map((j) => j.component)).toEqual(["named-from-params"]);
|
|
795
|
+
expect(result.buildParams).toEqual([{ name: "name", value: "named-from-params", source: "cli" }]);
|
|
796
|
+
});
|
|
797
|
+
});
|
|
798
|
+
|
|
581
799
|
describe("runComponents — onProgress (--progress-json wiring, M3)", () => {
|
|
582
800
|
let testDir: string;
|
|
583
801
|
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
import { discoverComponents } from "./discover";
|
|
26
|
+
import type { BuildParamProvenance } from "../provenance";
|
|
26
27
|
import { projectToJson, type Archetype } from "./component";
|
|
27
28
|
import {
|
|
28
29
|
resolveComponentGraph,
|
|
@@ -185,6 +186,8 @@ export interface GenerateComponentsResult {
|
|
|
185
186
|
/** Every generated job, for a machine-readable view (`--format json`). */
|
|
186
187
|
jobs?: Array<{ jobName: string; component: string; stage: string; needs: string[] }>;
|
|
187
188
|
error?: string;
|
|
189
|
+
/** This invocation's resolved build-time parameters (chant #1108) — the generate-mode counterpart of `../cli/commands/build.ts`'s `BuildResult.buildParams`. Empty when the project declares/supplies none. */
|
|
190
|
+
buildParams?: BuildParamProvenance[];
|
|
188
191
|
}
|
|
189
192
|
|
|
190
193
|
/**
|
|
@@ -221,6 +224,15 @@ export async function generateComponentsPipeline(
|
|
|
221
224
|
lexicon: GenerateLexicon,
|
|
222
225
|
options?: ComponentPipelineOptions,
|
|
223
226
|
sandbox?: boolean,
|
|
227
|
+
/**
|
|
228
|
+
* chant #1108 — this invocation's resolved build-time parameter values
|
|
229
|
+
* (../cli/handlers/build.ts resolves them, the same sequence `chant build`
|
|
230
|
+
* runs, BEFORE calling this function), forwarded into discovery so a
|
|
231
|
+
* `params.<name>` reference inside a discovered `*.component.ts` file
|
|
232
|
+
* resolves instead of reading `{}`. See `discoverComponents`'s
|
|
233
|
+
* `buildParams` option (./discover.ts) for the full doc.
|
|
234
|
+
*/
|
|
235
|
+
buildParams?: BuildParamProvenance[],
|
|
224
236
|
): Promise<GenerateComponentsResult> {
|
|
225
237
|
const plugin = await loadLexiconPlugin(lexicon);
|
|
226
238
|
if (!plugin?.generateComponentPipeline) {
|
|
@@ -230,7 +242,7 @@ export async function generateComponentsPipeline(
|
|
|
230
242
|
};
|
|
231
243
|
}
|
|
232
244
|
|
|
233
|
-
const result = await discoverComponents(path, { sandbox });
|
|
245
|
+
const result = await discoverComponents(path, { sandbox, buildParams });
|
|
234
246
|
if (result.errors.length > 0) {
|
|
235
247
|
return { success: false, error: result.errors.map((e) => e.message).join("\n") };
|
|
236
248
|
}
|
|
@@ -243,7 +255,7 @@ export async function generateComponentsPipeline(
|
|
|
243
255
|
|
|
244
256
|
try {
|
|
245
257
|
const { yaml, stages, jobs } = plugin.generateComponentPipeline(driverComponents, options);
|
|
246
|
-
return { success: true, yaml, stages, jobs };
|
|
258
|
+
return { success: true, yaml, stages, jobs, buildParams };
|
|
247
259
|
} catch (err) {
|
|
248
260
|
if (err instanceof UnknownDependencyError || err instanceof DependencyCycleError) {
|
|
249
261
|
return { success: false, error: err.message };
|
|
@@ -344,6 +356,21 @@ export interface RunComponentsOptions {
|
|
|
344
356
|
* `--progress-json`, in which case nothing changes.
|
|
345
357
|
*/
|
|
346
358
|
onProgress?: (event: RunProgressEvent) => void;
|
|
359
|
+
/**
|
|
360
|
+
* chant #1108 — this run's resolved build-time parameter values, resolved
|
|
361
|
+
* the exact same way `chant build` resolves them
|
|
362
|
+
* (../cli/build-params-cli.ts's `resolveCliBuildParams`, driven by
|
|
363
|
+
* `--param`/`--params-file`/a declared `env` mapping/`chant.config.ts`'s
|
|
364
|
+
* `buildParams` defaults). The CLI handler (../cli/handlers/run.ts)
|
|
365
|
+
* resolves + logs these BEFORE calling `runComponents`, the same
|
|
366
|
+
* sequencing `chant build` uses — this function only forwards the
|
|
367
|
+
* already-resolved values into discovery (`resolveComponentTargets` below,
|
|
368
|
+
* then `discoverComponents`); it does not resolve them itself, so
|
|
369
|
+
* `runComponents` stays free of CLI-flag-parsing/formatting concerns.
|
|
370
|
+
* Default: none — `params.*` stays `{}`, matching every caller (including
|
|
371
|
+
* every test in `cli-support.test.ts`) that doesn't supply this.
|
|
372
|
+
*/
|
|
373
|
+
buildParams?: BuildParamProvenance[];
|
|
347
374
|
}
|
|
348
375
|
|
|
349
376
|
/** Result of `chant run --components <name|all>`. */
|
|
@@ -356,6 +383,8 @@ export interface RunComponentsResult {
|
|
|
356
383
|
error?: string;
|
|
357
384
|
/** Set when a selected component (or one of its `deploy`/`rollback` phases) contains a `gate` the local executor cannot run. */
|
|
358
385
|
gateUnsupported?: { component: string; signalName: string };
|
|
386
|
+
/** This run's resolved build-time parameters (chant #1108) — the component-driver counterpart of `../cli/commands/build.ts`'s `BuildResult.buildParams`. Present only once the run actually reached dispatch (mirrors `BuildResult.buildParams`, which is likewise absent on an early-error return). */
|
|
387
|
+
buildParams?: BuildParamProvenance[];
|
|
359
388
|
}
|
|
360
389
|
|
|
361
390
|
/**
|
|
@@ -404,8 +433,10 @@ export async function resolveComponentTargets(
|
|
|
404
433
|
path: string,
|
|
405
434
|
selector: string,
|
|
406
435
|
sandbox?: boolean,
|
|
436
|
+
/** chant #1108 — this invocation's resolved build-time parameter values, forwarded into `discoverComponents` so a `params.<name>` reference inside a discovered `*.component.ts` file resolves instead of reading `{}`. See `discoverComponents`'s `buildParams` option (./discover.ts). */
|
|
437
|
+
buildParams?: BuildParamProvenance[],
|
|
407
438
|
): Promise<ResolvedComponentTargets> {
|
|
408
|
-
const result = await discoverComponents(path, { sandbox });
|
|
439
|
+
const result = await discoverComponents(path, { sandbox, buildParams });
|
|
409
440
|
if (result.errors.length > 0) {
|
|
410
441
|
return { success: false, targets: [], error: result.errors.map((e) => e.message).join("\n") };
|
|
411
442
|
}
|
|
@@ -433,7 +464,7 @@ export async function runComponents(
|
|
|
433
464
|
selector: string,
|
|
434
465
|
options: RunComponentsOptions = {},
|
|
435
466
|
): Promise<RunComponentsResult> {
|
|
436
|
-
const resolved = await resolveComponentTargets(path, selector, options.sandbox);
|
|
467
|
+
const resolved = await resolveComponentTargets(path, selector, options.sandbox, options.buildParams);
|
|
437
468
|
if (!resolved.success) {
|
|
438
469
|
return { success: false, selected: [], error: resolved.error };
|
|
439
470
|
}
|
|
@@ -479,7 +510,7 @@ export async function runComponents(
|
|
|
479
510
|
try {
|
|
480
511
|
if (selector === "all") {
|
|
481
512
|
const run = await runInterpretDriver(resolvedTargets, registry, { env, componentOutputs: seedOutputs, onProgress });
|
|
482
|
-
return { success: true, run, selected };
|
|
513
|
+
return { success: true, run, selected, buildParams: options.buildParams };
|
|
483
514
|
}
|
|
484
515
|
|
|
485
516
|
// Single-component invocation: run just this component, bypassing
|
|
@@ -518,7 +549,7 @@ export async function runComponents(
|
|
|
518
549
|
failedComponent: componentResult.ok ? undefined : componentResult.component,
|
|
519
550
|
componentOutputs,
|
|
520
551
|
};
|
|
521
|
-
return { success: componentResult.ok, run, selected };
|
|
552
|
+
return { success: componentResult.ok, run, selected, buildParams: options.buildParams };
|
|
522
553
|
} catch (err) {
|
|
523
554
|
if (err instanceof DriverRunFailure) {
|
|
524
555
|
return { success: false, run: err.result, selected, error: err.message };
|