@cascade-flow/runner 0.2.4 → 0.2.5
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/README.md +25 -78
- package/dist/index.d.ts +1 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +203 -495
- package/dist/index.js.map +7 -7
- package/dist/validation.d.ts +27 -0
- package/dist/validation.d.ts.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -9,8 +9,158 @@ var __export = (target, all) => {
|
|
|
9
9
|
});
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
-
// src/
|
|
13
|
-
import {
|
|
12
|
+
// src/subprocess-executor.ts
|
|
13
|
+
import { spawn } from "node:child_process";
|
|
14
|
+
import { resolve, dirname } from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
import { mkdir, readFile, unlink } from "node:fs/promises";
|
|
17
|
+
import { getMicrosecondTimestamp, ensureErrorMessage } from "@cascade-flow/backend-interface";
|
|
18
|
+
function createStreamHandler(streamType, attemptNumber, emitLog) {
|
|
19
|
+
let buffer = "";
|
|
20
|
+
const handler = (chunk) => {
|
|
21
|
+
buffer += chunk.toString();
|
|
22
|
+
const lines = buffer.split(`
|
|
23
|
+
`);
|
|
24
|
+
buffer = lines.pop() || "";
|
|
25
|
+
for (const line of lines) {
|
|
26
|
+
if (!line.trim())
|
|
27
|
+
continue;
|
|
28
|
+
const timestamp = getMicrosecondTimestamp();
|
|
29
|
+
emitLog({
|
|
30
|
+
timestamp,
|
|
31
|
+
stream: streamType,
|
|
32
|
+
message: line,
|
|
33
|
+
attemptNumber
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const getBuffer = () => buffer;
|
|
38
|
+
const flushBuffer = () => {
|
|
39
|
+
if (buffer.trim()) {
|
|
40
|
+
emitLog({
|
|
41
|
+
timestamp: getMicrosecondTimestamp(),
|
|
42
|
+
stream: streamType,
|
|
43
|
+
message: buffer,
|
|
44
|
+
attemptNumber
|
|
45
|
+
});
|
|
46
|
+
buffer = "";
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
return { handler, getBuffer, flushBuffer };
|
|
50
|
+
}
|
|
51
|
+
async function executeStepInSubprocess(stepFile, stepId, dependencies, ctx, attemptNumber, outputPath, onLog, options) {
|
|
52
|
+
const executorPath = resolve(dirname(fileURLToPath(import.meta.url)), "step-executor");
|
|
53
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
54
|
+
return new Promise((resolve2, reject) => {
|
|
55
|
+
const child = spawn("bun", [executorPath], {
|
|
56
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
57
|
+
env: {
|
|
58
|
+
...process.env,
|
|
59
|
+
STEP_OUTPUT_FILE: outputPath
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
const logs = [];
|
|
63
|
+
const logWritePromises = [];
|
|
64
|
+
let logError = null;
|
|
65
|
+
const emitLog = (entry) => {
|
|
66
|
+
logs.push(entry);
|
|
67
|
+
if (!onLog)
|
|
68
|
+
return;
|
|
69
|
+
const trackedPromise = Promise.resolve(onLog(entry)).catch((err) => {
|
|
70
|
+
if (!logError) {
|
|
71
|
+
logError = err instanceof Error ? err : new Error(String(err));
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
logWritePromises.push(trackedPromise);
|
|
75
|
+
};
|
|
76
|
+
const signal = options?.signal;
|
|
77
|
+
let aborted = false;
|
|
78
|
+
let abortReason;
|
|
79
|
+
const abortHandler = signal ? () => {
|
|
80
|
+
if (aborted)
|
|
81
|
+
return;
|
|
82
|
+
aborted = true;
|
|
83
|
+
abortReason = signal?.reason ?? new Error("Step execution aborted");
|
|
84
|
+
try {
|
|
85
|
+
child.kill("SIGKILL");
|
|
86
|
+
} catch {}
|
|
87
|
+
} : null;
|
|
88
|
+
const cleanup = () => {
|
|
89
|
+
if (signal && abortHandler) {
|
|
90
|
+
signal.removeEventListener("abort", abortHandler);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
if (signal) {
|
|
94
|
+
if (signal.aborted) {
|
|
95
|
+
abortHandler?.();
|
|
96
|
+
} else {
|
|
97
|
+
signal.addEventListener("abort", abortHandler);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const stdoutHandler = createStreamHandler("stdout", attemptNumber, emitLog);
|
|
101
|
+
const stderrHandler = createStreamHandler("stderr", attemptNumber, emitLog);
|
|
102
|
+
child.stdout.on("data", stdoutHandler.handler);
|
|
103
|
+
child.stderr.on("data", stderrHandler.handler);
|
|
104
|
+
child.on("error", (err) => {
|
|
105
|
+
cleanup();
|
|
106
|
+
reject(err);
|
|
107
|
+
});
|
|
108
|
+
child.on("close", async (code, signal2) => {
|
|
109
|
+
cleanup();
|
|
110
|
+
stdoutHandler.flushBuffer();
|
|
111
|
+
stderrHandler.flushBuffer();
|
|
112
|
+
try {
|
|
113
|
+
await Promise.all(logWritePromises);
|
|
114
|
+
} catch {}
|
|
115
|
+
if (logError) {
|
|
116
|
+
reject(logError);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (aborted) {
|
|
120
|
+
const reason = abortReason instanceof Error ? abortReason : new Error(String(abortReason ?? "Step execution aborted"));
|
|
121
|
+
reject(reason);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (code === 0) {
|
|
125
|
+
try {
|
|
126
|
+
const outputContent = await readFile(outputPath, "utf-8");
|
|
127
|
+
const result = JSON.parse(outputContent);
|
|
128
|
+
try {
|
|
129
|
+
await unlink(outputPath);
|
|
130
|
+
} catch {}
|
|
131
|
+
resolve2({ result, logs });
|
|
132
|
+
} catch (err) {
|
|
133
|
+
reject(new Error(`Failed to read/parse output file ${outputPath}: ${err}`));
|
|
134
|
+
}
|
|
135
|
+
} else if (signal2) {
|
|
136
|
+
const error = new Error(`Step process killed by signal: ${signal2}`);
|
|
137
|
+
error.isCrash = true;
|
|
138
|
+
reject(error);
|
|
139
|
+
} else {
|
|
140
|
+
const lastStderrLog = logs.filter((l) => l.stream === "stderr").pop();
|
|
141
|
+
if (lastStderrLog) {
|
|
142
|
+
try {
|
|
143
|
+
const errorObj = JSON.parse(lastStderrLog.message);
|
|
144
|
+
const errorMessage = ensureErrorMessage(errorObj.message);
|
|
145
|
+
const error = new Error(errorMessage);
|
|
146
|
+
error.stack = errorObj.stack;
|
|
147
|
+
error.name = errorObj.name || "Error";
|
|
148
|
+
reject(error);
|
|
149
|
+
} catch {
|
|
150
|
+
const errorMessage = logs.filter((l) => l.stream === "stderr").map((l) => l.message).join(`
|
|
151
|
+
`);
|
|
152
|
+
reject(new Error(errorMessage || `Step process exited with code ${code}`));
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
reject(new Error(`Step process exited with code ${code}`));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
const input = JSON.stringify({ stepPath: stepFile, dependencies, ctx });
|
|
160
|
+
child.stdin.write(input);
|
|
161
|
+
child.stdin.end();
|
|
162
|
+
});
|
|
163
|
+
}
|
|
14
164
|
|
|
15
165
|
// src/discovery.ts
|
|
16
166
|
import fs from "node:fs/promises";
|
|
@@ -12626,184 +12776,6 @@ async function discoverSteps(root = path.resolve("steps")) {
|
|
|
12626
12776
|
}
|
|
12627
12777
|
return loaded;
|
|
12628
12778
|
}
|
|
12629
|
-
|
|
12630
|
-
// src/validation.ts
|
|
12631
|
-
function detectCycles(steps) {
|
|
12632
|
-
const visiting = new Set;
|
|
12633
|
-
const visited = new Set;
|
|
12634
|
-
function dfs(s) {
|
|
12635
|
-
if (visited.has(s.id))
|
|
12636
|
-
return;
|
|
12637
|
-
if (visiting.has(s.id)) {
|
|
12638
|
-
throw new Error(`Cycle detected involving step "${s.name}" (id: ${s.id})`);
|
|
12639
|
-
}
|
|
12640
|
-
visiting.add(s.id);
|
|
12641
|
-
for (const dep of Object.values(s.dependencies))
|
|
12642
|
-
dfs(dep);
|
|
12643
|
-
visiting.delete(s.id);
|
|
12644
|
-
visited.add(s.id);
|
|
12645
|
-
}
|
|
12646
|
-
for (const s of steps)
|
|
12647
|
-
dfs(s);
|
|
12648
|
-
}
|
|
12649
|
-
|
|
12650
|
-
// src/subprocess-executor.ts
|
|
12651
|
-
import { spawn } from "node:child_process";
|
|
12652
|
-
import { resolve, dirname } from "node:path";
|
|
12653
|
-
import { fileURLToPath } from "node:url";
|
|
12654
|
-
import { mkdir, readFile, unlink } from "node:fs/promises";
|
|
12655
|
-
import { getMicrosecondTimestamp, ensureErrorMessage } from "@cascade-flow/backend-interface";
|
|
12656
|
-
function createStreamHandler(streamType, attemptNumber, emitLog) {
|
|
12657
|
-
let buffer = "";
|
|
12658
|
-
const handler = (chunk) => {
|
|
12659
|
-
buffer += chunk.toString();
|
|
12660
|
-
const lines = buffer.split(`
|
|
12661
|
-
`);
|
|
12662
|
-
buffer = lines.pop() || "";
|
|
12663
|
-
for (const line of lines) {
|
|
12664
|
-
if (!line.trim())
|
|
12665
|
-
continue;
|
|
12666
|
-
const timestamp = getMicrosecondTimestamp();
|
|
12667
|
-
emitLog({
|
|
12668
|
-
timestamp,
|
|
12669
|
-
stream: streamType,
|
|
12670
|
-
message: line,
|
|
12671
|
-
attemptNumber
|
|
12672
|
-
});
|
|
12673
|
-
}
|
|
12674
|
-
};
|
|
12675
|
-
const getBuffer = () => buffer;
|
|
12676
|
-
const flushBuffer = () => {
|
|
12677
|
-
if (buffer.trim()) {
|
|
12678
|
-
emitLog({
|
|
12679
|
-
timestamp: getMicrosecondTimestamp(),
|
|
12680
|
-
stream: streamType,
|
|
12681
|
-
message: buffer,
|
|
12682
|
-
attemptNumber
|
|
12683
|
-
});
|
|
12684
|
-
buffer = "";
|
|
12685
|
-
}
|
|
12686
|
-
};
|
|
12687
|
-
return { handler, getBuffer, flushBuffer };
|
|
12688
|
-
}
|
|
12689
|
-
async function executeStepInSubprocess(stepFile, stepId, dependencies, ctx, attemptNumber, outputPath, onLog, options) {
|
|
12690
|
-
const executorPath = resolve(dirname(fileURLToPath(import.meta.url)), "step-executor");
|
|
12691
|
-
await mkdir(dirname(outputPath), { recursive: true });
|
|
12692
|
-
return new Promise((resolve2, reject) => {
|
|
12693
|
-
const child = spawn("bun", [executorPath], {
|
|
12694
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
12695
|
-
env: {
|
|
12696
|
-
...process.env,
|
|
12697
|
-
STEP_OUTPUT_FILE: outputPath
|
|
12698
|
-
}
|
|
12699
|
-
});
|
|
12700
|
-
const logs = [];
|
|
12701
|
-
const logWritePromises = [];
|
|
12702
|
-
let logError = null;
|
|
12703
|
-
const emitLog = (entry) => {
|
|
12704
|
-
logs.push(entry);
|
|
12705
|
-
if (!onLog)
|
|
12706
|
-
return;
|
|
12707
|
-
const trackedPromise = Promise.resolve(onLog(entry)).catch((err) => {
|
|
12708
|
-
if (!logError) {
|
|
12709
|
-
logError = err instanceof Error ? err : new Error(String(err));
|
|
12710
|
-
}
|
|
12711
|
-
});
|
|
12712
|
-
logWritePromises.push(trackedPromise);
|
|
12713
|
-
};
|
|
12714
|
-
const signal = options?.signal;
|
|
12715
|
-
let aborted2 = false;
|
|
12716
|
-
let abortReason;
|
|
12717
|
-
const abortHandler = signal ? () => {
|
|
12718
|
-
if (aborted2)
|
|
12719
|
-
return;
|
|
12720
|
-
aborted2 = true;
|
|
12721
|
-
abortReason = signal?.reason ?? new Error("Step execution aborted");
|
|
12722
|
-
try {
|
|
12723
|
-
child.kill("SIGKILL");
|
|
12724
|
-
} catch {}
|
|
12725
|
-
} : null;
|
|
12726
|
-
const cleanup = () => {
|
|
12727
|
-
if (signal && abortHandler) {
|
|
12728
|
-
signal.removeEventListener("abort", abortHandler);
|
|
12729
|
-
}
|
|
12730
|
-
};
|
|
12731
|
-
if (signal) {
|
|
12732
|
-
if (signal.aborted) {
|
|
12733
|
-
abortHandler?.();
|
|
12734
|
-
} else {
|
|
12735
|
-
signal.addEventListener("abort", abortHandler);
|
|
12736
|
-
}
|
|
12737
|
-
}
|
|
12738
|
-
const stdoutHandler = createStreamHandler("stdout", attemptNumber, emitLog);
|
|
12739
|
-
const stderrHandler = createStreamHandler("stderr", attemptNumber, emitLog);
|
|
12740
|
-
child.stdout.on("data", stdoutHandler.handler);
|
|
12741
|
-
child.stderr.on("data", stderrHandler.handler);
|
|
12742
|
-
child.on("error", (err) => {
|
|
12743
|
-
cleanup();
|
|
12744
|
-
reject(err);
|
|
12745
|
-
});
|
|
12746
|
-
child.on("close", async (code, signal2) => {
|
|
12747
|
-
cleanup();
|
|
12748
|
-
stdoutHandler.flushBuffer();
|
|
12749
|
-
stderrHandler.flushBuffer();
|
|
12750
|
-
try {
|
|
12751
|
-
await Promise.all(logWritePromises);
|
|
12752
|
-
} catch {}
|
|
12753
|
-
if (logError) {
|
|
12754
|
-
reject(logError);
|
|
12755
|
-
return;
|
|
12756
|
-
}
|
|
12757
|
-
if (aborted2) {
|
|
12758
|
-
const reason = abortReason instanceof Error ? abortReason : new Error(String(abortReason ?? "Step execution aborted"));
|
|
12759
|
-
reject(reason);
|
|
12760
|
-
return;
|
|
12761
|
-
}
|
|
12762
|
-
if (code === 0) {
|
|
12763
|
-
try {
|
|
12764
|
-
const outputContent = await readFile(outputPath, "utf-8");
|
|
12765
|
-
const result = JSON.parse(outputContent);
|
|
12766
|
-
try {
|
|
12767
|
-
await unlink(outputPath);
|
|
12768
|
-
} catch {}
|
|
12769
|
-
resolve2({ result, logs });
|
|
12770
|
-
} catch (err) {
|
|
12771
|
-
reject(new Error(`Failed to read/parse output file ${outputPath}: ${err}`));
|
|
12772
|
-
}
|
|
12773
|
-
} else if (signal2) {
|
|
12774
|
-
const error46 = new Error(`Step process killed by signal: ${signal2}`);
|
|
12775
|
-
error46.isCrash = true;
|
|
12776
|
-
reject(error46);
|
|
12777
|
-
} else {
|
|
12778
|
-
const lastStderrLog = logs.filter((l) => l.stream === "stderr").pop();
|
|
12779
|
-
if (lastStderrLog) {
|
|
12780
|
-
try {
|
|
12781
|
-
const errorObj = JSON.parse(lastStderrLog.message);
|
|
12782
|
-
const errorMessage = ensureErrorMessage(errorObj.message);
|
|
12783
|
-
const error46 = new Error(errorMessage);
|
|
12784
|
-
error46.stack = errorObj.stack;
|
|
12785
|
-
error46.name = errorObj.name || "Error";
|
|
12786
|
-
reject(error46);
|
|
12787
|
-
} catch {
|
|
12788
|
-
const errorMessage = logs.filter((l) => l.stream === "stderr").map((l) => l.message).join(`
|
|
12789
|
-
`);
|
|
12790
|
-
reject(new Error(errorMessage || `Step process exited with code ${code}`));
|
|
12791
|
-
}
|
|
12792
|
-
} else {
|
|
12793
|
-
reject(new Error(`Step process exited with code ${code}`));
|
|
12794
|
-
}
|
|
12795
|
-
}
|
|
12796
|
-
});
|
|
12797
|
-
const input = JSON.stringify({ stepPath: stepFile, dependencies, ctx });
|
|
12798
|
-
child.stdin.write(input);
|
|
12799
|
-
child.stdin.end();
|
|
12800
|
-
});
|
|
12801
|
-
}
|
|
12802
|
-
|
|
12803
|
-
// src/index.ts
|
|
12804
|
-
import { getMicrosecondTimestamp as getMicrosecondTimestamp2 } from "@cascade-flow/backend-interface";
|
|
12805
|
-
import { Skip, isOptional as isOptional2 } from "@cascade-flow/workflow";
|
|
12806
|
-
|
|
12807
12779
|
// src/versioning.ts
|
|
12808
12780
|
import { createHash } from "node:crypto";
|
|
12809
12781
|
import { readFile as readFile2, readdir } from "node:fs/promises";
|
|
@@ -12882,332 +12854,68 @@ async function getGitInfo(workflowDir) {
|
|
|
12882
12854
|
return;
|
|
12883
12855
|
}
|
|
12884
12856
|
}
|
|
12857
|
+
// src/validation.ts
|
|
12858
|
+
function getAllDependents(targetStepId, allSteps) {
|
|
12859
|
+
const dependentsMap = new Map;
|
|
12860
|
+
for (const step of allSteps) {
|
|
12861
|
+
for (const depStep of Object.values(step.dependencies)) {
|
|
12862
|
+
if (!dependentsMap.has(depStep.id)) {
|
|
12863
|
+
dependentsMap.set(depStep.id, []);
|
|
12864
|
+
}
|
|
12865
|
+
dependentsMap.get(depStep.id).push(step.id);
|
|
12866
|
+
}
|
|
12867
|
+
}
|
|
12868
|
+
const allDependents = new Set;
|
|
12869
|
+
const visited = new Set;
|
|
12870
|
+
function dfs(stepId) {
|
|
12871
|
+
if (visited.has(stepId))
|
|
12872
|
+
return;
|
|
12873
|
+
visited.add(stepId);
|
|
12874
|
+
const dependents = dependentsMap.get(stepId) || [];
|
|
12875
|
+
for (const dependent of dependents) {
|
|
12876
|
+
allDependents.add(dependent);
|
|
12877
|
+
dfs(dependent);
|
|
12878
|
+
}
|
|
12879
|
+
}
|
|
12880
|
+
dfs(targetStepId);
|
|
12881
|
+
return allDependents;
|
|
12882
|
+
}
|
|
12883
|
+
async function validateWorkflowVersion(workflowSlug, parentRunId, currentVersionId, backend, log) {
|
|
12884
|
+
const workflowEvents = await backend.loadEvents(workflowSlug, parentRunId, { category: "workflow" });
|
|
12885
|
+
const workflowStartedEvent = workflowEvents.find((e) => e.type === "WorkflowStarted");
|
|
12886
|
+
if (!workflowStartedEvent || workflowStartedEvent.type !== "WorkflowStarted") {
|
|
12887
|
+
throw new Error(`Parent run ${parentRunId} has no WorkflowStarted event`);
|
|
12888
|
+
}
|
|
12889
|
+
const parentVersionId = workflowStartedEvent.versionId;
|
|
12890
|
+
if (parentVersionId !== currentVersionId && log) {
|
|
12891
|
+
const previousVersion = await backend.getWorkflowVersion(workflowSlug, parentVersionId);
|
|
12892
|
+
const currentVersion = await backend.getWorkflowVersion(workflowSlug, currentVersionId);
|
|
12893
|
+
const message = [
|
|
12894
|
+
`ℹ️ Workflow definition changed since parent run`,
|
|
12895
|
+
` Parent: ${parentVersionId}`,
|
|
12896
|
+
` Current: ${currentVersionId}`
|
|
12897
|
+
];
|
|
12898
|
+
if (previousVersion?.git && currentVersion?.git) {
|
|
12899
|
+
message.push(` Git: ${previousVersion.git.commit} → ${currentVersion.git.commit}`);
|
|
12900
|
+
}
|
|
12901
|
+
message.forEach((line) => log(line));
|
|
12902
|
+
}
|
|
12903
|
+
return parentVersionId;
|
|
12904
|
+
}
|
|
12885
12905
|
|
|
12886
12906
|
// src/index.ts
|
|
12887
12907
|
async function executeStepInProcess(stepFile, stepId, dependencies, ctx, attemptNumber, backend, onLog, options) {
|
|
12888
12908
|
const outputPath = backend.getStepOutputPath(ctx.workflow.slug, ctx.runId, stepId, attemptNumber);
|
|
12889
12909
|
return executeStepInSubprocess(stepFile, stepId, dependencies, ctx, attemptNumber, outputPath, onLog, options);
|
|
12890
12910
|
}
|
|
12891
|
-
async function runAll(options) {
|
|
12892
|
-
const workflows = await discoverWorkflows();
|
|
12893
|
-
if (workflows.length === 0) {
|
|
12894
|
-
throw new Error('No workflows found. Please create a "workflows" directory with at least one workflow.');
|
|
12895
|
-
}
|
|
12896
|
-
const workflow = workflows.find((w) => w.slug === options.workflow);
|
|
12897
|
-
if (!workflow) {
|
|
12898
|
-
const available = workflows.map((w) => w.slug).join(", ");
|
|
12899
|
-
throw new Error(`Workflow "${options.workflow}" not found. Available workflows: ${available}`);
|
|
12900
|
-
}
|
|
12901
|
-
const steps = await discoverSteps(workflow.stepsDir);
|
|
12902
|
-
detectCycles(steps);
|
|
12903
|
-
const byId = new Map(steps.map((s) => [s.id, s]));
|
|
12904
|
-
const selected = options?.only?.length ? options.only.map((id) => {
|
|
12905
|
-
const s = byId.get(id);
|
|
12906
|
-
if (!s)
|
|
12907
|
-
throw new Error(`Unknown step "${id}"`);
|
|
12908
|
-
return s;
|
|
12909
|
-
}) : steps;
|
|
12910
|
-
const backend = options.backend;
|
|
12911
|
-
const workflowSlug = workflow.slug;
|
|
12912
|
-
const cache = new Map;
|
|
12913
|
-
const skippedSteps = new Set;
|
|
12914
|
-
const runId = options?.runId ?? `${getMicrosecondTimestamp2()}`;
|
|
12915
|
-
const defaultCtx = {
|
|
12916
|
-
runId,
|
|
12917
|
-
workflow: {
|
|
12918
|
-
slug: workflow.slug,
|
|
12919
|
-
name: workflow.name
|
|
12920
|
-
},
|
|
12921
|
-
input: undefined,
|
|
12922
|
-
log: (...args) => console.log("[runner]", ...args),
|
|
12923
|
-
...options?.ctx
|
|
12924
|
-
};
|
|
12925
|
-
const workflowStartTime = getMicrosecondTimestamp2();
|
|
12926
|
-
await backend.initializeRun(workflowSlug, runId);
|
|
12927
|
-
const versionId = await calculateWorkflowHash(workflow);
|
|
12928
|
-
const git = await getGitInfo(workflow.dir);
|
|
12929
|
-
const stepManifest = steps.map((s) => s.id);
|
|
12930
|
-
await backend.createWorkflowVersion({
|
|
12931
|
-
workflowSlug,
|
|
12932
|
-
versionId,
|
|
12933
|
-
createdAt: getMicrosecondTimestamp2(),
|
|
12934
|
-
stepManifest,
|
|
12935
|
-
totalSteps: steps.length,
|
|
12936
|
-
git
|
|
12937
|
-
});
|
|
12938
|
-
const hasInputSchema = workflow.inputSchema !== undefined;
|
|
12939
|
-
const hasInput = options.input !== undefined;
|
|
12940
|
-
await backend.saveWorkflowStart(workflowSlug, runId, {
|
|
12941
|
-
versionId,
|
|
12942
|
-
workflowAttemptNumber: 1,
|
|
12943
|
-
hasInputSchema,
|
|
12944
|
-
hasInput
|
|
12945
|
-
});
|
|
12946
|
-
let validatedInput = undefined;
|
|
12947
|
-
if (workflow.inputSchema) {
|
|
12948
|
-
const parseResult = workflow.inputSchema.safeParse(options.input ?? {});
|
|
12949
|
-
if (!parseResult.success) {
|
|
12950
|
-
const validationErrors = parseResult.error.issues.map((e) => ({
|
|
12951
|
-
path: e.path.join("."),
|
|
12952
|
-
message: e.message
|
|
12953
|
-
}));
|
|
12954
|
-
const errorMessage = validationErrors.map((e) => ` ${e.path}: ${e.message}`).join(`
|
|
12955
|
-
`);
|
|
12956
|
-
const error46 = {
|
|
12957
|
-
name: "ValidationError",
|
|
12958
|
-
message: `Invalid workflow input:
|
|
12959
|
-
${errorMessage}`
|
|
12960
|
-
};
|
|
12961
|
-
await backend.saveWorkflowInputValidation(workflowSlug, runId, {
|
|
12962
|
-
workflowAttemptNumber: 1,
|
|
12963
|
-
hasSchema: true,
|
|
12964
|
-
success: false,
|
|
12965
|
-
error: error46,
|
|
12966
|
-
validationErrors
|
|
12967
|
-
});
|
|
12968
|
-
const duration3 = getMicrosecondTimestamp2() - workflowStartTime;
|
|
12969
|
-
await backend.saveWorkflowFailed(workflowSlug, runId, error46, {
|
|
12970
|
-
workflowAttemptNumber: 1,
|
|
12971
|
-
duration: duration3,
|
|
12972
|
-
completedSteps: 0
|
|
12973
|
-
}, "step-failed");
|
|
12974
|
-
throw new Error(error46.message);
|
|
12975
|
-
}
|
|
12976
|
-
validatedInput = parseResult.data;
|
|
12977
|
-
} else if (options.input !== undefined) {
|
|
12978
|
-
validatedInput = options.input;
|
|
12979
|
-
}
|
|
12980
|
-
defaultCtx.input = validatedInput;
|
|
12981
|
-
if (hasInputSchema || hasInput) {
|
|
12982
|
-
await backend.saveWorkflowInputValidation(workflowSlug, runId, {
|
|
12983
|
-
workflowAttemptNumber: 1,
|
|
12984
|
-
hasSchema: hasInputSchema,
|
|
12985
|
-
success: true
|
|
12986
|
-
});
|
|
12987
|
-
}
|
|
12988
|
-
if (options?.resume) {
|
|
12989
|
-
defaultCtx.log(`Resuming run ${defaultCtx.runId}...`);
|
|
12990
|
-
const workflowEvents = await backend.loadEvents(workflowSlug, defaultCtx.runId, { category: "workflow" });
|
|
12991
|
-
const workflowStartedEvent = workflowEvents.find((e) => e.type === "WorkflowStarted");
|
|
12992
|
-
if (workflowStartedEvent && workflowStartedEvent.type === "WorkflowStarted") {
|
|
12993
|
-
const previousVersionId = workflowStartedEvent.versionId;
|
|
12994
|
-
if (previousVersionId !== versionId) {
|
|
12995
|
-
const previousVersion = await backend.getWorkflowVersion(workflowSlug, previousVersionId);
|
|
12996
|
-
const currentVersion = await backend.getWorkflowVersion(workflowSlug, versionId);
|
|
12997
|
-
defaultCtx.log(`⚠️ Workflow definition changed since original run`);
|
|
12998
|
-
defaultCtx.log(` Original: ${previousVersionId}`);
|
|
12999
|
-
defaultCtx.log(` Current: ${versionId}`);
|
|
13000
|
-
if (previousVersion?.git && currentVersion?.git) {
|
|
13001
|
-
defaultCtx.log(` Git: ${previousVersion.git.commit} → ${currentVersion.git.commit}`);
|
|
13002
|
-
}
|
|
13003
|
-
}
|
|
13004
|
-
}
|
|
13005
|
-
const existingRecords = await backend.loadRun(workflowSlug, defaultCtx.runId);
|
|
13006
|
-
let resumedSteps = 0;
|
|
13007
|
-
for (const record2 of existingRecords) {
|
|
13008
|
-
if (record2.status === "completed" && record2.output !== undefined && record2.output !== null) {
|
|
13009
|
-
try {
|
|
13010
|
-
const output = JSON.parse(record2.output);
|
|
13011
|
-
cache.set(record2.stepId, Promise.resolve(output));
|
|
13012
|
-
const step = steps.find((s) => s.id === record2.stepId);
|
|
13013
|
-
const displayName = step?.name ?? record2.stepId;
|
|
13014
|
-
defaultCtx.log(`✓ ${displayName} (resumed from cache)`);
|
|
13015
|
-
resumedSteps++;
|
|
13016
|
-
} catch (err) {
|
|
13017
|
-
const step = steps.find((s) => s.id === record2.stepId);
|
|
13018
|
-
const displayName = step?.name ?? record2.stepId;
|
|
13019
|
-
defaultCtx.log(`⚠ Failed to deserialize ${displayName}, will re-run`);
|
|
13020
|
-
}
|
|
13021
|
-
}
|
|
13022
|
-
}
|
|
13023
|
-
const pendingSteps = steps.length - resumedSteps;
|
|
13024
|
-
await backend.saveWorkflowResumed(workflowSlug, defaultCtx.runId, {
|
|
13025
|
-
originalRunId: defaultCtx.runId,
|
|
13026
|
-
resumedSteps,
|
|
13027
|
-
pendingSteps
|
|
13028
|
-
});
|
|
13029
|
-
}
|
|
13030
|
-
async function execute(step, stack = []) {
|
|
13031
|
-
const existing = cache.get(step.id);
|
|
13032
|
-
if (existing)
|
|
13033
|
-
return existing;
|
|
13034
|
-
const p = (async () => {
|
|
13035
|
-
const startTime = getMicrosecondTimestamp2();
|
|
13036
|
-
await backend.saveStepStart(workflowSlug, defaultCtx.runId, step.id, "local", {
|
|
13037
|
-
dependencies: Object.keys(step.dependencies),
|
|
13038
|
-
timestamp: startTime,
|
|
13039
|
-
attemptNumber: 1
|
|
13040
|
-
});
|
|
13041
|
-
defaultCtx.log(`→ ${step.name} (waiting deps: ${Object.keys(step.dependencies).join(", ") || "none"})`);
|
|
13042
|
-
try {
|
|
13043
|
-
const depEntries = Object.entries(step.dependencies);
|
|
13044
|
-
const depOutputsPairs = await Promise.all(depEntries.map(async ([alias, dep]) => {
|
|
13045
|
-
const output = await execute(dep, [...stack, step.id]);
|
|
13046
|
-
const isSkipped = skippedSteps.has(dep.id);
|
|
13047
|
-
const isOptionalDep = isOptional2(step.dependencies[alias]);
|
|
13048
|
-
return [alias, isSkipped && isOptionalDep ? undefined : output];
|
|
13049
|
-
}));
|
|
13050
|
-
const depOutputs = Object.fromEntries(depOutputsPairs);
|
|
13051
|
-
const skippedRequiredDeps = depEntries.filter(([alias, dep]) => {
|
|
13052
|
-
const isSkipped = skippedSteps.has(dep.id);
|
|
13053
|
-
const isOptionalDep = isOptional2(step.dependencies[alias]);
|
|
13054
|
-
return isSkipped && !isOptionalDep;
|
|
13055
|
-
});
|
|
13056
|
-
if (skippedRequiredDeps.length > 0) {
|
|
13057
|
-
const cascadedFromStep = skippedRequiredDeps[0][1];
|
|
13058
|
-
const endTime2 = getMicrosecondTimestamp2();
|
|
13059
|
-
await backend.saveStepSkipped(workflowSlug, defaultCtx.runId, step.id, {
|
|
13060
|
-
skipType: "cascade",
|
|
13061
|
-
reason: `Dependency '${cascadedFromStep.name}' was skipped`,
|
|
13062
|
-
duration: endTime2 - startTime,
|
|
13063
|
-
attemptNumber: 1,
|
|
13064
|
-
cascadedFrom: cascadedFromStep.id
|
|
13065
|
-
});
|
|
13066
|
-
skippedSteps.add(step.id);
|
|
13067
|
-
defaultCtx.log(`⊘ ${step.name} (skipped: dependency '${cascadedFromStep.name}' was skipped)`);
|
|
13068
|
-
return {};
|
|
13069
|
-
}
|
|
13070
|
-
const stepFile = join2(step.dir, "step.ts");
|
|
13071
|
-
const maxRetries = step.maxRetries ?? 0;
|
|
13072
|
-
let lastError = null;
|
|
13073
|
-
for (let attemptNumber = 1;attemptNumber <= maxRetries + 1; attemptNumber++) {
|
|
13074
|
-
try {
|
|
13075
|
-
const { result, logs } = await executeStepInProcess(stepFile, step.id, depOutputs, defaultCtx, attemptNumber, backend);
|
|
13076
|
-
const endTime2 = getMicrosecondTimestamp2();
|
|
13077
|
-
await backend.saveStepComplete(workflowSlug, defaultCtx.runId, step.id, result, {
|
|
13078
|
-
timestamp: endTime2,
|
|
13079
|
-
duration: endTime2 - startTime,
|
|
13080
|
-
logs: logs.length > 0 ? logs : undefined,
|
|
13081
|
-
attemptNumber,
|
|
13082
|
-
output: result
|
|
13083
|
-
}, step.exportOutput ?? false);
|
|
13084
|
-
defaultCtx.log(`✓ ${step.name}`);
|
|
13085
|
-
return result;
|
|
13086
|
-
} catch (err) {
|
|
13087
|
-
lastError = err instanceof Error ? err : new Error(String(err));
|
|
13088
|
-
if (lastError.name === "Skip" || lastError instanceof Skip) {
|
|
13089
|
-
const endTime2 = getMicrosecondTimestamp2();
|
|
13090
|
-
const skipError = lastError;
|
|
13091
|
-
await backend.saveStepSkipped(workflowSlug, defaultCtx.runId, step.id, {
|
|
13092
|
-
skipType: "primary",
|
|
13093
|
-
reason: skipError.reason || skipError.message.replace("Step skipped: ", ""),
|
|
13094
|
-
metadata: skipError.metadata,
|
|
13095
|
-
duration: endTime2 - startTime,
|
|
13096
|
-
attemptNumber
|
|
13097
|
-
});
|
|
13098
|
-
skippedSteps.add(step.id);
|
|
13099
|
-
defaultCtx.log(`⊘ ${step.name} (skipped: ${skipError.reason || skipError.message})`);
|
|
13100
|
-
return {};
|
|
13101
|
-
}
|
|
13102
|
-
if (attemptNumber <= maxRetries) {
|
|
13103
|
-
const error47 = {
|
|
13104
|
-
message: lastError.message,
|
|
13105
|
-
stack: lastError.stack,
|
|
13106
|
-
name: lastError.name
|
|
13107
|
-
};
|
|
13108
|
-
const retryEvent = {
|
|
13109
|
-
category: "step",
|
|
13110
|
-
eventId: "",
|
|
13111
|
-
timestampUs: getMicrosecondTimestamp2(),
|
|
13112
|
-
workflowSlug,
|
|
13113
|
-
runId: defaultCtx.runId,
|
|
13114
|
-
stepId: step.id,
|
|
13115
|
-
type: "StepRetrying",
|
|
13116
|
-
attemptNumber,
|
|
13117
|
-
nextAttempt: attemptNumber + 1,
|
|
13118
|
-
error: error47,
|
|
13119
|
-
maxRetries
|
|
13120
|
-
};
|
|
13121
|
-
await backend.appendEvent(workflowSlug, defaultCtx.runId, retryEvent);
|
|
13122
|
-
defaultCtx.log(`⟳ ${step.name} retry ${attemptNumber + 1}/${maxRetries + 1} after error: ${error47.message}`);
|
|
13123
|
-
}
|
|
13124
|
-
}
|
|
13125
|
-
}
|
|
13126
|
-
const error46 = {
|
|
13127
|
-
message: lastError.message,
|
|
13128
|
-
stack: lastError.stack,
|
|
13129
|
-
name: lastError.name
|
|
13130
|
-
};
|
|
13131
|
-
const failureReason = lastError.isCrash ? "worker-crash" : "exhausted-retries";
|
|
13132
|
-
const endTime = getMicrosecondTimestamp2();
|
|
13133
|
-
await backend.saveStepFailed(workflowSlug, defaultCtx.runId, step.id, error46, {
|
|
13134
|
-
duration: endTime - startTime,
|
|
13135
|
-
attemptNumber: maxRetries + 1,
|
|
13136
|
-
terminal: true,
|
|
13137
|
-
failureReason
|
|
13138
|
-
});
|
|
13139
|
-
defaultCtx.log(`✗ ${step.name} failed after ${maxRetries + 1} attempts: ${error46.message}`);
|
|
13140
|
-
throw lastError;
|
|
13141
|
-
} catch (err) {
|
|
13142
|
-
throw err;
|
|
13143
|
-
}
|
|
13144
|
-
})();
|
|
13145
|
-
cache.set(step.id, p);
|
|
13146
|
-
return p;
|
|
13147
|
-
}
|
|
13148
|
-
try {
|
|
13149
|
-
await Promise.all(selected.map((s) => execute(s)));
|
|
13150
|
-
const results = {};
|
|
13151
|
-
for (const s of steps) {
|
|
13152
|
-
if (s.exportOutput && !skippedSteps.has(s.id)) {
|
|
13153
|
-
const val = cache.get(s.id);
|
|
13154
|
-
if (val)
|
|
13155
|
-
results[s.id] = await val;
|
|
13156
|
-
}
|
|
13157
|
-
}
|
|
13158
|
-
const workflowEndTime = getMicrosecondTimestamp2();
|
|
13159
|
-
const workflowDuration = workflowEndTime - workflowStartTime;
|
|
13160
|
-
await backend.saveWorkflowComplete(workflowSlug, defaultCtx.runId, results, {
|
|
13161
|
-
workflowAttemptNumber: 1,
|
|
13162
|
-
timestamp: workflowEndTime,
|
|
13163
|
-
duration: workflowDuration,
|
|
13164
|
-
totalSteps: steps.length
|
|
13165
|
-
});
|
|
13166
|
-
return results;
|
|
13167
|
-
} catch (err) {
|
|
13168
|
-
const workflowEndTime = getMicrosecondTimestamp2();
|
|
13169
|
-
const workflowDuration = workflowEndTime - workflowStartTime;
|
|
13170
|
-
let completedSteps = 0;
|
|
13171
|
-
for (const s of steps) {
|
|
13172
|
-
const val = cache.get(s.id);
|
|
13173
|
-
if (val) {
|
|
13174
|
-
try {
|
|
13175
|
-
await val;
|
|
13176
|
-
completedSteps++;
|
|
13177
|
-
} catch {}
|
|
13178
|
-
}
|
|
13179
|
-
}
|
|
13180
|
-
let failedStep;
|
|
13181
|
-
if (err instanceof Error && err.message) {
|
|
13182
|
-
for (const s of steps) {
|
|
13183
|
-
if (err.message.includes(s.name) || err.message.includes(s.id)) {
|
|
13184
|
-
failedStep = s.id;
|
|
13185
|
-
break;
|
|
13186
|
-
}
|
|
13187
|
-
}
|
|
13188
|
-
}
|
|
13189
|
-
const error46 = {
|
|
13190
|
-
message: err instanceof Error ? err.message : String(err),
|
|
13191
|
-
stack: err instanceof Error ? err.stack : undefined,
|
|
13192
|
-
name: err instanceof Error ? err.name : undefined
|
|
13193
|
-
};
|
|
13194
|
-
const failureReason = err.isCrash ? "worker-crash" : "step-failed";
|
|
13195
|
-
await backend.saveWorkflowFailed(workflowSlug, defaultCtx.runId, error46, {
|
|
13196
|
-
workflowAttemptNumber: 1,
|
|
13197
|
-
duration: workflowDuration,
|
|
13198
|
-
completedSteps,
|
|
13199
|
-
failedStep
|
|
13200
|
-
}, failureReason);
|
|
13201
|
-
throw err;
|
|
13202
|
-
}
|
|
13203
|
-
}
|
|
13204
12911
|
export {
|
|
13205
|
-
|
|
12912
|
+
validateWorkflowVersion,
|
|
13206
12913
|
getGitInfo,
|
|
12914
|
+
getAllDependents,
|
|
13207
12915
|
executeStepInProcess,
|
|
13208
12916
|
discoverWorkflows,
|
|
13209
12917
|
discoverSteps,
|
|
13210
12918
|
calculateWorkflowHash
|
|
13211
12919
|
};
|
|
13212
12920
|
|
|
13213
|
-
//# debugId=
|
|
12921
|
+
//# debugId=6283553AC8E9AA7C64756E2164756E21
|