@intentius/chant 0.59.0 → 0.61.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/build-params.d.ts +2 -2
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/handlers/lint.d.ts.map +1 -1
- package/dist/codegen/json-schema.d.ts +5 -2
- package/dist/codegen/json-schema.d.ts.map +1 -1
- package/dist/components/pilots/alb-ecs.pilot.d.ts +2 -2
- package/dist/config.d.ts +4 -4
- package/dist/graph-ir.d.ts +70 -2
- package/dist/graph-ir.d.ts.map +1 -1
- package/dist/lexicon.d.ts +48 -5
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/assert-live.d.ts.map +1 -1
- package/dist/lifecycle/observe.d.ts +4 -4
- package/dist/lifecycle/observe.d.ts.map +1 -1
- package/dist/observation.d.ts +21 -1
- package/dist/observation.d.ts.map +1 -1
- package/dist/op/activities/index.d.ts +1 -1
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activities/reconcile.d.ts +79 -7
- package/dist/op/activities/reconcile.d.ts.map +1 -1
- package/dist/op/gate-summary.d.ts +16 -4
- package/dist/op/gate-summary.d.ts.map +1 -1
- package/dist/params.d.ts +1 -1
- package/dist/project-root.d.ts +2 -2
- package/package.json +1 -1
- package/src/build-params.ts +2 -2
- package/src/cli/commands/build.ts +8 -8
- package/src/cli/commands/lint.test.ts +151 -0
- package/src/cli/commands/lint.ts +37 -4
- package/src/cli/handlers/components.ts +1 -1
- package/src/cli/handlers/graph.test.ts +4 -4
- package/src/cli/handlers/graph.ts +11 -11
- package/src/cli/handlers/lifecycle.ts +1 -0
- package/src/cli/handlers/lint.test.ts +107 -0
- package/src/cli/handlers/lint.ts +30 -0
- package/src/codegen/json-schema.test.ts +159 -0
- package/src/codegen/json-schema.ts +106 -8
- package/src/components/SPRAWL-VALIDATION.md +5 -5
- package/src/components/pilots/README.md +1 -1
- package/src/components/pilots/alb-ecs.pilot.ts +2 -2
- package/src/config.ts +4 -4
- package/src/discovery/fold-import.test.ts +1 -1
- package/src/discovery/fold-import.ts +3 -3
- package/src/graph-ir.test.ts +63 -0
- package/src/graph-ir.ts +125 -8
- package/src/lexicon.ts +49 -5
- package/src/lifecycle/assert-live.ts +1 -0
- package/src/lifecycle/observe.test.ts +2 -2
- package/src/lifecycle/observe.ts +11 -8
- package/src/lifecycle/release-ledger.test.ts +2 -2
- package/src/observation.test.ts +21 -9
- package/src/observation.ts +31 -2
- package/src/op/activities/index.ts +8 -1
- package/src/op/activities/reconcile.test.ts +238 -0
- package/src/op/activities/reconcile.ts +267 -13
- package/src/op/gate-summary.test.ts +62 -0
- package/src/op/gate-summary.ts +17 -5
- package/src/params.ts +1 -1
- package/src/project-root.ts +2 -2
|
@@ -6,6 +6,13 @@
|
|
|
6
6
|
* re-run (chant #2231). It therefore needs a pull-request trigger, and
|
|
7
7
|
* {@link resolvePullRequestContext} fails the step by name when the run has
|
|
8
8
|
* none.
|
|
9
|
+
*
|
|
10
|
+
* On GitLab the same mode writes a merge-request note (chant #2256): the same
|
|
11
|
+
* marker, the same edit-in-place, a different API. Which forge a run is on is
|
|
12
|
+
* read off the run's own CI variables rather than configured — a
|
|
13
|
+
* `merge_request_event` pipeline sets `CI_MERGE_REQUEST_IID`, a GitHub
|
|
14
|
+
* `pull_request` event sets `GITHUB_REPOSITORY`, and no run sets both. See
|
|
15
|
+
* {@link mergeRequestContextFrom}.
|
|
9
16
|
*/
|
|
10
17
|
export type ReconcileMode = "pull-request" | "issue" | "report" | "comment";
|
|
11
18
|
/** A change-set entry that triggered reconciliation. */
|
|
@@ -69,10 +76,12 @@ export interface ReconcileResult {
|
|
|
69
76
|
prUrl?: string;
|
|
70
77
|
/** Opened issue URL (issue mode). */
|
|
71
78
|
issueUrl?: string;
|
|
72
|
-
/** The posted or updated PR comment
|
|
79
|
+
/** The posted or updated PR comment / MR note URL (comment mode). */
|
|
73
80
|
commentUrl?: string;
|
|
74
|
-
/** The pull request the comment landed on, `owner/repo#number` (comment mode). */
|
|
81
|
+
/** The pull request the comment landed on, `owner/repo#number` (comment mode, GitHub). */
|
|
75
82
|
pullRequest?: string;
|
|
83
|
+
/** The merge request the note landed on, `group/project!iid` (comment mode, GitLab — #2256). */
|
|
84
|
+
mergeRequest?: string;
|
|
76
85
|
/** The markdown summary used as the PR/issue body. */
|
|
77
86
|
summary: string;
|
|
78
87
|
/** The entries that triggered the reconcile. */
|
|
@@ -101,7 +110,7 @@ export interface PullRequestContext {
|
|
|
101
110
|
* environments own two comments and each updates in place.
|
|
102
111
|
*/
|
|
103
112
|
export declare function commentMarker(env: string): string;
|
|
104
|
-
/** What a `comment`-mode step says when the run it is in has no pull request. */
|
|
113
|
+
/** What a `comment`-mode step says when the run it is in has no pull request and no merge request. */
|
|
105
114
|
export declare function noPullRequestContextMessage(): string;
|
|
106
115
|
/**
|
|
107
116
|
* Derive the triggering pull request from CI environment variables plus the
|
|
@@ -119,6 +128,66 @@ export declare function pullRequestContextFrom(env: Record<string, string | unde
|
|
|
119
128
|
* for it.
|
|
120
129
|
*/
|
|
121
130
|
export declare function resolvePullRequestContext(env?: Record<string, string | undefined>): Promise<PullRequestContext>;
|
|
131
|
+
/**
|
|
132
|
+
* The merge request a `comment`-mode run posts its note onto (#2256), as a
|
|
133
|
+
* GitLab CI job knows it. The GitLab counterpart of {@link
|
|
134
|
+
* PullRequestContext}.
|
|
135
|
+
*/
|
|
136
|
+
export interface MergeRequestContext {
|
|
137
|
+
/** REST v4 base, from `CI_API_V4_URL` or derived from `CI_SERVER_URL`. */
|
|
138
|
+
api: string;
|
|
139
|
+
/** The project holding the merge request — its numeric id, or a `group/project` path. */
|
|
140
|
+
project: string;
|
|
141
|
+
/** The merge request's `iid` (its per-project number, which is what the API path takes). */
|
|
142
|
+
iid: number;
|
|
143
|
+
/** `group/project`, for the human-readable `group/project!iid` on the result. */
|
|
144
|
+
path?: string;
|
|
145
|
+
/** The project's web URL, used to build the note's own URL. */
|
|
146
|
+
webUrl?: string;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Derive the triggering merge request from a GitLab job's CI variables. Pure
|
|
150
|
+
* — exported for testing, and the whole forge detection: nothing but a
|
|
151
|
+
* `merge_request_event` pipeline sets `CI_MERGE_REQUEST_IID`, so a run that
|
|
152
|
+
* has it is on GitLab and has a merge request, and a run that does not is
|
|
153
|
+
* neither.
|
|
154
|
+
*
|
|
155
|
+
* The project is the merge request's own (`CI_MERGE_REQUEST_PROJECT_ID`) in
|
|
156
|
+
* preference to the pipeline's (`CI_PROJECT_ID`): a merge request opened from
|
|
157
|
+
* a fork runs its pipeline in the fork, and the note belongs on the target
|
|
158
|
+
* project's merge request rather than on an iid that means something else in
|
|
159
|
+
* the fork.
|
|
160
|
+
*
|
|
161
|
+
* Returns undefined rather than throwing, so the caller owns the message.
|
|
162
|
+
*/
|
|
163
|
+
export declare function mergeRequestContextFrom(env: Record<string, string | undefined>): MergeRequestContext | undefined;
|
|
164
|
+
/** The credential a merge-request note is written with, and the header GitLab reads it from. */
|
|
165
|
+
export interface GitlabNoteToken {
|
|
166
|
+
/** `PRIVATE-TOKEN` for a personal/project/group access token, `JOB-TOKEN` for `CI_JOB_TOKEN`. */
|
|
167
|
+
header: "PRIVATE-TOKEN" | "JOB-TOKEN";
|
|
168
|
+
value: string;
|
|
169
|
+
/** The variable it came from, so a refusal or a log line can name it. */
|
|
170
|
+
source: string;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Resolve the token a merge-request note is written with, most specific
|
|
174
|
+
* first. Pure — exported for testing.
|
|
175
|
+
*
|
|
176
|
+
* Two headers, not one, because GitLab reads two different credentials from
|
|
177
|
+
* two different headers: an access token goes in `PRIVATE-TOKEN`, and the
|
|
178
|
+
* pipeline's own ephemeral `CI_JOB_TOKEN` goes in `JOB-TOKEN`. Sending one in
|
|
179
|
+
* the other's header is a 401, not a fallback.
|
|
180
|
+
*
|
|
181
|
+
* The access token is preferred because it is the one that reliably works:
|
|
182
|
+
* `CI_JOB_TOKEN` reaches only the endpoints GitLab's job-token allowlist
|
|
183
|
+
* names, and the notes API is not among them on current GitLab, so a project
|
|
184
|
+
* that has not widened that allowlist needs a token with `api` scope. It is
|
|
185
|
+
* still accepted last rather than refused, so a project on an instance whose
|
|
186
|
+
* allowlist does cover notes needs no long-lived credential at all.
|
|
187
|
+
*/
|
|
188
|
+
export declare function gitlabNoteTokenFrom(env: Record<string, string | undefined>): GitlabNoteToken | undefined;
|
|
189
|
+
/** What a `comment`-mode step says on a merge request it has no credential for. */
|
|
190
|
+
export declare function noGitlabNoteTokenMessage(iid: number): string;
|
|
122
191
|
/**
|
|
123
192
|
* Map a `chant lifecycle plan --json` ChangeSet to reconcile entries, dropping
|
|
124
193
|
* `noop` entries (nothing to reconcile). Pure — exported for testing.
|
|
@@ -131,10 +200,13 @@ export declare function entriesFromPlan(planJson: string): ReconcileEntry[];
|
|
|
131
200
|
* - `issue` — open a GitHub issue describing the drift (no code change).
|
|
132
201
|
* - `comment` — post the body as one comment on the pull request that
|
|
133
202
|
* triggered the run, editing that same comment on every re-run rather than
|
|
134
|
-
* stacking a new one (#2231)
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
203
|
+
* stacking a new one (#2231), or, on a GitLab `merge_request_event`
|
|
204
|
+
* pipeline, as one note on that merge request by the same recipe (#2256).
|
|
205
|
+
* Needs a pull-request- or merge-request-triggered run; fails by name when
|
|
206
|
+
* there is none. No code change, and the `pull-requests: write` the
|
|
207
|
+
* generated workflow already grants on that trigger is the whole scope it
|
|
208
|
+
* spends on GitHub; on GitLab the scope is whatever the token it is given
|
|
209
|
+
* carries.
|
|
138
210
|
* - `pull-request` — create a branch, regenerate source via
|
|
139
211
|
* `chant import --from <env>`, commit, push, and open a PR whose diff is the
|
|
140
212
|
* regenerated TypeScript. Never commits to the main branch.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reconcile.d.ts","sourceRoot":"","sources":["../../../src/op/activities/reconcile.ts"],"names":[],"mappings":"AAMA
|
|
1
|
+
{"version":3,"file":"reconcile.d.ts","sourceRoot":"","sources":["../../../src/op/activities/reconcile.ts"],"names":[],"mappings":"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"}
|
|
@@ -8,9 +8,14 @@
|
|
|
8
8
|
* markdown scratchpad through `GITHUB_STEP_SUMMARY`, rendered at the top of
|
|
9
9
|
* the run page; anything else that sets the variable gets the same block.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* GitLab CI sets no such variable and has no step summary at all: a job's
|
|
12
|
+
* surfaces are its log and its artifacts (#2256). So `CHANT_GATE_SUMMARY`
|
|
13
|
+
* names a second path, which the generated GitLab job sets to a file it also
|
|
14
|
+
* declares under `artifacts:`. Same block, same rule, one more variable.
|
|
15
|
+
*
|
|
16
|
+
* That is the whole forge coupling: two environment variables, appended to
|
|
17
|
+
* whichever is set, and nothing at all when neither is. No API call, no
|
|
18
|
+
* token, no flag to turn it on.
|
|
14
19
|
*/
|
|
15
20
|
/** What a gated run knows about the gate it stopped on. */
|
|
16
21
|
export interface GatedRunSummary {
|
|
@@ -33,7 +38,14 @@ export interface GatedRunSummary {
|
|
|
33
38
|
export declare function gatedRunSummaryMarkdown(summary: GatedRunSummary): string;
|
|
34
39
|
/**
|
|
35
40
|
* Append {@link gatedRunSummaryMarkdown} to the file `GITHUB_STEP_SUMMARY`
|
|
36
|
-
* names,
|
|
41
|
+
* names, or, where the forge sets no such variable, the one
|
|
42
|
+
* `CHANT_GATE_SUMMARY` names (#2256) — when it is set and the file can be
|
|
43
|
+
* written.
|
|
44
|
+
*
|
|
45
|
+
* The forge's own variable wins where both are set: a step summary is
|
|
46
|
+
* rendered on the run page, an artifact has to be downloaded, and writing
|
|
47
|
+
* both would put the same block in two places on a forge that already shows
|
|
48
|
+
* one of them.
|
|
37
49
|
*
|
|
38
50
|
* Returns the path written, or `undefined` when there was nothing to write to.
|
|
39
51
|
* A write that fails is swallowed: a run that already decided its outcome must
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gate-summary.d.ts","sourceRoot":"","sources":["../../src/op/gate-summary.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"gate-summary.d.ts","sourceRoot":"","sources":["../../src/op/gate-summary.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAMH,2DAA2D;AAC3D,MAAM,WAAW,eAAe;IAC9B,iDAAiD;IACjD,EAAE,EAAE,MAAM,CAAC;IACX,gEAAgE;IAChE,IAAI,EAAE,MAAM,CAAC;IACb,wDAAwD;IACxD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qDAAqD;IACrD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,+DAA+D;IAC/D,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,eAAe,GAAG,MAAM,CAuBxE;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,eAAe,EACxB,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,MAAM,GAAG,SAAS,CASpB"}
|
package/dist/params.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* is a deploy-time CloudFormation parameter: it emits a `Parameters:` block and
|
|
7
7
|
* resolves when the STACK deploys. A build-time parameter resolves before the
|
|
8
8
|
* template is even synthesized — its value can change WHICH resources are
|
|
9
|
-
* produced at all (
|
|
9
|
+
* produced at all (a `tier` parameter selecting `light` vs `production` vs
|
|
10
10
|
* `production-ha`), which a deploy-time `Parameter` structurally cannot do.
|
|
11
11
|
*
|
|
12
12
|
* Project source never reads `process.env` directly to vary a build — that
|
package/dist/project-root.d.ts
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* `src/<stack>` layout — `chant build src/<stack>` two or more levels below
|
|
10
10
|
* the project root — silently never found the root config: `buildParams`'
|
|
11
11
|
* declared `env:` mappings went inert, `ownership`/`lint.policies`/etc quietly
|
|
12
|
-
* fell back to defaults, and nothing warned
|
|
13
|
-
* `
|
|
12
|
+
* fell back to defaults, and nothing warned. That failure ran under every
|
|
13
|
+
* `npm run synth:*` of one project for two releases before anyone noticed it.
|
|
14
14
|
*
|
|
15
15
|
* `findProjectConfig` is the one walk every config-discovery call site now
|
|
16
16
|
* shares. It stops at the first of:
|
package/package.json
CHANGED
package/src/build-params.ts
CHANGED
|
@@ -33,8 +33,8 @@ export interface BuildParamDef {
|
|
|
33
33
|
/**
|
|
34
34
|
* Allowed values — a resolved value outside this list is a build error
|
|
35
35
|
* naming the parameter (never a thrown error from user source). Replaces
|
|
36
|
-
* the hand-written `if (!VALID.includes(raw)) throw ...` pattern
|
|
37
|
-
* `params.ts`
|
|
36
|
+
* the hand-written `if (!VALID.includes(raw)) throw ...` pattern a project's
|
|
37
|
+
* own `params.ts` writes before migrating to this mechanism.
|
|
38
38
|
*/
|
|
39
39
|
enum?: readonly BuildParamValue[];
|
|
40
40
|
/**
|
|
@@ -174,8 +174,8 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
|
|
|
174
174
|
// Resolve opt-in ownership marking from project config. chant #1117 — walks
|
|
175
175
|
// up from the infra dir to the project root (`loadChantConfigUpward`), not
|
|
176
176
|
// just the infra dir's immediate parent: a project whose stacks live two or
|
|
177
|
-
// more levels below `chant.config.ts` (
|
|
178
|
-
//
|
|
177
|
+
// more levels below `chant.config.ts` (a `src/<stack>` layout) otherwise
|
|
178
|
+
// never finds the root config at all, and every declared
|
|
179
179
|
// `buildParams`/`ownership`/`lint.policies` setting silently falls back to
|
|
180
180
|
// its default.
|
|
181
181
|
const loaded = await loadChantConfigUpward(infraPath);
|
|
@@ -233,10 +233,10 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
|
|
|
233
233
|
// BEFORE calling build() — a resolution failure (an unknown name, a
|
|
234
234
|
// missing required value, a type/enum mismatch) is reported as a chant
|
|
235
235
|
// build error naming the parameter, never a thrown error from inside user
|
|
236
|
-
// source (which is what
|
|
237
|
-
//
|
|
238
|
-
// count on success, or every resolved value (`[param] name =
|
|
239
|
-
// (source)`) under --verbose (#1424).
|
|
236
|
+
// source (which is what a hand-rolled `tierFromEnv()`-style validator in a
|
|
237
|
+
// project's own `params.ts` does before migrating to this mechanism). Also
|
|
238
|
+
// logs a one-line count on success, or every resolved value (`[param] name =
|
|
239
|
+
// value (source)`) under --verbose (#1424).
|
|
240
240
|
const paramsResolution = resolveCliBuildParams(config.buildParams, {
|
|
241
241
|
cli: options.params,
|
|
242
242
|
paramsFile: options.paramsFile,
|
|
@@ -248,8 +248,8 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
|
|
|
248
248
|
}
|
|
249
249
|
|
|
250
250
|
// chant #1117 — a project that declares buildParams but resolves NONE of
|
|
251
|
-
// them for this build is the exact shape that let
|
|
252
|
-
// releases: the discovered config wasn't the one the project author
|
|
251
|
+
// them for this build is the exact shape that let a silent misresolution run
|
|
252
|
+
// for two releases: the discovered config wasn't the one the project author
|
|
253
253
|
// expected (a stale --path, a workspace boundary that stopped the walk
|
|
254
254
|
// short), or every declared parameter's `env:` var went unset, and either
|
|
255
255
|
// way every `params.<name>` read silently falls back to `undefined` — with
|
|
@@ -5,6 +5,7 @@ import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
|
5
5
|
import { join, resolve } from "node:path";
|
|
6
6
|
import { tmpdir } from "node:os";
|
|
7
7
|
import { execFileSync } from "node:child_process";
|
|
8
|
+
import { params, setBuildParams } from "../../params";
|
|
8
9
|
|
|
9
10
|
describe("lintCommand", () => {
|
|
10
11
|
let testDir: string;
|
|
@@ -118,6 +119,69 @@ export default {
|
|
|
118
119
|
).toBe(true);
|
|
119
120
|
});
|
|
120
121
|
|
|
122
|
+
// #2251 — `chant lint` imports every `*.op.ts` file to read the Op it
|
|
123
|
+
// declares, and an Op that takes a step argument from `params.<name>`
|
|
124
|
+
// (`@intentius/chant/params`) evaluates that read at module load. Before
|
|
125
|
+
// this, nothing populated the shared parameters object first, so every such
|
|
126
|
+
// argument was `undefined` and OPS012 reported the activity contract
|
|
127
|
+
// violated on source that builds and runs. The fixture imports the real
|
|
128
|
+
// params module by absolute path — `@intentius/chant/params` maps to the
|
|
129
|
+
// same file (packages/core/package.json's `exports`), so it is the one
|
|
130
|
+
// module record `lintCommand` mutates.
|
|
131
|
+
describe("build parameters reach an Op's step arguments (#2251)", () => {
|
|
132
|
+
const paramsModule = resolve(import.meta.dirname, "../../params.ts");
|
|
133
|
+
|
|
134
|
+
async function writeParamReadingOp(): Promise<void> {
|
|
135
|
+
await writeFile(
|
|
136
|
+
join(testDir, "mini.op.ts"),
|
|
137
|
+
`
|
|
138
|
+
import { params } from ${JSON.stringify(paramsModule)};
|
|
139
|
+
|
|
140
|
+
export default {
|
|
141
|
+
[Symbol.for("chant.declarable")]: true,
|
|
142
|
+
entityType: "Chant::Op",
|
|
143
|
+
lexicon: "chant",
|
|
144
|
+
kind: "resource",
|
|
145
|
+
props: {
|
|
146
|
+
name: "mini",
|
|
147
|
+
overview: "test",
|
|
148
|
+
phases: [
|
|
149
|
+
{ name: "Greet", steps: [
|
|
150
|
+
{ kind: "activity", fn: "shellCmd", args: { cmd: params.greeting } },
|
|
151
|
+
] },
|
|
152
|
+
],
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
`,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
test("OPS012 does not fire when the invocation's parameters are supplied", async () => {
|
|
160
|
+
await writeParamReadingOp();
|
|
161
|
+
|
|
162
|
+
const result = await lintCommand({
|
|
163
|
+
path: testDir,
|
|
164
|
+
format: "stylish",
|
|
165
|
+
buildParams: [{ name: "greeting", value: "echo hi", source: "default" }],
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
expect(result.diagnostics.filter((d) => d.ruleId === "OPS012")).toEqual([]);
|
|
169
|
+
expect(result.success).toBe(true);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("OPS012 fires on the same source when no parameters are supplied", async () => {
|
|
173
|
+
await writeParamReadingOp();
|
|
174
|
+
|
|
175
|
+
const result = await lintCommand({ path: testDir, format: "stylish" });
|
|
176
|
+
|
|
177
|
+
expect(
|
|
178
|
+
result.diagnostics.some(
|
|
179
|
+
(d) => d.ruleId === "OPS012" && d.message.includes("args.cmd"),
|
|
180
|
+
),
|
|
181
|
+
).toBe(true);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
121
185
|
test("formats output as JSON", async () => {
|
|
122
186
|
await writeFile(
|
|
123
187
|
join(testDir, "nested.ts"),
|
|
@@ -686,3 +750,90 @@ describe("lintCommand — a declared lexicon that cannot be resolved (#2222)", (
|
|
|
686
750
|
expect(result.diagnostics.some((d) => d.ruleId === LEXICON_RESOLUTION_RULE_ID)).toBe(false);
|
|
687
751
|
});
|
|
688
752
|
});
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* chant #2249 — `lintCommand` resolves the project's own declared
|
|
756
|
+
* `buildParams` when its caller supplied none, so an in-process lint agrees
|
|
757
|
+
* with `chant lint` on the command line.
|
|
758
|
+
*
|
|
759
|
+
* #2251 taught the CLI handler (../handlers/lint.ts) to resolve
|
|
760
|
+
* `--param`/`--params-file`/declared defaults and pass them down, because
|
|
761
|
+
* the OPS* checks import every `*.op.ts` file and an Op reading
|
|
762
|
+
* `params.<name>` evaluates that read at module load. Every other caller of
|
|
763
|
+
* `lintCommand` has no flags to read and passed nothing, so `params` stayed
|
|
764
|
+
* empty and OPS012 reported the activity contract violated for source that
|
|
765
|
+
* lints clean from a shell. That is how the root-examples gate
|
|
766
|
+
* (examples/root-examples-gate.test.ts) went red on github-pr-preview while
|
|
767
|
+
* `chant lint .` in the same directory exited 0, and it applied equally to
|
|
768
|
+
* `handleLint` over MCP and to test-utils' example harness.
|
|
769
|
+
*
|
|
770
|
+
* These drive the real `lintCommand` against a temp project holding one
|
|
771
|
+
* `*.op.ts` file (enough for the OPS* pass to run its parameter binding) and
|
|
772
|
+
* read the shared `params` object the binding populates.
|
|
773
|
+
*/
|
|
774
|
+
describe("lintCommand build-time parameters (#2249)", () => {
|
|
775
|
+
let testDir: string;
|
|
776
|
+
|
|
777
|
+
beforeEach(async () => {
|
|
778
|
+
testDir = join(tmpdir(), `chant-lint-params-${Date.now()}-${Math.random()}`);
|
|
779
|
+
await mkdir(testDir, { recursive: true });
|
|
780
|
+
// The OPS* pass binds parameters only when there is an Op file to import.
|
|
781
|
+
// A plain default export is enough: it is skipped as not-an-Op after the
|
|
782
|
+
// binding has already happened.
|
|
783
|
+
await writeFile(join(testDir, "noop.op.ts"), `export default { props: {} };\n`);
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
afterEach(async () => {
|
|
787
|
+
await rm(testDir, { recursive: true, force: true });
|
|
788
|
+
delete process.env.CHANT_TEST_ENV_2249;
|
|
789
|
+
setBuildParams({});
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
test("a declared default reaches params with no caller-supplied provenance", async () => {
|
|
793
|
+
await writeFile(
|
|
794
|
+
join(testDir, "chant.config.json"),
|
|
795
|
+
JSON.stringify({ buildParams: { env: { type: "string", default: "local" } } }),
|
|
796
|
+
);
|
|
797
|
+
|
|
798
|
+
await lintCommand({ path: testDir, format: "stylish" });
|
|
799
|
+
|
|
800
|
+
expect(params.env).toBe("local");
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
test("a declared env mapping reaches params the same way chant lint resolves it", async () => {
|
|
804
|
+
await writeFile(
|
|
805
|
+
join(testDir, "chant.config.json"),
|
|
806
|
+
JSON.stringify({
|
|
807
|
+
buildParams: { env: { type: "string", default: "local", env: "CHANT_TEST_ENV_2249" } },
|
|
808
|
+
}),
|
|
809
|
+
);
|
|
810
|
+
process.env.CHANT_TEST_ENV_2249 = "pr-42";
|
|
811
|
+
|
|
812
|
+
await lintCommand({ path: testDir, format: "stylish" });
|
|
813
|
+
|
|
814
|
+
expect(params.env).toBe("pr-42");
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
test("caller-supplied parameters win over the declared defaults", async () => {
|
|
818
|
+
await writeFile(
|
|
819
|
+
join(testDir, "chant.config.json"),
|
|
820
|
+
JSON.stringify({ buildParams: { env: { type: "string", default: "local" } } }),
|
|
821
|
+
);
|
|
822
|
+
|
|
823
|
+
await lintCommand({
|
|
824
|
+
path: testDir,
|
|
825
|
+
format: "stylish",
|
|
826
|
+
buildParams: [{ name: "env", value: "pr-42", source: "cli" }],
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
expect(params.env).toBe("pr-42");
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
test("a project declaring none binds an empty parameter set", async () => {
|
|
833
|
+
await writeFile(join(testDir, "chant.config.json"), JSON.stringify({}));
|
|
834
|
+
|
|
835
|
+
await lintCommand({ path: testDir, format: "stylish" });
|
|
836
|
+
|
|
837
|
+
expect(params).toEqual({});
|
|
838
|
+
});
|
|
839
|
+
});
|
package/src/cli/commands/lint.ts
CHANGED
|
@@ -20,6 +20,8 @@ import { rule } from "../../lint/declarative";
|
|
|
20
20
|
import { watchDirectory, formatTimestamp, formatChangedFiles } from "../watch";
|
|
21
21
|
import { formatError, formatInfo } from "../format";
|
|
22
22
|
import { GENERATED_MARKER } from "../../discovery/files";
|
|
23
|
+
import { buildParamValues, resolveBuildParams } from "../../build-params";
|
|
24
|
+
import { setBuildParams } from "../../params";
|
|
23
25
|
import { isNoLexiconDetected } from "../../detectLexicon";
|
|
24
26
|
|
|
25
27
|
// Import config loader
|
|
@@ -535,11 +537,21 @@ async function runComponentCheckDiagnostics(
|
|
|
535
537
|
async function runOpCheckDiagnostics(
|
|
536
538
|
infraPath: string,
|
|
537
539
|
files: string[],
|
|
540
|
+
buildParams?: BuildParamProvenance[],
|
|
538
541
|
): Promise<{ diagnostics: LintDiagnostic[]; suppressed: Array<LintDiagnostic & { reason?: string }> }> {
|
|
539
542
|
const config = loadConfig(findProjectRoot(infraPath));
|
|
540
543
|
const opFiles = files.filter((f) => f.endsWith(".op.ts"));
|
|
541
544
|
if (opFiles.length === 0) return { diagnostics: [], suppressed: [] };
|
|
542
545
|
|
|
546
|
+
// chant #2251 — the same step `discover()` runs before it imports or folds a
|
|
547
|
+
// project file (../../discovery/index.ts): populate the shared build-time
|
|
548
|
+
// parameters object BEFORE the imports below, so an Op that takes a step
|
|
549
|
+
// argument from `params.<name>` reads the value this invocation resolved
|
|
550
|
+
// rather than `undefined`. Unconditional, so a stale value from a prior
|
|
551
|
+
// lint in the same process (a test, `--watch`) never leaks into one that
|
|
552
|
+
// resolved none.
|
|
553
|
+
setBuildParams(buildParamValues(buildParams ?? []));
|
|
554
|
+
|
|
543
555
|
const entities = new Map<string, unknown>();
|
|
544
556
|
const fileByOpName = new Map<string, string>();
|
|
545
557
|
for (const filePath of opFiles) {
|
|
@@ -640,10 +652,31 @@ export async function lintCommand(options: LintOptions): Promise<LintResult> {
|
|
|
640
652
|
// than a failure — `loadOkfBundle` already treats a missing directory as
|
|
641
653
|
// an empty bundle.
|
|
642
654
|
let knowledgeBundle: OkfBundle | undefined;
|
|
655
|
+
/**
|
|
656
|
+
* chant #2249 — this invocation's build-time parameters, falling back to
|
|
657
|
+
* the project's own declared `buildParams` when the caller passed none.
|
|
658
|
+
*
|
|
659
|
+
* `chant lint` resolves them in ./handlers/lint.ts (#2251) because only
|
|
660
|
+
* the CLI knows about `--param`/`--params-file`, and passes them here. An
|
|
661
|
+
* in-process caller has no such flags to read, and before this fell
|
|
662
|
+
* through to the empty parameter object: the OPS* checks import each
|
|
663
|
+
* `*.op.ts`, an Op taking a step argument from `params.<name>` read
|
|
664
|
+
* `undefined` at module load, and OPS012 reported the activity contract
|
|
665
|
+
* violated for source `chant lint` on the command line passes. That made
|
|
666
|
+
* `lintCommand` disagree with its own CLI (the root-examples gate,
|
|
667
|
+
* `handleLint` over MCP, test-utils' example harness). Resolving the
|
|
668
|
+
* declared defaults + `env` mappings here is what the command line
|
|
669
|
+
* already gets; anything a flag would override still arrives in
|
|
670
|
+
* `options.buildParams` and wins. Best-effort like `projectConfig`:
|
|
671
|
+
* `resolveBuildParams` collects failures per parameter without throwing,
|
|
672
|
+
* and the ones that did resolve are still better than none.
|
|
673
|
+
*/
|
|
674
|
+
let buildParams = options.buildParams;
|
|
643
675
|
try {
|
|
644
676
|
const chantConfig = (await loadChantConfig(projectRoot)).config;
|
|
645
677
|
projectConfig = chantConfig as LintProjectConfig;
|
|
646
678
|
knowledgeBundle = await loadOkfBundle(resolveKnowledgeDir(chantConfig, projectRoot));
|
|
679
|
+
buildParams ??= resolveBuildParams(chantConfig.buildParams, { env: process.env }).provenance;
|
|
647
680
|
} catch {
|
|
648
681
|
projectConfig = undefined;
|
|
649
682
|
knowledgeBundle = undefined;
|
|
@@ -696,14 +729,14 @@ export async function lintCommand(options: LintOptions): Promise<LintResult> {
|
|
|
696
729
|
// structurally distinct check family (whole-project, post-discovery,
|
|
697
730
|
// see ../../lint/component-checks.ts) but the same `chant lint` output and
|
|
698
731
|
// the same error-severity gating as every COR/EVL diagnostic.
|
|
699
|
-
const componentResult = await runComponentCheckDiagnostics(infraPath, options.sandbox,
|
|
732
|
+
const componentResult = await runComponentCheckDiagnostics(infraPath, options.sandbox, buildParams);
|
|
700
733
|
diagnostics.push(...componentResult.diagnostics);
|
|
701
734
|
suppressed.push(...componentResult.suppressed);
|
|
702
735
|
|
|
703
736
|
// Run the OPS* Op-model post-synth checks (#2122) over every `*.op.ts`
|
|
704
737
|
// file under the lint target — see runOpCheckDiagnostics's doc for why
|
|
705
738
|
// this needs no lexicon or build to fire.
|
|
706
|
-
const opResult = await runOpCheckDiagnostics(infraPath, files);
|
|
739
|
+
const opResult = await runOpCheckDiagnostics(infraPath, files, buildParams);
|
|
707
740
|
diagnostics.push(...opResult.diagnostics);
|
|
708
741
|
suppressed.push(...opResult.suppressed);
|
|
709
742
|
|
|
@@ -751,13 +784,13 @@ export async function lintCommand(options: LintOptions): Promise<LintResult> {
|
|
|
751
784
|
// `*.component.ts` file on their behalf), but a fix applied to another
|
|
752
785
|
// rule could still be in the same file a component was discovered from —
|
|
753
786
|
// re-run for consistency with the AST re-lint above.
|
|
754
|
-
const postComponentResult = await runComponentCheckDiagnostics(infraPath, options.sandbox,
|
|
787
|
+
const postComponentResult = await runComponentCheckDiagnostics(infraPath, options.sandbox, buildParams);
|
|
755
788
|
diagnostics.push(...postComponentResult.diagnostics);
|
|
756
789
|
suppressed.push(...postComponentResult.suppressed);
|
|
757
790
|
|
|
758
791
|
// OPS* checks have no `.fix` either; re-run for the same consistency
|
|
759
792
|
// reason as the COMP* re-run just above.
|
|
760
|
-
const postOpResult = await runOpCheckDiagnostics(infraPath, files);
|
|
793
|
+
const postOpResult = await runOpCheckDiagnostics(infraPath, files, buildParams);
|
|
761
794
|
diagnostics.push(...postOpResult.diagnostics);
|
|
762
795
|
suppressed.push(...postOpResult.suppressed);
|
|
763
796
|
}
|
|
@@ -591,7 +591,7 @@ export async function runComponentsStatus(ctx: CommandContext): Promise<number>
|
|
|
591
591
|
// rather than "stale" (recorded, and nothing live).
|
|
592
592
|
const message = err instanceof Error ? err.message : String(err);
|
|
593
593
|
console.error(formatWarning({ message: `${plugin.name}: describeResources failed — ${message} (components in this lexicon report unknown, not stale)` }));
|
|
594
|
-
observed = { resources: {}, unobserved: unobservedAll(declared, "read-failed", message, entities), queried: {}, notes: [] };
|
|
594
|
+
observed = { resources: {}, unobserved: unobservedAll(declared, "read-failed", message, entities), queried: {}, sources: {}, notes: [] };
|
|
595
595
|
}
|
|
596
596
|
const cs = buildChangeSet(environment, {
|
|
597
597
|
declared,
|
|
@@ -710,10 +710,10 @@ describe("runGraph", () => {
|
|
|
710
710
|
});
|
|
711
711
|
});
|
|
712
712
|
|
|
713
|
-
// The bug this branch fixes (#57): a multi-stack, per-component project
|
|
714
|
-
//
|
|
715
|
-
//
|
|
716
|
-
//
|
|
713
|
+
// The bug this branch fixes (#57): a multi-stack, per-component project has
|
|
714
|
+
// no stack literally named after the environment, so the live graph must
|
|
715
|
+
// resolve each component's own `cfn-deploy` stack(s) and pass them through
|
|
716
|
+
// to `observeResources` for the per-stack union.
|
|
717
717
|
test("multi-stack component project: resolves each component's cfn-deploy stack(s) and passes them to observeResources", async () => {
|
|
718
718
|
resolveLexMock.mockResolvedValue(["aws"]);
|
|
719
719
|
loadPluginsMock.mockResolvedValue([
|
|
@@ -253,17 +253,17 @@ async function runGraphLive(
|
|
|
253
253
|
return 1;
|
|
254
254
|
}
|
|
255
255
|
|
|
256
|
-
// Multi-stack, per-component projects (
|
|
257
|
-
//
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
//
|
|
261
|
-
//
|
|
262
|
-
//
|
|
263
|
-
//
|
|
264
|
-
//
|
|
265
|
-
//
|
|
266
|
-
//
|
|
256
|
+
// Multi-stack, per-component projects (#57): AWS's single-stack convention
|
|
257
|
+
// (`describeResources` defaults to a stack named after the environment,
|
|
258
|
+
// lexicons/aws/src/plugin.ts) never matches a per-component layout (e.g.
|
|
259
|
+
// `loom-local-a-<component>`), so the plain single call always observes zero
|
|
260
|
+
// nodes. Resolve every discovered component's `cfn-deploy` stack(s) — the
|
|
261
|
+
// same walk `chant components status --live` uses (`cfnDeployStacks`,
|
|
262
|
+
// ./components.ts) — and hand them to `observeResources`, which queries
|
|
263
|
+
// `describeResources` once per stack and unions the results. A project with
|
|
264
|
+
// no components (or whose discovery errors) yields no stacks, so
|
|
265
|
+
// `observeResources` falls back to its original single-stack call —
|
|
266
|
+
// unchanged.
|
|
267
267
|
const componentsDiscovery = await discoverComponents(resolve(args.src ?? config.sourceDir ?? "."), {
|
|
268
268
|
sandbox: args.sandbox,
|
|
269
269
|
});
|