@intentius/chant 0.54.1 → 0.54.3
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/audit/secrets.d.ts +2 -5
- package/dist/audit/secrets.d.ts.map +1 -1
- package/dist/lifecycle/status.d.ts +15 -1
- package/dist/lifecycle/status.d.ts.map +1 -1
- package/dist/op/discover.d.ts +4 -1
- package/dist/op/discover.d.ts.map +1 -1
- package/dist/terraform/manifest.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/audit/secrets.test.ts +15 -0
- package/src/audit/secrets.ts +33 -0
- package/src/cli/commands/carve-apply.test.ts +31 -1
- package/src/lifecycle/status.test.ts +38 -0
- package/src/lifecycle/status.ts +17 -2
- package/src/op/discover-root.test.ts +68 -0
- package/src/op/discover.ts +33 -4
- package/src/terraform/manifest.ts +12 -4
package/dist/audit/secrets.d.ts
CHANGED
|
@@ -86,10 +86,7 @@ export declare const ENTROPY_RULE_ID = "SEC010";
|
|
|
86
86
|
* trigger it.
|
|
87
87
|
*/
|
|
88
88
|
export declare function looksLikeIdentifierSlug(value: string): boolean;
|
|
89
|
-
/**
|
|
90
|
-
|
|
91
|
-
* the filesystem and network. Findings never carry the matched value —
|
|
92
|
-
* only its rule id, kind, file/line, and a redaction-safe fingerprint.
|
|
93
|
-
*/
|
|
89
|
+
/** Is this a dependency lockfile the secrets scan should skip? */
|
|
90
|
+
export declare function isLockfilePath(path: string): boolean;
|
|
94
91
|
export declare function scanForSecrets(files: ScannableFile[], opts?: SecretsScanOptions): AuditFinding[];
|
|
95
92
|
//# sourceMappingURL=secrets.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secrets.d.ts","sourceRoot":"","sources":["../../src/audit/secrets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAG3C,qFAAqF;AACrF,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,eAAe;IAC9B,gEAAgE;IAChE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,0EAA0E;IAC1E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,sEAAsE;IACtE,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,kBAAkB;IACjC,0GAA0G;IAC1G,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,kFAAkF;IAClF,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,yFAAyF;IACzF,KAAK,CAAC,EAAE,eAAe,EAAE,CAAC;CAC3B;AAOD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAYvD;AAkDD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,kBAAkB,CAuBtE;AA2GD,2FAA2F;AAC3F,eAAO,MAAM,eAAe,WAAW,CAAC;AAIxC;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAK9D;
|
|
1
|
+
{"version":3,"file":"secrets.d.ts","sourceRoot":"","sources":["../../src/audit/secrets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAG3C,qFAAqF;AACrF,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,eAAe;IAC9B,gEAAgE;IAChE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,0EAA0E;IAC1E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,sEAAsE;IACtE,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,kBAAkB;IACjC,0GAA0G;IAC1G,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,kFAAkF;IAClF,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,yFAAyF;IACzF,KAAK,CAAC,EAAE,eAAe,EAAE,CAAC;CAC3B;AAOD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAYvD;AAkDD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,kBAAkB,CAuBtE;AA2GD,2FAA2F;AAC3F,eAAO,MAAM,eAAe,WAAW,CAAC;AAIxC;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAK9D;AAqED,kEAAkE;AAClE,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAGpD;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,aAAa,EAAE,EAAE,IAAI,GAAE,kBAAuB,GAAG,YAAY,EAAE,CAmEpG"}
|
|
@@ -275,8 +275,22 @@ export interface CrossEnvComparison {
|
|
|
275
275
|
envB: string;
|
|
276
276
|
digestA?: string;
|
|
277
277
|
digestB?: string;
|
|
278
|
-
/**
|
|
278
|
+
/**
|
|
279
|
+
* True only when both envs have a recorded digest and their *comparable*
|
|
280
|
+
* identities match. For a record carrying `inputDigest` (a pinned helm
|
|
281
|
+
* deploy, chant#1242/#1243), that field is the comparable identity —
|
|
282
|
+
* `ReleaseRecord.inputDigest`'s own contract: profiles are per cluster, two
|
|
283
|
+
* environments legitimately render to different bytes, so cross-environment
|
|
284
|
+
* "is prod running what staging tested" joins on the input side while
|
|
285
|
+
* `digest` proves the exact bytes each cluster got. Records without it
|
|
286
|
+
* compare on `digest` exactly as before.
|
|
287
|
+
*/
|
|
279
288
|
same: boolean;
|
|
289
|
+
/** The identities `same` was decided on (`inputDigest ?? digest` per side), when both sides had one and they differ from the digests shown. */
|
|
290
|
+
comparedOn?: {
|
|
291
|
+
a: string;
|
|
292
|
+
b: string;
|
|
293
|
+
};
|
|
280
294
|
}
|
|
281
295
|
/** Compare the latest recorded digest for `component` between two environments' release records. */
|
|
282
296
|
export declare function compareAcrossEnvironments(component: string, envA: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/lifecycle/status.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/lifecycle/status.ts"],"names":[],"mappings":"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"}
|
package/dist/op/discover.d.ts
CHANGED
|
@@ -8,7 +8,10 @@ export interface OpDiscoveryResult {
|
|
|
8
8
|
errors: string[];
|
|
9
9
|
}
|
|
10
10
|
/**
|
|
11
|
-
* Discover all Op definitions from
|
|
11
|
+
* Discover all Op definitions from `*.op.ts` files under the nearest chant
|
|
12
|
+
* project root (the directory holding `chant.config.ts`/`.json`, walking up
|
|
13
|
+
* from `cwd`), or under the git root when no config exists — see
|
|
14
|
+
* {@link findDiscoveryRoot} (#2058).
|
|
12
15
|
*/
|
|
13
16
|
export declare function discoverOps(opts?: {
|
|
14
17
|
cwd?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"discover.d.ts","sourceRoot":"","sources":["../../src/op/discover.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"discover.d.ts","sourceRoot":"","sources":["../../src/op/discover.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAExC,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,QAAQ,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,iBAAiB;IAChC,GAAG,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAC/B,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AA0DD;;;;;GAKG;AACH,wBAAsB,WAAW,CAAC,IAAI,CAAC,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAuCrF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../src/terraform/manifest.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC3C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAEpD,eAAO,MAAM,qBAAqB,gBAAgB,CAAC;AAEnD;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,IAAI,CAAC;AAExC,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,CAAC,GAAG,CAAC,CAAC;IACf,6EAA6E;IAC7E,MAAM,EAAE,MAAM,CAAC;IACf,2CAA2C;IAC3C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iEAAiE;IACjE,IAAI,EAAE,MAAM,CAAC;IACb,qEAAqE;IACrE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uEAAuE;IACvE,QAAQ,EAAE,WAAW,CAAC;IACtB,gCAAgC;IAChC,IAAI,CAAC,EAAE;QACL,MAAM,EAAE,SAAS,GAAG,MAAM,CAAC;QAC3B;;;;;;WAMG;QACH,KAAK,EAAE,MAAM,EAAE,CAAC;QAChB;;;WAGG;QACH,MAAM,CAAC,EAAE,MAAM,CACb,MAAM,EACN;YACE,8EAA8E;YAC9E,MAAM,EAAE,MAAM,CAAC;YACf,mEAAmE;YACnE,QAAQ,EAAE,MAAM,CAAC;YACjB,kCAAkC;YAClC,KAAK,EAAE,MAAM,EAAE,CAAC;YAChB,2CAA2C;YAC3C,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;SACrC,CACF,CAAC;QACF,EAAE,EAAE,MAAM,CAAC;KACZ,CAAC;IACF,kCAAkC;IAClC,MAAM,CAAC,EAAE;QACP,6FAA6F;QAC7F,OAAO,EAAE,MAAM,EAAE,CAAC;QAClB,cAAc,EAAE,OAAO,CAAC;QACxB,2IAA2I;QAC3I,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,yEAAyE;QACzE,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;QACnB,EAAE,EAAE,MAAM,CAAC;KACZ,CAAC;IACF,iCAAiC;IACjC,KAAK,CAAC,EAAE;QACN,MAAM,EAAE,eAAe,CAAC;QACxB,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACtC,4IAA4I;QAC5I,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;QACxB,EAAE,EAAE,MAAM,CAAC;KACZ,CAAC;CACH;AAED,oFAAoF;AACpF,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,kEAAkE;AAClE,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAExE;
|
|
1
|
+
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../src/terraform/manifest.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -300,4 +300,19 @@ describe("scanForSecrets — file handling", () => {
|
|
|
300
300
|
expect(findings[0].severity).toBe("error");
|
|
301
301
|
expect(findings[0].lexicon).toBe("secrets");
|
|
302
302
|
});
|
|
303
|
+
|
|
304
|
+
test("dependency lockfiles are skipped entirely — integrity hashes are not secrets", () => {
|
|
305
|
+
// The exact false-positive class that failed the first dogfooding repo:
|
|
306
|
+
// one lockfile's sha512 integrity fields read as hundreds of SEC010s.
|
|
307
|
+
const integrity = `"integrity": "sha512-${"a1B2c3D4e5F6g7H8i9J0k1L2m3N4o5P6q7R8s9T0u1V2w3X4y5Z6".repeat(2)}"`;
|
|
308
|
+
for (const path of ["package-lock.json", "web/package-lock.json", "yarn.lock", "pnpm-lock.yaml", "Cargo.lock", "go.sum"]) {
|
|
309
|
+
expect(scanForSecrets([{ path, content: `${integrity}\n` }]), path).toEqual([]);
|
|
310
|
+
}
|
|
311
|
+
// Even a real credential shape stays silent there — the file is skipped,
|
|
312
|
+
// not entropy-tuned; a credential does not live in a machine-written
|
|
313
|
+
// lockfile, and per-file skipping is what keeps the rule explainable.
|
|
314
|
+
expect(scanForSecrets([{ path: "package-lock.json", content: `${FAKE_AWS_KEY}\n` }])).toEqual([]);
|
|
315
|
+
// A non-lockfile with the same content still fires.
|
|
316
|
+
expect(scanForSecrets([{ path: "config.json", content: `${integrity}\n` }]).length).toBeGreaterThan(0);
|
|
317
|
+
});
|
|
303
318
|
});
|
package/src/audit/secrets.ts
CHANGED
|
@@ -337,6 +337,38 @@ function lineNumberFor(starts: number[], index: number): number {
|
|
|
337
337
|
* the filesystem and network. Findings never carry the matched value —
|
|
338
338
|
* only its rule id, kind, file/line, and a redaction-safe fingerprint.
|
|
339
339
|
*/
|
|
340
|
+
/**
|
|
341
|
+
* Machine-generated dependency lockfiles. Skipped entirely: they are wall-to-
|
|
342
|
+
* wall content-integrity hashes (`sha512-…` and friends), which the entropy
|
|
343
|
+
* heuristic reads as hundreds of "possible secrets" per file — a single
|
|
344
|
+
* `package-lock.json` produced 438 merge-worthy SEC010 findings on the first
|
|
345
|
+
* repo that dogfooded this at `--fail-on merge-worthy`. Integrity hashes are
|
|
346
|
+
* public by construction, a credential does not live in a lockfile, and no
|
|
347
|
+
* other scanner reads them.
|
|
348
|
+
*/
|
|
349
|
+
const LOCKFILE_NAMES = new Set([
|
|
350
|
+
"package-lock.json",
|
|
351
|
+
"npm-shrinkwrap.json",
|
|
352
|
+
"yarn.lock",
|
|
353
|
+
"pnpm-lock.yaml",
|
|
354
|
+
"bun.lock",
|
|
355
|
+
"deno.lock",
|
|
356
|
+
"Cargo.lock",
|
|
357
|
+
"poetry.lock",
|
|
358
|
+
"uv.lock",
|
|
359
|
+
"Pipfile.lock",
|
|
360
|
+
"composer.lock",
|
|
361
|
+
"Gemfile.lock",
|
|
362
|
+
"go.sum",
|
|
363
|
+
"flake.lock",
|
|
364
|
+
]);
|
|
365
|
+
|
|
366
|
+
/** Is this a dependency lockfile the secrets scan should skip? */
|
|
367
|
+
export function isLockfilePath(path: string): boolean {
|
|
368
|
+
const name = path.split("/").pop() ?? path;
|
|
369
|
+
return LOCKFILE_NAMES.has(name);
|
|
370
|
+
}
|
|
371
|
+
|
|
340
372
|
export function scanForSecrets(files: ScannableFile[], opts: SecretsScanOptions = {}): AuditFinding[] {
|
|
341
373
|
const entropyThreshold = opts.entropyThreshold ?? DEFAULT_ENTROPY_THRESHOLD;
|
|
342
374
|
const entropyMinLength = opts.entropyMinLength ?? DEFAULT_ENTROPY_MIN_LENGTH;
|
|
@@ -347,6 +379,7 @@ export function scanForSecrets(files: ScannableFile[], opts: SecretsScanOptions
|
|
|
347
379
|
|
|
348
380
|
for (const file of files) {
|
|
349
381
|
if (!file.content) continue;
|
|
382
|
+
if (isLockfilePath(file.path)) continue;
|
|
350
383
|
const lines = file.content.split("\n");
|
|
351
384
|
const starts = lineStarts(file.content);
|
|
352
385
|
const claimed: Array<[number, number]> = [];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, test, expect } from "vitest";
|
|
2
|
-
import { mkdtempSync, writeFileSync, rmSync, readFileSync, existsSync } from "fs";
|
|
2
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, readFileSync, existsSync } from "fs";
|
|
3
3
|
import { tmpdir } from "os";
|
|
4
4
|
import { join } from "path";
|
|
5
5
|
import { carveApply, formatCarveApply } from "./carve-apply";
|
|
@@ -108,6 +108,36 @@ describe("carveApply", () => {
|
|
|
108
108
|
});
|
|
109
109
|
});
|
|
110
110
|
|
|
111
|
+
test("a RELATIVE --output still records stampedFiles relative to the manifest's directory, not the cwd (#2059)", async () => {
|
|
112
|
+
if (!parserAvailable) return;
|
|
113
|
+
await withEstate(async (dir) => {
|
|
114
|
+
// The behold carve-demo shape: run FROM the project dir with
|
|
115
|
+
// `--output carveout`. The bug: resolveManifestFilePath joined against
|
|
116
|
+
// the relative outDir, producing a cwd-relative path ("carveout/…")
|
|
117
|
+
// that normalize-on-write passed through — two spellings in one manifest.
|
|
118
|
+
const out = join(dir, "carveout");
|
|
119
|
+
const manifest = manifestFor("aws_s3_bucket.assets", dir);
|
|
120
|
+
manifest.emit = { source: "tfstate", files: [join("src", "assets.ts")], at: "t" };
|
|
121
|
+
writeCarveManifest(out, manifest);
|
|
122
|
+
mkdirSync(join(out, "src"), { recursive: true });
|
|
123
|
+
writeFileSync(join(out, "src", "assets.ts"), 'export const assets = new Bucket({\n BucketName: "b",\n});\n');
|
|
124
|
+
|
|
125
|
+
const prevCwd = process.cwd();
|
|
126
|
+
process.chdir(dir);
|
|
127
|
+
try {
|
|
128
|
+
const res = await carveApply({ from: ".", output: "carveout", env: "prod", stack: "assets", writeSource: true });
|
|
129
|
+
expect(res.ok).toBe(true);
|
|
130
|
+
const m = readCarveManifest(res.manifestPath!)!;
|
|
131
|
+
// One spelling: manifest-dir-relative, same as emit.files — never
|
|
132
|
+
// "carveout/src/assets.ts" (relative to wherever the command ran).
|
|
133
|
+
expect(m.apply!.stampedFiles).toEqual([join("src", "assets.ts")]);
|
|
134
|
+
expect(m.emit!.files).toEqual([join("src", "assets.ts")]);
|
|
135
|
+
} finally {
|
|
136
|
+
process.chdir(prevCwd);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
111
141
|
test("--write-source without an emit record in the manifest is a clear error", async () => {
|
|
112
142
|
if (!parserAvailable) return;
|
|
113
143
|
await withEstate(async (dir) => {
|
|
@@ -413,6 +413,44 @@ describe("status", () => {
|
|
|
413
413
|
expect(result.digestB).toBe("sha256:abc");
|
|
414
414
|
expect(result.same).toBe(false);
|
|
415
415
|
});
|
|
416
|
+
|
|
417
|
+
test("pinned helm records compare on inputDigest — per-cluster bytes legitimately differ (ReleaseRecord.inputDigest's own contract)", () => {
|
|
418
|
+
// Same chart+values rendered against two clusters' capability profiles:
|
|
419
|
+
// contentDigests (recorded as `digest`) differ, inputDigests match —
|
|
420
|
+
// prod IS running what staging tested.
|
|
421
|
+
const result = compareAcrossEnvironments(
|
|
422
|
+
"web",
|
|
423
|
+
{ name: "staging", records: [record({ component: "web", env: "staging", digest: "sha256:staging-bytes", inputDigest: "sha256:inputs" })] },
|
|
424
|
+
{ name: "prod", records: [record({ component: "web", env: "prod", digest: "sha256:prod-bytes", inputDigest: "sha256:inputs" })] },
|
|
425
|
+
);
|
|
426
|
+
expect(result.same).toBe(true);
|
|
427
|
+
expect(result.comparedOn).toEqual({ a: "sha256:inputs", b: "sha256:inputs" });
|
|
428
|
+
// The exact bytes each cluster got stay visible.
|
|
429
|
+
expect(result.digestA).toBe("sha256:staging-bytes");
|
|
430
|
+
expect(result.digestB).toBe("sha256:prod-bytes");
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
test("differing inputDigests are a real difference, whatever the bytes say", () => {
|
|
434
|
+
const result = compareAcrossEnvironments(
|
|
435
|
+
"web",
|
|
436
|
+
{ name: "staging", records: [record({ component: "web", env: "staging", digest: "sha256:same-bytes", inputDigest: "sha256:inputs-v2" })] },
|
|
437
|
+
{ name: "prod", records: [record({ component: "web", env: "prod", digest: "sha256:same-bytes", inputDigest: "sha256:inputs-v1" })] },
|
|
438
|
+
);
|
|
439
|
+
expect(result.same).toBe(false);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
test("one side input-identified, the other content-identified: compared honestly on what each carries", () => {
|
|
443
|
+
// Mixed deploys (a pinned helm release in prod, an unpinned one in
|
|
444
|
+
// staging where the record's digest IS the input digest): staging's
|
|
445
|
+
// digest against prod's inputDigest is the only meaningful join.
|
|
446
|
+
const result = compareAcrossEnvironments(
|
|
447
|
+
"web",
|
|
448
|
+
{ name: "staging", records: [record({ component: "web", env: "staging", digest: "sha256:inputs" })] },
|
|
449
|
+
{ name: "prod", records: [record({ component: "web", env: "prod", digest: "sha256:prod-bytes", inputDigest: "sha256:inputs" })] },
|
|
450
|
+
);
|
|
451
|
+
expect(result.same).toBe(true);
|
|
452
|
+
expect(result.comparedOn).toEqual({ a: "sha256:inputs", b: "sha256:inputs" });
|
|
453
|
+
});
|
|
416
454
|
});
|
|
417
455
|
|
|
418
456
|
describe("mergeLiveEvidence (#57 — per-component stack presence overlay)", () => {
|
package/src/lifecycle/status.ts
CHANGED
|
@@ -478,8 +478,19 @@ export interface CrossEnvComparison {
|
|
|
478
478
|
envB: string;
|
|
479
479
|
digestA?: string;
|
|
480
480
|
digestB?: string;
|
|
481
|
-
/**
|
|
481
|
+
/**
|
|
482
|
+
* True only when both envs have a recorded digest and their *comparable*
|
|
483
|
+
* identities match. For a record carrying `inputDigest` (a pinned helm
|
|
484
|
+
* deploy, chant#1242/#1243), that field is the comparable identity —
|
|
485
|
+
* `ReleaseRecord.inputDigest`'s own contract: profiles are per cluster, two
|
|
486
|
+
* environments legitimately render to different bytes, so cross-environment
|
|
487
|
+
* "is prod running what staging tested" joins on the input side while
|
|
488
|
+
* `digest` proves the exact bytes each cluster got. Records without it
|
|
489
|
+
* compare on `digest` exactly as before.
|
|
490
|
+
*/
|
|
482
491
|
same: boolean;
|
|
492
|
+
/** The identities `same` was decided on (`inputDigest ?? digest` per side), when both sides had one and they differ from the digests shown. */
|
|
493
|
+
comparedOn?: { a: string; b: string };
|
|
483
494
|
}
|
|
484
495
|
|
|
485
496
|
/** Compare the latest recorded digest for `component` between two environments' release records. */
|
|
@@ -490,12 +501,16 @@ export function compareAcrossEnvironments(
|
|
|
490
501
|
): CrossEnvComparison {
|
|
491
502
|
const latestA = latestPerComponent(envA.records).get(component);
|
|
492
503
|
const latestB = latestPerComponent(envB.records).get(component);
|
|
504
|
+
const idA = latestA ? latestA.inputDigest ?? latestA.digest : undefined;
|
|
505
|
+
const idB = latestB ? latestB.inputDigest ?? latestB.digest : undefined;
|
|
506
|
+
const comparedOnInputs = (latestA?.inputDigest ?? latestB?.inputDigest) !== undefined;
|
|
493
507
|
return {
|
|
494
508
|
component,
|
|
495
509
|
envA: envA.name,
|
|
496
510
|
envB: envB.name,
|
|
497
511
|
digestA: latestA?.digest,
|
|
498
512
|
digestB: latestB?.digest,
|
|
499
|
-
same: !!
|
|
513
|
+
same: !!idA && !!idB && idA === idB,
|
|
514
|
+
...(comparedOnInputs && idA && idB ? { comparedOn: { a: idA, b: idB } } : {}),
|
|
500
515
|
};
|
|
501
516
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { describe, test, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, realpathSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { discoverOps } from "./discover";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* #2058 — Op discovery scoped to the project root, the way entity discovery
|
|
9
|
+
* is. The repro: a chant project nested in a larger checkout (behold's
|
|
10
|
+
* committed `example-carve/app` beside `example-writes`, `example-k8s`)
|
|
11
|
+
* discovered every sibling project's `*.op.ts` as its own, because the scan
|
|
12
|
+
* root was the git root unconditionally.
|
|
13
|
+
*
|
|
14
|
+
* The fake git root is a temp tree; the runtime mock answers `git rev-parse`
|
|
15
|
+
* with whatever the current test set (`realpath`ed, since macOS's tmpdir is a
|
|
16
|
+
* symlink and the walk compares resolved paths).
|
|
17
|
+
*/
|
|
18
|
+
let fakeGitRoot = "";
|
|
19
|
+
vi.mock("../runtime-adapter", () => ({
|
|
20
|
+
getRuntime: () => ({
|
|
21
|
+
spawn: async (cmd: string[]) =>
|
|
22
|
+
cmd[0] === "git" && cmd[1] === "rev-parse"
|
|
23
|
+
? { stdout: fakeGitRoot, stderr: "", exitCode: 0 }
|
|
24
|
+
: { stdout: "", stderr: "", exitCode: 0 },
|
|
25
|
+
}),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
/** A minimal default export shaped the way discovery validates (entity.props = OpConfig). */
|
|
29
|
+
const OP_FILE = (name: string): string =>
|
|
30
|
+
`export default { props: { name: ${JSON.stringify(name)}, overview: "t", phases: [{ name: "Run", steps: [] }] } };\n`;
|
|
31
|
+
|
|
32
|
+
describe("discoverOps — the scan root is the project, not the checkout (#2058)", () => {
|
|
33
|
+
let checkout: string;
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
checkout = realpathSync(mkdtempSync(join(tmpdir(), "chant-op-root-")));
|
|
36
|
+
fakeGitRoot = checkout;
|
|
37
|
+
// The monorepo shape from the issue: two sibling chant projects, each
|
|
38
|
+
// with its own config and ops, nested in one git checkout.
|
|
39
|
+
mkdirSync(join(checkout, "app", "ops"), { recursive: true });
|
|
40
|
+
writeFileSync(join(checkout, "app", "chant.config.json"), JSON.stringify({ lexicons: ["aws"] }));
|
|
41
|
+
writeFileSync(join(checkout, "app", "ops", "mine.op.ts"), OP_FILE("mine"));
|
|
42
|
+
mkdirSync(join(checkout, "sibling", "ops"), { recursive: true });
|
|
43
|
+
writeFileSync(join(checkout, "sibling", "chant.config.json"), JSON.stringify({ lexicons: ["aws"] }));
|
|
44
|
+
writeFileSync(join(checkout, "sibling", "ops", "theirs.op.ts"), OP_FILE("theirs"));
|
|
45
|
+
// A configless dir at the checkout root, with an op of its own.
|
|
46
|
+
mkdirSync(join(checkout, "ops"), { recursive: true });
|
|
47
|
+
writeFileSync(join(checkout, "ops", "root.op.ts"), OP_FILE("root"));
|
|
48
|
+
});
|
|
49
|
+
afterEach(() => rmSync(checkout, { recursive: true, force: true }));
|
|
50
|
+
|
|
51
|
+
test("from inside a project, only that project's Ops are discovered — never a sibling's", async () => {
|
|
52
|
+
const { ops, errors } = await discoverOps({ cwd: join(checkout, "app") });
|
|
53
|
+
expect(errors).toEqual([]);
|
|
54
|
+
expect([...ops.keys()]).toEqual(["mine"]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("the walk finds the config from a subdirectory of the project too (ops/ beside src/, #1675)", async () => {
|
|
58
|
+
const { ops } = await discoverOps({ cwd: join(checkout, "app", "ops") });
|
|
59
|
+
expect([...ops.keys()]).toEqual(["mine"]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("with no chant config anywhere up to the git root, the git root stands — #1675's original scope", async () => {
|
|
63
|
+
rmSync(join(checkout, "app", "chant.config.json"));
|
|
64
|
+
rmSync(join(checkout, "sibling", "chant.config.json"));
|
|
65
|
+
const { ops } = await discoverOps({ cwd: join(checkout, "app") });
|
|
66
|
+
expect([...ops.keys()].sort()).toEqual(["mine", "root", "theirs"]);
|
|
67
|
+
});
|
|
68
|
+
});
|
package/src/op/discover.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getRuntime } from "../runtime-adapter";
|
|
2
2
|
import { readdir } from "node:fs/promises";
|
|
3
|
-
import {
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
5
|
import type { OpConfig } from "./types";
|
|
5
6
|
|
|
6
7
|
export interface DiscoveredOp {
|
|
@@ -20,6 +21,31 @@ async function findGitRoot(cwd?: string): Promise<string> {
|
|
|
20
21
|
return result.stdout.trim();
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
/**
|
|
25
|
+
* The root the `*.op.ts` scan starts from (#2058): the nearest chant project
|
|
26
|
+
* root — the directory holding `chant.config.ts`/`chant.config.json`, walking
|
|
27
|
+
* up from `cwd` and never past the git root. #1675's convention keeps Op
|
|
28
|
+
* files OUTSIDE `sourceDir` (`ops/` beside `src/`), which is why discovery
|
|
29
|
+
* walks up at all — but "up" used to mean the git root unconditionally, so a
|
|
30
|
+
* chant project nested in a larger checkout (a monorepo, behold's committed
|
|
31
|
+
* examples) discovered every SIBLING project's Ops as its own. The config
|
|
32
|
+
* file is the project boundary, the same one entity discovery respects.
|
|
33
|
+
* With no config anywhere up to the git root, the git root stands —
|
|
34
|
+
* #1675's original scope, kept for configless layouts.
|
|
35
|
+
*/
|
|
36
|
+
async function findDiscoveryRoot(cwd?: string): Promise<string> {
|
|
37
|
+
const gitRoot = resolve(await findGitRoot(cwd));
|
|
38
|
+
let dir = resolve(cwd ?? process.cwd());
|
|
39
|
+
for (;;) {
|
|
40
|
+
if (existsSync(join(dir, "chant.config.ts")) || existsSync(join(dir, "chant.config.json"))) return dir;
|
|
41
|
+
if (dir === gitRoot) break;
|
|
42
|
+
const parent = dirname(dir);
|
|
43
|
+
if (parent === dir) break; // filesystem root — cwd was outside the git root
|
|
44
|
+
dir = parent;
|
|
45
|
+
}
|
|
46
|
+
return gitRoot;
|
|
47
|
+
}
|
|
48
|
+
|
|
23
49
|
async function collectOpFiles(dir: string): Promise<string[]> {
|
|
24
50
|
const files: string[] = [];
|
|
25
51
|
let entries;
|
|
@@ -45,14 +71,17 @@ async function collectOpFiles(dir: string): Promise<string[]> {
|
|
|
45
71
|
}
|
|
46
72
|
|
|
47
73
|
/**
|
|
48
|
-
* Discover all Op definitions from
|
|
74
|
+
* Discover all Op definitions from `*.op.ts` files under the nearest chant
|
|
75
|
+
* project root (the directory holding `chant.config.ts`/`.json`, walking up
|
|
76
|
+
* from `cwd`), or under the git root when no config exists — see
|
|
77
|
+
* {@link findDiscoveryRoot} (#2058).
|
|
49
78
|
*/
|
|
50
79
|
export async function discoverOps(opts?: { cwd?: string }): Promise<OpDiscoveryResult> {
|
|
51
80
|
const errors: string[] = [];
|
|
52
81
|
const ops = new Map<string, DiscoveredOp>();
|
|
53
82
|
|
|
54
|
-
const
|
|
55
|
-
const files = await collectOpFiles(
|
|
83
|
+
const root = await findDiscoveryRoot(opts?.cwd);
|
|
84
|
+
const files = await collectOpFiles(root);
|
|
56
85
|
|
|
57
86
|
const nameToFile = new Map<string, string>();
|
|
58
87
|
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs";
|
|
12
|
-
import { isAbsolute, join, relative } from "path";
|
|
12
|
+
import { isAbsolute, join, relative, resolve } from "path";
|
|
13
13
|
import type { CarveReport } from "./carve";
|
|
14
14
|
import type { OwnershipMarker } from "../ownership";
|
|
15
15
|
|
|
@@ -100,10 +100,14 @@ export function carveManifestPath(outDir: string, target: string): string {
|
|
|
100
100
|
/**
|
|
101
101
|
* Relative-to-`outDir` spelling of a recorded file path. Absolute entries
|
|
102
102
|
* (the run-time spelling every step naturally produces, and every version-1
|
|
103
|
-
* manifest on disk) are rewritten
|
|
103
|
+
* manifest on disk) are rewritten against the RESOLVED outDir — a caller's
|
|
104
|
+
* relative `--output` must not change what "relative to the manifest's
|
|
105
|
+
* directory" means (#2059). Already-relative entries pass through: they can
|
|
106
|
+
* only have come from a reader honouring the contract, and re-basing them
|
|
107
|
+
* against an unknowable original cwd would corrupt, not normalize.
|
|
104
108
|
*/
|
|
105
109
|
function relativizePath(outDir: string, p: string): string {
|
|
106
|
-
return isAbsolute(p) ? relative(outDir, p) : p;
|
|
110
|
+
return isAbsolute(p) ? relative(resolve(outDir), p) : p;
|
|
107
111
|
}
|
|
108
112
|
|
|
109
113
|
/**
|
|
@@ -136,7 +140,11 @@ function normalizeManifest(outDir: string, manifest: CarveManifest): CarveManife
|
|
|
136
140
|
* on-disk spellings read the same.
|
|
137
141
|
*/
|
|
138
142
|
export function resolveManifestFilePath(manifestDir: string, p: string): string {
|
|
139
|
-
|
|
143
|
+
// Always ABSOLUTE (#2059): `join` with a relative manifestDir produced a
|
|
144
|
+
// cwd-relative path, which `relativizePath` then passed through untouched —
|
|
145
|
+
// so `carve apply --output carveout` recorded `stampedFiles` relative to
|
|
146
|
+
// wherever the command happened to run, two spellings in one manifest.
|
|
147
|
+
return isAbsolute(p) ? p : resolve(manifestDir, p);
|
|
140
148
|
}
|
|
141
149
|
|
|
142
150
|
/** Write (or overwrite) a manifest into the output dir. Returns the path. */
|