@intentius/chant 0.18.29 → 0.18.31
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/components.d.ts +27 -0
- package/dist/cli/handlers/components.d.ts.map +1 -1
- package/dist/cli/handlers/graph.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +2 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/components/cli-support.d.ts +13 -0
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/driver.d.ts +22 -1
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/run-progress.d.ts +94 -0
- package/dist/components/run-progress.d.ts.map +1 -0
- package/dist/lifecycle/observe.d.ts +13 -0
- package/dist/lifecycle/observe.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/handlers/components.ts +8 -2
- package/src/cli/handlers/graph.test.ts +92 -0
- package/src/cli/handlers/graph.ts +27 -1
- package/src/cli/handlers/run.test.ts +54 -0
- package/src/cli/handlers/run.ts +9 -1
- package/src/cli/main.test.ts +14 -0
- package/src/cli/main.ts +7 -0
- package/src/cli/registry.ts +2 -0
- package/src/components/cli-support.test.ts +92 -1
- package/src/components/cli-support.ts +36 -2
- package/src/components/driver.test.ts +190 -0
- package/src/components/driver.ts +100 -33
- package/src/components/run-progress.ts +117 -0
- package/src/lifecycle/observe.test.ts +48 -0
- package/src/lifecycle/observe.ts +37 -8
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured progress events for `chant run --components <sel> --progress-json`
|
|
3
|
+
* (behold roadmap M3: a consumer that renders live wave/phase/step progress
|
|
4
|
+
* instead of tailing raw logs).
|
|
5
|
+
*
|
|
6
|
+
* This is purely additive observation over the interpret driver's existing
|
|
7
|
+
* wave -> component -> phase -> step execution loop (./driver.ts): an
|
|
8
|
+
* optional `onProgress` callback, threaded through
|
|
9
|
+
* `runInterpretDriver`/`runComponentDeploy`/`runPhase` (and the CLI's
|
|
10
|
+
* single-component path, ./cli-support.ts's `runComponents`), that never
|
|
11
|
+
* changes run ordering, gating, `onFailure`, rollback, or exit codes — it
|
|
12
|
+
* only reports what already happened, as it happens. See ./driver.ts's
|
|
13
|
+
* module doc for what actually executes; streaming progress on the *durable*
|
|
14
|
+
* (Temporal) path is a separate, later concern (Temporal already exposes
|
|
15
|
+
* durable run state via `chant run status`/`log`).
|
|
16
|
+
*
|
|
17
|
+
* `RunProgressEvent` is a discriminated union on `type`, one JSON object per
|
|
18
|
+
* NDJSON line (see `ndjsonProgressSink` below and
|
|
19
|
+
* ../cli/handlers/run.ts's `--progress-json` wiring).
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** Terminal status for a wave/component/phase/run — mirrors DriverStepRecord's ok/fail split, collapsed to the two outcomes a consumer renders progress against. */
|
|
23
|
+
export type RunProgressStatus = "ok" | "failed";
|
|
24
|
+
|
|
25
|
+
/** The run is about to start. `waves` are the parallel-safe waves the run will attempt, in order (see resolveComponentGraph) — 1-based wave numbers in every other event index into this array. */
|
|
26
|
+
export interface RunStartEvent {
|
|
27
|
+
type: "run-start";
|
|
28
|
+
waves: string[][];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** A wave is about to start; its components may run concurrently (independent components share a wave). */
|
|
32
|
+
export interface WaveStartEvent {
|
|
33
|
+
type: "wave-start";
|
|
34
|
+
/** 1-based wave number. */
|
|
35
|
+
wave: number;
|
|
36
|
+
components: string[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** A component within a wave is about to start its `deploy` composition. */
|
|
40
|
+
export interface ComponentStartEvent {
|
|
41
|
+
type: "component-start";
|
|
42
|
+
wave: number;
|
|
43
|
+
component: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** A named phase of a component's composition is about to run its steps. Emitted for nested (fan-out) phases too, keyed by the nested phase's own name. */
|
|
47
|
+
export interface PhaseStartEvent {
|
|
48
|
+
type: "phase-start";
|
|
49
|
+
component: string;
|
|
50
|
+
phase: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** A single capability step within a phase — one event when it starts, one when it settles. */
|
|
54
|
+
export interface StepEvent {
|
|
55
|
+
type: "step";
|
|
56
|
+
component: string;
|
|
57
|
+
phase: string;
|
|
58
|
+
step: string;
|
|
59
|
+
status: "running" | "ok" | "failed";
|
|
60
|
+
/** Present only alongside `status: "failed"`, when the capability threw. */
|
|
61
|
+
error?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** A phase finished — `ok` if every step in it succeeded, `failed` if any step failed (the phase's remaining steps were skipped, per the driver's fail-fast-within-a-phase semantics). */
|
|
65
|
+
export interface PhaseDoneEvent {
|
|
66
|
+
type: "phase-done";
|
|
67
|
+
component: string;
|
|
68
|
+
phase: string;
|
|
69
|
+
status: RunProgressStatus;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** A component's `deploy` composition finished (after any saga rollback + component-level `rollback` phases the driver ran on failure). */
|
|
73
|
+
export interface ComponentDoneEvent {
|
|
74
|
+
type: "component-done";
|
|
75
|
+
wave: number;
|
|
76
|
+
component: string;
|
|
77
|
+
status: RunProgressStatus;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** A wave finished — `failed` if any component in it failed, which also stops the run before any later wave starts. */
|
|
81
|
+
export interface WaveDoneEvent {
|
|
82
|
+
type: "wave-done";
|
|
83
|
+
wave: number;
|
|
84
|
+
status: RunProgressStatus;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The run finished. Mirrors the driver's own terminal `DriverRunResult.ok` / exit code. */
|
|
88
|
+
export interface RunDoneEvent {
|
|
89
|
+
type: "run-done";
|
|
90
|
+
status: RunProgressStatus;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export type RunProgressEvent =
|
|
94
|
+
| RunStartEvent
|
|
95
|
+
| WaveStartEvent
|
|
96
|
+
| ComponentStartEvent
|
|
97
|
+
| PhaseStartEvent
|
|
98
|
+
| StepEvent
|
|
99
|
+
| PhaseDoneEvent
|
|
100
|
+
| ComponentDoneEvent
|
|
101
|
+
| WaveDoneEvent
|
|
102
|
+
| RunDoneEvent;
|
|
103
|
+
|
|
104
|
+
/** A sink that receives progress events as they occur. The driver only ever calls this — it never writes to a stream directly, so it stays testable without stdout. */
|
|
105
|
+
export type RunProgressSink = (event: RunProgressEvent) => void;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Build a sink that writes `JSON.stringify(event) + "\n"` to `write` (default:
|
|
109
|
+
* `process.stdout.write`), one line per event, as they happen — the
|
|
110
|
+
* `--progress-json` CLI wiring's sink (../cli/handlers/run.ts). Kept separate
|
|
111
|
+
* from `driver-output.ts`'s end-of-run renderers: this emits *during* the
|
|
112
|
+
* run, one line at a time; `renderDriverJson`/`renderDriverHuman` render the
|
|
113
|
+
* completed `DriverRunResult` once, after the run finishes.
|
|
114
|
+
*/
|
|
115
|
+
export function ndjsonProgressSink(write: (chunk: string) => void = (s) => void process.stdout.write(s)): RunProgressSink {
|
|
116
|
+
return (event) => write(JSON.stringify(event) + "\n");
|
|
117
|
+
}
|
|
@@ -62,4 +62,52 @@ describe("observeResources", () => {
|
|
|
62
62
|
const { observations } = await observeResources("prod", [empty, noObserve], mockBuild());
|
|
63
63
|
expect(observations).toEqual([]);
|
|
64
64
|
});
|
|
65
|
+
|
|
66
|
+
it("with no stacks: calls describeResources exactly once with no `stack` key (unchanged single-stack path)", async () => {
|
|
67
|
+
const calls: Array<{ stack?: string }> = [];
|
|
68
|
+
const plugins = [
|
|
69
|
+
awsPlugin((opts) => {
|
|
70
|
+
calls.push(opts as { stack?: string });
|
|
71
|
+
return { "web-vpc": { type: "AWS::EC2::VPC", status: "CREATE_COMPLETE" } };
|
|
72
|
+
}),
|
|
73
|
+
];
|
|
74
|
+
const { observations } = await observeResources("prod", plugins, mockBuild());
|
|
75
|
+
expect(calls).toHaveLength(1);
|
|
76
|
+
expect(calls[0]).not.toHaveProperty("stack");
|
|
77
|
+
expect(Object.keys(observations[0].resources)).toEqual(["web-vpc"]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("with stacks: [s1, s2] — calls describeResources once per stack and unions the results", async () => {
|
|
81
|
+
const calls: Array<string | undefined> = [];
|
|
82
|
+
const plugins = [
|
|
83
|
+
awsPlugin((opts) => {
|
|
84
|
+
const stack = (opts as { stack?: string }).stack;
|
|
85
|
+
calls.push(stack);
|
|
86
|
+
// Different resources per stack — the multi-stack, per-component case.
|
|
87
|
+
const resources: Record<string, ResourceMetadata> =
|
|
88
|
+
stack === "s1"
|
|
89
|
+
? { "db-a": { type: "AWS::RDS::DBInstance", status: "AVAILABLE" } }
|
|
90
|
+
: { "db-b": { type: "AWS::RDS::DBInstance", status: "AVAILABLE" } };
|
|
91
|
+
return resources;
|
|
92
|
+
}),
|
|
93
|
+
];
|
|
94
|
+
const { observations, errors } = await observeResources("prod", plugins, mockBuild(), { stacks: ["s1", "s2"] });
|
|
95
|
+
expect(calls).toEqual(["s1", "s2"]);
|
|
96
|
+
expect(errors).toEqual([]);
|
|
97
|
+
expect(observations).toHaveLength(1);
|
|
98
|
+
expect(Object.keys(observations[0].resources).sort()).toEqual(["db-a", "db-b"]);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("with an empty stacks array — falls back to the single unstacked call", async () => {
|
|
102
|
+
const calls: Array<{ stack?: string }> = [];
|
|
103
|
+
const plugins = [
|
|
104
|
+
awsPlugin((opts) => {
|
|
105
|
+
calls.push(opts as { stack?: string });
|
|
106
|
+
return { "web-vpc": { type: "AWS::EC2::VPC", status: "CREATE_COMPLETE" } };
|
|
107
|
+
}),
|
|
108
|
+
];
|
|
109
|
+
await observeResources("prod", plugins, mockBuild(), { stacks: [] });
|
|
110
|
+
expect(calls).toHaveLength(1);
|
|
111
|
+
expect(calls[0]).not.toHaveProperty("stack");
|
|
112
|
+
});
|
|
65
113
|
});
|
package/src/lifecycle/observe.ts
CHANGED
|
@@ -27,14 +27,27 @@ export interface ObserveResult {
|
|
|
27
27
|
* marker channel logs and returns everything (its own contract). Plugins that
|
|
28
28
|
* throw are collected into `errors` and skipped — one failing lexicon never
|
|
29
29
|
* sinks the whole graph.
|
|
30
|
+
*
|
|
31
|
+
* `stacks` (#57) is for a multi-stack, per-component project (e.g. loomster)
|
|
32
|
+
* where there is no single stack named after the environment — AWS's
|
|
33
|
+
* single-stack convention (`lexicons/aws/src/plugin.ts`'s `describeResources`,
|
|
34
|
+
* absent an explicit `stack`) queries a stack that simply doesn't exist there,
|
|
35
|
+
* so the single-call path always observes zero nodes. When `stacks` is
|
|
36
|
+
* present and non-empty, each observing plugin's `describeResources` is
|
|
37
|
+
* called once per stack (same `environment`/`entities`/`entityNames`, only
|
|
38
|
+
* `stack` varies) and the returned resource maps are unioned — a resource
|
|
39
|
+
* appears under whichever stack contains its logical id. When `stacks` is
|
|
40
|
+
* absent or empty, behavior is exactly the single call of before (no `stack`
|
|
41
|
+
* key at all), so a single-stack project is unaffected.
|
|
30
42
|
*/
|
|
31
43
|
export async function observeResources(
|
|
32
44
|
environment: string,
|
|
33
45
|
plugins: ObservationLexicon[],
|
|
34
46
|
buildResult: BuildResult,
|
|
35
|
-
opts?: { owned?: boolean },
|
|
47
|
+
opts?: { owned?: boolean; stacks?: string[] },
|
|
36
48
|
): Promise<ObserveResult> {
|
|
37
49
|
const owned = opts?.owned ?? true;
|
|
50
|
+
const stacks = opts?.stacks ?? [];
|
|
38
51
|
const observations: LiveObservation[] = [];
|
|
39
52
|
const warnings: string[] = [];
|
|
40
53
|
const errors: string[] = [];
|
|
@@ -64,13 +77,29 @@ export async function observeResources(
|
|
|
64
77
|
}
|
|
65
78
|
|
|
66
79
|
try {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
80
|
+
let resources: Record<string, ResourceMetadata>;
|
|
81
|
+
if (stacks.length > 0) {
|
|
82
|
+
resources = {};
|
|
83
|
+
for (const stack of stacks) {
|
|
84
|
+
const perStack = await plugin.describeResources({
|
|
85
|
+
environment,
|
|
86
|
+
buildOutput,
|
|
87
|
+
entityNames,
|
|
88
|
+
entities,
|
|
89
|
+
owned,
|
|
90
|
+
stack,
|
|
91
|
+
});
|
|
92
|
+
Object.assign(resources, perStack);
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
resources = await plugin.describeResources({
|
|
96
|
+
environment,
|
|
97
|
+
buildOutput,
|
|
98
|
+
entityNames,
|
|
99
|
+
entities,
|
|
100
|
+
owned,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
74
103
|
if (Object.keys(resources).length > 0) {
|
|
75
104
|
observations.push({ lexicon: plugin.name, resources });
|
|
76
105
|
}
|