@intentius/chant 0.41.0 → 0.41.1
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 +24 -0
- package/dist/cli/build-params-cli.d.ts.map +1 -1
- package/dist/cli/handlers/components.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/components/deploy-units.d.ts +49 -0
- package/dist/components/deploy-units.d.ts.map +1 -0
- package/dist/config.d.ts +10 -0
- package/dist/config.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/build-params-cli.ts +35 -0
- package/src/cli/handlers/components.ts +29 -23
- package/src/cli/handlers/graph.test.ts +51 -1
- package/src/cli/handlers/graph.ts +16 -1
- package/src/cli/handlers/lifecycle.ts +18 -3
- package/src/components/deploy-units.test.ts +42 -0
- package/src/components/deploy-units.ts +82 -0
- package/src/config.test.ts +53 -0
- package/src/config.ts +41 -3
|
@@ -52,4 +52,28 @@ export interface CliBuildParamsResolution {
|
|
|
52
52
|
* the same way #1022's fold decisions are.
|
|
53
53
|
*/
|
|
54
54
|
export declare function resolveCliBuildParams(buildParamsConfig: BuildParamsConfig | undefined, args: CliBuildParamsArgs): CliBuildParamsResolution;
|
|
55
|
+
/**
|
|
56
|
+
* Resolve a command's declared build-time parameters, the same way `chant
|
|
57
|
+
* build` does, so a `build()`/`discover()` on the declared side sees the values
|
|
58
|
+
* the source will actually read.
|
|
59
|
+
*
|
|
60
|
+
* Every command that reads declared source needs this and only `chant build`
|
|
61
|
+
* had it. `chant graph` was fixed in #1483; the lifecycle family — `diff`,
|
|
62
|
+
* `snapshot`, `plan`, and `components status --live` — built the declared side
|
|
63
|
+
* on parameter *defaults* while the live side was whatever is really deployed.
|
|
64
|
+
*
|
|
65
|
+
* For a project whose parameters choose which resources *exist* that is not a
|
|
66
|
+
* near miss. `chant lifecycle diff dev --live` on kubemicrovm-ops returned
|
|
67
|
+
* byte-identical output at `KMV_TIER=minimal` and `KMV_TIER=prod-ha`, and
|
|
68
|
+
* reported the tier label itself as drift — `minimal → prod-ha`, declared
|
|
69
|
+
* against live — which is the comparison announcing it is against the wrong
|
|
70
|
+
* declaration.
|
|
71
|
+
*
|
|
72
|
+
* Returns `undefined` when resolution failed, having printed why: the caller
|
|
73
|
+
* should stop rather than compare against source that will not build.
|
|
74
|
+
*/
|
|
75
|
+
export declare function commandBuildParams(buildParamsConfig: BuildParamsConfig | undefined, args: {
|
|
76
|
+
param?: string[];
|
|
77
|
+
paramsFile?: string;
|
|
78
|
+
}): Promise<BuildParamProvenance[] | undefined>;
|
|
55
79
|
//# sourceMappingURL=build-params-cli.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build-params-cli.d.ts","sourceRoot":"","sources":["../../src/cli/build-params-cli.ts"],"names":[],"mappings":"AAEA,OAAO,EAAsB,KAAK,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAC7E,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAG1D;;;;;;;;;;;;;;;GAeG;AAEH,4SAA4S;AAC5S,wBAAgB,eAAe,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAQtF;AAED,qOAAqO;AACrO,MAAM,WAAW,kBAAkB;IACjC,mNAAmN;IACnN,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,gGAAgG;IAChG,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,wBAAwB;IACvC,OAAO,EAAE,OAAO,CAAC;IACjB,iHAAiH;IACjH,UAAU,EAAE,oBAAoB,EAAE,CAAC;IACnC,4FAA4F;IAC5F,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,qBAAqB,CACnC,iBAAiB,EAAE,iBAAiB,GAAG,SAAS,EAChD,IAAI,EAAE,kBAAkB,GACvB,wBAAwB,CAkC1B"}
|
|
1
|
+
{"version":3,"file":"build-params-cli.d.ts","sourceRoot":"","sources":["../../src/cli/build-params-cli.ts"],"names":[],"mappings":"AAEA,OAAO,EAAsB,KAAK,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAC7E,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAG1D;;;;;;;;;;;;;;;GAeG;AAEH,4SAA4S;AAC5S,wBAAgB,eAAe,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAQtF;AAED,qOAAqO;AACrO,MAAM,WAAW,kBAAkB;IACjC,mNAAmN;IACnN,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,gGAAgG;IAChG,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,wBAAwB;IACvC,OAAO,EAAE,OAAO,CAAC;IACjB,iHAAiH;IACjH,UAAU,EAAE,oBAAoB,EAAE,CAAC;IACnC,4FAA4F;IAC5F,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,qBAAqB,CACnC,iBAAiB,EAAE,iBAAiB,GAAG,SAAS,EAChD,IAAI,EAAE,kBAAkB,GACvB,wBAAwB,CAkC1B;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,kBAAkB,CACtC,iBAAiB,EAAE,iBAAiB,GAAG,SAAS,EAChD,IAAI,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,GAC9C,OAAO,CAAC,oBAAoB,EAAE,GAAG,SAAS,CAAC,CAU7C"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"components.d.ts","sourceRoot":"","sources":["../../../src/cli/handlers/components.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"components.d.ts","sourceRoot":"","sources":["../../../src/cli/handlers/components.ts"],"names":[],"mappings":"AA+CA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAGlD,OAAO,KAAK,EAAE,KAAK,EAAa,MAAM,4BAA4B,CAAC;AAGnE;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,0BAA0B,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CA4ErF;AA4DD;;;;;;;;;;;;;;;GAeG;AACH;;;;;;;;6EAQ6E;AAC7E,wBAAgB,eAAe,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,EAAE,CAMzD;AA4DD,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CA6R9E;AAED,kEAAkE;AAClE,wBAAsB,oBAAoB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAM/E"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../../../src/cli/handlers/lifecycle.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../../../src/cli/handlers/lifecycle.ts"],"names":[],"mappings":"AAmCA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAqDlD;;GAEG;AACH,wBAAsB,oBAAoB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAwH/E;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CA4C3E;AAED;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CA8B/E;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CA2J3E;AAoiBD;;;;;;;;;GASG;AACH,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CA0J3E;AAED;;GAEG;AACH,wBAAsB,eAAe,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAiB1E;AAED;;GAEG;AACH,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAM9E;AAkBD;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CA+C/E"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deploy units — which live unit(s) a component's composition targets (#1495).
|
|
3
|
+
*
|
|
4
|
+
* `chant components status --live` (and `chant graph --live`) need to know
|
|
5
|
+
* what a component deployed in order to observe it. That answer used to be a
|
|
6
|
+
* string literal in the walk — `kind === "cfn-deploy"` — which made the whole
|
|
7
|
+
* chain CloudFormation-shaped: a component whose deploy phase is a server-side
|
|
8
|
+
* apply or a Helm upgrade contributed nothing and was skipped, not
|
|
9
|
+
* "unobserved", simply absent from the result.
|
|
10
|
+
*
|
|
11
|
+
* This module is the seam that fixes the walk (piece 1 of #1495's
|
|
12
|
+
* decomposition): a registry mapping a deploy-family step `kind` to the field
|
|
13
|
+
* naming its unit and the lexicon whose `describeStackStatus` can observe it.
|
|
14
|
+
* The registry is data, not a rule about what steps look like — a kind not
|
|
15
|
+
* listed here contributes no unit, exactly as before.
|
|
16
|
+
*/
|
|
17
|
+
import type { Phase } from "./component.js";
|
|
18
|
+
/** A deploy-family step kind that names a live unit, and who observes it. */
|
|
19
|
+
export interface DeployUnitRule {
|
|
20
|
+
/** The step `kind` (the capability's registered name). */
|
|
21
|
+
kind: string;
|
|
22
|
+
/** The step field carrying the unit's name (`stack`, `release`). */
|
|
23
|
+
field: string;
|
|
24
|
+
/** The lexicon whose `describeStackStatus` reads this kind of unit. */
|
|
25
|
+
lexicon: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The deploy-family kinds that name a unit. cfn-deploy's unit is its stack;
|
|
29
|
+
* kubectl-apply's is the ownership stack its labels carry (#1495 piece 2);
|
|
30
|
+
* helm-upgrade's is its release (#1495 piece 4). Listing a kind here is safe
|
|
31
|
+
* before its lexicon implements `describeStackStatus` — units whose lexicon
|
|
32
|
+
* has no observer are skipped by the caller, the same absent-observer path as
|
|
33
|
+
* before.
|
|
34
|
+
*/
|
|
35
|
+
export declare const DEPLOY_UNIT_RULES: readonly DeployUnitRule[];
|
|
36
|
+
/** One resolved unit a component's deploy composition targets. */
|
|
37
|
+
export interface DeployUnit {
|
|
38
|
+
/** The unit's name — a stack, a release. */
|
|
39
|
+
unit: string;
|
|
40
|
+
/** The lexicon that observes this kind of unit. */
|
|
41
|
+
lexicon: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Every distinct deploy unit a component's phases target, in declaration
|
|
45
|
+
* order. A step may itself be a nested `Phase`, so the walk recurses; a
|
|
46
|
+
* resolved component carries the unit as a concrete string. Pure.
|
|
47
|
+
*/
|
|
48
|
+
export declare function deployUnits(deploy: Phase[]): DeployUnit[];
|
|
49
|
+
//# sourceMappingURL=deploy-units.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deploy-units.d.ts","sourceRoot":"","sources":["../../src/components/deploy-units.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAEzC,6EAA6E;AAC7E,MAAM,WAAW,cAAc;IAC7B,0DAA0D;IAC1D,IAAI,EAAE,MAAM,CAAC;IACb,oEAAoE;IACpE,KAAK,EAAE,MAAM,CAAC;IACd,uEAAuE;IACvE,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,EAAE,SAAS,cAAc,EAItD,CAAC;AAEF,kEAAkE;AAClE,MAAM,WAAW,UAAU;IACzB,4CAA4C;IAC5C,IAAI,EAAE,MAAM,CAAC;IACb,mDAAmD;IACnD,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,UAAU,EAAE,CA0BzD"}
|
package/dist/config.d.ts
CHANGED
|
@@ -350,6 +350,16 @@ export declare function loadChantConfig(dir: string): Promise<ResolvedConfig>;
|
|
|
350
350
|
* same walk `chant lint`/`chant graph` already used ({@link findProjectConfig},
|
|
351
351
|
* shared with `./lint/config.ts`'s `findProjectRoot`) — one config-discovery
|
|
352
352
|
* contract for the whole CLI.
|
|
353
|
+
*
|
|
354
|
+
* chant #1502 — a lint-scoping fragment does not end the walk. The convention
|
|
355
|
+
* of a `src/chant.config.json` holding only `extends`/`rules` (cc-aws-canonical
|
|
356
|
+
* and most of examples/) sits BETWEEN the build directory and the real
|
|
357
|
+
* `chant.config.ts`, and stopping there re-introduced the exact silent
|
|
358
|
+
* fallback this walk exists to prevent: `chant build src` resolved the
|
|
359
|
+
* fragment, found no `ownership`, and built unstamped manifests that every
|
|
360
|
+
* owned-scoped live read then withheld. A fragment is skipped, not merged —
|
|
361
|
+
* lint resolution keeps its own nearest-wins walk untouched, and a JSON
|
|
362
|
+
* config declaring any project-level key still wins where it stands.
|
|
353
363
|
*/
|
|
354
364
|
export declare function loadChantConfigUpward(startDir: string): Promise<ResolvedConfig>;
|
|
355
365
|
/**
|
package/dist/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -105,3 +105,38 @@ export function resolveCliBuildParams(
|
|
|
105
105
|
|
|
106
106
|
return { success: true, provenance: resolution.provenance, errors: [] };
|
|
107
107
|
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Resolve a command's declared build-time parameters, the same way `chant
|
|
111
|
+
* build` does, so a `build()`/`discover()` on the declared side sees the values
|
|
112
|
+
* the source will actually read.
|
|
113
|
+
*
|
|
114
|
+
* Every command that reads declared source needs this and only `chant build`
|
|
115
|
+
* had it. `chant graph` was fixed in #1483; the lifecycle family — `diff`,
|
|
116
|
+
* `snapshot`, `plan`, and `components status --live` — built the declared side
|
|
117
|
+
* on parameter *defaults* while the live side was whatever is really deployed.
|
|
118
|
+
*
|
|
119
|
+
* For a project whose parameters choose which resources *exist* that is not a
|
|
120
|
+
* near miss. `chant lifecycle diff dev --live` on kubemicrovm-ops returned
|
|
121
|
+
* byte-identical output at `KMV_TIER=minimal` and `KMV_TIER=prod-ha`, and
|
|
122
|
+
* reported the tier label itself as drift — `minimal → prod-ha`, declared
|
|
123
|
+
* against live — which is the comparison announcing it is against the wrong
|
|
124
|
+
* declaration.
|
|
125
|
+
*
|
|
126
|
+
* Returns `undefined` when resolution failed, having printed why: the caller
|
|
127
|
+
* should stop rather than compare against source that will not build.
|
|
128
|
+
*/
|
|
129
|
+
export async function commandBuildParams(
|
|
130
|
+
buildParamsConfig: BuildParamsConfig | undefined,
|
|
131
|
+
args: { param?: string[]; paramsFile?: string },
|
|
132
|
+
): Promise<BuildParamProvenance[] | undefined> {
|
|
133
|
+
const resolution = resolveCliBuildParams(buildParamsConfig, {
|
|
134
|
+
cli: parseParamFlags(args.param),
|
|
135
|
+
paramsFile: args.paramsFile,
|
|
136
|
+
});
|
|
137
|
+
if (!resolution.success) {
|
|
138
|
+
for (const message of resolution.errors) console.error(message);
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
return resolution.provenance;
|
|
142
|
+
}
|
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
InvalidReleaseRecordError,
|
|
36
36
|
} from "../../lifecycle/release-ledger";
|
|
37
37
|
import { reconcileStatus, liveEvidenceFromChangeSet, compareAcrossEnvironments, mergeLiveEvidence, type LiveComponentEvidence } from "../../lifecycle/status";
|
|
38
|
+
import { commandBuildParams } from "../build-params-cli";
|
|
38
39
|
import { buildChangeSet } from "../../lifecycle/change-set";
|
|
39
40
|
import { buildLedgerEntries, componentBomSummary, type BuildLedgerEntry } from "../../lifecycle/build-ledger";
|
|
40
41
|
import { findBuildManifestByArtifactDigest } from "../../lifecycle/build-ledger-store";
|
|
@@ -48,6 +49,7 @@ import type { CommandContext } from "../registry";
|
|
|
48
49
|
import type { LexiconPlugin } from "../../lexicon";
|
|
49
50
|
import { normalizeObservation, mergeObservations, unobservedAll, type NormalizedObservation } from "../../observation";
|
|
50
51
|
import type { Phase, Component } from "../../components/component";
|
|
52
|
+
import { deployUnits } from "../../components/deploy-units";
|
|
51
53
|
|
|
52
54
|
/**
|
|
53
55
|
* chant components release <env> --component <name> --digest <sha256:...>
|
|
@@ -228,22 +230,11 @@ interface StatusJsonRow {
|
|
|
228
230
|
* observe on a multi-stack, per-component project (`describeResources`'s
|
|
229
231
|
* single-stack-named-after-the-environment convention never matches one). */
|
|
230
232
|
export function cfnDeployStacks(deploy: Phase[]): string[] {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
const nested = (step as { steps?: unknown }).steps;
|
|
237
|
-
if (Array.isArray(nested)) {
|
|
238
|
-
walkSteps(nested as Phase["steps"]);
|
|
239
|
-
continue;
|
|
240
|
-
}
|
|
241
|
-
const s = step as { kind?: string; stack?: unknown };
|
|
242
|
-
if (s.kind === "cfn-deploy" && typeof s.stack === "string") stacks.add(s.stack);
|
|
243
|
-
}
|
|
244
|
-
};
|
|
245
|
-
for (const phase of deploy) walkSteps(phase.steps);
|
|
246
|
-
return [...stacks];
|
|
233
|
+
// The CloudFormation slice of the generic deploy-unit walk (#1495) — kept
|
|
234
|
+
// because `chant graph --live`'s stack enrichment is genuinely CFN-shaped.
|
|
235
|
+
return deployUnits(deploy)
|
|
236
|
+
.filter((u) => u.lexicon === "aws")
|
|
237
|
+
.map((u) => u.unit);
|
|
247
238
|
}
|
|
248
239
|
|
|
249
240
|
/**
|
|
@@ -258,15 +249,22 @@ export function cfnDeployStacks(deploy: Phase[]): string[] {
|
|
|
258
249
|
*/
|
|
259
250
|
async function observeComponentStacks(
|
|
260
251
|
components: Map<string, { component: Component }>,
|
|
261
|
-
|
|
252
|
+
plugins: LexiconPlugin[],
|
|
262
253
|
environment: string,
|
|
263
254
|
): Promise<Map<string, LiveComponentEvidence>> {
|
|
255
|
+
// Observer per unit's own lexicon (#1495): a component's kubectl-apply unit
|
|
256
|
+
// is read by the k8s lexicon, its cfn-deploy stack by aws. A unit whose
|
|
257
|
+
// lexicon ships no describeStackStatus is skipped — the same absent-observer
|
|
258
|
+
// degradation as before, per unit instead of per project.
|
|
259
|
+
const observerFor = (lexicon: string) =>
|
|
260
|
+
plugins.find((p) => p.name === lexicon && p.describeStackStatus);
|
|
264
261
|
const evidence = new Map<string, LiveComponentEvidence>();
|
|
265
262
|
for (const [name, { component }] of components) {
|
|
266
|
-
const
|
|
263
|
+
const units = deployUnits(component.deploy).filter((u) => observerFor(u.lexicon));
|
|
264
|
+
const stacks = units.map((u) => u.unit);
|
|
267
265
|
if (stacks.length === 0) continue;
|
|
268
266
|
const observed = await Promise.all(
|
|
269
|
-
|
|
267
|
+
units.map((u) => observerFor(u.lexicon)!.describeStackStatus!({ environment, stack: u.unit }).catch(() => null)),
|
|
270
268
|
);
|
|
271
269
|
const determinate = observed.filter((o): o is NonNullable<typeof o> => o !== null);
|
|
272
270
|
if (determinate.length === 0) {
|
|
@@ -355,7 +353,15 @@ export async function runComponentsStatus(ctx: CommandContext): Promise<number>
|
|
|
355
353
|
if (endpointResult.notice) console.error(formatWarning({ message: endpointResult.notice }));
|
|
356
354
|
try {
|
|
357
355
|
const targetSerializers = serializers;
|
|
358
|
-
|
|
356
|
+
// With this invocation's parameters (#1483). Built on defaults, the
|
|
357
|
+
// declared half of the comparison is a different estate from the one
|
|
358
|
+
// deployed, and every resource the real parameter declares reads as
|
|
359
|
+
// absent.
|
|
360
|
+
const statusParams = await commandBuildParams(config.buildParams, args);
|
|
361
|
+
if (!statusParams) return 1;
|
|
362
|
+
const buildResult = await build(resolve(args.src ?? config.sourceDir ?? "."), targetSerializers, undefined, {
|
|
363
|
+
buildParams: statusParams,
|
|
364
|
+
});
|
|
359
365
|
// Which deployed stack(s) to read the change set from (behold#100).
|
|
360
366
|
//
|
|
361
367
|
// `describeResources` defaults to the single-stack convention — the
|
|
@@ -431,9 +437,9 @@ export async function runComponentsStatus(ctx: CommandContext): Promise<number>
|
|
|
431
437
|
// invisible to the entity-keyed, single-stack `describeResources` above —
|
|
432
438
|
// observe each component's own cfn-deploy stack directly and overlay it as
|
|
433
439
|
// the authoritative presence signal (#57).
|
|
434
|
-
const
|
|
435
|
-
if (
|
|
436
|
-
const stackEvidence = await observeComponentStacks(discovery.components,
|
|
440
|
+
const anyObserver = plugins.some((p) => p.describeStackStatus);
|
|
441
|
+
if (anyObserver) {
|
|
442
|
+
const stackEvidence = await observeComponentStacks(discovery.components, plugins, environment);
|
|
437
443
|
liveEvidence = mergeLiveEvidence(liveEvidence, stackEvidence);
|
|
438
444
|
}
|
|
439
445
|
} finally {
|
|
@@ -85,8 +85,15 @@ vi.mock("../../config", async () => {
|
|
|
85
85
|
loadChantConfigUpward: (...a: unknown[]) => loadChantConfigUpwardMock(...a),
|
|
86
86
|
};
|
|
87
87
|
});
|
|
88
|
+
const buildMock = vi.fn();
|
|
88
89
|
vi.mock("../../build", () => ({
|
|
89
|
-
|
|
90
|
+
// Forwards its arguments so a test can assert what the live path passed —
|
|
91
|
+
// `buildParams` in particular (#1483), the same reason `discover` above
|
|
92
|
+
// forwards for #1359.
|
|
93
|
+
build: (...a: unknown[]) => {
|
|
94
|
+
buildMock(...a);
|
|
95
|
+
return Promise.resolve({ errors: [] });
|
|
96
|
+
},
|
|
90
97
|
partitionByLexicon: () => ({}),
|
|
91
98
|
computeStackGraph: () => ({}),
|
|
92
99
|
}));
|
|
@@ -136,6 +143,7 @@ describe("runGraph", () => {
|
|
|
136
143
|
// Default: no components — the single-stack --live path most tests exercise.
|
|
137
144
|
discoverComponentsMock.mockResolvedValue({ components: new Map(), sourceFiles: [], errors: [] });
|
|
138
145
|
observeMock.mockReset();
|
|
146
|
+
buildMock.mockReset();
|
|
139
147
|
loadPluginsMock.mockReset();
|
|
140
148
|
resolveLexMock.mockReset();
|
|
141
149
|
loadChantConfigMock.mockReset();
|
|
@@ -480,6 +488,48 @@ describe("runGraph", () => {
|
|
|
480
488
|
});
|
|
481
489
|
|
|
482
490
|
describe("live graph (--live)", () => {
|
|
491
|
+
// #1483 — the live path builds the source to learn which entities to
|
|
492
|
+
// observe, and did so on default parameters while the declared overlay
|
|
493
|
+
// resolved the caller's. For a project whose parameters choose *which
|
|
494
|
+
// resources exist* — a tier, a size, a profile — that observed one estate
|
|
495
|
+
// and compared it against another, so every resource the real parameter
|
|
496
|
+
// declares read as absent and every resource the default declares read as
|
|
497
|
+
// pending. A confidently wrong overlay, not an empty one.
|
|
498
|
+
describe("build-time parameters reach the observed build (#1483)", () => {
|
|
499
|
+
const liveWithTier = async (param?: string[]) => {
|
|
500
|
+
loadChantConfigUpwardMock.mockResolvedValue({
|
|
501
|
+
config: { buildParams: { tier: { type: "string", enum: ["light", "prod"], default: "light" } } },
|
|
502
|
+
});
|
|
503
|
+
resolveLexMock.mockResolvedValue(["aws"]);
|
|
504
|
+
loadPluginsMock.mockResolvedValue([
|
|
505
|
+
{ name: "aws", serializer: {}, emulator: awsEmulatorStub, describeResources: () => Promise.resolve({}) },
|
|
506
|
+
]);
|
|
507
|
+
observeMock.mockResolvedValue({ observations: [], errors: [], warnings: [] });
|
|
508
|
+
return runGraph({
|
|
509
|
+
args: makeArgs({ format: "ir", live: true, env: "prod", ...(param ? { param } : {}) }),
|
|
510
|
+
plugins: [],
|
|
511
|
+
serializers: [],
|
|
512
|
+
});
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
const paramsOf = (call: unknown[]) =>
|
|
516
|
+
(call[3] as { buildParams?: Array<{ name: string; value: unknown }> } | undefined)?.buildParams;
|
|
517
|
+
|
|
518
|
+
test("--param reaches the build the observation is scoped from", async () => {
|
|
519
|
+
expect(await liveWithTier(["tier=prod"])).toBe(0);
|
|
520
|
+
expect(paramsOf(buildMock.mock.calls[0]!)).toContainEqual(
|
|
521
|
+
expect.objectContaining({ name: "tier", value: "prod" }),
|
|
522
|
+
);
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
test("a declared default reaches it too, rather than nothing at all", async () => {
|
|
526
|
+
expect(await liveWithTier()).toBe(0);
|
|
527
|
+
expect(paramsOf(buildMock.mock.calls[0]!)).toContainEqual(
|
|
528
|
+
expect.objectContaining({ name: "tier", value: "light" }),
|
|
529
|
+
);
|
|
530
|
+
});
|
|
531
|
+
});
|
|
532
|
+
|
|
483
533
|
// Regression: `graph` is not `requiresPlugins`, so `ctx.plugins` is empty. The
|
|
484
534
|
// live path must load the project's plugins itself — otherwise it wrongly
|
|
485
535
|
// reports "No lexicons implement describeResources" and observes nothing.
|
|
@@ -144,7 +144,22 @@ async function runGraphLive(
|
|
|
144
144
|
|
|
145
145
|
// Build to get each lexicon's entity names + output (the scope
|
|
146
146
|
// describeResources needs), mirroring `chant lifecycle snapshot`.
|
|
147
|
-
|
|
147
|
+
//
|
|
148
|
+
// With the same build params the source graph resolves (#1483). Without
|
|
149
|
+
// them this build ran on defaults while the declared overlay below ran on
|
|
150
|
+
// the caller's, so a project whose parameters choose *which resources
|
|
151
|
+
// exist* — a tier, a size, a profile — observed one estate and compared it
|
|
152
|
+
// against another. Every resource the real parameter declares read as
|
|
153
|
+
// absent and every resource the default declares read as pending, which is
|
|
154
|
+
// a confidently wrong overlay rather than an empty one.
|
|
155
|
+
const liveBuildParams = await graphBuildParams(ctx, projectPath);
|
|
156
|
+
if (!liveBuildParams) return 1;
|
|
157
|
+
const buildResult = await build(
|
|
158
|
+
resolve(args.src ?? config.sourceDir ?? "."),
|
|
159
|
+
plugins.map((p) => p.serializer),
|
|
160
|
+
undefined,
|
|
161
|
+
{ buildParams: liveBuildParams },
|
|
162
|
+
);
|
|
148
163
|
if (buildResult.errors.length > 0) {
|
|
149
164
|
console.error(formatError({ message: "Build failed — fix errors before graphing live state" }));
|
|
150
165
|
return 1;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
|
+
import { commandBuildParams } from "../build-params-cli";
|
|
2
3
|
import { build } from "../../build";
|
|
3
4
|
import { takeSnapshot } from "../../lifecycle/snapshot";
|
|
4
5
|
import { readSnapshot, readSnapshotAt, readEnvironmentSnapshots, listSnapshots, fetchLifecycle, pushLifecycle, snapshotStorageKey, StaleLifecycleBranchError } from "../../lifecycle/git";
|
|
@@ -101,6 +102,10 @@ export async function runLifecycleSnapshot(ctx: CommandContext): Promise<number>
|
|
|
101
102
|
// Validate environment against config
|
|
102
103
|
const projectPath = resolve(".");
|
|
103
104
|
const { config } = await loadChantConfig(projectPath);
|
|
105
|
+
// This invocation's parameters, so the declared side of the comparison is the estate
|
|
106
|
+
// the caller asked for rather than the parameter defaults (#1483).
|
|
107
|
+
const declaredParams = await commandBuildParams(config.buildParams, args);
|
|
108
|
+
if (!declaredParams) return 1;
|
|
104
109
|
const declaredEnvNames = environmentNames(config.environments);
|
|
105
110
|
if (declaredEnvNames && !declaredEnvNames.includes(environment)) {
|
|
106
111
|
console.error(formatError({
|
|
@@ -146,7 +151,7 @@ export async function runLifecycleSnapshot(ctx: CommandContext): Promise<number>
|
|
|
146
151
|
const built: Array<{ target: (typeof targets)[number]; buildResult: Awaited<ReturnType<typeof build>> }> = [];
|
|
147
152
|
for (const target of targets) {
|
|
148
153
|
const label = target.stack ? `stack "${target.stack}"` : "project";
|
|
149
|
-
const buildResult = await build(target.root, targetSerializers);
|
|
154
|
+
const buildResult = await build(target.root, targetSerializers, undefined, { buildParams: declaredParams });
|
|
150
155
|
if (buildResult.errors.length > 0) {
|
|
151
156
|
console.error(formatError({ message: `Build failed for ${label} — fix errors before taking a snapshot` }));
|
|
152
157
|
anyHardError = true;
|
|
@@ -326,6 +331,10 @@ export async function runLifecycleDiff(ctx: CommandContext): Promise<number> {
|
|
|
326
331
|
|
|
327
332
|
// Fetch previous snapshots once (all stacks share the orphan branch).
|
|
328
333
|
const { config } = await loadChantConfig(resolve("."));
|
|
334
|
+
// This invocation's parameters, so the declared side of the comparison is the estate
|
|
335
|
+
// the caller asked for rather than the parameter defaults (#1483).
|
|
336
|
+
const declaredParams = await commandBuildParams(config.buildParams, args);
|
|
337
|
+
if (!declaredParams) return 1;
|
|
329
338
|
await fetchLifecycle();
|
|
330
339
|
|
|
331
340
|
// One target per stack (single-stack projects: exactly one), each built from
|
|
@@ -355,7 +364,7 @@ export async function runLifecycleDiff(ctx: CommandContext): Promise<number> {
|
|
|
355
364
|
|
|
356
365
|
try {
|
|
357
366
|
for (const target of targets) {
|
|
358
|
-
const buildResult = await build(target.root, targetSerializers);
|
|
367
|
+
const buildResult = await build(target.root, targetSerializers, undefined, { buildParams: declaredParams });
|
|
359
368
|
if (buildResult.errors.length > 0) {
|
|
360
369
|
const label = target.stack ? `stack "${target.stack}"` : "project";
|
|
361
370
|
console.error(formatError({ message: `Build failed for ${label} — fix errors before diffing` }));
|
|
@@ -1021,7 +1030,13 @@ export async function runLifecyclePlan(ctx: CommandContext): Promise<number> {
|
|
|
1021
1030
|
: serializers;
|
|
1022
1031
|
|
|
1023
1032
|
const { config } = await loadChantConfig(resolve("."));
|
|
1024
|
-
|
|
1033
|
+
// This invocation's parameters, so the declared side of the comparison is the estate
|
|
1034
|
+
// the caller asked for rather than the parameter defaults (#1483).
|
|
1035
|
+
const declaredParams = await commandBuildParams(config.buildParams, args);
|
|
1036
|
+
if (!declaredParams) return 1;
|
|
1037
|
+
const buildResult = await build(resolveBuildRoot(args, config), targetSerializers, undefined, {
|
|
1038
|
+
buildParams: declaredParams,
|
|
1039
|
+
});
|
|
1025
1040
|
if (buildResult.errors.length > 0) {
|
|
1026
1041
|
console.error(formatError({ message: "Build failed — fix errors before planning" }));
|
|
1027
1042
|
return 1;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import { deployUnits } from "./deploy-units";
|
|
3
|
+
import type { Phase } from "./component";
|
|
4
|
+
|
|
5
|
+
const phase = (name: string, steps: Phase["steps"]): Phase => ({ phase: name, steps });
|
|
6
|
+
|
|
7
|
+
describe("deployUnits (#1495 piece 1)", () => {
|
|
8
|
+
test("resolves a cfn-deploy step to its stack, keyed to the aws observer", () => {
|
|
9
|
+
const deploy = [phase("Apply", [{ kind: "cfn-deploy", stack: "cc-canonical", template: "t.json" }])];
|
|
10
|
+
expect(deployUnits(deploy)).toEqual([{ unit: "cc-canonical", lexicon: "aws" }]);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("resolves kubectl-apply and helm-upgrade units once their steps exist", () => {
|
|
14
|
+
const deploy = [
|
|
15
|
+
phase("Apply", [
|
|
16
|
+
{ kind: "kubectl-apply", stack: "kubemicrovm-ops" },
|
|
17
|
+
{ kind: "helm-upgrade", release: "operator" },
|
|
18
|
+
]),
|
|
19
|
+
];
|
|
20
|
+
expect(deployUnits(deploy)).toEqual([
|
|
21
|
+
{ unit: "kubemicrovm-ops", lexicon: "k8s" },
|
|
22
|
+
{ unit: "operator", lexicon: "helm" },
|
|
23
|
+
]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("walks nested phases, dedupes per lexicon, and skips unitless steps", () => {
|
|
27
|
+
const deploy = [
|
|
28
|
+
phase("Outer", [
|
|
29
|
+
phase("Inner", [{ kind: "cfn-deploy", stack: "web" }]) as never,
|
|
30
|
+
{ kind: "cfn-deploy", stack: "web" },
|
|
31
|
+
{ kind: "shell", cmd: "echo", reason: "no capability yet" },
|
|
32
|
+
{ kind: "cfn-deploy" }, // no stack named — contributes nothing
|
|
33
|
+
]),
|
|
34
|
+
];
|
|
35
|
+
expect(deployUnits(deploy)).toEqual([{ unit: "web", lexicon: "aws" }]);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("an unlisted kind contributes no unit — the registry is the rule", () => {
|
|
39
|
+
const deploy = [phase("Apply", [{ kind: "gcloud-deploy", stack: "x" }])];
|
|
40
|
+
expect(deployUnits(deploy)).toEqual([]);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deploy units — which live unit(s) a component's composition targets (#1495).
|
|
3
|
+
*
|
|
4
|
+
* `chant components status --live` (and `chant graph --live`) need to know
|
|
5
|
+
* what a component deployed in order to observe it. That answer used to be a
|
|
6
|
+
* string literal in the walk — `kind === "cfn-deploy"` — which made the whole
|
|
7
|
+
* chain CloudFormation-shaped: a component whose deploy phase is a server-side
|
|
8
|
+
* apply or a Helm upgrade contributed nothing and was skipped, not
|
|
9
|
+
* "unobserved", simply absent from the result.
|
|
10
|
+
*
|
|
11
|
+
* This module is the seam that fixes the walk (piece 1 of #1495's
|
|
12
|
+
* decomposition): a registry mapping a deploy-family step `kind` to the field
|
|
13
|
+
* naming its unit and the lexicon whose `describeStackStatus` can observe it.
|
|
14
|
+
* The registry is data, not a rule about what steps look like — a kind not
|
|
15
|
+
* listed here contributes no unit, exactly as before.
|
|
16
|
+
*/
|
|
17
|
+
import type { Phase } from "./component";
|
|
18
|
+
|
|
19
|
+
/** A deploy-family step kind that names a live unit, and who observes it. */
|
|
20
|
+
export interface DeployUnitRule {
|
|
21
|
+
/** The step `kind` (the capability's registered name). */
|
|
22
|
+
kind: string;
|
|
23
|
+
/** The step field carrying the unit's name (`stack`, `release`). */
|
|
24
|
+
field: string;
|
|
25
|
+
/** The lexicon whose `describeStackStatus` reads this kind of unit. */
|
|
26
|
+
lexicon: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The deploy-family kinds that name a unit. cfn-deploy's unit is its stack;
|
|
31
|
+
* kubectl-apply's is the ownership stack its labels carry (#1495 piece 2);
|
|
32
|
+
* helm-upgrade's is its release (#1495 piece 4). Listing a kind here is safe
|
|
33
|
+
* before its lexicon implements `describeStackStatus` — units whose lexicon
|
|
34
|
+
* has no observer are skipped by the caller, the same absent-observer path as
|
|
35
|
+
* before.
|
|
36
|
+
*/
|
|
37
|
+
export const DEPLOY_UNIT_RULES: readonly DeployUnitRule[] = [
|
|
38
|
+
{ kind: "cfn-deploy", field: "stack", lexicon: "aws" },
|
|
39
|
+
{ kind: "kubectl-apply", field: "stack", lexicon: "k8s" },
|
|
40
|
+
{ kind: "helm-upgrade", field: "release", lexicon: "helm" },
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
/** One resolved unit a component's deploy composition targets. */
|
|
44
|
+
export interface DeployUnit {
|
|
45
|
+
/** The unit's name — a stack, a release. */
|
|
46
|
+
unit: string;
|
|
47
|
+
/** The lexicon that observes this kind of unit. */
|
|
48
|
+
lexicon: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Every distinct deploy unit a component's phases target, in declaration
|
|
53
|
+
* order. A step may itself be a nested `Phase`, so the walk recurses; a
|
|
54
|
+
* resolved component carries the unit as a concrete string. Pure.
|
|
55
|
+
*/
|
|
56
|
+
export function deployUnits(deploy: Phase[]): DeployUnit[] {
|
|
57
|
+
const byKind = new Map(DEPLOY_UNIT_RULES.map((r) => [r.kind, r]));
|
|
58
|
+
const seen = new Set<string>();
|
|
59
|
+
const units: DeployUnit[] = [];
|
|
60
|
+
const walkSteps = (steps: Phase["steps"]): void => {
|
|
61
|
+
for (const step of steps) {
|
|
62
|
+
// A step may itself be a nested Phase (it carries its own `steps`). Step
|
|
63
|
+
// is open-typed (capability inputs), so discriminate structurally.
|
|
64
|
+
const nested = (step as { steps?: unknown }).steps;
|
|
65
|
+
if (Array.isArray(nested)) {
|
|
66
|
+
walkSteps(nested as Phase["steps"]);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const s = step as { kind?: string } & Record<string, unknown>;
|
|
70
|
+
const rule = s.kind ? byKind.get(s.kind) : undefined;
|
|
71
|
+
if (!rule) continue;
|
|
72
|
+
const unit = s[rule.field];
|
|
73
|
+
if (typeof unit !== "string" || unit.length === 0) continue;
|
|
74
|
+
const key = `${rule.lexicon}\0${unit}`;
|
|
75
|
+
if (seen.has(key)) continue;
|
|
76
|
+
seen.add(key);
|
|
77
|
+
units.push({ unit, lexicon: rule.lexicon });
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
for (const phase of deploy) walkSteps(phase.steps);
|
|
81
|
+
return units;
|
|
82
|
+
}
|
package/src/config.test.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, test, expect, beforeEach, afterEach } from "vitest";
|
|
2
2
|
import {
|
|
3
3
|
loadChantConfig,
|
|
4
|
+
loadChantConfigUpward,
|
|
4
5
|
DEFAULT_CHANT_CONFIG,
|
|
5
6
|
resolveAutoReleaseDisabled,
|
|
6
7
|
resolveFoldEnabled,
|
|
@@ -137,6 +138,58 @@ describe("loadChantConfig", () => {
|
|
|
137
138
|
});
|
|
138
139
|
});
|
|
139
140
|
|
|
141
|
+
// #1502 — the upward walk skips lint-scoping fragments. A `src/chant.config.json`
|
|
142
|
+
// holding only `extends`/`rules` (the examples/ convention) must not shadow the
|
|
143
|
+
// project config above it, or `chant build src` silently loses `ownership`/
|
|
144
|
+
// `buildParams` — the exact fallback #1117's walk exists to prevent.
|
|
145
|
+
describe("loadChantConfigUpward (#1502 — lint fragments do not end the walk)", () => {
|
|
146
|
+
const SRC = join(TEST_DIR, "src");
|
|
147
|
+
|
|
148
|
+
test("walks past a lint-only src/chant.config.json to the project config", async () => {
|
|
149
|
+
mkdirSync(SRC, { recursive: true });
|
|
150
|
+
writeFileSync(
|
|
151
|
+
join(SRC, "chant.config.json"),
|
|
152
|
+
JSON.stringify({ extends: ["@intentius/chant/lint/presets/strict"], rules: { COR001: "off" } }),
|
|
153
|
+
);
|
|
154
|
+
writeFileSync(
|
|
155
|
+
join(TEST_DIR, "chant.config.json"),
|
|
156
|
+
JSON.stringify({ ownership: { stack: "billing", env: "prod" } }),
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
const result = await loadChantConfigUpward(SRC);
|
|
160
|
+
expect(result.config.ownership).toEqual({ stack: "billing", env: "prod" });
|
|
161
|
+
expect(result.configPath).toBe(join(TEST_DIR, "chant.config.json"));
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("a src/chant.config.json declaring any project-level key still wins in place", async () => {
|
|
165
|
+
mkdirSync(SRC, { recursive: true });
|
|
166
|
+
writeFileSync(
|
|
167
|
+
join(SRC, "chant.config.json"),
|
|
168
|
+
JSON.stringify({ ownership: { stack: "nested" }, rules: { COR001: "off" } }),
|
|
169
|
+
);
|
|
170
|
+
writeFileSync(
|
|
171
|
+
join(TEST_DIR, "chant.config.json"),
|
|
172
|
+
JSON.stringify({ ownership: { stack: "root" } }),
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
const result = await loadChantConfigUpward(SRC);
|
|
176
|
+
expect(result.config.ownership?.stack).toBe("nested");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("a fragment-only project resolves to the default config at the boundary", async () => {
|
|
180
|
+
mkdirSync(SRC, { recursive: true });
|
|
181
|
+
writeFileSync(join(TEST_DIR, "package.json"), JSON.stringify({ name: "boundary" }));
|
|
182
|
+
writeFileSync(
|
|
183
|
+
join(SRC, "chant.config.json"),
|
|
184
|
+
JSON.stringify({ extends: ["@intentius/chant/lint/presets/strict"] }),
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
const result = await loadChantConfigUpward(SRC);
|
|
188
|
+
expect(result.config).toEqual(DEFAULT_CHANT_CONFIG);
|
|
189
|
+
expect(result.configPath).toBeUndefined();
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
140
193
|
// #1166 — `environments` accepts either a bare name (unchanged) or
|
|
141
194
|
// `{ name, endpoint }`, so a declared environment can be self-sufficient for
|
|
142
195
|
// `--live` reads without an ambient AWS_ENDPOINT_URL export.
|
package/src/config.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { existsSync } from "fs";
|
|
2
|
-
import { join } from "path";
|
|
1
|
+
import { existsSync, readFileSync } from "fs";
|
|
2
|
+
import { dirname, join } from "path";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import type { LintConfig } from "./lint/config";
|
|
5
5
|
import type { OwnershipMarker } from "./ownership";
|
|
@@ -384,12 +384,50 @@ export async function loadChantConfig(dir: string): Promise<ResolvedConfig> {
|
|
|
384
384
|
* same walk `chant lint`/`chant graph` already used ({@link findProjectConfig},
|
|
385
385
|
* shared with `./lint/config.ts`'s `findProjectRoot`) — one config-discovery
|
|
386
386
|
* contract for the whole CLI.
|
|
387
|
+
*
|
|
388
|
+
* chant #1502 — a lint-scoping fragment does not end the walk. The convention
|
|
389
|
+
* of a `src/chant.config.json` holding only `extends`/`rules` (cc-aws-canonical
|
|
390
|
+
* and most of examples/) sits BETWEEN the build directory and the real
|
|
391
|
+
* `chant.config.ts`, and stopping there re-introduced the exact silent
|
|
392
|
+
* fallback this walk exists to prevent: `chant build src` resolved the
|
|
393
|
+
* fragment, found no `ownership`, and built unstamped manifests that every
|
|
394
|
+
* owned-scoped live read then withheld. A fragment is skipped, not merged —
|
|
395
|
+
* lint resolution keeps its own nearest-wins walk untouched, and a JSON
|
|
396
|
+
* config declaring any project-level key still wins where it stands.
|
|
387
397
|
*/
|
|
388
398
|
export async function loadChantConfigUpward(startDir: string): Promise<ResolvedConfig> {
|
|
389
|
-
|
|
399
|
+
let { dir, configPath } = findProjectConfig(startDir);
|
|
400
|
+
while (configPath && isLintOnlyFragment(configPath)) {
|
|
401
|
+
const parent = dirname(dir);
|
|
402
|
+
if (parent === dir) break;
|
|
403
|
+
({ dir, configPath } = findProjectConfig(parent));
|
|
404
|
+
}
|
|
390
405
|
return loadChantConfig(dir);
|
|
391
406
|
}
|
|
392
407
|
|
|
408
|
+
/**
|
|
409
|
+
* The top-level keys of `./lint/config.ts`'s `LintConfigSchema` (plus the
|
|
410
|
+
* `$schema` editor convention). A `chant.config.json` whose keys all come from
|
|
411
|
+
* this set is a lint-scoping fragment, not a project config — see
|
|
412
|
+
* {@link loadChantConfigUpward}. `chant.config.ts` is never a fragment: it is
|
|
413
|
+
* project-authored code, and inspecting it would mean evaluating it.
|
|
414
|
+
*/
|
|
415
|
+
const LINT_FRAGMENT_KEYS = new Set(["$schema", "extends", "rules", "overrides", "plugins", "policies"]);
|
|
416
|
+
|
|
417
|
+
function isLintOnlyFragment(configPath: string): boolean {
|
|
418
|
+
if (!configPath.endsWith("chant.config.json")) return false;
|
|
419
|
+
try {
|
|
420
|
+
const parsed = JSON.parse(readFileSync(configPath, "utf-8")) as unknown;
|
|
421
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return false;
|
|
422
|
+
const keys = Object.keys(parsed);
|
|
423
|
+
return keys.length > 0 && keys.every((k) => LINT_FRAGMENT_KEYS.has(k));
|
|
424
|
+
} catch {
|
|
425
|
+
// Unreadable/unparseable JSON: let loadChantConfig surface the real error
|
|
426
|
+
// in place rather than silently walking past it.
|
|
427
|
+
return false;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
393
431
|
/**
|
|
394
432
|
* Resolve the ownership marker to stamp from project config, or undefined when
|
|
395
433
|
* ownership marking is off (no `stack`, or `enabled: false`).
|