@intentius/chant 0.112.1 → 0.113.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/lifecycle/digest.d.ts.map +1 -1
- package/dist/op/activities/lifecycle.d.ts +5 -3
- package/dist/op/activities/lifecycle.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/lifecycle/digest.test.ts +26 -0
- package/src/lifecycle/digest.ts +26 -1
- package/src/op/activities/lifecycle.test.ts +31 -0
- package/src/op/activities/lifecycle.ts +8 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"digest.d.ts","sourceRoot":"","sources":["../../src/lifecycle/digest.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAE5C,OAAO,KAAK,EAAE,WAAW,EAAkB,UAAU,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"digest.d.ts","sourceRoot":"","sources":["../../src/lifecycle/digest.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAE5C,OAAO,KAAK,EAAE,WAAW,EAAkB,UAAU,EAAE,MAAM,SAAS,CAAC;AAKvE;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAGhD;AA0BD;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,WAAW,GAAG,WAAW,CA4BxE;AAED;;GAEG;AACH,wBAAgB,WAAW,CACzB,OAAO,EAAE,WAAW,EACpB,QAAQ,EAAE,WAAW,GAAG,SAAS,GAChC,UAAU,CAiCZ"}
|
|
@@ -23,8 +23,8 @@ export interface LifecycleDiffResult {
|
|
|
23
23
|
exitCode: number;
|
|
24
24
|
/**
|
|
25
25
|
* True when the diff output contains any drift indicators
|
|
26
|
-
* (MISSING / ORPHAN / DRIFTED / DISAPPEARED
|
|
27
|
-
* `chant lifecycle diff --live`).
|
|
26
|
+
* (MISSING / ORPHAN / DRIFTED / DISAPPEARED / PROPERTY DRIFT section
|
|
27
|
+
* headers from `chant lifecycle diff --live`).
|
|
28
28
|
*/
|
|
29
29
|
drifted: boolean;
|
|
30
30
|
/**
|
|
@@ -55,13 +55,15 @@ export interface LifecycleDiffResult {
|
|
|
55
55
|
* approval would be stale the moment it was written.
|
|
56
56
|
*/
|
|
57
57
|
export declare function normalizeDiffChangeSet(output: string): string[];
|
|
58
|
+
/** True when a `chant lifecycle diff` render carries any of {@link DRIFT_HEADERS}' sections. */
|
|
59
|
+
export declare function detectDrift(output: string): boolean;
|
|
58
60
|
/**
|
|
59
61
|
* Run `chant lifecycle diff <env>` and return the output + structured drift
|
|
60
62
|
* flag. Read-only; intended for use inside watch/observation Ops.
|
|
61
63
|
* Uses fastIdempotent profile.
|
|
62
64
|
*
|
|
63
65
|
* The `drifted` field is computed by scanning the output for any of the
|
|
64
|
-
* MISSING / ORPHAN / DRIFTED / DISAPPEARED section headers documented in
|
|
66
|
+
* MISSING / ORPHAN / DRIFTED / DISAPPEARED / PROPERTY DRIFT section headers documented in
|
|
65
67
|
* cli/state.mdx. Pair with `outcomeAttribute: { name: "Drift", from: "drifted" }`
|
|
66
68
|
* on a WatchOp activity step to surface drift as the run's `Drift` outcome.
|
|
67
69
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../../../src/op/activities/lifecycle.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,qBAAqB;IACpC,wDAAwD;IACxD,GAAG,EAAE,MAAM,CAAC;CACb;AAED;;;GAGG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,qBAAqB,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAIxG;AAED,MAAM,WAAW,iBAAiB;IAChC,wDAAwD;IACxD,GAAG,EAAE,MAAM,CAAC;IACZ;;;OAGG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,mBAAmB;IAClC,uDAAuD;IACvD,MAAM,EAAE,MAAM,CAAC;IACf,uCAAuC;IACvC,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,OAAO,EAAE,OAAO,CAAC;IACjB;;;;;;;;;OASG;IACH,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAM/D;
|
|
1
|
+
{"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../../../src/op/activities/lifecycle.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,qBAAqB;IACpC,wDAAwD;IACxD,GAAG,EAAE,MAAM,CAAC;CACb;AAED;;;GAGG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,qBAAqB,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAIxG;AAED,MAAM,WAAW,iBAAiB;IAChC,wDAAwD;IACxD,GAAG,EAAE,MAAM,CAAC;IACZ;;;OAGG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,mBAAmB;IAClC,uDAAuD;IACvD,MAAM,EAAE,MAAM,CAAC;IACf,uCAAuC;IACvC,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,OAAO,EAAE,OAAO,CAAC;IACjB;;;;;;;;;OASG;IACH,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAM/D;AAmBD,gGAAgG;AAChG,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAEnD;AAgBD;;;;;;;;;GASG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE,iBAAiB,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAa/G"}
|
package/package.json
CHANGED
|
@@ -2,6 +2,8 @@ import { describe, test, expect } from "vitest";
|
|
|
2
2
|
import { computeBuildDigest, diffDigests, hashProps } from "./digest";
|
|
3
3
|
import type { BuildResult } from "../build";
|
|
4
4
|
import type { BuildDigest } from "./types";
|
|
5
|
+
import { AttrRef } from "../attrref";
|
|
6
|
+
import { LOGICAL_NAME_SYMBOL } from "../utils";
|
|
5
7
|
|
|
6
8
|
function makeBuildResult(entitiesByLexicon: Record<string, Array<{ name: string; type: string; props: unknown }>>): BuildResult {
|
|
7
9
|
const entities = new Map();
|
|
@@ -37,6 +39,30 @@ describe("hashProps", () => {
|
|
|
37
39
|
test("produces different hashes for different props", () => {
|
|
38
40
|
expect(hashProps({ a: 1 })).not.toBe(hashProps({ a: 2 }));
|
|
39
41
|
});
|
|
42
|
+
|
|
43
|
+
// #3642: a sql index's `columns` holds AttrRefs the build never names, and
|
|
44
|
+
// their toJSON throws, so a snapshot of the project failed.
|
|
45
|
+
describe("an AttrRef the build did not name", () => {
|
|
46
|
+
const parent = { [LOGICAL_NAME_SYMBOL]: "orders" };
|
|
47
|
+
|
|
48
|
+
test("hashes instead of throwing, as the envelope named by its parent", () => {
|
|
49
|
+
const nested = { columns: [new AttrRef(parent, "status")] };
|
|
50
|
+
expect(() => JSON.stringify(nested)).toThrow(/logical name not set/);
|
|
51
|
+
const named = new AttrRef({}, "status");
|
|
52
|
+
named._setLogicalName("orders");
|
|
53
|
+
expect(hashProps(nested)).toBe(hashProps({ columns: [named] }));
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("tells two attributes apart", () => {
|
|
57
|
+
expect(hashProps({ c: [new AttrRef(parent, "status")] })).not.toBe(hashProps({ c: [new AttrRef(parent, "amount")] }));
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("hashes a named AttrRef as it always has", () => {
|
|
61
|
+
const ref = new AttrRef({}, "arn");
|
|
62
|
+
ref._setLogicalName("bucket");
|
|
63
|
+
expect(hashProps({ a: ref })).toBe(hashProps({ a: { __attrRef: { entity: "bucket", attribute: "arn" } } }));
|
|
64
|
+
});
|
|
65
|
+
});
|
|
40
66
|
});
|
|
41
67
|
|
|
42
68
|
describe("computeBuildDigest", () => {
|
package/src/lifecycle/digest.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type { Declarable } from "../declarable";
|
|
|
9
9
|
import type { BuildDigest, ResourceDigest, DigestDiff } from "./types";
|
|
10
10
|
import { canonicalJson } from "../effect-receipt";
|
|
11
11
|
import { contentDigest } from "../content-digest";
|
|
12
|
+
import { isAttrRefLike, LOGICAL_NAME_SYMBOL } from "../utils";
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Hash an entity's props deterministically: bare hex SHA-256 over their
|
|
@@ -18,10 +19,34 @@ import { contentDigest } from "../content-digest";
|
|
|
18
19
|
* hashes rather than throwing.
|
|
19
20
|
*/
|
|
20
21
|
export function hashProps(props: unknown): string {
|
|
21
|
-
const plain: unknown = JSON.parse(JSON.stringify(props) ?? "null");
|
|
22
|
+
const plain: unknown = JSON.parse(JSON.stringify(namedRefs(props)) ?? "null");
|
|
22
23
|
return contentDigest(canonicalJson(plain)).slice("sha256:".length);
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
/**
|
|
27
|
+
* `props` with each AttrRef that has no logical name of its own written as
|
|
28
|
+
* the envelope its `toJSON` gives, named by its parent's logical name. The
|
|
29
|
+
* build names only an entity's top-level AttrRefs; one held in a record,
|
|
30
|
+
* such as a sql table's `columns`, is never named, and `toJSON` throws on
|
|
31
|
+
* it (chant#3642). Plain objects and arrays are walked; anything else is
|
|
32
|
+
* left for `JSON.stringify`, so a named AttrRef hashes as it always has.
|
|
33
|
+
*/
|
|
34
|
+
function namedRefs(value: unknown): unknown {
|
|
35
|
+
if (isAttrRefLike(value)) {
|
|
36
|
+
if (value.getLogicalName()) return value;
|
|
37
|
+
const parent = value.parent.deref() as Record<symbol, unknown> | undefined;
|
|
38
|
+
const entity = parent?.[LOGICAL_NAME_SYMBOL];
|
|
39
|
+
return { __attrRef: { entity: typeof entity === "string" ? entity : null, attribute: value.attribute } };
|
|
40
|
+
}
|
|
41
|
+
if (Array.isArray(value)) return value.map(namedRefs);
|
|
42
|
+
if (value !== null && typeof value === "object") {
|
|
43
|
+
const proto = Object.getPrototypeOf(value);
|
|
44
|
+
if (proto !== Object.prototype && proto !== null) return value;
|
|
45
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, namedRefs(v)]));
|
|
46
|
+
}
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
|
|
25
50
|
/**
|
|
26
51
|
* Compute a full build digest from a BuildResult.
|
|
27
52
|
*/
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { detectDrift } from "./lifecycle";
|
|
3
|
+
|
|
4
|
+
const BOLD = "\u001b[1m";
|
|
5
|
+
const RESET = "\u001b[0m";
|
|
6
|
+
|
|
7
|
+
// Renders as `chant lifecycle diff <env> --live` printed them against the sql
|
|
8
|
+
// emulator (#3642), bold headers included: the activity reads them off a
|
|
9
|
+
// child process, colour and all.
|
|
10
|
+
const HEAD = `${BOLD}sql${RESET} — environment: watch\n`;
|
|
11
|
+
const CLEAN = `${HEAD}0 missing, 0 orphan, 0 disappeared, 0 newly observed, 0 drifted, 3 unchanged\n${"-".repeat(80)}\n\u001b[32mNo drift detected across 1 lexicon(s)${RESET}`;
|
|
12
|
+
const PROPERTY = `${HEAD}0 missing, 0 orphan, 0 disappeared, 0 newly observed, 0 drifted, 3 unchanged\n${"-".repeat(80)}\n\n${BOLD}sql (properties)${RESET}\n1 property drift across 1 resource(s), 0 accepted, 2 unchanged\n${"-".repeat(80)}\n${BOLD}\nPROPERTY DRIFT (declared vs live; baseline shown where one exists):${RESET}\n - events (ClickHouse::Table)\n ttl: ts + INTERVAL 180 DAY → ts + toIntervalDay(30) [from: authored]`;
|
|
13
|
+
const MISSING = `${HEAD}1 missing, 0 orphan, 0 disappeared, 0 newly observed, 0 drifted, 2 unchanged\n${"-".repeat(80)}\n${BOLD}\nMISSING (declared, provider reports not in cloud):${RESET}\n - byKind [queried http://127.0.0.1:8123 yodel_3642.by_kind]`;
|
|
14
|
+
|
|
15
|
+
describe("detectDrift", () => {
|
|
16
|
+
test("a clean render is not drift", () => {
|
|
17
|
+
expect(detectDrift(CLEAN)).toBe(false);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("an owned object's property changed out of band is drift (PROPERTY DRIFT)", () => {
|
|
21
|
+
expect(detectDrift(PROPERTY)).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("a declared object gone from the server is drift (MISSING)", () => {
|
|
25
|
+
expect(detectDrift(MISSING)).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("the counts line alone, lower case, is not drift", () => {
|
|
29
|
+
expect(detectDrift("1 property drift across 1 resource(s)")).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -36,8 +36,8 @@ export interface LifecycleDiffResult {
|
|
|
36
36
|
exitCode: number;
|
|
37
37
|
/**
|
|
38
38
|
* True when the diff output contains any drift indicators
|
|
39
|
-
* (MISSING / ORPHAN / DRIFTED / DISAPPEARED
|
|
40
|
-
* `chant lifecycle diff --live`).
|
|
39
|
+
* (MISSING / ORPHAN / DRIFTED / DISAPPEARED / PROPERTY DRIFT section
|
|
40
|
+
* headers from `chant lifecycle diff --live`).
|
|
41
41
|
*/
|
|
42
42
|
drifted: boolean;
|
|
43
43
|
/**
|
|
@@ -85,12 +85,16 @@ const DRIFT_HEADERS = [
|
|
|
85
85
|
"ORPHAN",
|
|
86
86
|
"DISAPPEARED",
|
|
87
87
|
"DRIFTED",
|
|
88
|
+
// An owned resource whose live properties differ from its declaration (the
|
|
89
|
+
// deep read): a sql table's TTL changed out of band, say (#3642).
|
|
90
|
+
"PROPERTY DRIFT",
|
|
88
91
|
"ARTIFACTS ADDED",
|
|
89
92
|
"ARTIFACTS REMOVED",
|
|
90
93
|
"ARTIFACTS CHANGED",
|
|
91
94
|
];
|
|
92
95
|
|
|
93
|
-
|
|
96
|
+
/** True when a `chant lifecycle diff` render carries any of {@link DRIFT_HEADERS}' sections. */
|
|
97
|
+
export function detectDrift(output: string): boolean {
|
|
94
98
|
return DRIFT_HEADERS.some((h) => output.includes(`${h} (`) || output.includes(`\n${h}`));
|
|
95
99
|
}
|
|
96
100
|
|
|
@@ -114,7 +118,7 @@ function lifecycleDiffDigest(args: LifecycleDiffArgs, output: string): string {
|
|
|
114
118
|
* Uses fastIdempotent profile.
|
|
115
119
|
*
|
|
116
120
|
* The `drifted` field is computed by scanning the output for any of the
|
|
117
|
-
* MISSING / ORPHAN / DRIFTED / DISAPPEARED section headers documented in
|
|
121
|
+
* MISSING / ORPHAN / DRIFTED / DISAPPEARED / PROPERTY DRIFT section headers documented in
|
|
118
122
|
* cli/state.mdx. Pair with `outcomeAttribute: { name: "Drift", from: "drifted" }`
|
|
119
123
|
* on a WatchOp activity step to surface drift as the run's `Drift` outcome.
|
|
120
124
|
*/
|