@intentius/chant 0.18.29 → 0.18.30
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/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/package.json +1 -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
|
@@ -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;
|
|
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;AA2ClD,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,CAsE1E;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/cli/main.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../src/cli/main.ts"],"names":[],"mappings":";AAMA,OAAO,EAAmC,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAkB9E;;GAEG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,UAAU,
|
|
1
|
+
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../src/cli/main.ts"],"names":[],"mappings":";AAMA,OAAO,EAAmC,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAkB9E;;GAEG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,UAAU,CA6LpD;AA2VD;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,CAAC,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAuB5E"}
|
package/dist/cli/registry.d.ts
CHANGED
|
@@ -25,6 +25,8 @@ export interface ParsedArgs {
|
|
|
25
25
|
temporal?: boolean;
|
|
26
26
|
/** `chant run` — emit the structured OpRunResult as JSON on stdout. */
|
|
27
27
|
json?: boolean;
|
|
28
|
+
/** `chant run --components <name|all> --progress-json` — stream one NDJSON `RunProgressEvent` (../../components/run-progress.ts) per line to stdout while the run executes (local executor only), so a consumer can render live wave/component/phase/step progress instead of tailing raw logs. Purely additive: run semantics, ordering, and exit code are unchanged; omitted (undefined, not false) when the flag isn't passed. */
|
|
29
|
+
progressJson?: boolean;
|
|
28
30
|
live: boolean;
|
|
29
31
|
/** `chant migrate --from <name>` (default "github") */
|
|
30
32
|
migrateFrom?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/cli/registry.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/cli/registry.ts"],"names":[],"mappings":"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"}
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
import { type Archetype } from "./component.js";
|
|
25
25
|
import { type DriverComponent, type DriverRunResult } from "./driver.js";
|
|
26
26
|
import { type ComponentPipelineOptions } from "../lexicon.js";
|
|
27
|
+
import type { RunProgressEvent } from "./run-progress.js";
|
|
27
28
|
import type { CapabilityRegistry } from "./capability.js";
|
|
28
29
|
import { type ChantConfig } from "../config.js";
|
|
29
30
|
/** One component in `chant list --components` output. */
|
|
@@ -147,6 +148,18 @@ export interface RunComponentsOptions {
|
|
|
147
148
|
* ran elsewhere. Merged with, and overridden by, outputs this run produces.
|
|
148
149
|
*/
|
|
149
150
|
componentOutputs?: Record<string, Record<string, unknown>>;
|
|
151
|
+
/**
|
|
152
|
+
* Opt-in structured progress observer (`chant run --components <name|all>
|
|
153
|
+
* --progress-json`, see ./run-progress.ts). Threaded straight through to
|
|
154
|
+
* `runInterpretDriver` for `selector === "all"`; for a single-component
|
|
155
|
+
* selector (which bypasses `runInterpretDriver` — see the docstring above),
|
|
156
|
+
* this function emits the same `run-start`/`wave-start`/`component-start`/
|
|
157
|
+
* …/`run-done` envelope itself, treating the one component as a run with a
|
|
158
|
+
* single wave, so `--progress-json` produces a well-formed event stream
|
|
159
|
+
* regardless of selector. Left `undefined` by every caller that didn't pass
|
|
160
|
+
* `--progress-json`, in which case nothing changes.
|
|
161
|
+
*/
|
|
162
|
+
onProgress?: (event: RunProgressEvent) => void;
|
|
150
163
|
}
|
|
151
164
|
/** Result of `chant run --components <name|all>`. */
|
|
152
165
|
export interface RunComponentsResult {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli-support.d.ts","sourceRoot":"","sources":["../../src/components/cli-support.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAGH,OAAO,EAAiB,KAAK,SAAS,EAAE,MAAM,aAAa,CAAC;AAC5D,OAAO,EAQL,KAAK,eAAe,EAEpB,KAAK,eAAe,EACrB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAuC,KAAK,wBAAwB,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"cli-support.d.ts","sourceRoot":"","sources":["../../src/components/cli-support.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAGH,OAAO,EAAiB,KAAK,SAAS,EAAE,MAAM,aAAa,CAAC;AAC5D,OAAO,EAQL,KAAK,eAAe,EAEpB,KAAK,eAAe,EACrB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAuC,KAAK,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAChG,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAGvD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAEvD,OAAO,EAAmB,KAAK,WAAW,EAAE,MAAM,WAAW,CAAC;AAE9D,yDAAyD;AACzD,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,SAAS,CAAC;IACrB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,eAAe,EAAE,CAAC;IAC9B,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,qFAAqF;AACrF,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAqBhF;AAED,+FAA+F;AAC/F,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,0FAA0F;IAC1F,IAAI,EAAE,OAAO,CAAC;CACf;AAED,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,kBAAkB,CAAC;IAC/B,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,sFAAsF;AACtF,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAuBpG;AAED,4GAA4G;AAC5G,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,KAAK,EAAE,MAAM,EAAE,EAAE,CAAC;IAClB,KAAK,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC3C;;gEAE4D;IAC5D,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,6FAA6F;AAC7F,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,CAAC,CA+BvF;AAED,iJAAiJ;AACjJ,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC;AAErC,iEAAiE;AACjE,MAAM,WAAW,wBAAwB;IACvC,OAAO,EAAE,OAAO,CAAC;IACjB,uDAAuD;IACvD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,qDAAqD;IACrD,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,0EAA0E;IAC1E,IAAI,CAAC,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;IACrF,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAqBD;;;;;;;;;GASG;AACH,wBAAsB,0BAA0B,CAC9C,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,eAAe,EACxB,OAAO,CAAC,EAAE,wBAAwB,GACjC,OAAO,CAAC,wBAAwB,CAAC,CA6BnC;AAcD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,eAAe,GAAG;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAkBhG;AAED,6FAA6F;AAC7F,MAAM,WAAW,oBAAoB;IACnC,wGAAwG;IACxG,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,6HAA6H;IAC7H,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC7B;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,kBAAkB,CAAC;IAC9B;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;;;;;;OAOG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAC3D;;;;;;;;;;OAUG;IACH,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,IAAI,CAAC;CAChD;AAED,qDAAqD;AACrD,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,OAAO,CAAC;IACjB,6FAA6F;IAC7F,GAAG,CAAC,EAAE,eAAe,CAAC;IACtB,uEAAuE;IACvE,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gIAAgI;IAChI,eAAe,CAAC,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC;CAC7D;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,0KAA0K;AAC1K,MAAM,WAAW,wBAAwB;IACvC,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;GAQG;AACH,wBAAsB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,wBAAwB,CAAC,CAsB/G;AAED,wBAAsB,aAAa,CACjC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,mBAAmB,CAAC,CAuG9B"}
|
|
@@ -41,6 +41,8 @@
|
|
|
41
41
|
* `CapabilityRegistry` the caller supplies.
|
|
42
42
|
*/
|
|
43
43
|
import type { CapabilityRegistry, DeployContext } from "./capability.js";
|
|
44
|
+
import type { RunProgressEvent } from "./run-progress.js";
|
|
45
|
+
export type { RunProgressEvent } from "./run-progress.js";
|
|
44
46
|
/** A wiring reference or literal value, as it appears in a step's fields (schema `WiringValue`). */
|
|
45
47
|
export type WiringValue = string | {
|
|
46
48
|
stackOutput: {
|
|
@@ -181,8 +183,17 @@ export declare function resolveStepInput(input: Record<string, unknown>, phaseOu
|
|
|
181
183
|
* handling. Cross-component artifact outputs this component published (if
|
|
182
184
|
* any) are recorded into `componentOutputs` under its own name so downstream
|
|
183
185
|
* components can reference `@<name>.publish.*`.
|
|
186
|
+
*
|
|
187
|
+
* `onProgress`, when supplied, is forwarded to every `runPhase` call (both the
|
|
188
|
+
* forward `deploy` phases and, on failure, the component's own authored
|
|
189
|
+
* `rollback` phases) so a `--progress-json` consumer sees `phase-start`/
|
|
190
|
+
* `step`/`phase-done` events for whichever phases actually ran. The saga
|
|
191
|
+
* unwind step-by-step compensation (`rollbackExecuted` below) is not part of
|
|
192
|
+
* the `RunProgressEvent` contract and stays silent — it isn't a `deploy`
|
|
193
|
+
* phase, and its record statuses (`rolled-back`/`rollback-opted-out`) don't
|
|
194
|
+
* map onto the `step` event's `running`/`ok`/`failed` shape.
|
|
184
195
|
*/
|
|
185
|
-
export declare function runComponentDeploy(component: DriverComponent, ctx: DeployContext, registry: CapabilityRegistry, componentOutputs: Record<string, Record<string, unknown
|
|
196
|
+
export declare function runComponentDeploy(component: DriverComponent, ctx: DeployContext, registry: CapabilityRegistry, componentOutputs: Record<string, Record<string, unknown>>, onProgress?: (event: RunProgressEvent) => void): Promise<DriverComponentResult>;
|
|
186
197
|
export interface InterpretRunOptions {
|
|
187
198
|
/** Target environment name, threaded into every capability's `DeployContext`. */
|
|
188
199
|
env: string;
|
|
@@ -190,6 +201,16 @@ export interface InterpretRunOptions {
|
|
|
190
201
|
vars?: Record<string, unknown>;
|
|
191
202
|
/** Pre-seeded cross-component/cross-stack outputs (e.g. from a prior run, or a caller resolving `stackOutput` externally). Merged with outputs this run produces. */
|
|
192
203
|
componentOutputs?: Record<string, Record<string, unknown>>;
|
|
204
|
+
/**
|
|
205
|
+
* Opt-in structured progress observer (`chant run --components all
|
|
206
|
+
* --progress-json`, see ./run-progress.ts). Called with `run-start`/
|
|
207
|
+
* `wave-start`/`component-start`/…/`run-done` events as the run executes;
|
|
208
|
+
* never consulted for control flow, so leaving it `undefined` (the default
|
|
209
|
+
* for every caller that didn't pass `--progress-json`) makes every
|
|
210
|
+
* `onProgress?.(...)` call below a no-op and this function's behavior is
|
|
211
|
+
* byte-for-byte the same as before this option existed.
|
|
212
|
+
*/
|
|
213
|
+
onProgress?: (event: RunProgressEvent) => void;
|
|
193
214
|
}
|
|
194
215
|
/**
|
|
195
216
|
* Run a set of components to completion on the local in-process executor:
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"driver.d.ts","sourceRoot":"","sources":["../../src/components/driver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AAGH,OAAO,KAAK,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"driver.d.ts","sourceRoot":"","sources":["../../src/components/driver.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,94 @@
|
|
|
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
|
+
/** Terminal status for a wave/component/phase/run — mirrors DriverStepRecord's ok/fail split, collapsed to the two outcomes a consumer renders progress against. */
|
|
22
|
+
export type RunProgressStatus = "ok" | "failed";
|
|
23
|
+
/** 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. */
|
|
24
|
+
export interface RunStartEvent {
|
|
25
|
+
type: "run-start";
|
|
26
|
+
waves: string[][];
|
|
27
|
+
}
|
|
28
|
+
/** A wave is about to start; its components may run concurrently (independent components share a wave). */
|
|
29
|
+
export interface WaveStartEvent {
|
|
30
|
+
type: "wave-start";
|
|
31
|
+
/** 1-based wave number. */
|
|
32
|
+
wave: number;
|
|
33
|
+
components: string[];
|
|
34
|
+
}
|
|
35
|
+
/** A component within a wave is about to start its `deploy` composition. */
|
|
36
|
+
export interface ComponentStartEvent {
|
|
37
|
+
type: "component-start";
|
|
38
|
+
wave: number;
|
|
39
|
+
component: string;
|
|
40
|
+
}
|
|
41
|
+
/** 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. */
|
|
42
|
+
export interface PhaseStartEvent {
|
|
43
|
+
type: "phase-start";
|
|
44
|
+
component: string;
|
|
45
|
+
phase: string;
|
|
46
|
+
}
|
|
47
|
+
/** A single capability step within a phase — one event when it starts, one when it settles. */
|
|
48
|
+
export interface StepEvent {
|
|
49
|
+
type: "step";
|
|
50
|
+
component: string;
|
|
51
|
+
phase: string;
|
|
52
|
+
step: string;
|
|
53
|
+
status: "running" | "ok" | "failed";
|
|
54
|
+
/** Present only alongside `status: "failed"`, when the capability threw. */
|
|
55
|
+
error?: string;
|
|
56
|
+
}
|
|
57
|
+
/** 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). */
|
|
58
|
+
export interface PhaseDoneEvent {
|
|
59
|
+
type: "phase-done";
|
|
60
|
+
component: string;
|
|
61
|
+
phase: string;
|
|
62
|
+
status: RunProgressStatus;
|
|
63
|
+
}
|
|
64
|
+
/** A component's `deploy` composition finished (after any saga rollback + component-level `rollback` phases the driver ran on failure). */
|
|
65
|
+
export interface ComponentDoneEvent {
|
|
66
|
+
type: "component-done";
|
|
67
|
+
wave: number;
|
|
68
|
+
component: string;
|
|
69
|
+
status: RunProgressStatus;
|
|
70
|
+
}
|
|
71
|
+
/** A wave finished — `failed` if any component in it failed, which also stops the run before any later wave starts. */
|
|
72
|
+
export interface WaveDoneEvent {
|
|
73
|
+
type: "wave-done";
|
|
74
|
+
wave: number;
|
|
75
|
+
status: RunProgressStatus;
|
|
76
|
+
}
|
|
77
|
+
/** The run finished. Mirrors the driver's own terminal `DriverRunResult.ok` / exit code. */
|
|
78
|
+
export interface RunDoneEvent {
|
|
79
|
+
type: "run-done";
|
|
80
|
+
status: RunProgressStatus;
|
|
81
|
+
}
|
|
82
|
+
export type RunProgressEvent = RunStartEvent | WaveStartEvent | ComponentStartEvent | PhaseStartEvent | StepEvent | PhaseDoneEvent | ComponentDoneEvent | WaveDoneEvent | RunDoneEvent;
|
|
83
|
+
/** 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. */
|
|
84
|
+
export type RunProgressSink = (event: RunProgressEvent) => void;
|
|
85
|
+
/**
|
|
86
|
+
* Build a sink that writes `JSON.stringify(event) + "\n"` to `write` (default:
|
|
87
|
+
* `process.stdout.write`), one line per event, as they happen — the
|
|
88
|
+
* `--progress-json` CLI wiring's sink (../cli/handlers/run.ts). Kept separate
|
|
89
|
+
* from `driver-output.ts`'s end-of-run renderers: this emits *during* the
|
|
90
|
+
* run, one line at a time; `renderDriverJson`/`renderDriverHuman` render the
|
|
91
|
+
* completed `DriverRunResult` once, after the run finishes.
|
|
92
|
+
*/
|
|
93
|
+
export declare function ndjsonProgressSink(write?: (chunk: string) => void): RunProgressSink;
|
|
94
|
+
//# sourceMappingURL=run-progress.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-progress.d.ts","sourceRoot":"","sources":["../../src/components/run-progress.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,oKAAoK;AACpK,MAAM,MAAM,iBAAiB,GAAG,IAAI,GAAG,QAAQ,CAAC;AAEhD,mMAAmM;AACnM,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,WAAW,CAAC;IAClB,KAAK,EAAE,MAAM,EAAE,EAAE,CAAC;CACnB;AAED,2GAA2G;AAC3G,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,YAAY,CAAC;IACnB,2BAA2B;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB;AAED,4EAA4E;AAC5E,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,iBAAiB,CAAC;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,2JAA2J;AAC3J,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,aAAa,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,+FAA+F;AAC/F,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,SAAS,GAAG,IAAI,GAAG,QAAQ,CAAC;IACpC,4EAA4E;IAC5E,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,0LAA0L;AAC1L,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,YAAY,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,iBAAiB,CAAC;CAC3B;AAED,2IAA2I;AAC3I,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,gBAAgB,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,iBAAiB,CAAC;CAC3B;AAED,uHAAuH;AACvH,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,WAAW,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,iBAAiB,CAAC;CAC3B;AAED,4FAA4F;AAC5F,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,UAAU,CAAC;IACjB,MAAM,EAAE,iBAAiB,CAAC;CAC3B;AAED,MAAM,MAAM,gBAAgB,GACxB,aAAa,GACb,cAAc,GACd,mBAAmB,GACnB,eAAe,GACf,SAAS,GACT,cAAc,GACd,kBAAkB,GAClB,aAAa,GACb,YAAY,CAAC;AAEjB,uKAAuK;AACvK,MAAM,MAAM,eAAe,GAAG,CAAC,KAAK,EAAE,gBAAgB,KAAK,IAAI,CAAC;AAEhE;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,GAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAA0C,GAAG,eAAe,CAExH"}
|
package/package.json
CHANGED
|
@@ -945,6 +945,60 @@ describe("runOpComponents", () => {
|
|
|
945
945
|
vi.restoreAllMocks();
|
|
946
946
|
});
|
|
947
947
|
|
|
948
|
+
// ── --progress-json (M3, behold roadmap) ──────────────────────────────────
|
|
949
|
+
|
|
950
|
+
test("--progress-json streams NDJSON progress events to stdout as runComponents emits them", async () => {
|
|
951
|
+
runComponentsMock.mockImplementation(
|
|
952
|
+
async (_path: string, _selector: string, options: { onProgress?: (e: unknown) => void }) => {
|
|
953
|
+
options.onProgress?.({ type: "run-start", waves: [["svc"]] });
|
|
954
|
+
options.onProgress?.({ type: "run-done", status: "ok" });
|
|
955
|
+
return {
|
|
956
|
+
success: true,
|
|
957
|
+
selected: ["svc"],
|
|
958
|
+
run: { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, records: [] }], ok: true },
|
|
959
|
+
};
|
|
960
|
+
},
|
|
961
|
+
);
|
|
962
|
+
const stdoutWrite = vi.spyOn(process.stdout, "write").mockImplementation(() => true);
|
|
963
|
+
vi.spyOn(process.stderr, "write").mockImplementation(() => true);
|
|
964
|
+
|
|
965
|
+
const exit = await runOpComponents({
|
|
966
|
+
args: makeArgs({ path: "svc", progressJson: true, temporal: false }),
|
|
967
|
+
plugins: [],
|
|
968
|
+
serializers: [],
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
expect(exit).toBe(0);
|
|
972
|
+
const lines = stdoutWrite.mock.calls.map((c) => String(c[0])).filter((s) => s.trim().length > 0);
|
|
973
|
+
expect(lines.map((l) => JSON.parse(l))).toEqual([
|
|
974
|
+
{ type: "run-start", waves: [["svc"]] },
|
|
975
|
+
{ type: "run-done", status: "ok" },
|
|
976
|
+
]);
|
|
977
|
+
// Every line is a single, complete JSON object — real NDJSON, not one big blob.
|
|
978
|
+
for (const line of lines) expect(() => JSON.parse(line)).not.toThrow();
|
|
979
|
+
vi.restoreAllMocks();
|
|
980
|
+
});
|
|
981
|
+
|
|
982
|
+
test("without --progress-json, runComponents receives onProgress: undefined and nothing streams", async () => {
|
|
983
|
+
runComponentsMock.mockResolvedValue({
|
|
984
|
+
success: true,
|
|
985
|
+
selected: ["svc"],
|
|
986
|
+
run: { order: ["svc"], waves: [["svc"]], results: [{ component: "svc", ok: true, records: [] }], ok: true },
|
|
987
|
+
});
|
|
988
|
+
const stdoutWrite = vi.spyOn(process.stdout, "write").mockImplementation(() => true);
|
|
989
|
+
vi.spyOn(process.stderr, "write").mockImplementation(() => true);
|
|
990
|
+
|
|
991
|
+
await runOpComponents({ args: makeArgs({ path: "svc", temporal: false }), plugins: [], serializers: [] });
|
|
992
|
+
|
|
993
|
+
expect(runComponentsMock).toHaveBeenCalledWith(expect.any(String), "svc", {
|
|
994
|
+
env: undefined,
|
|
995
|
+
componentOutputs: {},
|
|
996
|
+
onProgress: undefined,
|
|
997
|
+
});
|
|
998
|
+
expect(stdoutWrite).not.toHaveBeenCalled();
|
|
999
|
+
vi.restoreAllMocks();
|
|
1000
|
+
});
|
|
1001
|
+
|
|
948
1002
|
test("all: dispatches the 'all' selector and renders every component", async () => {
|
|
949
1003
|
runComponentsMock.mockResolvedValue({
|
|
950
1004
|
success: true,
|
package/src/cli/handlers/run.ts
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
import { generateReport, writeReport } from "./run-report";
|
|
22
22
|
import { runComponents, resolveComponentTargets, findComponentGate, listComponents } from "../../components/cli-support";
|
|
23
23
|
import { renderDriverHuman, renderDriverJson } from "../../components/driver-output";
|
|
24
|
+
import { ndjsonProgressSink } from "../../components/run-progress";
|
|
24
25
|
import { loadComponentTemporalCodegen } from "../../components/temporal-codegen-loader";
|
|
25
26
|
import { applyConfigDefaults } from "../../components/config-defaults";
|
|
26
27
|
import { maybeRecordAutoRelease, extractRunDigestFromPhaseOutputs } from "../../components/auto-release";
|
|
@@ -686,7 +687,14 @@ export async function runOpComponents(ctx: CommandContext): Promise<number> {
|
|
|
686
687
|
return 1;
|
|
687
688
|
}
|
|
688
689
|
}
|
|
689
|
-
|
|
690
|
+
// `--progress-json` (#M3, behold roadmap): stream one NDJSON RunProgressEvent
|
|
691
|
+
// per line to stdout while the run executes, so a consumer (e.g. behold) can
|
|
692
|
+
// render live wave/component/phase/step progress instead of tailing raw
|
|
693
|
+
// logs. Purely additive: when the flag is absent, `onProgress` stays
|
|
694
|
+
// `undefined` and every `onProgress?.(...)` call in the driver is a no-op —
|
|
695
|
+
// behavior is byte-for-byte unchanged from before this flag existed.
|
|
696
|
+
const onProgress = ctx.args.progressJson ? ndjsonProgressSink() : undefined;
|
|
697
|
+
const result = await runComponents(resolve("."), selector, { env: ctx.args.env, componentOutputs: seededOutputs, onProgress });
|
|
690
698
|
|
|
691
699
|
// Dump the accumulated outputs for a downstream job to seed from. Written
|
|
692
700
|
// even on failure (partial outputs) so a resumed run still has what completed.
|
package/src/cli/main.test.ts
CHANGED
|
@@ -234,6 +234,20 @@ describe("parseArgs", () => {
|
|
|
234
234
|
const result = parseArgs(["run", "--components", "search-service"]);
|
|
235
235
|
expect(result.noReleaseRecord).toBeUndefined();
|
|
236
236
|
});
|
|
237
|
+
|
|
238
|
+
// ── --progress-json (M3, behold roadmap) ─────────────────────────────────
|
|
239
|
+
|
|
240
|
+
test("parses --progress-json for run --components", () => {
|
|
241
|
+
const result = parseArgs(["run", "--components", "search-service", "--env", "staging", "--progress-json"]);
|
|
242
|
+
expect(result.components).toBe(true);
|
|
243
|
+
expect(result.env).toBe("staging");
|
|
244
|
+
expect(result.progressJson).toBe(true);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("--progress-json is undefined (not false) when omitted", () => {
|
|
248
|
+
const result = parseArgs(["run", "--components", "search-service"]);
|
|
249
|
+
expect(result.progressJson).toBeUndefined();
|
|
250
|
+
});
|
|
237
251
|
});
|
|
238
252
|
|
|
239
253
|
// ── resolveCommand tests ──────────────────────────────────────────
|
package/src/cli/main.ts
CHANGED
|
@@ -172,6 +172,8 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
172
172
|
result.temporal = true;
|
|
173
173
|
} else if (arg === "--json") {
|
|
174
174
|
result.json = true;
|
|
175
|
+
} else if (arg === "--progress-json") {
|
|
176
|
+
result.progressJson = true;
|
|
175
177
|
} else if (arg === "--update-snapshot") {
|
|
176
178
|
result.updateSnapshot = true;
|
|
177
179
|
} else if (arg === "--run-examples") {
|
|
@@ -262,6 +264,11 @@ Ops:
|
|
|
262
264
|
On success, auto-emits a release-ledger record per
|
|
263
265
|
component that published a digest (default: on;
|
|
264
266
|
--no-release-record to opt out; #597)
|
|
267
|
+
--progress-json: stream one NDJSON RunProgressEvent
|
|
268
|
+
per line to stdout while the run executes, for a
|
|
269
|
+
consumer to render live wave/component/phase/step
|
|
270
|
+
progress instead of tailing raw logs (additive; run
|
|
271
|
+
semantics/exit code unchanged)
|
|
265
272
|
|
|
266
273
|
graph Show Op dependency graph (--stacks for cross-stack order,
|
|
267
274
|
--format ir|mermaid|dot|layout for the lint-gated graph IR,
|
package/src/cli/registry.ts
CHANGED
|
@@ -26,6 +26,8 @@ export interface ParsedArgs {
|
|
|
26
26
|
temporal?: boolean;
|
|
27
27
|
/** `chant run` — emit the structured OpRunResult as JSON on stdout. */
|
|
28
28
|
json?: boolean;
|
|
29
|
+
/** `chant run --components <name|all> --progress-json` — stream one NDJSON `RunProgressEvent` (../../components/run-progress.ts) per line to stdout while the run executes (local executor only), so a consumer can render live wave/component/phase/step progress instead of tailing raw logs. Purely additive: run semantics, ordering, and exit code are unchanged; omitted (undefined, not false) when the flag isn't passed. */
|
|
30
|
+
progressJson?: boolean;
|
|
29
31
|
live: boolean;
|
|
30
32
|
/** `chant migrate --from <name>` (default "github") */
|
|
31
33
|
migrateFrom?: string;
|
|
@@ -16,7 +16,7 @@ import Ajv2020 from "ajv/dist/2020";
|
|
|
16
16
|
import componentSchema from "./component.schema.json";
|
|
17
17
|
import { listComponents, describeComponent, computeComponentGraph, runComponents, findComponentGate } from "./cli-support";
|
|
18
18
|
import { CapabilityRegistry, type DeployContext } from "./capability";
|
|
19
|
-
import type { DriverComponent } from "./driver";
|
|
19
|
+
import type { DriverComponent, RunProgressEvent } from "./driver";
|
|
20
20
|
|
|
21
21
|
describe("listComponents", () => {
|
|
22
22
|
let testDir: string;
|
|
@@ -577,3 +577,94 @@ describe("runComponents", () => {
|
|
|
577
577
|
expect(capability.calls[0]?.input).toMatchObject({ format: "cyclonedx" });
|
|
578
578
|
});
|
|
579
579
|
});
|
|
580
|
+
|
|
581
|
+
describe("runComponents — onProgress (--progress-json wiring, M3)", () => {
|
|
582
|
+
let testDir: string;
|
|
583
|
+
|
|
584
|
+
beforeEach(async () => {
|
|
585
|
+
testDir = join(tmpdir(), `chant-run-components-progress-test-${Date.now()}-${Math.random()}`);
|
|
586
|
+
await mkdir(testDir, { recursive: true });
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
afterEach(async () => {
|
|
590
|
+
await rm(testDir, { recursive: true, force: true });
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
test("selector 'all': onProgress sees the same run-start/wave-*/component-*/run-done envelope runInterpretDriver emits", async () => {
|
|
594
|
+
await writeFile(
|
|
595
|
+
join(testDir, "alb.component.ts"),
|
|
596
|
+
`export const alb = { name: "shared-alb", dependsOn: [], deploy: [{ phase: "Apply", steps: [{ kind: "deploy-thing" }] }] };`,
|
|
597
|
+
);
|
|
598
|
+
await writeFile(
|
|
599
|
+
join(testDir, "svc.component.ts"),
|
|
600
|
+
`export const svc = { name: "search-service", dependsOn: ["shared-alb"], deploy: [{ phase: "Apply", steps: [{ kind: "deploy-thing" }] }] };`,
|
|
601
|
+
);
|
|
602
|
+
|
|
603
|
+
const registry = fakeRegistry();
|
|
604
|
+
const events: RunProgressEvent[] = [];
|
|
605
|
+
const result = await runComponents(testDir, "all", { registry, onProgress: (e) => events.push(e) });
|
|
606
|
+
|
|
607
|
+
expect(result.success).toBe(true);
|
|
608
|
+
expect(events[0]).toEqual({ type: "run-start", waves: [["shared-alb"], ["search-service"]] });
|
|
609
|
+
expect(events.at(-1)).toEqual({ type: "run-done", status: "ok" });
|
|
610
|
+
expect(events.filter((e) => e.type === "component-start")).toEqual([
|
|
611
|
+
{ type: "component-start", wave: 1, component: "shared-alb" },
|
|
612
|
+
{ type: "component-start", wave: 2, component: "search-service" },
|
|
613
|
+
]);
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
test("single-component selector: onProgress still gets a well-formed single-wave envelope (bypasses runInterpretDriver)", async () => {
|
|
617
|
+
await writeFile(
|
|
618
|
+
join(testDir, "svc.component.ts"),
|
|
619
|
+
`export const svc = { name: "svc", dependsOn: [], deploy: [{ phase: "Apply", steps: [{ kind: "deploy-thing" }] }] };`,
|
|
620
|
+
);
|
|
621
|
+
|
|
622
|
+
const registry = fakeRegistry();
|
|
623
|
+
const events: RunProgressEvent[] = [];
|
|
624
|
+
const result = await runComponents(testDir, "svc", { registry, onProgress: (e) => events.push(e) });
|
|
625
|
+
|
|
626
|
+
expect(result.success).toBe(true);
|
|
627
|
+
expect(events.map((e) => e.type)).toEqual([
|
|
628
|
+
"run-start",
|
|
629
|
+
"wave-start",
|
|
630
|
+
"component-start",
|
|
631
|
+
"phase-start",
|
|
632
|
+
"step",
|
|
633
|
+
"step",
|
|
634
|
+
"phase-done",
|
|
635
|
+
"component-done",
|
|
636
|
+
"wave-done",
|
|
637
|
+
"run-done",
|
|
638
|
+
]);
|
|
639
|
+
expect(events[0]).toEqual({ type: "run-start", waves: [["svc"]] });
|
|
640
|
+
expect(events[2]).toEqual({ type: "component-start", wave: 1, component: "svc" });
|
|
641
|
+
expect(events.at(-1)).toEqual({ type: "run-done", status: "ok" });
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
test("single-component selector: a failing step still yields component-done/wave-done/run-done status:\"failed\"", async () => {
|
|
645
|
+
await writeFile(
|
|
646
|
+
join(testDir, "svc.component.ts"),
|
|
647
|
+
`export const svc = { name: "svc", dependsOn: [], deploy: [{ phase: "Apply", steps: [{ kind: "deploy-thing" }] }] };`,
|
|
648
|
+
);
|
|
649
|
+
|
|
650
|
+
const registry = fakeRegistry({ failRun: true });
|
|
651
|
+
const events: RunProgressEvent[] = [];
|
|
652
|
+
const result = await runComponents(testDir, "svc", { registry, onProgress: (e) => events.push(e) });
|
|
653
|
+
|
|
654
|
+
expect(result.success).toBe(false);
|
|
655
|
+
expect(events.at(-3)).toEqual({ type: "component-done", wave: 1, component: "svc", status: "failed" });
|
|
656
|
+
expect(events.at(-2)).toEqual({ type: "wave-done", wave: 1, status: "failed" });
|
|
657
|
+
expect(events.at(-1)).toEqual({ type: "run-done", status: "failed" });
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
test("no onProgress passed → no behavior change (result identical, and it's simply never called)", async () => {
|
|
661
|
+
await writeFile(
|
|
662
|
+
join(testDir, "svc.component.ts"),
|
|
663
|
+
`export const svc = { name: "svc", dependsOn: [], deploy: [{ phase: "Apply", steps: [{ kind: "deploy-thing" }] }] };`,
|
|
664
|
+
);
|
|
665
|
+
|
|
666
|
+
const result = await runComponents(testDir, "svc", { registry: fakeRegistry() });
|
|
667
|
+
expect(result.success).toBe(true);
|
|
668
|
+
expect(result.run?.ok).toBe(true);
|
|
669
|
+
});
|
|
670
|
+
});
|
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
type DriverRunResult,
|
|
38
38
|
} from "./driver";
|
|
39
39
|
import { isLexiconPlugin, type LexiconPlugin, type ComponentPipelineOptions } from "../lexicon";
|
|
40
|
+
import type { RunProgressEvent } from "./run-progress";
|
|
40
41
|
import { relative } from "node:path";
|
|
41
42
|
import { buildCapabilityRegistry } from "./capability-plugin-loader";
|
|
42
43
|
import type { CapabilityRegistry } from "./capability";
|
|
@@ -323,6 +324,18 @@ export interface RunComponentsOptions {
|
|
|
323
324
|
* ran elsewhere. Merged with, and overridden by, outputs this run produces.
|
|
324
325
|
*/
|
|
325
326
|
componentOutputs?: Record<string, Record<string, unknown>>;
|
|
327
|
+
/**
|
|
328
|
+
* Opt-in structured progress observer (`chant run --components <name|all>
|
|
329
|
+
* --progress-json`, see ./run-progress.ts). Threaded straight through to
|
|
330
|
+
* `runInterpretDriver` for `selector === "all"`; for a single-component
|
|
331
|
+
* selector (which bypasses `runInterpretDriver` — see the docstring above),
|
|
332
|
+
* this function emits the same `run-start`/`wave-start`/`component-start`/
|
|
333
|
+
* …/`run-done` envelope itself, treating the one component as a run with a
|
|
334
|
+
* single wave, so `--progress-json` produces a well-formed event stream
|
|
335
|
+
* regardless of selector. Left `undefined` by every caller that didn't pass
|
|
336
|
+
* `--progress-json`, in which case nothing changes.
|
|
337
|
+
*/
|
|
338
|
+
onProgress?: (event: RunProgressEvent) => void;
|
|
326
339
|
}
|
|
327
340
|
|
|
328
341
|
/** Result of `chant run --components <name|all>`. */
|
|
@@ -449,10 +462,11 @@ export async function runComponents(
|
|
|
449
462
|
// from a caller (a downstream CI job passing an upstream job's dumped
|
|
450
463
|
// outputs via `--seed-outputs`); grows as this run's components complete.
|
|
451
464
|
const seedOutputs = options.componentOutputs ?? {};
|
|
465
|
+
const { onProgress } = options;
|
|
452
466
|
|
|
453
467
|
try {
|
|
454
468
|
if (selector === "all") {
|
|
455
|
-
const run = await runInterpretDriver(resolvedTargets, registry, { env, componentOutputs: seedOutputs });
|
|
469
|
+
const run = await runInterpretDriver(resolvedTargets, registry, { env, componentOutputs: seedOutputs, onProgress });
|
|
456
470
|
return { success: true, run, selected };
|
|
457
471
|
}
|
|
458
472
|
|
|
@@ -462,8 +476,28 @@ export async function runComponents(
|
|
|
462
476
|
// so a single-component run still resolves their `stackOutput()`/publish
|
|
463
477
|
// references. `runComponentDeploy` mutates this map with this component's
|
|
464
478
|
// own outputs, which `run.componentOutputs` then exposes for `--dump-outputs`.
|
|
479
|
+
//
|
|
480
|
+
// There's no `runInterpretDriver` call here to thread progress through
|
|
481
|
+
// (that's the whole point of the single-component bypass), so the
|
|
482
|
+
// run-start/wave-start/component-start/…/run-done envelope is emitted by
|
|
483
|
+
// hand, treating this one component as a single-wave run — the same shape
|
|
484
|
+
// `run.waves`/`run.order` below already give it.
|
|
465
485
|
const componentOutputs = { ...seedOutputs };
|
|
466
|
-
const
|
|
486
|
+
const componentName = resolvedTargets[0].name;
|
|
487
|
+
onProgress?.({ type: "run-start", waves: [selected] });
|
|
488
|
+
onProgress?.({ type: "wave-start", wave: 1, components: selected });
|
|
489
|
+
onProgress?.({ type: "component-start", wave: 1, component: componentName });
|
|
490
|
+
const componentResult = await runComponentDeploy(
|
|
491
|
+
resolvedTargets[0],
|
|
492
|
+
{ env, component: componentName },
|
|
493
|
+
registry,
|
|
494
|
+
componentOutputs,
|
|
495
|
+
onProgress,
|
|
496
|
+
);
|
|
497
|
+
const status: "ok" | "failed" = componentResult.ok ? "ok" : "failed";
|
|
498
|
+
onProgress?.({ type: "component-done", wave: 1, component: componentName, status });
|
|
499
|
+
onProgress?.({ type: "wave-done", wave: 1, status });
|
|
500
|
+
onProgress?.({ type: "run-done", status });
|
|
467
501
|
const run: DriverRunResult = {
|
|
468
502
|
order: selected,
|
|
469
503
|
waves: [selected],
|
|
@@ -23,6 +23,8 @@ import {
|
|
|
23
23
|
runComponentDeploy,
|
|
24
24
|
runInterpretDriver,
|
|
25
25
|
type DriverComponent,
|
|
26
|
+
type DriverStepRecord,
|
|
27
|
+
type RunProgressEvent,
|
|
26
28
|
} from "./driver";
|
|
27
29
|
import { neo4jCluster } from "./pilots/neo4j-fanout.pilot";
|
|
28
30
|
import { ordersTable } from "./pilots/dynamodb.pilot";
|
|
@@ -618,6 +620,194 @@ describe("runInterpretDriver — end to end", () => {
|
|
|
618
620
|
});
|
|
619
621
|
});
|
|
620
622
|
|
|
623
|
+
describe("onProgress — --progress-json event stream (M3, additive over the wave/component/phase/step loop)", () => {
|
|
624
|
+
/** Drop the nondeterministic `durationMs` field so a run's records can be compared for equality across two invocations. */
|
|
625
|
+
function stripTiming(records: DriverStepRecord[]): Omit<DriverStepRecord, "durationMs">[] {
|
|
626
|
+
return records.map(({ durationMs: _durationMs, ...rest }) => rest);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
it("emits run-start -> wave-start -> component-start -> phase-start -> step(running/ok) -> phase-done -> component-done -> wave-done, once per wave, then run-done", async () => {
|
|
630
|
+
const registry = new CapabilityRegistry();
|
|
631
|
+
registry.register(fakeCapability("step-a", { run: () => ({ ok: true }) }).capability);
|
|
632
|
+
registry.register(fakeCapability("step-b", { run: () => ({ ok: true }) }).capability);
|
|
633
|
+
|
|
634
|
+
const a: DriverComponent = { name: "a", dependsOn: [], deploy: [{ phase: "Apply", steps: [{ kind: "step-a" }] }] };
|
|
635
|
+
const b: DriverComponent = { name: "b", dependsOn: ["a"], deploy: [{ phase: "Apply", steps: [{ kind: "step-b" }] }] };
|
|
636
|
+
|
|
637
|
+
const events: RunProgressEvent[] = [];
|
|
638
|
+
const result = await runInterpretDriver([a, b], registry, { env: "dev", onProgress: (e) => events.push(e) });
|
|
639
|
+
|
|
640
|
+
expect(result.ok).toBe(true);
|
|
641
|
+
expect(events.map((e) => e.type)).toEqual([
|
|
642
|
+
"run-start",
|
|
643
|
+
"wave-start",
|
|
644
|
+
"component-start",
|
|
645
|
+
"phase-start",
|
|
646
|
+
"step",
|
|
647
|
+
"step",
|
|
648
|
+
"phase-done",
|
|
649
|
+
"component-done",
|
|
650
|
+
"wave-done",
|
|
651
|
+
"wave-start",
|
|
652
|
+
"component-start",
|
|
653
|
+
"phase-start",
|
|
654
|
+
"step",
|
|
655
|
+
"step",
|
|
656
|
+
"phase-done",
|
|
657
|
+
"component-done",
|
|
658
|
+
"wave-done",
|
|
659
|
+
"run-done",
|
|
660
|
+
]);
|
|
661
|
+
|
|
662
|
+
// Wave 1: component "a".
|
|
663
|
+
expect(events[0]).toEqual({ type: "run-start", waves: [["a"], ["b"]] });
|
|
664
|
+
expect(events[1]).toEqual({ type: "wave-start", wave: 1, components: ["a"] });
|
|
665
|
+
expect(events[2]).toEqual({ type: "component-start", wave: 1, component: "a" });
|
|
666
|
+
expect(events[3]).toEqual({ type: "phase-start", component: "a", phase: "Apply" });
|
|
667
|
+
expect(events[4]).toEqual({ type: "step", component: "a", phase: "Apply", step: "step-a", status: "running" });
|
|
668
|
+
expect(events[5]).toEqual({ type: "step", component: "a", phase: "Apply", step: "step-a", status: "ok" });
|
|
669
|
+
expect(events[6]).toEqual({ type: "phase-done", component: "a", phase: "Apply", status: "ok" });
|
|
670
|
+
expect(events[7]).toEqual({ type: "component-done", wave: 1, component: "a", status: "ok" });
|
|
671
|
+
expect(events[8]).toEqual({ type: "wave-done", wave: 1, status: "ok" });
|
|
672
|
+
|
|
673
|
+
// Wave 2: component "b" (1-based wave numbering).
|
|
674
|
+
expect(events[9]).toEqual({ type: "wave-start", wave: 2, components: ["b"] });
|
|
675
|
+
expect(events[10]).toEqual({ type: "component-start", wave: 2, component: "b" });
|
|
676
|
+
expect(events[15]).toEqual({ type: "component-done", wave: 2, component: "b", status: "ok" });
|
|
677
|
+
expect(events[16]).toEqual({ type: "wave-done", wave: 2, status: "ok" });
|
|
678
|
+
|
|
679
|
+
expect(events[17]).toEqual({ type: "run-done", status: "ok" });
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
it("a failing step yields step:failed (with error), phase-done:failed, component-done:failed, wave-done:failed, run-done:failed — and a later wave never starts", async () => {
|
|
683
|
+
const registry = new CapabilityRegistry();
|
|
684
|
+
registry.register(fakeCapability("bad-step", { failRun: true }).capability);
|
|
685
|
+
registry.register(fakeCapability("step-b", { run: () => ({ ok: true }) }).capability);
|
|
686
|
+
|
|
687
|
+
const a: DriverComponent = { name: "a", dependsOn: [], deploy: [{ phase: "Apply", steps: [{ kind: "bad-step" }] }] };
|
|
688
|
+
const b: DriverComponent = { name: "b", dependsOn: ["a"], deploy: [{ phase: "Apply", steps: [{ kind: "step-b" }] }] };
|
|
689
|
+
|
|
690
|
+
const events: RunProgressEvent[] = [];
|
|
691
|
+
await expect(
|
|
692
|
+
runInterpretDriver([a, b], registry, { env: "dev", onProgress: (e) => events.push(e) }),
|
|
693
|
+
).rejects.toThrow(DriverRunFailure);
|
|
694
|
+
|
|
695
|
+
expect(events.map((e) => e.type)).toEqual([
|
|
696
|
+
"run-start",
|
|
697
|
+
"wave-start",
|
|
698
|
+
"component-start",
|
|
699
|
+
"phase-start",
|
|
700
|
+
"step",
|
|
701
|
+
"step",
|
|
702
|
+
"phase-done",
|
|
703
|
+
"component-done",
|
|
704
|
+
"wave-done",
|
|
705
|
+
"run-done",
|
|
706
|
+
]);
|
|
707
|
+
// Only wave 1 ever starts — the driver stops at the first failed
|
|
708
|
+
// component, so wave 2 (containing "b") never runs.
|
|
709
|
+
expect(events.filter((e) => e.type === "wave-start")).toHaveLength(1);
|
|
710
|
+
expect(events.filter((e) => e.type === "component-start")).toEqual([{ type: "component-start", wave: 1, component: "a" }]);
|
|
711
|
+
|
|
712
|
+
expect(events[4]).toEqual({ type: "step", component: "a", phase: "Apply", step: "bad-step", status: "running" });
|
|
713
|
+
expect(events[5]).toEqual({
|
|
714
|
+
type: "step",
|
|
715
|
+
component: "a",
|
|
716
|
+
phase: "Apply",
|
|
717
|
+
step: "bad-step",
|
|
718
|
+
status: "failed",
|
|
719
|
+
error: "bad-step failed",
|
|
720
|
+
});
|
|
721
|
+
expect(events[6]).toEqual({ type: "phase-done", component: "a", phase: "Apply", status: "failed" });
|
|
722
|
+
expect(events[7]).toEqual({ type: "component-done", wave: 1, component: "a", status: "failed" });
|
|
723
|
+
expect(events[8]).toEqual({ type: "wave-done", wave: 1, status: "failed" });
|
|
724
|
+
expect(events[9]).toEqual({ type: "run-done", status: "failed" });
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
it("produces the identical DriverRunResult (minus timing) whether or not onProgress is passed", async () => {
|
|
728
|
+
function buildRegistry(): CapabilityRegistry {
|
|
729
|
+
const registry = new CapabilityRegistry();
|
|
730
|
+
registry.register(fakeCapability("step-a", { run: () => ({ ok: true }) }).capability);
|
|
731
|
+
registry.register(fakeCapability("step-b", { run: () => ({ ok: true }) }).capability);
|
|
732
|
+
return registry;
|
|
733
|
+
}
|
|
734
|
+
const a: DriverComponent = { name: "a", dependsOn: [], deploy: [{ phase: "Apply", steps: [{ kind: "step-a" }] }] };
|
|
735
|
+
const b: DriverComponent = { name: "b", dependsOn: ["a"], deploy: [{ phase: "Apply", steps: [{ kind: "step-b" }] }] };
|
|
736
|
+
|
|
737
|
+
const withoutProgress = await runInterpretDriver([a, b], buildRegistry(), { env: "dev" });
|
|
738
|
+
const events: RunProgressEvent[] = [];
|
|
739
|
+
const withProgress = await runInterpretDriver([a, b], buildRegistry(), {
|
|
740
|
+
env: "dev",
|
|
741
|
+
onProgress: (e) => events.push(e),
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
// onProgress was actually exercised — otherwise this comparison would be vacuous.
|
|
745
|
+
expect(events.length).toBeGreaterThan(0);
|
|
746
|
+
|
|
747
|
+
const normalize = (r: typeof withoutProgress) => ({
|
|
748
|
+
...r,
|
|
749
|
+
results: r.results.map((cr) => ({ ...cr, records: stripTiming(cr.records) })),
|
|
750
|
+
});
|
|
751
|
+
expect(normalize(withProgress)).toEqual(normalize(withoutProgress));
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
it("runComponentDeploy behaves identically with onProgress omitted (undefined-safe, no-op)", async () => {
|
|
755
|
+
const registry = new CapabilityRegistry();
|
|
756
|
+
registry.register(fakeCapability("step-a", { run: () => ({ ok: true }) }).capability);
|
|
757
|
+
const component: DriverComponent = { name: "c", deploy: [{ phase: "Apply", steps: [{ kind: "step-a" }] }] };
|
|
758
|
+
|
|
759
|
+
const result = await runComponentDeploy(component, { env: "dev", component: "c" }, registry, {});
|
|
760
|
+
|
|
761
|
+
expect(result.ok).toBe(true);
|
|
762
|
+
expect(stripTiming(result.records)).toEqual([
|
|
763
|
+
{ component: "c", phase: "Apply", kind: "step-a", status: "ok", output: { ok: true } },
|
|
764
|
+
]);
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
it("onFailure saga rollback: rollback-unwind steps are not reported as `step` progress events (only the forward failing step and the component's own authored rollback phase are)", async () => {
|
|
768
|
+
const registry = new CapabilityRegistry();
|
|
769
|
+
registry.register(
|
|
770
|
+
fakeCapability("provision", { run: () => ({ id: "res-1" }), rollback: () => {} }).capability,
|
|
771
|
+
);
|
|
772
|
+
registry.register(fakeCapability("apply-final", { failRun: true }).capability);
|
|
773
|
+
registry.register(fakeCapability("compensate", { run: () => ({ restored: true }) }).capability);
|
|
774
|
+
|
|
775
|
+
const component: DriverComponent = {
|
|
776
|
+
name: "c",
|
|
777
|
+
deploy: [
|
|
778
|
+
{ phase: "Provision", steps: [{ kind: "provision" }] },
|
|
779
|
+
{ phase: "Apply", steps: [{ kind: "apply-final" }] },
|
|
780
|
+
],
|
|
781
|
+
rollback: [{ phase: "Rollback", steps: [{ kind: "compensate" }] }],
|
|
782
|
+
};
|
|
783
|
+
|
|
784
|
+
const events: RunProgressEvent[] = [];
|
|
785
|
+
const result = await runComponentDeploy(component, { env: "dev", component: "c" }, registry, {}, (e) =>
|
|
786
|
+
events.push(e),
|
|
787
|
+
);
|
|
788
|
+
|
|
789
|
+
expect(result.ok).toBe(false);
|
|
790
|
+
const stepEvents = events.filter((e): e is Extract<RunProgressEvent, { type: "step" }> => e.type === "step");
|
|
791
|
+
// "provision" (forward, ok), "apply-final" (forward, failed), "compensate"
|
|
792
|
+
// (the component's own authored rollback phase) — never the saga unwind's
|
|
793
|
+
// reverse-order capability.rollback() call for "provision", which the
|
|
794
|
+
// driver still performs but doesn't surface as a `step` event.
|
|
795
|
+
expect(stepEvents.map((e) => `${e.step}:${e.status}`)).toEqual([
|
|
796
|
+
"provision:running",
|
|
797
|
+
"provision:ok",
|
|
798
|
+
"apply-final:running",
|
|
799
|
+
"apply-final:failed",
|
|
800
|
+
"compensate:running",
|
|
801
|
+
"compensate:ok",
|
|
802
|
+
]);
|
|
803
|
+
expect(events.filter((e) => e.type === "phase-start").map((e) => (e as { phase: string }).phase)).toEqual([
|
|
804
|
+
"Provision",
|
|
805
|
+
"Apply",
|
|
806
|
+
"Rollback",
|
|
807
|
+
]);
|
|
808
|
+
});
|
|
809
|
+
});
|
|
810
|
+
|
|
621
811
|
describe("capabilities are consumed exactly as the registry provides them", () => {
|
|
622
812
|
// No starter verb is a stub any more, but the stub *mechanism*
|
|
623
813
|
// (./verbs/stub.ts) stays for third-party plugins / future verbs. A stub
|
package/src/components/driver.ts
CHANGED
|
@@ -43,6 +43,9 @@
|
|
|
43
43
|
|
|
44
44
|
import { topoSort } from "../codegen/topo-sort";
|
|
45
45
|
import type { CapabilityRegistry, DeployContext } from "./capability";
|
|
46
|
+
import type { RunProgressEvent } from "./run-progress";
|
|
47
|
+
|
|
48
|
+
export type { RunProgressEvent } from "./run-progress";
|
|
46
49
|
|
|
47
50
|
// ── Component-shaped input (mirrors component.schema.json / ./component.ts) ──
|
|
48
51
|
|
|
@@ -375,6 +378,15 @@ async function runCapabilityStep(
|
|
|
375
378
|
* inheriting `parallel` from its own definition, not its parent's). Steps run
|
|
376
379
|
* sequentially unless `phase.parallel` is set, in which case they run via
|
|
377
380
|
* `Promise.all`, matching ../op/local-executor.ts's phase semantics.
|
|
381
|
+
*
|
|
382
|
+
* `onProgress`, when supplied, is called with a `phase-start` event before any
|
|
383
|
+
* step runs, a `step` event around each capability invocation (`"running"`
|
|
384
|
+
* then `"ok"`/`"failed"`), and a `phase-done` event once the phase settles —
|
|
385
|
+
* purely additive observation, never consulted for control flow. Recursion
|
|
386
|
+
* into a nested fan-out phase passes the same callback through, so a nested
|
|
387
|
+
* phase's events use its own name. Left `undefined` by every caller that
|
|
388
|
+
* doesn't opt into `--progress-json` (see ./run-progress.ts), in which case
|
|
389
|
+
* every `onProgress?.(...)` call below is a no-op and behavior is unchanged.
|
|
378
390
|
*/
|
|
379
391
|
async function runPhase(
|
|
380
392
|
phaseDef: DriverPhase,
|
|
@@ -382,6 +394,7 @@ async function runPhase(
|
|
|
382
394
|
registry: CapabilityRegistry,
|
|
383
395
|
phaseOutputs: Record<string, Record<string, unknown>>,
|
|
384
396
|
componentOutputs: Record<string, Record<string, unknown>>,
|
|
397
|
+
onProgress?: (event: RunProgressEvent) => void,
|
|
385
398
|
): Promise<{ records: DriverStepRecord[]; executed: ExecutedStep[] }> {
|
|
386
399
|
const gate = phaseDef.steps.find(isGateStep);
|
|
387
400
|
if (gate) throw new DriverGateUnsupportedError(ctx.component, gate.signalName);
|
|
@@ -393,13 +406,14 @@ async function runPhase(
|
|
|
393
406
|
): Promise<{ records: DriverStepRecord[]; executed: ExecutedStep[]; failed: boolean }> => {
|
|
394
407
|
if (isPhaseStep(entry)) {
|
|
395
408
|
try {
|
|
396
|
-
const nested = await runPhase(entry, ctx, registry, phaseOutputs, componentOutputs);
|
|
409
|
+
const nested = await runPhase(entry, ctx, registry, phaseOutputs, componentOutputs, onProgress);
|
|
397
410
|
return { ...nested, failed: false };
|
|
398
411
|
} catch (err) {
|
|
399
412
|
if (err instanceof StepFailure) return { records: err.records, executed: err.executed, failed: true };
|
|
400
413
|
throw err;
|
|
401
414
|
}
|
|
402
415
|
}
|
|
416
|
+
onProgress?.({ type: "step", component: ctx.component, phase: phaseDef.phase, step: entry.kind, status: "running" });
|
|
403
417
|
const { record, resolvedInput, output } = await runCapabilityStep(
|
|
404
418
|
entry,
|
|
405
419
|
phaseDef.phase,
|
|
@@ -408,6 +422,14 @@ async function runPhase(
|
|
|
408
422
|
phaseOutputs,
|
|
409
423
|
componentOutputs,
|
|
410
424
|
);
|
|
425
|
+
onProgress?.({
|
|
426
|
+
type: "step",
|
|
427
|
+
component: ctx.component,
|
|
428
|
+
phase: phaseDef.phase,
|
|
429
|
+
step: entry.kind,
|
|
430
|
+
status: record.status === "ok" ? "ok" : "failed",
|
|
431
|
+
...(record.error !== undefined ? { error: record.error } : {}),
|
|
432
|
+
});
|
|
411
433
|
if (record.status === "ok") {
|
|
412
434
|
phaseOutputs[phaseDef.phase] = { ...(phaseOutputs[phaseDef.phase] ?? {}), ...(output as object) };
|
|
413
435
|
}
|
|
@@ -418,35 +440,49 @@ async function runPhase(
|
|
|
418
440
|
};
|
|
419
441
|
};
|
|
420
442
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
443
|
+
const runEntries = async (): Promise<{ records: DriverStepRecord[]; executed: ExecutedStep[] }> => {
|
|
444
|
+
if (phaseDef.parallel) {
|
|
445
|
+
const results = await Promise.all(entries.map(runEntry));
|
|
446
|
+
const records = results.flatMap((r) => r.records);
|
|
447
|
+
const executed = results.flatMap((r) => r.executed);
|
|
448
|
+
if (results.some((r) => r.failed)) throw new StepFailure(records, executed);
|
|
449
|
+
return { records, executed };
|
|
450
|
+
}
|
|
428
451
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
452
|
+
const records: DriverStepRecord[] = [];
|
|
453
|
+
const executed: ExecutedStep[] = [];
|
|
454
|
+
for (let i = 0; i < entries.length; i++) {
|
|
455
|
+
const result = await runEntry(entries[i]);
|
|
456
|
+
records.push(...result.records);
|
|
457
|
+
executed.push(...result.executed);
|
|
458
|
+
if (result.failed) {
|
|
459
|
+
for (const skipped of entries.slice(i + 1)) {
|
|
460
|
+
const skippedKind = isPhaseStep(skipped) ? skipped.phase : (skipped as DriverStep).kind;
|
|
461
|
+
records.push({
|
|
462
|
+
component: ctx.component,
|
|
463
|
+
phase: phaseDef.phase,
|
|
464
|
+
kind: skippedKind,
|
|
465
|
+
status: "skipped",
|
|
466
|
+
durationMs: 0,
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
throw new StepFailure(records, executed);
|
|
445
470
|
}
|
|
446
|
-
throw new StepFailure(records, executed);
|
|
447
471
|
}
|
|
472
|
+
return { records, executed };
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
onProgress?.({ type: "phase-start", component: ctx.component, phase: phaseDef.phase });
|
|
476
|
+
try {
|
|
477
|
+
const result = await runEntries();
|
|
478
|
+
onProgress?.({ type: "phase-done", component: ctx.component, phase: phaseDef.phase, status: "ok" });
|
|
479
|
+
return result;
|
|
480
|
+
} catch (err) {
|
|
481
|
+
if (err instanceof StepFailure) {
|
|
482
|
+
onProgress?.({ type: "phase-done", component: ctx.component, phase: phaseDef.phase, status: "failed" });
|
|
483
|
+
}
|
|
484
|
+
throw err;
|
|
448
485
|
}
|
|
449
|
-
return { records, executed };
|
|
450
486
|
}
|
|
451
487
|
|
|
452
488
|
/**
|
|
@@ -517,12 +553,22 @@ async function rollbackExecuted(
|
|
|
517
553
|
* handling. Cross-component artifact outputs this component published (if
|
|
518
554
|
* any) are recorded into `componentOutputs` under its own name so downstream
|
|
519
555
|
* components can reference `@<name>.publish.*`.
|
|
556
|
+
*
|
|
557
|
+
* `onProgress`, when supplied, is forwarded to every `runPhase` call (both the
|
|
558
|
+
* forward `deploy` phases and, on failure, the component's own authored
|
|
559
|
+
* `rollback` phases) so a `--progress-json` consumer sees `phase-start`/
|
|
560
|
+
* `step`/`phase-done` events for whichever phases actually ran. The saga
|
|
561
|
+
* unwind step-by-step compensation (`rollbackExecuted` below) is not part of
|
|
562
|
+
* the `RunProgressEvent` contract and stays silent — it isn't a `deploy`
|
|
563
|
+
* phase, and its record statuses (`rolled-back`/`rollback-opted-out`) don't
|
|
564
|
+
* map onto the `step` event's `running`/`ok`/`failed` shape.
|
|
520
565
|
*/
|
|
521
566
|
export async function runComponentDeploy(
|
|
522
567
|
component: DriverComponent,
|
|
523
568
|
ctx: DeployContext,
|
|
524
569
|
registry: CapabilityRegistry,
|
|
525
570
|
componentOutputs: Record<string, Record<string, unknown>>,
|
|
571
|
+
onProgress?: (event: RunProgressEvent) => void,
|
|
526
572
|
): Promise<DriverComponentResult> {
|
|
527
573
|
const phaseOutputs: Record<string, Record<string, unknown>> = {};
|
|
528
574
|
const records: DriverStepRecord[] = [];
|
|
@@ -530,7 +576,7 @@ export async function runComponentDeploy(
|
|
|
530
576
|
|
|
531
577
|
try {
|
|
532
578
|
for (const phaseDef of component.deploy) {
|
|
533
|
-
const result = await runPhase(phaseDef, ctx, registry, phaseOutputs, componentOutputs);
|
|
579
|
+
const result = await runPhase(phaseDef, ctx, registry, phaseOutputs, componentOutputs, onProgress);
|
|
534
580
|
records.push(...result.records);
|
|
535
581
|
allExecuted.push(...result.executed);
|
|
536
582
|
}
|
|
@@ -546,7 +592,7 @@ export async function runComponentDeploy(
|
|
|
546
592
|
|
|
547
593
|
for (const phaseDef of [...(component.rollback ?? [])].reverse()) {
|
|
548
594
|
try {
|
|
549
|
-
const result = await runPhase(phaseDef, ctx, registry, phaseOutputs, componentOutputs);
|
|
595
|
+
const result = await runPhase(phaseDef, ctx, registry, phaseOutputs, componentOutputs, onProgress);
|
|
550
596
|
records.push(...result.records);
|
|
551
597
|
} catch (compErr) {
|
|
552
598
|
if (compErr instanceof StepFailure) records.push(...compErr.records);
|
|
@@ -635,6 +681,16 @@ export interface InterpretRunOptions {
|
|
|
635
681
|
vars?: Record<string, unknown>;
|
|
636
682
|
/** Pre-seeded cross-component/cross-stack outputs (e.g. from a prior run, or a caller resolving `stackOutput` externally). Merged with outputs this run produces. */
|
|
637
683
|
componentOutputs?: Record<string, Record<string, unknown>>;
|
|
684
|
+
/**
|
|
685
|
+
* Opt-in structured progress observer (`chant run --components all
|
|
686
|
+
* --progress-json`, see ./run-progress.ts). Called with `run-start`/
|
|
687
|
+
* `wave-start`/`component-start`/…/`run-done` events as the run executes;
|
|
688
|
+
* never consulted for control flow, so leaving it `undefined` (the default
|
|
689
|
+
* for every caller that didn't pass `--progress-json`) makes every
|
|
690
|
+
* `onProgress?.(...)` call below a no-op and this function's behavior is
|
|
691
|
+
* byte-for-byte the same as before this option existed.
|
|
692
|
+
*/
|
|
693
|
+
onProgress?: (event: RunProgressEvent) => void;
|
|
638
694
|
}
|
|
639
695
|
|
|
640
696
|
/**
|
|
@@ -659,24 +715,34 @@ export async function runInterpretDriver(
|
|
|
659
715
|
const { order, waves } = resolveComponentGraph(components);
|
|
660
716
|
const byName = new Map(components.map((c) => [c.name, c]));
|
|
661
717
|
const componentOutputs: Record<string, Record<string, unknown>> = { ...(options.componentOutputs ?? {}) };
|
|
718
|
+
const { onProgress } = options;
|
|
719
|
+
|
|
720
|
+
onProgress?.({ type: "run-start", waves });
|
|
662
721
|
|
|
663
722
|
const results: DriverComponentResult[] = [];
|
|
664
723
|
let failedComponent: string | undefined;
|
|
665
724
|
|
|
666
|
-
waveLoop: for (const wave of waves) {
|
|
725
|
+
waveLoop: for (const [waveIndex, wave] of waves.entries()) {
|
|
726
|
+
const waveNum = waveIndex + 1;
|
|
727
|
+
onProgress?.({ type: "wave-start", wave: waveNum, components: wave });
|
|
667
728
|
const waveComponents = wave.map((name) => byName.get(name)!);
|
|
668
729
|
const waveResults = await Promise.all(
|
|
669
|
-
waveComponents.map((component) =>
|
|
670
|
-
|
|
730
|
+
waveComponents.map(async (component) => {
|
|
731
|
+
onProgress?.({ type: "component-start", wave: waveNum, component: component.name });
|
|
732
|
+
const result = await runComponentDeploy(
|
|
671
733
|
component,
|
|
672
734
|
{ env: options.env, component: component.name, vars: options.vars },
|
|
673
735
|
registry,
|
|
674
736
|
componentOutputs,
|
|
675
|
-
|
|
676
|
-
|
|
737
|
+
onProgress,
|
|
738
|
+
);
|
|
739
|
+
onProgress?.({ type: "component-done", wave: waveNum, component: component.name, status: result.ok ? "ok" : "failed" });
|
|
740
|
+
return result;
|
|
741
|
+
}),
|
|
677
742
|
);
|
|
678
743
|
results.push(...waveResults);
|
|
679
744
|
const failed = waveResults.find((r) => !r.ok);
|
|
745
|
+
onProgress?.({ type: "wave-done", wave: waveNum, status: failed ? "failed" : "ok" });
|
|
680
746
|
if (failed) {
|
|
681
747
|
failedComponent = failed.component;
|
|
682
748
|
break waveLoop;
|
|
@@ -684,6 +750,7 @@ export async function runInterpretDriver(
|
|
|
684
750
|
}
|
|
685
751
|
|
|
686
752
|
const ok = failedComponent === undefined;
|
|
753
|
+
onProgress?.({ type: "run-done", status: ok ? "ok" : "failed" });
|
|
687
754
|
const result: DriverRunResult = { order, waves, results, ok, failedComponent, componentOutputs };
|
|
688
755
|
if (!ok) throw new DriverRunFailure(result);
|
|
689
756
|
return result;
|
|
@@ -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
|
+
}
|