@intentius/chant 0.18.9 → 0.18.12
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/commands/build.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.d.ts +8 -0
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/graph-ir.d.ts +15 -3
- package/dist/graph-ir.d.ts.map +1 -1
- package/dist/lifecycle/rollback.d.ts +23 -0
- package/dist/lifecycle/rollback.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/cli/commands/build.test.ts +33 -0
- package/src/cli/commands/build.ts +28 -0
- package/src/cli/handlers/lifecycle.ts +33 -0
- package/src/cli/handlers/run.ts +43 -0
- package/src/cli/main.ts +2 -1
- package/src/graph-ir.test.ts +35 -0
- package/src/graph-ir.ts +60 -11
- package/src/lifecycle/rollback.test.ts +25 -0
- package/src/lifecycle/rollback.ts +83 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/build.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAoB,MAAM,kBAAkB,CAAC;AACrE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AASnD;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,uCAAuC;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,4CAA4C;IAC5C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oBAAoB;IACpB,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,2CAA2C;IAC3C,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,8CAA8C;IAC9C,OAAO,CAAC,EAAE,aAAa,EAAE,CAAC;IAC1B,8BAA8B;IAC9B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;OAIG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,MAAM,EAAE,MAAM,GAAG,SAAS,GACzB;IAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CAoB/C;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,kCAAkC;IAClC,OAAO,EAAE,OAAO,CAAC;IACjB,gCAAgC;IAChC,aAAa,EAAE,MAAM,CAAC;IACtB,uCAAuC;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB,qBAAqB;IACrB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,uBAAuB;IACvB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED;;GAEG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,
|
|
1
|
+
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/build.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAoB,MAAM,kBAAkB,CAAC;AACrE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AASnD;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,uCAAuC;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,4CAA4C;IAC5C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oBAAoB;IACpB,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,2CAA2C;IAC3C,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,8CAA8C;IAC9C,OAAO,CAAC,EAAE,aAAa,EAAE,CAAC;IAC1B,8BAA8B;IAC9B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;OAIG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,MAAM,EAAE,MAAM,GAAG,SAAS,GACzB;IAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CAoB/C;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,kCAAkC;IAClC,OAAO,EAAE,OAAO,CAAC;IACjB,gCAAgC;IAChC,aAAa,EAAE,MAAM,CAAC;IACtB,uCAAuC;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB,qBAAqB;IACrB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,uBAAuB;IACvB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED;;GAEG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CAkR9E;AAiDD;;GAEG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,IAAI,CAIlD;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,IAAI,CAItD;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,YAAY,EACrB,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,GACxC,MAAM,IAAI,CAmCZ"}
|
|
@@ -7,6 +7,14 @@ export declare function runLifecycleSnapshot(ctx: CommandContext): Promise<numbe
|
|
|
7
7
|
* chant lifecycle show <environment> [lexicon]
|
|
8
8
|
*/
|
|
9
9
|
export declare function runLifecycleShow(ctx: CommandContext): Promise<number>;
|
|
10
|
+
/**
|
|
11
|
+
* chant lifecycle rollback [<environment>] --to <ref>
|
|
12
|
+
*
|
|
13
|
+
* Open a PR restoring `sourceDir` to a prior git revision (#873). Source-level,
|
|
14
|
+
* reviewable, no cloud mutation — a human merges, then a gated apply rolls the
|
|
15
|
+
* env back. The `<env>` positional is PR context.
|
|
16
|
+
*/
|
|
17
|
+
export declare function runLifecycleRollback(ctx: CommandContext): Promise<number>;
|
|
10
18
|
/**
|
|
11
19
|
* chant lifecycle diff <environment> [lexicon]
|
|
12
20
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../../../src/cli/handlers/lifecycle.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../../../src/cli/handlers/lifecycle.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAoBlD;;GAEG;AACH,wBAAsB,oBAAoB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAwE/E;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAoC3E;AAED;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAuB/E;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CA+C3E;AA6TD;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CA6G3E;AAED;;GAEG;AACH,wBAAsB,eAAe,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAiB1E;AAED;;GAEG;AACH,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAM9E;AAkBD;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CA+C/E"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../../src/cli/handlers/run.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AA0ClD,wBAAsB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,EAAE,WAAW,EAAE,MAAM;;;;GAO5F;
|
|
1
|
+
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../../src/cli/handlers/run.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AA0ClD,wBAAsB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,EAAE,WAAW,EAAE,MAAM;;;;GAO5F;AA8BD,wBAAsB,SAAS,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAgEpE;AAoFD,wBAAsB,WAAW,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAuCtE;AA0CD,wBAAsB,QAAQ,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CA6CnE;AA2DD;;;;;;;;GAQG;AACH,wBAAsB,WAAW,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAwBtE;AAID,2LAA2L;AAC3L,wBAAsB,WAAW,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CA8BtE;AAmCD;;;;;;;;;;;;GAYG;AACH,wBAAsB,KAAK,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAchE;AA6ED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAsB,eAAe,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CA+D1E;AAkOD;;;;GAIG;AACH,wBAAsB,UAAU,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAiFrE;AAuLD,wBAAgB,YAAY,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAMjE"}
|
package/dist/graph-ir.d.ts
CHANGED
|
@@ -93,15 +93,27 @@ export interface IRExport {
|
|
|
93
93
|
/** Producer-side attribute the output reads (e.g. "Arn"). */
|
|
94
94
|
attr?: string;
|
|
95
95
|
}
|
|
96
|
+
/** A cross-stack import this stack consumes — a `Parameter` fed from another
|
|
97
|
+
* stack's output at deploy time. A viewer matches an import `name` to another
|
|
98
|
+
* stack's export `name` to draw the cross-stack edge; the parameter's in-stack
|
|
99
|
+
* consumers are ordinary `$ref` edges to `node` (#513). */
|
|
100
|
+
export interface IRImport {
|
|
101
|
+
/** Parameter name — the cross-stack handle (e.g. "clusterArn"). */
|
|
102
|
+
name: string;
|
|
103
|
+
/** Logical name of the parameter node. */
|
|
104
|
+
node: string;
|
|
105
|
+
}
|
|
96
106
|
/** The full graph IR for a project at the default (declarable) detail level. */
|
|
97
107
|
export interface GraphIR {
|
|
98
108
|
nodes: IRNode[];
|
|
99
109
|
edges: IREdge[];
|
|
100
110
|
groups: IRGroups;
|
|
101
|
-
/** Outputs this stack publishes for other stacks to import.
|
|
102
|
-
* nodes (`AWS::CloudFormation::Parameter` etc.); a viewer matches an import's
|
|
103
|
-
* name to an export's name to draw the cross-stack edge to its producer (#513). */
|
|
111
|
+
/** Outputs this stack publishes for other stacks to import (#513). */
|
|
104
112
|
exports?: IRExport[];
|
|
113
|
+
/** Parameters this stack imports from other stacks. A viewer matches an import's
|
|
114
|
+
* `name` to another stack's export `name` to draw the cross-stack edge; the
|
|
115
|
+
* parameter's in-stack consumers are ordinary `$ref` edges to it (#513). */
|
|
116
|
+
imports?: IRImport[];
|
|
105
117
|
}
|
|
106
118
|
/**
|
|
107
119
|
* Build the graph IR from resolved entities (the output of `discover`). Pure and
|
package/dist/graph-ir.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph-ir.d.ts","sourceRoot":"","sources":["../src/graph-ir.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,UAAU,EAAgB,MAAM,cAAc,CAAC;AAI7D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAElD;;;;;;;;GAQG;AAEH,iFAAiF;AACjF,MAAM,WAAW,SAAS;IACxB,8EAA8E;IAC9E,IAAI,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,wEAAwE;AACxE,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;CACd;AAED,iCAAiC;AACjC,MAAM,WAAW,MAAM;IACrB,kEAAkE;IAClE,EAAE,EAAE,MAAM,CAAC;IACX,wCAAwC;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,mDAAmD;IACnD,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,yEAAyE;IACzE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,mCAAmC;IACnC,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CACjC;AAED,qEAAqE;AACrE,MAAM,WAAW,MAAM;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,yCAAyC;IACzC,IAAI,EAAE,KAAK,CAAC;IACZ,8EAA8E;IAC9E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,8EAA8E;IAC9E,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,qFAAqF;AACrF,MAAM,WAAW,QAAQ;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACrC,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACvC;;iFAE6E;IAC7E,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACnC;;;;iFAI6E;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;CACxC;AAED;qFACqF;AACrF,MAAM,WAAW,QAAQ;IACvB,gEAAgE;IAChE,IAAI,EAAE,MAAM,CAAC;IACb,2DAA2D;IAC3D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,6DAA6D;IAC7D,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,gFAAgF;AAChF,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,MAAM,EAAE,QAAQ,CAAC;IACjB;;
|
|
1
|
+
{"version":3,"file":"graph-ir.d.ts","sourceRoot":"","sources":["../src/graph-ir.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Branch name for a rollback PR. Pure. */
|
|
2
|
+
export declare function rollbackBranchName(env: string | undefined, shortRef: string): string;
|
|
3
|
+
/** PR title for a rollback. Pure. */
|
|
4
|
+
export declare function rollbackTitle(env: string | undefined, ref: string): string;
|
|
5
|
+
/** PR body — explains that merging + applying completes the rollback. Pure. */
|
|
6
|
+
export declare function rollbackBody(env: string | undefined, ref: string, sourceDir: string): string;
|
|
7
|
+
export interface RollbackResult {
|
|
8
|
+
/** true when the source already matched the ref — nothing to roll back. */
|
|
9
|
+
noop: boolean;
|
|
10
|
+
branch?: string;
|
|
11
|
+
prUrl?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Open a rollback PR restoring `sourceDir` to `ref`. Isolated worktree; the
|
|
15
|
+
* caller's branch is untouched. Throws on an unknown ref or a git/gh failure.
|
|
16
|
+
*/
|
|
17
|
+
export declare function rollbackToRevision(opts: {
|
|
18
|
+
ref: string;
|
|
19
|
+
env: string | undefined;
|
|
20
|
+
sourceDir: string;
|
|
21
|
+
cwd: string;
|
|
22
|
+
}): Promise<RollbackResult>;
|
|
23
|
+
//# sourceMappingURL=rollback.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rollback.d.ts","sourceRoot":"","sources":["../../src/lifecycle/rollback.ts"],"names":[],"mappings":"AAiBA,2CAA2C;AAC3C,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEpF;AAED,qCAAqC;AACrC,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAE1E;AAED,+EAA+E;AAC/E,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,EAAE,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAO5F;AAED,MAAM,WAAW,cAAc;IAC7B,2EAA2E;IAC3E,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CAAC,IAAI,EAAE;IAC7C,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,GAAG,SAAS,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;CACb,GAAG,OAAO,CAAC,cAAc,CAAC,CA6B1B"}
|
package/package.json
CHANGED
|
@@ -201,6 +201,39 @@ export const testEntity = {
|
|
|
201
201
|
expect(existsSync(join(testDir, "dist", "ops", "alb-deploy", "worker.ts"))).toBe(true);
|
|
202
202
|
});
|
|
203
203
|
|
|
204
|
+
test("op worker files go to <project>/dist/ops even with no --output (#878)", async () => {
|
|
205
|
+
// The generated Op worker must land where `chant run <op> --temporal` reads it
|
|
206
|
+
// (`<project>/dist/ops/<name>/worker.ts`) even when the build has no --output
|
|
207
|
+
// (a Temporal Op project often has no primary resource manifest to route).
|
|
208
|
+
const opSerializer: Serializer = {
|
|
209
|
+
name: "multi",
|
|
210
|
+
rulePrefix: "MULTI",
|
|
211
|
+
serialize: () => ({
|
|
212
|
+
primary: "{}",
|
|
213
|
+
files: {
|
|
214
|
+
"ops/durable-hello/workflow.ts": "// workflow\n",
|
|
215
|
+
"ops/durable-hello/worker.ts": "// worker\n",
|
|
216
|
+
"ops/durable-hello/activities.ts": "// activities\n",
|
|
217
|
+
},
|
|
218
|
+
}),
|
|
219
|
+
};
|
|
220
|
+
await writeFile(
|
|
221
|
+
join(testDir, "infra.ts"),
|
|
222
|
+
`export const x = { [Symbol.for("chant.declarable")]: true, entityType: "X", lexicon: "multi", kind: "resource", props: {}, attributes: {} };`,
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
const result = await buildCommand({
|
|
226
|
+
path: testDir,
|
|
227
|
+
format: "json",
|
|
228
|
+
serializers: [opSerializer],
|
|
229
|
+
// no `output`
|
|
230
|
+
} as BuildOptions);
|
|
231
|
+
|
|
232
|
+
expect(result.errors).toEqual([]);
|
|
233
|
+
expect(existsSync(join(testDir, "dist", "ops", "durable-hello", "worker.ts"))).toBe(true);
|
|
234
|
+
expect(existsSync(join(testDir, "dist", "ops", "durable-hello", "workflow.ts"))).toBe(true);
|
|
235
|
+
});
|
|
236
|
+
|
|
204
237
|
// ── #284 bug 2: array-of-objects must serialize to valid YAML ──────────
|
|
205
238
|
test("yaml format serializes an array of objects to valid, round-trippable YAML", async () => {
|
|
206
239
|
// A serializer that emits a CloudFormation-shaped template with a tag list.
|
|
@@ -264,6 +264,34 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
|
|
|
264
264
|
}
|
|
265
265
|
}
|
|
266
266
|
|
|
267
|
+
// Op worker artifacts (`ops/<name>/{workflow,worker,activities}.ts`) always
|
|
268
|
+
// go to `<project>/dist/ops/` — the fixed location `chant run <op> --temporal`
|
|
269
|
+
// reads (`join(projectPath, "dist", "ops", ...)`) — independent of `--output`,
|
|
270
|
+
// which routes the primary resource manifest. Without this, a bare `chant build`
|
|
271
|
+
// only printed them to stderr and `--output foo.yaml` scattered them next to
|
|
272
|
+
// `foo.yaml`, so the durable-run worker was never where `run --temporal` looks.
|
|
273
|
+
const projectDist = resolve(options.path ?? ".", "dist");
|
|
274
|
+
let opsWritten = 0;
|
|
275
|
+
for (const [filename, content] of [...additionalFiles]) {
|
|
276
|
+
if (!filename.startsWith("ops/")) continue;
|
|
277
|
+
additionalFiles.delete(filename);
|
|
278
|
+
try {
|
|
279
|
+
const targetPath = join(projectDist, filename);
|
|
280
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
281
|
+
writeFileSync(targetPath, content);
|
|
282
|
+
opsWritten += 1;
|
|
283
|
+
} catch (err) {
|
|
284
|
+
errors.push(
|
|
285
|
+
formatError({
|
|
286
|
+
message: `Failed to write Op worker file ${filename}: ${err instanceof Error ? err.message : String(err)}`,
|
|
287
|
+
}),
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (opsWritten > 0) {
|
|
292
|
+
console.error(formatInfo(`Wrote ${opsWritten} Op worker file(s) under ${join(options.path ?? ".", "dist", "ops")}/`));
|
|
293
|
+
}
|
|
294
|
+
|
|
267
295
|
if (options.output) {
|
|
268
296
|
// Write to file — ensure parent directories exist for both the primary
|
|
269
297
|
// output path and any nested additional-file paths (e.g. ops/<name>/...).
|
|
@@ -6,6 +6,7 @@ import { computeBuildDigest, diffDigests } from "../../lifecycle/digest";
|
|
|
6
6
|
import { diffLive, diffLiveArtifacts, diffSnapshots, type LiveDiffResult, type LiveArtifactDiffResult, type SnapshotDiffResult } from "../../lifecycle/live-diff";
|
|
7
7
|
import { buildChangeSet, renderChangeSet, gitlabMrReport, type ChangeSet } from "../../lifecycle/change-set";
|
|
8
8
|
import { affectedStacks } from "../../lifecycle/affected";
|
|
9
|
+
import { rollbackToRevision } from "../../lifecycle/rollback";
|
|
9
10
|
import { loadChantConfig } from "../../config";
|
|
10
11
|
import { formatError, formatWarning, formatSuccess, formatBold } from "../format";
|
|
11
12
|
import type { CommandContext } from "../registry";
|
|
@@ -146,6 +147,38 @@ export async function runLifecycleShow(ctx: CommandContext): Promise<number> {
|
|
|
146
147
|
return 0;
|
|
147
148
|
}
|
|
148
149
|
|
|
150
|
+
/**
|
|
151
|
+
* chant lifecycle rollback [<environment>] --to <ref>
|
|
152
|
+
*
|
|
153
|
+
* Open a PR restoring `sourceDir` to a prior git revision (#873). Source-level,
|
|
154
|
+
* reviewable, no cloud mutation — a human merges, then a gated apply rolls the
|
|
155
|
+
* env back. The `<env>` positional is PR context.
|
|
156
|
+
*/
|
|
157
|
+
export async function runLifecycleRollback(ctx: CommandContext): Promise<number> {
|
|
158
|
+
const { args } = ctx;
|
|
159
|
+
const environment = args.extraPositional; // optional — PR context/title
|
|
160
|
+
const ref = args.migrateTo; // the --to value
|
|
161
|
+
if (!ref) {
|
|
162
|
+
console.error(formatError({ message: "Target revision required: chant lifecycle rollback [<env>] --to <ref>" }));
|
|
163
|
+
return 1;
|
|
164
|
+
}
|
|
165
|
+
const { config } = await loadChantConfig(resolve("."));
|
|
166
|
+
const sourceDir = config.sourceDir ?? ".";
|
|
167
|
+
try {
|
|
168
|
+
const result = await rollbackToRevision({ ref, env: environment, sourceDir, cwd: resolve(".") });
|
|
169
|
+
if (result.noop) {
|
|
170
|
+
console.error(formatSuccess(`${sourceDir} already matches ${ref} — nothing to roll back`));
|
|
171
|
+
return 0;
|
|
172
|
+
}
|
|
173
|
+
console.log(result.prUrl); // the PR URL — the consumer (behold) reads this from stdout
|
|
174
|
+
console.error(formatSuccess(`Opened rollback PR on ${result.branch}`));
|
|
175
|
+
return 0;
|
|
176
|
+
} catch (err) {
|
|
177
|
+
console.error(formatError({ message: `rollback failed — ${err instanceof Error ? err.message : String(err)}` }));
|
|
178
|
+
return 1;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
149
182
|
/**
|
|
150
183
|
* chant lifecycle diff <environment> [lexicon]
|
|
151
184
|
*/
|
package/src/cli/handlers/run.ts
CHANGED
|
@@ -59,6 +59,32 @@ export async function makeTemporalClient(profileName: string | undefined, projec
|
|
|
59
59
|
return { client, profile, config };
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Register the Keyword search attributes the generated workflow upserts
|
|
64
|
+
* (`OpName`, `Phase`, plus any op-declared) on the namespace, so the first
|
|
65
|
+
* workflow task does not fail with `BadSearchAttributes`. Registered one at a
|
|
66
|
+
* time so an already-present attribute (`ALREADY_EXISTS`) does not block the
|
|
67
|
+
* rest. Only used for the autoStart dev server — a real server (autoStart=false)
|
|
68
|
+
* is expected to have these pre-registered (it may reject registration).
|
|
69
|
+
*/
|
|
70
|
+
async function ensureSearchAttributes(client: unknown, namespace: string, names: string[]): Promise<void> {
|
|
71
|
+
const operatorService = (
|
|
72
|
+
client as { connection?: { operatorService?: { addSearchAttributes?: (r: unknown) => Promise<unknown> } } }
|
|
73
|
+
).connection?.operatorService;
|
|
74
|
+
if (!operatorService?.addSearchAttributes) return;
|
|
75
|
+
const KEYWORD = 2; // Temporal IndexedValueType KEYWORD
|
|
76
|
+
for (const name of names) {
|
|
77
|
+
try {
|
|
78
|
+
await operatorService.addSearchAttributes({ namespace, searchAttributes: { [name]: KEYWORD } });
|
|
79
|
+
} catch (err) {
|
|
80
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
81
|
+
if (!/already|exists/i.test(msg)) {
|
|
82
|
+
console.error(formatWarning({ message: `Could not register search attribute "${name}": ${msg}` }));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
62
88
|
// ── chant run list ──────────────────────────────────���─────────────────────────
|
|
63
89
|
|
|
64
90
|
export async function runOpList(ctx: CommandContext): Promise<number> {
|
|
@@ -803,6 +829,12 @@ async function runComponentTemporal(ctx: CommandContext, selector: string): Prom
|
|
|
803
829
|
return 1;
|
|
804
830
|
}
|
|
805
831
|
|
|
832
|
+
// Register the search attributes the generated component workflow upserts
|
|
833
|
+
// (ComponentName/Phase) on the autoStart dev server (mirrors runOpTemporal).
|
|
834
|
+
if (profile.autoStart) {
|
|
835
|
+
await ensureSearchAttributes(client, profile.namespace, ["ComponentName", "Phase"]);
|
|
836
|
+
}
|
|
837
|
+
|
|
806
838
|
const profileName = ctx.args.profile ??
|
|
807
839
|
(((chantConfig as Record<string, unknown>).temporal as Record<string, unknown> | undefined)?.defaultProfile as string | undefined) ??
|
|
808
840
|
"local";
|
|
@@ -1098,6 +1130,17 @@ async function runOpTemporal(ctx: CommandContext): Promise<number> {
|
|
|
1098
1130
|
return 1;
|
|
1099
1131
|
}
|
|
1100
1132
|
|
|
1133
|
+
// On the autoStart dev server, register the search attributes the generated
|
|
1134
|
+
// workflow upserts (OpName/Phase + any op-declared) so the first workflow task
|
|
1135
|
+
// does not fail with BadSearchAttributes.
|
|
1136
|
+
if (profile.autoStart) {
|
|
1137
|
+
await ensureSearchAttributes(client, profile.namespace, [
|
|
1138
|
+
"OpName",
|
|
1139
|
+
"Phase",
|
|
1140
|
+
...Object.keys((config as { searchAttributes?: Record<string, string> }).searchAttributes ?? {}),
|
|
1141
|
+
]);
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1101
1144
|
// Spawn worker process
|
|
1102
1145
|
const profileName = ctx.args.profile ??
|
|
1103
1146
|
(((chantConfig as Record<string, unknown>).temporal as Record<string, unknown> | undefined)?.defaultProfile as string | undefined) ??
|
package/src/cli/main.ts
CHANGED
|
@@ -16,7 +16,7 @@ import { runInit, runInitLexicon } from "./handlers/init";
|
|
|
16
16
|
import { runList, runDescribe, runImport, runAudit, runUpdate, runDoctor } from "./handlers/misc";
|
|
17
17
|
import { runVendor } from "./handlers/vendor";
|
|
18
18
|
import { runMigrate } from "./handlers/migrate";
|
|
19
|
-
import { runLifecycleSnapshot, runLifecycleShow, runLifecycleDiff, runLifecyclePlan, runLifecycleAffected, runLifecycleLog, runLifecycleUnknown } from "./handlers/lifecycle";
|
|
19
|
+
import { runLifecycleSnapshot, runLifecycleShow, runLifecycleDiff, runLifecycleRollback, runLifecyclePlan, runLifecycleAffected, runLifecycleLog, runLifecycleUnknown } from "./handlers/lifecycle";
|
|
20
20
|
import { runComponentsStatus, runComponentsReleaseRecord, runComponentsUnknown } from "./handlers/components";
|
|
21
21
|
import { runGraph } from "./handlers/graph";
|
|
22
22
|
import { runOp, runOpList, runOpStatus, runOpSignal, runOpCancel, runOpLog } from "./handlers/run";
|
|
@@ -445,6 +445,7 @@ const registry: CommandDef[] = [
|
|
|
445
445
|
{ name: "lifecycle snapshot", requiresPlugins: true, handler: runLifecycleSnapshot },
|
|
446
446
|
{ name: "lifecycle show", handler: runLifecycleShow },
|
|
447
447
|
{ name: "lifecycle diff", requiresPlugins: true, handler: runLifecycleDiff },
|
|
448
|
+
{ name: "lifecycle rollback", handler: runLifecycleRollback },
|
|
448
449
|
{ name: "lifecycle plan", requiresPlugins: true, handler: runLifecyclePlan },
|
|
449
450
|
{ name: "lifecycle affected", requiresPlugins: true, handler: runLifecycleAffected },
|
|
450
451
|
{ name: "lifecycle log", handler: runLifecycleLog },
|
package/src/graph-ir.test.ts
CHANGED
|
@@ -5,11 +5,17 @@ import { AttrRef } from "./attrref";
|
|
|
5
5
|
import { LexiconOutput } from "./lexicon-output";
|
|
6
6
|
import { resolveAttrRefs } from "./discovery/resolve";
|
|
7
7
|
import { setProvenance } from "./provenance";
|
|
8
|
+
import { INTRINSIC_MARKER } from "./intrinsic";
|
|
8
9
|
|
|
9
10
|
function decl<T extends object>(base: T): Declarable & T {
|
|
10
11
|
return { [DECLARABLE_MARKER]: true, ...base } as Declarable & T;
|
|
11
12
|
}
|
|
12
13
|
|
|
14
|
+
/** A `Ref`-style intrinsic (CloudFormation `{ Ref: name }` shape). */
|
|
15
|
+
function ref(targetName: string): object {
|
|
16
|
+
return { [INTRINSIC_MARKER]: true, toJSON: () => ({ Ref: targetName }) };
|
|
17
|
+
}
|
|
18
|
+
|
|
13
19
|
describe("buildGraphIr", () => {
|
|
14
20
|
test("emits a node per resource with kind, lexicon, and scrubbed attrs", () => {
|
|
15
21
|
const vpc = decl({ lexicon: "gcp", entityType: "Vpc", props: { autoCreateSubnetworks: false } });
|
|
@@ -26,6 +32,35 @@ describe("buildGraphIr", () => {
|
|
|
26
32
|
expect(ir.nodes[0].attrs).not.toHaveProperty("entityType");
|
|
27
33
|
});
|
|
28
34
|
|
|
35
|
+
test("resolves a Ref-to-node intrinsic into an edge + $ref, and lists parameter imports (#513)", () => {
|
|
36
|
+
const cluster = decl({ lexicon: "aws", entityType: "AWS::CloudFormation::Parameter" });
|
|
37
|
+
const service = decl({ lexicon: "aws", entityType: "AWS::ECS::Service", props: { Cluster: ref("clusterArn") } });
|
|
38
|
+
const entities = new Map<string, Declarable>([
|
|
39
|
+
["clusterArn", cluster],
|
|
40
|
+
["service", service],
|
|
41
|
+
]);
|
|
42
|
+
resolveAttrRefs(entities);
|
|
43
|
+
|
|
44
|
+
const ir = buildGraphIr(entities);
|
|
45
|
+
// The Ref becomes a real dependency edge, not a dropped intrinsic.
|
|
46
|
+
expect(ir.edges).toContainEqual({ from: "service", to: "clusterArn", kind: "ref", viaAttr: "Cluster" });
|
|
47
|
+
// attrs mirror the edge as a $ref envelope, not {$intrinsic}.
|
|
48
|
+
expect(ir.nodes.find((n) => n.id === "service")!.attrs.Cluster).toEqual({ $ref: "clusterArn" });
|
|
49
|
+
// Parameter nodes are surfaced as import handles.
|
|
50
|
+
expect(ir.imports).toEqual([{ name: "clusterArn", node: "clusterArn" }]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("a Ref to a non-node (pseudo-parameter) stays an opaque intrinsic — no phantom edge (#513)", () => {
|
|
54
|
+
const service = decl({ lexicon: "aws", entityType: "AWS::ECS::Service", props: { Region: ref("AWS::Region") } });
|
|
55
|
+
const entities = new Map<string, Declarable>([["service", service]]);
|
|
56
|
+
resolveAttrRefs(entities);
|
|
57
|
+
|
|
58
|
+
const ir = buildGraphIr(entities);
|
|
59
|
+
expect(ir.edges).toEqual([]);
|
|
60
|
+
expect(ir.nodes[0].attrs.Region).toEqual({ $intrinsic: true });
|
|
61
|
+
expect(ir.imports).toBeUndefined();
|
|
62
|
+
});
|
|
63
|
+
|
|
29
64
|
test("derives ref edges labelled with the consumer property", () => {
|
|
30
65
|
const vpc = decl({ lexicon: "gcp", entityType: "Vpc" });
|
|
31
66
|
const subnet = decl({ lexicon: "gcp", entityType: "Subnet", props: { network: new AttrRef(vpc, "id") } });
|
package/src/graph-ir.ts
CHANGED
|
@@ -107,15 +107,28 @@ export interface IRExport {
|
|
|
107
107
|
attr?: string;
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
/** A cross-stack import this stack consumes — a `Parameter` fed from another
|
|
111
|
+
* stack's output at deploy time. A viewer matches an import `name` to another
|
|
112
|
+
* stack's export `name` to draw the cross-stack edge; the parameter's in-stack
|
|
113
|
+
* consumers are ordinary `$ref` edges to `node` (#513). */
|
|
114
|
+
export interface IRImport {
|
|
115
|
+
/** Parameter name — the cross-stack handle (e.g. "clusterArn"). */
|
|
116
|
+
name: string;
|
|
117
|
+
/** Logical name of the parameter node. */
|
|
118
|
+
node: string;
|
|
119
|
+
}
|
|
120
|
+
|
|
110
121
|
/** The full graph IR for a project at the default (declarable) detail level. */
|
|
111
122
|
export interface GraphIR {
|
|
112
123
|
nodes: IRNode[];
|
|
113
124
|
edges: IREdge[];
|
|
114
125
|
groups: IRGroups;
|
|
115
|
-
/** Outputs this stack publishes for other stacks to import.
|
|
116
|
-
* nodes (`AWS::CloudFormation::Parameter` etc.); a viewer matches an import's
|
|
117
|
-
* name to an export's name to draw the cross-stack edge to its producer (#513). */
|
|
126
|
+
/** Outputs this stack publishes for other stacks to import (#513). */
|
|
118
127
|
exports?: IRExport[];
|
|
128
|
+
/** Parameters this stack imports from other stacks. A viewer matches an import's
|
|
129
|
+
* `name` to another stack's export `name` to draw the cross-stack edge; the
|
|
130
|
+
* parameter's in-stack consumers are ordinary `$ref` edges to it (#513). */
|
|
131
|
+
imports?: IRImport[];
|
|
119
132
|
}
|
|
120
133
|
|
|
121
134
|
/** A node is anything that serializes to a resource — not a property or output. */
|
|
@@ -134,6 +147,22 @@ function relFile(file: string | undefined, projectPath?: string): string | undef
|
|
|
134
147
|
return file;
|
|
135
148
|
}
|
|
136
149
|
|
|
150
|
+
/** The logical name a `Ref`-style intrinsic points at, or undefined. Lexicon-
|
|
151
|
+
* agnostic: reads the intrinsic's `toJSON()` for the CloudFormation `{ Ref: name }`
|
|
152
|
+
* shape (aws `RefIntrinsic` etc.). A `Ref` to a node is a real dependency edge,
|
|
153
|
+
* not an opaque intrinsic (#513) — so we resolve it rather than flatten it. */
|
|
154
|
+
function refIntrinsicTarget(value: unknown): string | undefined {
|
|
155
|
+
try {
|
|
156
|
+
const json = (value as { toJSON?: () => unknown }).toJSON?.();
|
|
157
|
+
if (json && typeof json === "object" && typeof (json as { Ref?: unknown }).Ref === "string") {
|
|
158
|
+
return (json as { Ref: string }).Ref;
|
|
159
|
+
}
|
|
160
|
+
} catch {
|
|
161
|
+
// Unresolvable at graph-build time — leave it as an opaque intrinsic.
|
|
162
|
+
}
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
|
|
137
166
|
/** Resolve the producer (logical name) an AttrRef points at. */
|
|
138
167
|
function refTarget(ref: AttrRef, reverse: Map<object, string>): string | undefined {
|
|
139
168
|
// Nested refs (under `props`) aren't assigned a logical name by resolveAttrRefs,
|
|
@@ -143,7 +172,7 @@ function refTarget(ref: AttrRef, reverse: Map<object, string>): string | undefin
|
|
|
143
172
|
}
|
|
144
173
|
|
|
145
174
|
/** Project a value into a JSON-safe, scrubbed form. References become `{ $ref }`. */
|
|
146
|
-
function project(value: unknown, seen: Set<unknown>, reverse: Map<object, string>): unknown {
|
|
175
|
+
function project(value: unknown, seen: Set<unknown>, reverse: Map<object, string>, nodeIds: Set<string>): unknown {
|
|
147
176
|
if (value === null) return null;
|
|
148
177
|
const t = typeof value;
|
|
149
178
|
if (t === "string" || t === "number" || t === "boolean") return value;
|
|
@@ -157,8 +186,12 @@ function project(value: unknown, seen: Set<unknown>, reverse: Map<object, string
|
|
|
157
186
|
const to = refTarget(value, reverse);
|
|
158
187
|
return { $ref: to ? `${to}.${value.attribute}` : value.attribute } satisfies AttrRefEnvelope;
|
|
159
188
|
}
|
|
160
|
-
// Other intrinsics (interpolations, pseudo-parameters): mark, don't inline.
|
|
161
189
|
if ((value as Record<symbol, unknown>)[INTRINSIC_MARKER] === true) {
|
|
190
|
+
// A `Ref` to a known node is a real reference — surface it as `$ref` so attrs
|
|
191
|
+
// match the edge (#513). Other intrinsics (interpolations, pseudo-parameters,
|
|
192
|
+
// Ref to a non-node) stay opaque.
|
|
193
|
+
const to = refIntrinsicTarget(value);
|
|
194
|
+
if (to && nodeIds.has(to)) return { $ref: to } satisfies AttrRefEnvelope;
|
|
162
195
|
return { $intrinsic: true };
|
|
163
196
|
}
|
|
164
197
|
|
|
@@ -168,20 +201,20 @@ function project(value: unknown, seen: Set<unknown>, reverse: Map<object, string
|
|
|
168
201
|
// Nested declarables (e.g. an inlined property-kind resource) keep their
|
|
169
202
|
// config in a non-enumerable `props` bag, like top-level nodes — project that.
|
|
170
203
|
if (isDeclarable(value)) {
|
|
171
|
-
const out = projectConfig(value, seen, reverse);
|
|
204
|
+
const out = projectConfig(value, seen, reverse, nodeIds);
|
|
172
205
|
seen.delete(value);
|
|
173
206
|
return out;
|
|
174
207
|
}
|
|
175
208
|
|
|
176
209
|
if (Array.isArray(value)) {
|
|
177
|
-
const out = value.map((v) => project(v, seen, reverse)).filter((v) => v !== undefined);
|
|
210
|
+
const out = value.map((v) => project(v, seen, reverse, nodeIds)).filter((v) => v !== undefined);
|
|
178
211
|
seen.delete(value);
|
|
179
212
|
return out;
|
|
180
213
|
}
|
|
181
214
|
|
|
182
215
|
const out: Record<string, unknown> = {};
|
|
183
216
|
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
|
184
|
-
const p = project(v, seen, reverse);
|
|
217
|
+
const p = project(v, seen, reverse, nodeIds);
|
|
185
218
|
if (p !== undefined) out[k] = p;
|
|
186
219
|
}
|
|
187
220
|
seen.delete(value);
|
|
@@ -212,10 +245,11 @@ function projectConfig(
|
|
|
212
245
|
entity: Declarable,
|
|
213
246
|
seen: Set<unknown>,
|
|
214
247
|
reverse: Map<object, string>,
|
|
248
|
+
nodeIds: Set<string>,
|
|
215
249
|
): Record<string, unknown> {
|
|
216
250
|
const out: Record<string, unknown> = {};
|
|
217
251
|
for (const [k, v] of configRoots(entity)) {
|
|
218
|
-
const p = project(v, seen, reverse);
|
|
252
|
+
const p = project(v, seen, reverse, nodeIds);
|
|
219
253
|
if (p !== undefined) out[k] = p;
|
|
220
254
|
}
|
|
221
255
|
return out;
|
|
@@ -239,7 +273,13 @@ function collectEdges(
|
|
|
239
273
|
}
|
|
240
274
|
return;
|
|
241
275
|
}
|
|
242
|
-
if ((value as Record<symbol, unknown>)[INTRINSIC_MARKER] === true)
|
|
276
|
+
if ((value as Record<symbol, unknown>)[INTRINSIC_MARKER] === true) {
|
|
277
|
+
// A `Ref` to a known node (a Parameter, or a resource) is a real dependency
|
|
278
|
+
// — resolve it to an edge instead of dropping it as an opaque intrinsic (#513).
|
|
279
|
+
const to = refIntrinsicTarget(value);
|
|
280
|
+
if (to && to !== from && nodeIds.has(to)) edges.push({ from, to, kind: "ref", viaAttr });
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
243
283
|
if (seen.has(value)) return;
|
|
244
284
|
seen.add(value);
|
|
245
285
|
if (Array.isArray(value)) {
|
|
@@ -287,7 +327,7 @@ export function buildGraphIr(
|
|
|
287
327
|
id: name,
|
|
288
328
|
kind: entity.entityType,
|
|
289
329
|
lexicon: entity.lexicon,
|
|
290
|
-
attrs: projectConfig(entity, new Set(), reverse),
|
|
330
|
+
attrs: projectConfig(entity, new Set(), reverse, nodeIds),
|
|
291
331
|
};
|
|
292
332
|
if (prov?.composite) node.compositeParent = prov.composite;
|
|
293
333
|
if (prov?.compositeInstance) node.compositeInstance = prov.compositeInstance;
|
|
@@ -339,8 +379,17 @@ export function buildGraphIr(
|
|
|
339
379
|
}
|
|
340
380
|
exports.sort((a, b) => a.name.localeCompare(b.name));
|
|
341
381
|
|
|
382
|
+
// Cross-stack imports: parameter nodes are the handles this stack imports by,
|
|
383
|
+
// matched to another stack's export names (#513). Detected by kind — the aws
|
|
384
|
+
// `Parameter` is the cross-stack input mechanism.
|
|
385
|
+
const imports: IRImport[] = nodes
|
|
386
|
+
.filter((n) => /(^|::)Parameter$/i.test(n.kind))
|
|
387
|
+
.map((n) => ({ name: n.id, node: n.id }))
|
|
388
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
389
|
+
|
|
342
390
|
const ir: GraphIR = { nodes, edges, groups };
|
|
343
391
|
if (exports.length) ir.exports = exports;
|
|
392
|
+
if (imports.length) ir.imports = imports;
|
|
344
393
|
return ir;
|
|
345
394
|
}
|
|
346
395
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import { rollbackBranchName, rollbackTitle, rollbackBody } from "./rollback";
|
|
3
|
+
|
|
4
|
+
describe("rollback helpers (#873)", () => {
|
|
5
|
+
test("branch name includes env and short ref", () => {
|
|
6
|
+
expect(rollbackBranchName("prod", "a1b2c3d")).toBe("chant/rollback-prod-a1b2c3d");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
test("branch name falls back to 'src' when env is absent", () => {
|
|
10
|
+
expect(rollbackBranchName(undefined, "a1b2c3d")).toBe("chant/rollback-src-a1b2c3d");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("title names the env and ref", () => {
|
|
14
|
+
expect(rollbackTitle("prod", "v1.2.0")).toBe("rollback prod source to v1.2.0");
|
|
15
|
+
expect(rollbackTitle(undefined, "v1.2.0")).toBe("rollback source to v1.2.0");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("body references the sourceDir, ref, and the gated-apply step", () => {
|
|
19
|
+
const body = rollbackBody("prod", "abc123", "src");
|
|
20
|
+
expect(body).toContain("`src`");
|
|
21
|
+
expect(body).toContain("`abc123`");
|
|
22
|
+
expect(body).toContain("the prod environment");
|
|
23
|
+
expect(body).toMatch(/approval gate|Sync|apply/i);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lifecycle rollback (#873) — open a PR that restores the source to a prior git
|
|
3
|
+
* revision. Rollback = make an env match a past state of the declared source; the
|
|
4
|
+
* PR's diff *is* the rollback delta. No cloud mutation here — a human merges, then
|
|
5
|
+
* the existing gated apply rolls the env back.
|
|
6
|
+
*
|
|
7
|
+
* Runs entirely in an isolated `git worktree` (like {@link ./affected}), so the
|
|
8
|
+
* caller's working branch/tree is never touched — important because the consumer
|
|
9
|
+
* (behold) serves a live checkout.
|
|
10
|
+
*/
|
|
11
|
+
import { execFile } from "node:child_process";
|
|
12
|
+
import { promisify } from "node:util";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
|
|
16
|
+
const execFileAsync = promisify(execFile);
|
|
17
|
+
|
|
18
|
+
/** Branch name for a rollback PR. Pure. */
|
|
19
|
+
export function rollbackBranchName(env: string | undefined, shortRef: string): string {
|
|
20
|
+
return `chant/rollback-${env ?? "src"}-${shortRef}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** PR title for a rollback. Pure. */
|
|
24
|
+
export function rollbackTitle(env: string | undefined, ref: string): string {
|
|
25
|
+
return `rollback ${env ? env + " " : ""}source to ${ref}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** PR body — explains that merging + applying completes the rollback. Pure. */
|
|
29
|
+
export function rollbackBody(env: string | undefined, ref: string, sourceDir: string): string {
|
|
30
|
+
const target = env ? `the ${env} environment` : "the environment";
|
|
31
|
+
return (
|
|
32
|
+
`Restores \`${sourceDir}\` to its state at \`${ref}\`.\n\n` +
|
|
33
|
+
`The diff below is the rollback delta. Merge this PR, then apply (Sync) to roll ${target} back — ` +
|
|
34
|
+
`a destructive apply still passes through its approval gate. No cloud change was made by opening this PR.`
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface RollbackResult {
|
|
39
|
+
/** true when the source already matched the ref — nothing to roll back. */
|
|
40
|
+
noop: boolean;
|
|
41
|
+
branch?: string;
|
|
42
|
+
prUrl?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Open a rollback PR restoring `sourceDir` to `ref`. Isolated worktree; the
|
|
47
|
+
* caller's branch is untouched. Throws on an unknown ref or a git/gh failure.
|
|
48
|
+
*/
|
|
49
|
+
export async function rollbackToRevision(opts: {
|
|
50
|
+
ref: string;
|
|
51
|
+
env: string | undefined;
|
|
52
|
+
sourceDir: string;
|
|
53
|
+
cwd: string;
|
|
54
|
+
}): Promise<RollbackResult> {
|
|
55
|
+
const { ref, env, sourceDir, cwd } = opts;
|
|
56
|
+
const git = (args: string[], wd: string): Promise<{ stdout: string }> => execFileAsync("git", args, { cwd: wd });
|
|
57
|
+
|
|
58
|
+
const repoRoot = (await git(["rev-parse", "--show-toplevel"], cwd)).stdout.trim();
|
|
59
|
+
// Verify the ref resolves to a commit (throws with a clear message otherwise).
|
|
60
|
+
await execFileAsync("git", ["rev-parse", "--verify", `${ref}^{commit}`], { cwd: repoRoot });
|
|
61
|
+
const shortRef = (await git(["rev-parse", "--short", ref], repoRoot)).stdout.trim();
|
|
62
|
+
const branch = rollbackBranchName(env, shortRef);
|
|
63
|
+
const wt = join(tmpdir(), `chant-rollback-${shortRef}-${process.pid}`);
|
|
64
|
+
|
|
65
|
+
await git(["worktree", "add", wt, "-b", branch, "HEAD"], repoRoot);
|
|
66
|
+
try {
|
|
67
|
+
// Restore just the source tree to the target revision (stages the changes).
|
|
68
|
+
await git(["checkout", ref, "--", sourceDir], wt);
|
|
69
|
+
const staged = (await git(["status", "--porcelain", "--", sourceDir], wt)).stdout.trim();
|
|
70
|
+
if (!staged) return { noop: true };
|
|
71
|
+
|
|
72
|
+
await git(["commit", "-m", rollbackTitle(env, ref)], wt);
|
|
73
|
+
await git(["push", "-u", "origin", branch], wt);
|
|
74
|
+
const { stdout } = await execFileAsync(
|
|
75
|
+
"gh",
|
|
76
|
+
["pr", "create", "--title", rollbackTitle(env, ref), "--body", rollbackBody(env, ref, sourceDir), "--head", branch],
|
|
77
|
+
{ cwd: wt },
|
|
78
|
+
);
|
|
79
|
+
return { noop: false, branch, prUrl: stdout.trim() };
|
|
80
|
+
} finally {
|
|
81
|
+
await git(["worktree", "remove", "--force", wt], repoRoot).catch(() => {});
|
|
82
|
+
}
|
|
83
|
+
}
|