@intentius/chant 0.112.1 → 0.114.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/lifecycle/change-set.d.ts +7 -0
- package/dist/lifecycle/change-set.d.ts.map +1 -1
- package/dist/lifecycle/digest.d.ts +12 -0
- package/dist/lifecycle/digest.d.ts.map +1 -1
- package/dist/lifecycle/live-diff.d.ts +24 -0
- package/dist/lifecycle/live-diff.d.ts.map +1 -1
- package/dist/op/activities/lifecycle.d.ts +12 -3
- package/dist/op/activities/lifecycle.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/handlers/lifecycle.ts +17 -7
- package/src/lifecycle/change-set.test.ts +39 -0
- package/src/lifecycle/change-set.ts +9 -0
- package/src/lifecycle/digest.test.ts +26 -0
- package/src/lifecycle/digest.ts +45 -1
- package/src/lifecycle/live-diff.test.ts +25 -1
- package/src/lifecycle/live-diff.ts +41 -0
- package/src/op/activities/lifecycle.test.ts +83 -0
- package/src/op/activities/lifecycle.ts +9 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../../../src/cli/handlers/lifecycle.ts"],"names":[],"mappings":"AA+CA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAKlD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEhD;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,WAAW,GAAG,MAAM,CAE9E;AAmCD;;GAEG;AACH,wBAAsB,oBAAoB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAyH/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,CA+L3E;
|
|
1
|
+
{"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../../../src/cli/handlers/lifecycle.ts"],"names":[],"mappings":"AA+CA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAKlD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEhD;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,WAAW,GAAG,MAAM,CAE9E;AAmCD;;GAEG;AACH,wBAAsB,oBAAoB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAyH/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,CA+L3E;AAmsBD;;;;;;;;;GASG;AACH,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAwR3E;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,oBAAoB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CA2L/E;AAsBD;;GAEG;AACH,wBAAsB,eAAe,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAiB1E;AAgBD;;;;;;;;GAQG;AACH,wBAAsB,kBAAkB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAmE7E;AAiCD;;GAEG;AACH,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAM9E;AAkBD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,oBAAoB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CA8D/E"}
|
|
@@ -115,6 +115,13 @@ export interface ChangeSetEntry {
|
|
|
115
115
|
* place" (a defaulted namespace, an endpoint override, the wrong region).
|
|
116
116
|
*/
|
|
117
117
|
queried?: string;
|
|
118
|
+
/**
|
|
119
|
+
* The declared definition's fingerprint, for `action: "create"` (#3652),
|
|
120
|
+
* when the caller passed `definitions`. A create carries no deltas, so
|
|
121
|
+
* without it the plan says what it would create by name only, and the
|
|
122
|
+
* plan's digest does not move when the declaration does.
|
|
123
|
+
*/
|
|
124
|
+
definition?: string;
|
|
118
125
|
/** The declared entity this resource's owner chain resolves to, for `action: "runtime"` (#1077). */
|
|
119
126
|
runtimeOwner?: string;
|
|
120
127
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"change-set.d.ts","sourceRoot":"","sources":["../../src/lifecycle/change-set.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAAY,KAAK,eAAe,EAAE,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AACjF,OAAO,EAAwB,KAAK,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAC7E,OAAO,EAAyC,KAAK,UAAU,EAAE,MAAM,cAAc,CAAC;AACtF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,GAAG,QAAQ,GAAG,YAAY,CAAC;AAEnH;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,gBAAgB,GAAG,gBAAgB,GAAG,eAAe,GAAG,kBAAkB,CAAC;AAEvF;;;;GAIG;AACH,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,SAAS,GAAG,SAAS,CAAC;AAExD,MAAM,WAAW,cAAc;IAC7B;;;;OAIG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,kDAAkD;IAClD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,YAAY,CAAC;IACrB,kEAAkE;IAClE,QAAQ,EAAE;QACR,oCAAoC;QACpC,QAAQ,EAAE,OAAO,CAAC;QAClB,oCAAoC;QACpC,UAAU,EAAE,OAAO,CAAC;QACpB,qDAAqD;QACrD,IAAI,EAAE,OAAO,CAAC;QACd;;;;;WAKG;QACH,QAAQ,EAAE,OAAO,CAAC;KACnB,CAAC;IACF,6CAA6C;IAC7C,MAAM,CAAC,EAAE,eAAe,EAAE,CAAC;IAC3B,oDAAoD;IACpD,SAAS,EAAE,SAAS,CAAC;IACrB,gFAAgF;IAChF,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;IACpC,gGAAgG;IAChG,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,oGAAoG;IACpG,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,4DAA4D;IAC5D,YAAY,CAAC,EAAE,gBAAgB,CAAC;IAChC,gGAAgG;IAChG,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;;;;;;OAUG;IACH,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,4DAA4D;IAC5D,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC7B,0GAA0G;IAC1G,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,sNAAsN;AACtN,MAAM,WAAW,aAAc,SAAQ,cAAc;IACnD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,aAAa,EAAE,CAAC;CACxB;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,gBAAgB;IAC/B,qEAAqE;IACrE,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,SAAS,
|
|
1
|
+
{"version":3,"file":"change-set.d.ts","sourceRoot":"","sources":["../../src/lifecycle/change-set.ts"],"names":[],"mappings":"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"}
|
|
@@ -14,6 +14,18 @@ import type { BuildDigest, DigestDiff } from "./types.js";
|
|
|
14
14
|
* hashes rather than throwing.
|
|
15
15
|
*/
|
|
16
16
|
export declare function hashProps(props: unknown): string;
|
|
17
|
+
/**
|
|
18
|
+
* The fingerprint of one entity's declared definition (#3652): its type and
|
|
19
|
+
* its build props, hashed by {@link hashProps}. A live diff and a lifecycle
|
|
20
|
+
* plan print it beside each create, so the plan digest a gate binds moves
|
|
21
|
+
* when what would be created changes, not only when its name does.
|
|
22
|
+
*/
|
|
23
|
+
export declare function definitionDigest(entityType: string, props: unknown): string;
|
|
24
|
+
/** {@link definitionDigest} for every entity in `entities`, keyed by name. */
|
|
25
|
+
export declare function declaredDefinitions(entities: Iterable<readonly [string, {
|
|
26
|
+
entityType: string;
|
|
27
|
+
props?: unknown;
|
|
28
|
+
}]>): Record<string, string>;
|
|
17
29
|
/**
|
|
18
30
|
* Compute a full build digest from a BuildResult.
|
|
19
31
|
*/
|
|
@@ -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;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,CAE3E;AAED,8EAA8E;AAC9E,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,QAAQ,CAAC,SAAS,CAAC,MAAM,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC,CAAC,GAC7E,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAIxB;AAED;;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"}
|
|
@@ -91,6 +91,14 @@ export interface LiveDiffResult {
|
|
|
91
91
|
* namespace, and only this field makes that visible.
|
|
92
92
|
*/
|
|
93
93
|
queried?: Record<string, string>;
|
|
94
|
+
/**
|
|
95
|
+
* The declared definition of each `missing` entity, fingerprinted (#3652):
|
|
96
|
+
* what applying the diff would create, not only its name. A plan digest is
|
|
97
|
+
* taken over the diff, and a create listed by name alone let an approval for
|
|
98
|
+
* "create events" stand after `events` was edited. Present only when the
|
|
99
|
+
* caller supplied {@link DiffLiveInput.definitions} and something is missing.
|
|
100
|
+
*/
|
|
101
|
+
definitions?: Record<string, string>;
|
|
94
102
|
}
|
|
95
103
|
export interface DiffLiveInput {
|
|
96
104
|
/** Entity names from the current build. */
|
|
@@ -111,6 +119,13 @@ export interface DiffLiveInput {
|
|
|
111
119
|
* never consulted for classification.
|
|
112
120
|
*/
|
|
113
121
|
queried?: Record<string, string>;
|
|
122
|
+
/**
|
|
123
|
+
* Each declared entity's definition fingerprint (#3652), from
|
|
124
|
+
* `declaredDefinitions` in ./digest.ts. Carried onto the result for the
|
|
125
|
+
* entities that are missing, so a diff that proposes a create names what it
|
|
126
|
+
* would create. Never consulted for classification.
|
|
127
|
+
*/
|
|
128
|
+
definitions?: Record<string, string>;
|
|
114
129
|
}
|
|
115
130
|
/** The delta between two saved snapshots (#822): a two-way observed diff. */
|
|
116
131
|
export interface SnapshotDiffResult {
|
|
@@ -131,6 +146,15 @@ export interface SnapshotDiffResult {
|
|
|
131
146
|
*/
|
|
132
147
|
export declare function diffSnapshots(prev: Record<string, ResourceMetadata>, next: Record<string, ResourceMetadata>): SnapshotDiffResult;
|
|
133
148
|
export declare function diffLive(input: DiffLiveInput): LiveDiffResult;
|
|
149
|
+
/**
|
|
150
|
+
* One row of a live diff's MISSING section, as `chant lifecycle diff --live`
|
|
151
|
+
* prints it. The address the read went to (#1620) is the line between "not
|
|
152
|
+
* there" and "looked in the wrong place". The definition's fingerprint
|
|
153
|
+
* (#3652) is what the create would create: `ApplyOp` digests these rows, and
|
|
154
|
+
* a row naming the entity alone left an approval standing after the
|
|
155
|
+
* declaration changed.
|
|
156
|
+
*/
|
|
157
|
+
export declare function missingRow(diff: Pick<LiveDiffResult, "queried" | "definitions">, name: string): string;
|
|
134
158
|
export interface LiveArtifactDiffResult {
|
|
135
159
|
/** Observed now, not in previous snapshot. */
|
|
136
160
|
added: string[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"live-diff.d.ts","sourceRoot":"","sources":["../../src/lifecycle/live-diff.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AACrE,OAAO,KAAK,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAEzE,MAAM,WAAW,eAAe;IAC9B,2EAA2E;IAC3E,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,eAAe,EAAE,CAAC;CAC5B;AAED,wFAAwF;AACxF,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,gBAAgB,CAAC;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,sGAAsG;IACtG,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;GAKG;AACH,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,yEAAyE;IACzE,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,cAAc;IAC7B;;;;OAIG;IACH,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB;;;;;;OAMG;IACH,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB;;;;OAIG;IACH,eAAe,EAAE,oBAAoB,EAAE,CAAC;IACxC,mDAAmD;IACnD,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,mEAAmE;IACnE,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,oDAAoD;IACpD,oBAAoB,EAAE,aAAa,EAAE,CAAC;IACtC,sDAAsD;IACtD,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB;;;;OAIG;IACH,UAAU,EAAE,kBAAkB,EAAE,CAAC;IACjC;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"live-diff.d.ts","sourceRoot":"","sources":["../../src/lifecycle/live-diff.ts"],"names":[],"mappings":"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"}
|
|
@@ -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,22 @@ 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;
|
|
60
|
+
/**
|
|
61
|
+
* The identity of one `chant lifecycle diff` result (#2300). The environment
|
|
62
|
+
* and the `--live` flag are hashed alongside the change set because they say
|
|
63
|
+
* what the change set is *of*: the same rows against `staging` are not an
|
|
64
|
+
* approval to apply against `prod`.
|
|
65
|
+
*/
|
|
66
|
+
export declare function lifecycleDiffDigest(args: LifecycleDiffArgs, output: string): string;
|
|
58
67
|
/**
|
|
59
68
|
* Run `chant lifecycle diff <env>` and return the output + structured drift
|
|
60
69
|
* flag. Read-only; intended for use inside watch/observation Ops.
|
|
61
70
|
* Uses fastIdempotent profile.
|
|
62
71
|
*
|
|
63
72
|
* The `drifted` field is computed by scanning the output for any of the
|
|
64
|
-
* MISSING / ORPHAN / DRIFTED / DISAPPEARED section headers documented in
|
|
73
|
+
* MISSING / ORPHAN / DRIFTED / DISAPPEARED / PROPERTY DRIFT section headers documented in
|
|
65
74
|
* cli/state.mdx. Pair with `outcomeAttribute: { name: "Drift", from: "drifted" }`
|
|
66
75
|
* on a WatchOp activity step to surface drift as the run's `Drift` outcome.
|
|
67
76
|
*/
|
|
@@ -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;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,iBAAiB,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAMnF;AAED;;;;;;;;;GASG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE,iBAAiB,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAa/G"}
|
package/package.json
CHANGED
|
@@ -18,8 +18,8 @@ import {
|
|
|
18
18
|
type DeviationToAccept,
|
|
19
19
|
type ObservationBaseline,
|
|
20
20
|
} from "../../lifecycle/observation-baseline";
|
|
21
|
-
import { computeBuildDigest, diffDigests } from "../../lifecycle/digest";
|
|
22
|
-
import { diffLive, diffLiveArtifacts, diffSnapshots, type LiveDiffResult, type LiveArtifactDiffResult, type SnapshotDiffResult } from "../../lifecycle/live-diff";
|
|
21
|
+
import { computeBuildDigest, declaredDefinitions, diffDigests } from "../../lifecycle/digest";
|
|
22
|
+
import { diffLive, diffLiveArtifacts, diffSnapshots, missingRow, type LiveDiffResult, type LiveArtifactDiffResult, type SnapshotDiffResult } from "../../lifecycle/live-diff";
|
|
23
23
|
import { buildChangeSet, renderChangeSet, renderChangeSetMarkdown, gitlabMrReport, unobservedPlanNotice, type ChangeSet, type HeldEntitySet } from "../../lifecycle/change-set";
|
|
24
24
|
import { mergeReceiptEntries, observedValueResolver, planReceipts, readReceiptValue, type ReceiptReading } from "../../lifecycle/receipt-plan";
|
|
25
25
|
import { annotateDisruption, disruptionNotices } from "../../lifecycle/disruption";
|
|
@@ -876,7 +876,17 @@ async function runLifecycleDiffLive(args: LiveDiffArgs): Promise<LiveDiffOutcome
|
|
|
876
876
|
});
|
|
877
877
|
const observedNow = observed.resources;
|
|
878
878
|
const observedThen = prevSnapshot?.resources;
|
|
879
|
-
const diff = diffLive({
|
|
879
|
+
const diff = diffLive({
|
|
880
|
+
declared,
|
|
881
|
+
observedNow,
|
|
882
|
+
observedThen,
|
|
883
|
+
unobserved: observed.unobserved,
|
|
884
|
+
queried: observed.queried,
|
|
885
|
+
// What each create would create (#3652), so the diff, and the plan
|
|
886
|
+
// digest ApplyOp's gate binds to it, moves when a missing entity's
|
|
887
|
+
// declaration does.
|
|
888
|
+
definitions: declaredDefinitions(entities),
|
|
889
|
+
});
|
|
880
890
|
// Unobserved entities are deliberately NOT drift: a hole in the read is
|
|
881
891
|
// not a change in the cloud. They are reported separately (#1089) so a
|
|
882
892
|
// "no drift detected" line can never be built on top of a failed read.
|
|
@@ -1128,10 +1138,7 @@ function renderLiveDiff(lexiconName: string, environment: string, diff: LiveDiff
|
|
|
1128
1138
|
if (diff.missing.length > 0) {
|
|
1129
1139
|
console.log(formatBold("\nMISSING (declared, provider reports not in cloud):"));
|
|
1130
1140
|
for (const name of diff.missing) {
|
|
1131
|
-
|
|
1132
|
-
// there" and "looked in the wrong place" (a defaulted namespace, say).
|
|
1133
|
-
const queried = diff.queried?.[name];
|
|
1134
|
-
console.log(` - ${name}${queried ? ` [queried ${queried}]` : ""}`);
|
|
1141
|
+
console.log(missingRow(diff, name));
|
|
1135
1142
|
}
|
|
1136
1143
|
}
|
|
1137
1144
|
if (diff.orphan.length > 0) {
|
|
@@ -1415,6 +1422,9 @@ export async function runLifecyclePlan(ctx: CommandContext): Promise<number> {
|
|
|
1415
1422
|
// The resolved read address rides every entry, not just unobserved
|
|
1416
1423
|
// ones — the diff path already passes it (#1620); plan lost it.
|
|
1417
1424
|
queried: observed.queried,
|
|
1425
|
+
// A create carries its declaration's fingerprint (#3652), so the
|
|
1426
|
+
// plan's digest moves when what it would create does.
|
|
1427
|
+
definitions: declaredDefinitions(entities),
|
|
1418
1428
|
}, {
|
|
1419
1429
|
// Attribution survives the flat merge below (#1674).
|
|
1420
1430
|
lexicon: lexiconName,
|
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
type HeldEntitySet,
|
|
12
12
|
} from "./change-set";
|
|
13
13
|
import type { ResourceMetadata } from "../lexicon";
|
|
14
|
+
import { declaredDefinitions, definitionDigest } from "./digest";
|
|
15
|
+
import { lifecyclePlanPart } from "../change-set";
|
|
14
16
|
|
|
15
17
|
const meta = (over: Partial<ResourceMetadata> = {}): ResourceMetadata => ({
|
|
16
18
|
type: "Fake::Resource",
|
|
@@ -638,3 +640,40 @@ describe("ChangeSet.held (#2162)", () => {
|
|
|
638
640
|
expect(renderChangeSetMarkdown(cs)).not.toContain("HELD");
|
|
639
641
|
});
|
|
640
642
|
});
|
|
643
|
+
|
|
644
|
+
// #3652: a create carries no deltas, so the plan named what it would create
|
|
645
|
+
// by name alone and its digest stood still when the declaration changed.
|
|
646
|
+
describe("a create's declared definition (#3652)", () => {
|
|
647
|
+
const plan = (columns: Record<string, unknown>) =>
|
|
648
|
+
buildChangeSet("dev", {
|
|
649
|
+
declared: new Set(["events", "users"]),
|
|
650
|
+
observedNow: { users: meta() },
|
|
651
|
+
observedThen: undefined,
|
|
652
|
+
definitions: declaredDefinitions([
|
|
653
|
+
["events", { entityType: "PG::Table", props: { columns } }],
|
|
654
|
+
["users", { entityType: "PG::Table", props: { columns: { id: "int" } } }],
|
|
655
|
+
]),
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
test("rides the create, and only the create", () => {
|
|
659
|
+
const cs = plan({ id: "int" });
|
|
660
|
+
const events = cs.entries.find((e) => e.name === "events")!;
|
|
661
|
+
const users = cs.entries.find((e) => e.name === "users")!;
|
|
662
|
+
expect(events.action).toBe("create");
|
|
663
|
+
expect(events.definition).toBe(definitionDigest("PG::Table", { columns: { id: "int" } }));
|
|
664
|
+
expect(users.action).toBe("noop");
|
|
665
|
+
expect(users.definition).toBeUndefined();
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
test("moves the lifecycle plan's digest when the created object's props change, and only then", () => {
|
|
669
|
+
const digest = (cs: ChangeSet) => lifecyclePlanPart({ member: "db", plan: cs }).member.planDigest;
|
|
670
|
+
expect(digest(plan({ id: "int" }))).toBe(digest(plan({ id: "int" })));
|
|
671
|
+
expect(digest(plan({ id: "int" }))).not.toBe(digest(plan({ id: "int", kind: "text" })));
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
test("is left off when the caller passes no definitions", () => {
|
|
675
|
+
const cs = buildChangeSet("dev", { declared: new Set(["events"]), observedNow: {}, observedThen: undefined });
|
|
676
|
+
expect(cs.entries[0].action).toBe("create");
|
|
677
|
+
expect("definition" in cs.entries[0]).toBe(false);
|
|
678
|
+
});
|
|
679
|
+
});
|
|
@@ -119,6 +119,13 @@ export interface ChangeSetEntry {
|
|
|
119
119
|
* place" (a defaulted namespace, an endpoint override, the wrong region).
|
|
120
120
|
*/
|
|
121
121
|
queried?: string;
|
|
122
|
+
/**
|
|
123
|
+
* The declared definition's fingerprint, for `action: "create"` (#3652),
|
|
124
|
+
* when the caller passed `definitions`. A create carries no deltas, so
|
|
125
|
+
* without it the plan says what it would create by name only, and the
|
|
126
|
+
* plan's digest does not move when the declaration does.
|
|
127
|
+
*/
|
|
128
|
+
definition?: string;
|
|
122
129
|
/** The declared entity this resource's owner chain resolves to, for `action: "runtime"` (#1077). */
|
|
123
130
|
runtimeOwner?: string;
|
|
124
131
|
/**
|
|
@@ -265,6 +272,7 @@ export function buildChangeSet(env: string, input: DiffLiveInput, options?: Chan
|
|
|
265
272
|
// there is one, else the observation's queried map. Diagnostic only; the
|
|
266
273
|
// classification above never reads it.
|
|
267
274
|
const queried = unobservedEntry?.queried ?? input.queried?.[name];
|
|
275
|
+
const definition = action === "create" ? input.definitions?.[name] : undefined;
|
|
268
276
|
|
|
269
277
|
// The provider's id for the row (#1674). Live first; the snapshot's only
|
|
270
278
|
// when the resource is no longer live (a snapshot-only noop).
|
|
@@ -286,6 +294,7 @@ export function buildChangeSet(env: string, input: DiffLiveInput, options?: Chan
|
|
|
286
294
|
}
|
|
287
295
|
: {}),
|
|
288
296
|
...(queried ? { queried } : {}),
|
|
297
|
+
...(definition ? { definition } : {}),
|
|
289
298
|
...(runtimeOwner ? { runtimeOwner } : {}),
|
|
290
299
|
});
|
|
291
300
|
}
|
|
@@ -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,53 @@ 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
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The fingerprint of one entity's declared definition (#3652): its type and
|
|
52
|
+
* its build props, hashed by {@link hashProps}. A live diff and a lifecycle
|
|
53
|
+
* plan print it beside each create, so the plan digest a gate binds moves
|
|
54
|
+
* when what would be created changes, not only when its name does.
|
|
55
|
+
*/
|
|
56
|
+
export function definitionDigest(entityType: string, props: unknown): string {
|
|
57
|
+
return hashProps({ type: entityType, props: props ?? {} });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** {@link definitionDigest} for every entity in `entities`, keyed by name. */
|
|
61
|
+
export function declaredDefinitions(
|
|
62
|
+
entities: Iterable<readonly [string, { entityType: string; props?: unknown }]>,
|
|
63
|
+
): Record<string, string> {
|
|
64
|
+
const out: Record<string, string> = {};
|
|
65
|
+
for (const [name, entity] of entities) out[name] = definitionDigest(entity.entityType, entity.props);
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
|
|
25
69
|
/**
|
|
26
70
|
* Compute a full build digest from a BuildResult.
|
|
27
71
|
*/
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, test, expect } from "vitest";
|
|
2
|
-
import { diffLive, diffLiveArtifacts, diffSnapshots } from "./live-diff";
|
|
2
|
+
import { diffLive, diffLiveArtifacts, diffSnapshots, missingRow } from "./live-diff";
|
|
3
3
|
import type { ResourceMetadata, ArtifactMetadata } from "../lexicon";
|
|
4
4
|
|
|
5
5
|
const meta = (overrides: Partial<ResourceMetadata> = {}): ResourceMetadata => ({
|
|
@@ -409,3 +409,27 @@ describe("attribute comparison is key-order insensitive (#1279)", () => {
|
|
|
409
409
|
expect(result.driftedSinceSnapshot).toHaveLength(1);
|
|
410
410
|
});
|
|
411
411
|
});
|
|
412
|
+
|
|
413
|
+
describe("diffLive definitions (#3652)", () => {
|
|
414
|
+
test("carries the fingerprint of each missing entity, and of nothing else", () => {
|
|
415
|
+
const diff = diffLive({
|
|
416
|
+
declared: new Set(["a", "b"]),
|
|
417
|
+
observedNow: { b: { type: "T", status: "OK" } },
|
|
418
|
+
observedThen: undefined,
|
|
419
|
+
definitions: { a: "aaa", b: "bbb" },
|
|
420
|
+
});
|
|
421
|
+
expect(diff.missing).toEqual(["a"]);
|
|
422
|
+
expect(diff.definitions).toEqual({ a: "aaa" });
|
|
423
|
+
expect(missingRow(diff, "a")).toBe(" - a [definition aaa]");
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
test("is absent when nothing is missing, so a no-op diff is unchanged", () => {
|
|
427
|
+
const diff = diffLive({
|
|
428
|
+
declared: new Set(["b"]),
|
|
429
|
+
observedNow: { b: { type: "T", status: "OK" } },
|
|
430
|
+
observedThen: undefined,
|
|
431
|
+
definitions: { b: "bbb" },
|
|
432
|
+
});
|
|
433
|
+
expect("definitions" in diff).toBe(false);
|
|
434
|
+
});
|
|
435
|
+
});
|
|
@@ -96,6 +96,14 @@ export interface LiveDiffResult {
|
|
|
96
96
|
* namespace, and only this field makes that visible.
|
|
97
97
|
*/
|
|
98
98
|
queried?: Record<string, string>;
|
|
99
|
+
/**
|
|
100
|
+
* The declared definition of each `missing` entity, fingerprinted (#3652):
|
|
101
|
+
* what applying the diff would create, not only its name. A plan digest is
|
|
102
|
+
* taken over the diff, and a create listed by name alone let an approval for
|
|
103
|
+
* "create events" stand after `events` was edited. Present only when the
|
|
104
|
+
* caller supplied {@link DiffLiveInput.definitions} and something is missing.
|
|
105
|
+
*/
|
|
106
|
+
definitions?: Record<string, string>;
|
|
99
107
|
}
|
|
100
108
|
|
|
101
109
|
export interface DiffLiveInput {
|
|
@@ -117,6 +125,13 @@ export interface DiffLiveInput {
|
|
|
117
125
|
* never consulted for classification.
|
|
118
126
|
*/
|
|
119
127
|
queried?: Record<string, string>;
|
|
128
|
+
/**
|
|
129
|
+
* Each declared entity's definition fingerprint (#3652), from
|
|
130
|
+
* `declaredDefinitions` in ./digest.ts. Carried onto the result for the
|
|
131
|
+
* entities that are missing, so a diff that proposes a create names what it
|
|
132
|
+
* would create. Never consulted for classification.
|
|
133
|
+
*/
|
|
134
|
+
definitions?: Record<string, string>;
|
|
120
135
|
}
|
|
121
136
|
|
|
122
137
|
const TRACKED_FIELDS: Array<keyof ResourceMetadata> = [
|
|
@@ -351,9 +366,35 @@ export function diffLive(input: DiffLiveInput): LiveDiffResult {
|
|
|
351
366
|
unchanged: unchanged.sort(),
|
|
352
367
|
unobserved: unobserved.sort((a, b) => a.name.localeCompare(b.name)),
|
|
353
368
|
...(input.queried && Object.keys(input.queried).length > 0 ? { queried: input.queried } : {}),
|
|
369
|
+
...missingDefinitions(missing, input.definitions),
|
|
354
370
|
};
|
|
355
371
|
}
|
|
356
372
|
|
|
373
|
+
/**
|
|
374
|
+
* One row of a live diff's MISSING section, as `chant lifecycle diff --live`
|
|
375
|
+
* prints it. The address the read went to (#1620) is the line between "not
|
|
376
|
+
* there" and "looked in the wrong place". The definition's fingerprint
|
|
377
|
+
* (#3652) is what the create would create: `ApplyOp` digests these rows, and
|
|
378
|
+
* a row naming the entity alone left an approval standing after the
|
|
379
|
+
* declaration changed.
|
|
380
|
+
*/
|
|
381
|
+
export function missingRow(diff: Pick<LiveDiffResult, "queried" | "definitions">, name: string): string {
|
|
382
|
+
const queried = diff.queried?.[name];
|
|
383
|
+
const definition = diff.definitions?.[name];
|
|
384
|
+
return ` - ${name}${queried ? ` [queried ${queried}]` : ""}${definition ? ` [definition ${definition}]` : ""}`;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** `{ definitions }` for the missing entities that have one, or nothing at all. */
|
|
388
|
+
function missingDefinitions(missing: readonly string[], definitions: Record<string, string> | undefined): { definitions?: Record<string, string> } {
|
|
389
|
+
if (!definitions) return {};
|
|
390
|
+
const out: Record<string, string> = {};
|
|
391
|
+
for (const name of [...missing].sort()) {
|
|
392
|
+
const d = definitions[name];
|
|
393
|
+
if (d !== undefined) out[name] = d;
|
|
394
|
+
}
|
|
395
|
+
return Object.keys(out).length > 0 ? { definitions: out } : {};
|
|
396
|
+
}
|
|
397
|
+
|
|
357
398
|
// ── Artifact diff (no `declared` axis) ──────────────────────────────────────
|
|
358
399
|
|
|
359
400
|
export interface LiveArtifactDiffResult {
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { detectDrift, lifecycleDiffDigest } from "./lifecycle";
|
|
3
|
+
import { declaredDefinitions } from "../../lifecycle/digest";
|
|
4
|
+
import { diffLive, missingRow } from "../../lifecycle/live-diff";
|
|
5
|
+
import type { ResourceMetadata } from "../../lexicon";
|
|
6
|
+
|
|
7
|
+
const BOLD = "\u001b[1m";
|
|
8
|
+
const RESET = "\u001b[0m";
|
|
9
|
+
|
|
10
|
+
// Renders as `chant lifecycle diff <env> --live` printed them against the sql
|
|
11
|
+
// emulator (#3642), bold headers included: the activity reads them off a
|
|
12
|
+
// child process, colour and all.
|
|
13
|
+
const HEAD = `${BOLD}sql${RESET} — environment: watch\n`;
|
|
14
|
+
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}`;
|
|
15
|
+
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]`;
|
|
16
|
+
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]`;
|
|
17
|
+
|
|
18
|
+
describe("detectDrift", () => {
|
|
19
|
+
test("a clean render is not drift", () => {
|
|
20
|
+
expect(detectDrift(CLEAN)).toBe(false);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("an owned object's property changed out of band is drift (PROPERTY DRIFT)", () => {
|
|
24
|
+
expect(detectDrift(PROPERTY)).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("a declared object gone from the server is drift (MISSING)", () => {
|
|
28
|
+
expect(detectDrift(MISSING)).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("the counts line alone, lower case, is not drift", () => {
|
|
32
|
+
expect(detectDrift("1 property drift across 1 resource(s)")).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// #3652: the digest ApplyOp's gate binds is taken over the diff render. A
|
|
37
|
+
// create used to be listed by name only, so an approval for "create events"
|
|
38
|
+
// stood after `events` was edited. The MISSING row now carries the declared
|
|
39
|
+
// definition's fingerprint, and the digest moves with it.
|
|
40
|
+
describe("lifecycleDiffDigest over a create (#3652)", () => {
|
|
41
|
+
const env = { env: "dev", live: true };
|
|
42
|
+
const eventsA = { entityType: "ClickHouse::Table", props: { name: "events", columns: { id: { type: "UInt64" } } } };
|
|
43
|
+
const eventsB = {
|
|
44
|
+
entityType: "ClickHouse::Table",
|
|
45
|
+
props: { name: "events", columns: { id: { type: "UInt64" }, kind: { type: "String" } } },
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
function render(events: { entityType: string; props: Record<string, unknown> }, live: Record<string, ResourceMetadata> = {}): string {
|
|
49
|
+
const entities = new Map([["events", events]]);
|
|
50
|
+
const diff = diffLive({
|
|
51
|
+
declared: new Set(entities.keys()),
|
|
52
|
+
observedNow: live,
|
|
53
|
+
observedThen: undefined,
|
|
54
|
+
queried: { events: "http://emulator:8123 yodel_3652.events" },
|
|
55
|
+
definitions: declaredDefinitions(entities),
|
|
56
|
+
});
|
|
57
|
+
const rows = diff.missing.map((name) => missingRow(diff, name));
|
|
58
|
+
return `${HEAD}${diff.missing.length} missing\n${rows.length > 0 ? `MISSING (declared, provider reports not in cloud):\n${rows.join("\n")}` : ""}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
test("a create of different props is a different plan", () => {
|
|
62
|
+
expect(lifecycleDiffDigest(env, render(eventsA))).not.toBe(lifecycleDiffDigest(env, render(eventsB)));
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("a create of the same props is the same plan, whatever order the props were written in", () => {
|
|
66
|
+
const reordered = { entityType: "ClickHouse::Table", props: { columns: { id: { type: "UInt64" } }, name: "events" } };
|
|
67
|
+
expect(lifecycleDiffDigest(env, render(eventsA))).toBe(lifecycleDiffDigest(env, render(reordered)));
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("a create of another type with the same props is a different plan", () => {
|
|
71
|
+
const view = { ...eventsA, entityType: "ClickHouse::View" };
|
|
72
|
+
expect(lifecycleDiffDigest(env, render(eventsA))).not.toBe(lifecycleDiffDigest(env, render(view)));
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("a no-op prints no definition, so its digest is what it was before #3652 and stable across runs", () => {
|
|
76
|
+
const live = { events: { type: "ClickHouse::Table", status: "present" } as ResourceMetadata };
|
|
77
|
+
const once = render(eventsA, live);
|
|
78
|
+
expect(once).not.toContain("[definition");
|
|
79
|
+
expect(lifecycleDiffDigest(env, once)).toBe(lifecycleDiffDigest(env, render(eventsA, live)));
|
|
80
|
+
// An edit to an object that exists is the deep read's to report, not this row's.
|
|
81
|
+
expect(lifecycleDiffDigest(env, once)).toBe(lifecycleDiffDigest(env, render(eventsB, live)));
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -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
|
|
|
@@ -100,7 +104,7 @@ function detectDrift(output: string): boolean {
|
|
|
100
104
|
* what the change set is *of*: the same rows against `staging` are not an
|
|
101
105
|
* approval to apply against `prod`.
|
|
102
106
|
*/
|
|
103
|
-
function lifecycleDiffDigest(args: LifecycleDiffArgs, output: string): string {
|
|
107
|
+
export function lifecycleDiffDigest(args: LifecycleDiffArgs, output: string): string {
|
|
104
108
|
return computePlanDigest("lifecycle-diff", {
|
|
105
109
|
env: args.env,
|
|
106
110
|
live: args.live === true,
|
|
@@ -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
|
*/
|