drej 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -0
- package/package.json +1 -1
- package/src/client.ts +119 -23
- package/src/index.ts +6 -1
- package/src/workflow.ts +214 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
1
1
|
# drej
|
|
2
2
|
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- d0486df: Add fluent workflow builder API to TypeScript SDK.
|
|
8
|
+
|
|
9
|
+
`workflow(id)` returns a `WorkflowBuilder` with chainable `.sandbox()` and `.parallel()` methods. Inside a sandbox scope, `SandboxStepBuilder` provides `.exec()`, `.writeFile()`, `.retry()`, `.forEach()`, `.when()`, and `.parallel()`. The `forEach` callback receives `(s, item)` where `item` serialises to `{{name}}` in template literals, enabling natural JS interpolation. Top-level `.parallel()` supports multiple concurrent sandbox sessions via `WorkflowParallelBuilder`. `DrejClient.run(w)` accepts a built workflow directly. The `sandbox()` helper defaults the entrypoint to `["tail", "-f", "/dev/null"]`.
|
|
10
|
+
|
|
11
|
+
Adds a server-side `sequence` step type that runs child steps sequentially, used internally by the builder to represent multi-step parallel branches.
|
|
12
|
+
|
|
13
|
+
- e1f9bb8: Add `snapshotConfig` option to `client.run()` and a `replayFromSnapshot()` method.
|
|
14
|
+
|
|
15
|
+
Pass `snapshotConfig: { afterSteps?: number[]; everyNSteps?: number }` to capture sandbox snapshots at specific points in a workflow. Call `client.replayFromSnapshot(name, runId, workflow)` to start a new run booted from the latest captured snapshot — skipping any setup steps already baked into the image.
|
|
16
|
+
|
|
17
|
+
- 9a30c31: Introduce per-run ledger with workflow name / run ID separation.
|
|
18
|
+
|
|
19
|
+
Each workflow execution now has a stable **workflow name** (user-defined) and an auto-generated **run ID** (UUID). Ledger files are stored at `ledgers/<name>/<runId>.ndjson` so all runs of a workflow are grouped together.
|
|
20
|
+
|
|
21
|
+
API changes:
|
|
22
|
+
|
|
23
|
+
- `POST /v1/workflows/:name/runs` — starts a run; first SSE event is `run_started` carrying the run ID
|
|
24
|
+
- `POST /v1/workflows/:name/runs/:runId/resume` — resumes a specific run
|
|
25
|
+
- `GET /v1/workflows/:name/runs` — lists all run IDs for a workflow
|
|
26
|
+
- `GET /v1/workflows/:name/runs/:runId/ledger` — fetches ledger for a specific run
|
|
27
|
+
|
|
28
|
+
SDK changes:
|
|
29
|
+
|
|
30
|
+
- `client.run(w)` is now `async` and returns `Promise<WorkflowRun>`; `run.id` gives the run ID, `run.name` the workflow name, and it is async-iterable for events
|
|
31
|
+
- `client.resumeRun(name, runId, w)` resumes a run
|
|
32
|
+
- `client.listWorkflowRuns(name)` lists runs
|
|
33
|
+
- `client.getWorkflowLedger(name, runId)` fetches the ledger
|
|
34
|
+
- `WorkflowEvent` fields renamed: `workflowId` → `workflowName` + `runId`
|
|
35
|
+
|
|
36
|
+
### Patch Changes
|
|
37
|
+
|
|
38
|
+
- b3c0bc9: feat: lifecycle hooks, append-only WAL, and clean adapter layer
|
|
39
|
+
|
|
40
|
+
- Add `WorkflowHooks` interface with `onStepStart`, `onStepComplete`, `onStepFailed`, `onStepRolledBack`, `onWorkflowComplete`, `onWorkflowFailed` callbacks on `WorkflowDeps`
|
|
41
|
+
- Fix `NdjsonLedger.append` to use `appendFileSync` (O_APPEND) instead of read-then-overwrite (O_TRUNC), preventing ledger truncation on crash
|
|
42
|
+
- Make `NdjsonLedger.readAll` resilient to malformed lines from partial writes
|
|
43
|
+
- Add `OpenSandboxControlAdapter` and `OpenSandboxExecFactory` to `@drej/opensandbox` — concrete implementations of `ISandboxControl` and `IExecClientFactory` that encapsulate execd readiness polling
|
|
44
|
+
- Remove `as unknown as` double-cast from `apps/api`; adapter wiring is now explicit and type-safe
|
|
45
|
+
|
|
3
46
|
## 0.3.0
|
|
4
47
|
|
|
5
48
|
### Minor Changes
|
package/package.json
CHANGED
package/src/client.ts
CHANGED
|
@@ -185,6 +185,7 @@ export interface DrejClientOptions {
|
|
|
185
185
|
// ── Workflow types ─────────────────────────────────────────────────────────
|
|
186
186
|
|
|
187
187
|
export type WorkflowEventKind =
|
|
188
|
+
| "run_started"
|
|
188
189
|
| "step_start"
|
|
189
190
|
| "step_complete"
|
|
190
191
|
| "step_failed"
|
|
@@ -192,19 +193,44 @@ export type WorkflowEventKind =
|
|
|
192
193
|
| "workflow_complete"
|
|
193
194
|
| "workflow_failed"
|
|
194
195
|
| "checkpoint"
|
|
195
|
-
| "exec_event"
|
|
196
|
+
| "exec_event"
|
|
197
|
+
| "snapshot";
|
|
198
|
+
|
|
199
|
+
export interface SnapshotConfig {
|
|
200
|
+
/** Take a snapshot after these step indices (0-based). */
|
|
201
|
+
afterSteps?: number[];
|
|
202
|
+
/** Take a snapshot after every N steps. */
|
|
203
|
+
everyNSteps?: number;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export interface RunOptions {
|
|
207
|
+
snapshotConfig?: SnapshotConfig;
|
|
208
|
+
}
|
|
196
209
|
|
|
197
210
|
export interface WorkflowEvent {
|
|
198
211
|
ts: number;
|
|
199
|
-
|
|
212
|
+
workflowName: string;
|
|
213
|
+
runId: string;
|
|
200
214
|
stepIndex: number;
|
|
201
|
-
branch?: number;
|
|
215
|
+
branch?: number;
|
|
202
216
|
event: WorkflowEventKind;
|
|
203
217
|
payload?: unknown;
|
|
204
218
|
error?: string;
|
|
205
219
|
result?: unknown;
|
|
206
220
|
}
|
|
207
221
|
|
|
222
|
+
export class WorkflowRun implements AsyncIterable<WorkflowEvent> {
|
|
223
|
+
constructor(
|
|
224
|
+
public readonly name: string,
|
|
225
|
+
public readonly id: string,
|
|
226
|
+
private readonly _events: AsyncGenerator<WorkflowEvent>,
|
|
227
|
+
) {}
|
|
228
|
+
|
|
229
|
+
[Symbol.asyncIterator](): AsyncIterator<WorkflowEvent> {
|
|
230
|
+
return this._events;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
208
234
|
export type Predicate =
|
|
209
235
|
| { op: "eq" | "neq"; field: string; value: unknown }
|
|
210
236
|
| { op: "gt" | "lt" | "gte" | "lte"; field: string; value: number }
|
|
@@ -229,7 +255,8 @@ export type StepDef =
|
|
|
229
255
|
| { type: "retry"; step: StepDef; maxAttempts: number; delayMs?: number; backoff?: "fixed" | "exponential" }
|
|
230
256
|
| { type: "conditional"; condition: Predicate; then: StepDef[]; else?: StepDef[] }
|
|
231
257
|
| { type: "loop"; over?: string; items?: unknown[]; as: string; steps: StepDef[]; concurrently?: boolean }
|
|
232
|
-
| { type: "parallel"; steps: StepDef[] }
|
|
258
|
+
| { type: "parallel"; steps: StepDef[] }
|
|
259
|
+
| { type: "sequence"; steps: StepDef[] };
|
|
233
260
|
|
|
234
261
|
// ── SSE parsers ────────────────────────────────────────────────────────────
|
|
235
262
|
|
|
@@ -560,29 +587,98 @@ export class DrejClient {
|
|
|
560
587
|
|
|
561
588
|
// ── Workflows ────────────────────────────────────────────────────────────
|
|
562
589
|
|
|
563
|
-
async
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
590
|
+
async run(
|
|
591
|
+
w: { build(): { name: string; steps: StepDef[] } },
|
|
592
|
+
options?: RunOptions,
|
|
593
|
+
): Promise<WorkflowRun> {
|
|
594
|
+
const { name, steps } = w.build();
|
|
595
|
+
return this._startRun(name, steps, options?.snapshotConfig);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Start a new run using a snapshot captured from a previous run.
|
|
600
|
+
*
|
|
601
|
+
* Reads the ledger for the given run, finds the latest snapshot entry,
|
|
602
|
+
* and injects the snapshotId into the first `create_sandbox` step of the
|
|
603
|
+
* provided workflow — so the new sandbox boots from that snapshot with the
|
|
604
|
+
* previous environment already in place.
|
|
605
|
+
*/
|
|
606
|
+
async replayFromSnapshot(
|
|
607
|
+
name: string,
|
|
608
|
+
runId: string,
|
|
609
|
+
w: { build(): { name: string; steps: StepDef[] } },
|
|
610
|
+
): Promise<WorkflowRun> {
|
|
611
|
+
const ledger = await this.getWorkflowLedger(name, runId);
|
|
612
|
+
const snapEntry = [...ledger].reverse().find((e) => e.event === "snapshot");
|
|
613
|
+
if (!snapEntry) throw new DrejError(`No snapshot found in ledger for ${name}/${runId}`, 404);
|
|
614
|
+
const { snapshotId } = snapEntry.payload as { snapshotId: string };
|
|
615
|
+
|
|
616
|
+
const { name: wfName, steps } = w.build();
|
|
617
|
+
const replaySteps: StepDef[] = steps.map((step) =>
|
|
618
|
+
step.type === "create_sandbox" ? { ...step, snapshotId } : step,
|
|
619
|
+
);
|
|
620
|
+
|
|
621
|
+
return this._startRun(wfName, replaySteps);
|
|
572
622
|
}
|
|
573
623
|
|
|
574
|
-
async
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
624
|
+
async resumeRun(
|
|
625
|
+
name: string,
|
|
626
|
+
runId: string,
|
|
627
|
+
w: { build(): { name: string; steps: StepDef[] } } | StepDef[],
|
|
628
|
+
): Promise<WorkflowRun> {
|
|
629
|
+
const steps = Array.isArray(w) ? w : w.build().steps;
|
|
630
|
+
const res = await fetch(
|
|
631
|
+
`${this.baseUrl}/v1/workflows/${encodeURIComponent(name)}/runs/${encodeURIComponent(runId)}/resume`,
|
|
632
|
+
{
|
|
633
|
+
method: "POST",
|
|
634
|
+
headers: { "Content-Type": "application/json" },
|
|
635
|
+
body: JSON.stringify({ steps }),
|
|
636
|
+
},
|
|
637
|
+
);
|
|
580
638
|
if (!res.ok) throw new DrejError("drej API error", res.status);
|
|
581
|
-
if (!res.body)
|
|
582
|
-
|
|
639
|
+
if (!res.body) throw new DrejError("empty response body", 500);
|
|
640
|
+
return this._consumeRunStarted(name, res.body);
|
|
583
641
|
}
|
|
584
642
|
|
|
585
|
-
|
|
586
|
-
return this.request("GET", `/v1/workflows/${
|
|
643
|
+
listWorkflowRuns(name: string): Promise<{ runs: string[] }> {
|
|
644
|
+
return this.request("GET", `/v1/workflows/${encodeURIComponent(name)}/runs`);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
getWorkflowLedger(name: string, runId: string): Promise<WorkflowEvent[]> {
|
|
648
|
+
return this.request(
|
|
649
|
+
"GET",
|
|
650
|
+
`/v1/workflows/${encodeURIComponent(name)}/runs/${encodeURIComponent(runId)}/ledger`,
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
private async _startRun(
|
|
655
|
+
name: string,
|
|
656
|
+
steps: StepDef[],
|
|
657
|
+
snapshotConfig?: SnapshotConfig,
|
|
658
|
+
): Promise<WorkflowRun> {
|
|
659
|
+
const res = await fetch(
|
|
660
|
+
`${this.baseUrl}/v1/workflows/${encodeURIComponent(name)}/runs`,
|
|
661
|
+
{
|
|
662
|
+
method: "POST",
|
|
663
|
+
headers: { "Content-Type": "application/json" },
|
|
664
|
+
body: JSON.stringify({ steps, ...(snapshotConfig ? { snapshotConfig } : {}) }),
|
|
665
|
+
},
|
|
666
|
+
);
|
|
667
|
+
if (!res.ok) throw new DrejError("drej API error", res.status);
|
|
668
|
+
if (!res.body) throw new DrejError("empty response body", 500);
|
|
669
|
+
return this._consumeRunStarted(name, res.body);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
private async _consumeRunStarted(
|
|
673
|
+
name: string,
|
|
674
|
+
body: ReadableStream<Uint8Array>,
|
|
675
|
+
): Promise<WorkflowRun> {
|
|
676
|
+
const stream = parseWorkflowSSE(body);
|
|
677
|
+
const first = await stream.next();
|
|
678
|
+
if (first.done || first.value.event !== "run_started") {
|
|
679
|
+
throw new DrejError("expected run_started as first SSE event", 500);
|
|
680
|
+
}
|
|
681
|
+
const { runId } = first.value.payload as { runId: string };
|
|
682
|
+
return new WorkflowRun(name, runId, stream);
|
|
587
683
|
}
|
|
588
684
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { DrejClient, DrejError } from "./client";
|
|
1
|
+
export { DrejClient, DrejError, WorkflowRun } from "./client";
|
|
2
2
|
export type {
|
|
3
3
|
DrejClientOptions,
|
|
4
4
|
Resources,
|
|
@@ -27,4 +27,9 @@ export type {
|
|
|
27
27
|
WorkflowEvent,
|
|
28
28
|
WorkflowEventKind,
|
|
29
29
|
StepDef,
|
|
30
|
+
SnapshotConfig,
|
|
31
|
+
RunOptions,
|
|
30
32
|
} from "./client";
|
|
33
|
+
|
|
34
|
+
export { workflow, WorkflowBuilder, SandboxStepBuilder } from "./workflow";
|
|
35
|
+
export type { SandboxOpts, LoopItem } from "./workflow";
|
package/src/workflow.ts
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import type { StepDef, ImageSpec, Resources, Predicate } from "./client";
|
|
2
|
+
|
|
3
|
+
// Placeholder that serialises to {{name}} inside template literals.
|
|
4
|
+
// Used as the `item` parameter in forEach callbacks so users write
|
|
5
|
+
// `s.exec(`upload ${item}`)` instead of `s.exec("upload {{item}}")`.
|
|
6
|
+
class LoopVar {
|
|
7
|
+
constructor(private name: string) {}
|
|
8
|
+
toString() {
|
|
9
|
+
return `{{${this.name}}}`;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type LoopItem = { toString(): string };
|
|
14
|
+
|
|
15
|
+
export type SandboxOpts = {
|
|
16
|
+
image?: ImageSpec;
|
|
17
|
+
snapshotId?: string;
|
|
18
|
+
timeout?: number;
|
|
19
|
+
entrypoint?: string[];
|
|
20
|
+
env?: Record<string, string>;
|
|
21
|
+
metadata?: Record<string, string>;
|
|
22
|
+
resourceLimits?: Resources;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type ForEachOpts = {
|
|
26
|
+
concurrency?: number;
|
|
27
|
+
as?: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type ForEachSource = unknown[] | { from: string };
|
|
31
|
+
type ForEachCallback = (s: SandboxStepBuilder, item: LoopItem) => SandboxStepBuilder | string;
|
|
32
|
+
|
|
33
|
+
function wrapSteps(steps: StepDef[]): StepDef {
|
|
34
|
+
return steps.length === 1 ? steps[0] : { type: "sequence", steps };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ── SandboxStepBuilder ────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
export class SandboxStepBuilder {
|
|
40
|
+
protected _steps: StepDef[] = [];
|
|
41
|
+
|
|
42
|
+
exec(command: string, opts?: { cwd?: string; envs?: Record<string, string> }): this {
|
|
43
|
+
this._steps.push({ type: "exec_command", command, ...opts });
|
|
44
|
+
return this;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
writeFile(path: string, content: string, encoding?: "utf8" | "base64"): this {
|
|
48
|
+
this._steps.push({ type: "write_file", path, content, ...(encoding ? { encoding } : {}) });
|
|
49
|
+
return this;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
retry(
|
|
53
|
+
maxAttempts: number,
|
|
54
|
+
fn: (s: SandboxStepBuilder) => SandboxStepBuilder,
|
|
55
|
+
opts?: { delayMs?: number; backoff?: "fixed" | "exponential" },
|
|
56
|
+
): this {
|
|
57
|
+
const inner = new SandboxStepBuilder();
|
|
58
|
+
fn(inner);
|
|
59
|
+
this._steps.push({ type: "retry", step: wrapSteps(inner.build()), maxAttempts, ...opts });
|
|
60
|
+
return this;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
forEach(source: ForEachSource, fn: ForEachCallback): this;
|
|
64
|
+
forEach(source: ForEachSource, opts: ForEachOpts, fn: ForEachCallback): this;
|
|
65
|
+
forEach(
|
|
66
|
+
source: ForEachSource,
|
|
67
|
+
optsOrFn: ForEachOpts | ForEachCallback,
|
|
68
|
+
fn?: ForEachCallback,
|
|
69
|
+
): this {
|
|
70
|
+
const opts: ForEachOpts = typeof optsOrFn === "function" ? {} : optsOrFn;
|
|
71
|
+
const callback: ForEachCallback = typeof optsOrFn === "function" ? optsOrFn : fn!;
|
|
72
|
+
|
|
73
|
+
const varName = opts.as ?? "item";
|
|
74
|
+
const loopVar = new LoopVar(varName);
|
|
75
|
+
const inner = new SandboxStepBuilder();
|
|
76
|
+
const result = callback(inner, loopVar);
|
|
77
|
+
|
|
78
|
+
const steps: StepDef[] =
|
|
79
|
+
typeof result === "string"
|
|
80
|
+
? [{ type: "exec_command", command: result }]
|
|
81
|
+
: result.build();
|
|
82
|
+
|
|
83
|
+
this._steps.push({
|
|
84
|
+
type: "loop",
|
|
85
|
+
as: varName,
|
|
86
|
+
steps,
|
|
87
|
+
...(Array.isArray(source) ? { items: source } : { over: source.from }),
|
|
88
|
+
...(opts.concurrency !== undefined && opts.concurrency > 1 ? { concurrently: true } : {}),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return this;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
when(
|
|
95
|
+
condition: Predicate,
|
|
96
|
+
thenFn: (s: SandboxStepBuilder) => SandboxStepBuilder,
|
|
97
|
+
elseFn?: (s: SandboxStepBuilder) => SandboxStepBuilder,
|
|
98
|
+
): this {
|
|
99
|
+
const thenBuilder = new SandboxStepBuilder();
|
|
100
|
+
thenFn(thenBuilder);
|
|
101
|
+
|
|
102
|
+
const elseSteps = elseFn
|
|
103
|
+
? (() => {
|
|
104
|
+
const b = new SandboxStepBuilder();
|
|
105
|
+
elseFn(b);
|
|
106
|
+
return b.build();
|
|
107
|
+
})()
|
|
108
|
+
: undefined;
|
|
109
|
+
|
|
110
|
+
this._steps.push({
|
|
111
|
+
type: "conditional",
|
|
112
|
+
condition,
|
|
113
|
+
then: thenBuilder.build(),
|
|
114
|
+
...(elseSteps ? { else: elseSteps } : {}),
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
return this;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
parallel(fn: (p: SandboxParallelBuilder) => SandboxParallelBuilder): this {
|
|
121
|
+
const pb = new SandboxParallelBuilder();
|
|
122
|
+
fn(pb);
|
|
123
|
+
this._steps.push({ type: "parallel", steps: pb.build() });
|
|
124
|
+
return this;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
build(): StepDef[] {
|
|
128
|
+
return [...this._steps];
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── SandboxParallelBuilder ────────────────────────────────────────────────────
|
|
133
|
+
// Used inside a sandbox scope — branches share the same sandbox, no new ones.
|
|
134
|
+
|
|
135
|
+
class SandboxParallelBuilder {
|
|
136
|
+
private _branches: StepDef[] = [];
|
|
137
|
+
|
|
138
|
+
branch(fn: (s: SandboxStepBuilder) => SandboxStepBuilder): this {
|
|
139
|
+
const sb = new SandboxStepBuilder();
|
|
140
|
+
fn(sb);
|
|
141
|
+
this._branches.push(wrapSteps(sb.build()));
|
|
142
|
+
return this;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
build(): StepDef[] {
|
|
146
|
+
return this._branches;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ── WorkflowParallelBuilder ───────────────────────────────────────────────────
|
|
151
|
+
// Used at the top-level workflow scope — each branch can own its own sandbox.
|
|
152
|
+
|
|
153
|
+
class WorkflowParallelBuilder {
|
|
154
|
+
private _branches: StepDef[] = [];
|
|
155
|
+
|
|
156
|
+
sandbox(opts: SandboxOpts, fn: (s: SandboxStepBuilder) => SandboxStepBuilder): this {
|
|
157
|
+
const sb = new SandboxStepBuilder();
|
|
158
|
+
fn(sb);
|
|
159
|
+
this._branches.push({
|
|
160
|
+
type: "sequence",
|
|
161
|
+
steps: [
|
|
162
|
+
{ type: "create_sandbox", entrypoint: ["tail", "-f", "/dev/null"], ...opts },
|
|
163
|
+
...sb.build(),
|
|
164
|
+
{ type: "delete_sandbox" },
|
|
165
|
+
],
|
|
166
|
+
});
|
|
167
|
+
return this;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
branch(fn: (s: SandboxStepBuilder) => SandboxStepBuilder): this {
|
|
171
|
+
const sb = new SandboxStepBuilder();
|
|
172
|
+
fn(sb);
|
|
173
|
+
this._branches.push(wrapSteps(sb.build()));
|
|
174
|
+
return this;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
build(): StepDef[] {
|
|
178
|
+
return this._branches;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ── WorkflowBuilder ───────────────────────────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
export class WorkflowBuilder {
|
|
185
|
+
private _steps: StepDef[] = [];
|
|
186
|
+
|
|
187
|
+
constructor(private _name: string) {}
|
|
188
|
+
|
|
189
|
+
sandbox(opts: SandboxOpts, fn: (s: SandboxStepBuilder) => SandboxStepBuilder): this {
|
|
190
|
+
const sb = new SandboxStepBuilder();
|
|
191
|
+
fn(sb);
|
|
192
|
+
this._steps.push(
|
|
193
|
+
{ type: "create_sandbox", entrypoint: ["tail", "-f", "/dev/null"], ...opts },
|
|
194
|
+
...sb.build(),
|
|
195
|
+
{ type: "delete_sandbox" },
|
|
196
|
+
);
|
|
197
|
+
return this;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
parallel(fn: (p: WorkflowParallelBuilder) => WorkflowParallelBuilder): this {
|
|
201
|
+
const pb = new WorkflowParallelBuilder();
|
|
202
|
+
fn(pb);
|
|
203
|
+
this._steps.push({ type: "parallel", steps: pb.build() });
|
|
204
|
+
return this;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
build(): { name: string; steps: StepDef[] } {
|
|
208
|
+
return { name: this._name, steps: this._steps };
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function workflow(name: string): WorkflowBuilder {
|
|
213
|
+
return new WorkflowBuilder(name);
|
|
214
|
+
}
|