@livingdata/pipex 0.0.9 → 0.0.10
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 +154 -14
- package/dist/__tests__/errors.js +162 -0
- package/dist/__tests__/helpers.js +41 -0
- package/dist/__tests__/types.js +8 -0
- package/dist/cli/__tests__/condition.js +23 -0
- package/dist/cli/__tests__/dag.js +154 -0
- package/dist/cli/__tests__/pipeline-loader.js +267 -0
- package/dist/cli/__tests__/pipeline-runner.js +257 -0
- package/dist/cli/__tests__/state-persistence.js +80 -0
- package/dist/cli/__tests__/state.js +58 -0
- package/dist/cli/__tests__/step-runner.js +116 -0
- package/dist/cli/commands/bundle.js +35 -0
- package/dist/cli/commands/cat.js +54 -0
- package/dist/cli/commands/exec.js +89 -0
- package/dist/cli/commands/export.js +2 -2
- package/dist/cli/commands/inspect.js +1 -1
- package/dist/cli/commands/list.js +2 -1
- package/dist/cli/commands/logs.js +1 -1
- package/dist/cli/commands/prune.js +1 -1
- package/dist/cli/commands/rm-step.js +41 -0
- package/dist/cli/commands/run-bundle.js +59 -0
- package/dist/cli/commands/run.js +9 -4
- package/dist/cli/commands/show.js +42 -7
- package/dist/cli/condition.js +11 -0
- package/dist/cli/dag.js +143 -0
- package/dist/cli/index.js +6 -0
- package/dist/cli/interactive-reporter.js +227 -0
- package/dist/cli/pipeline-loader.js +10 -110
- package/dist/cli/pipeline-runner.js +164 -78
- package/dist/cli/reporter.js +2 -158
- package/dist/cli/state.js +8 -0
- package/dist/cli/step-loader.js +25 -0
- package/dist/cli/step-resolver.js +111 -0
- package/dist/cli/step-runner.js +226 -0
- package/dist/cli/utils.js +0 -46
- package/dist/core/__tests__/bundle.js +663 -0
- package/dist/core/__tests__/condition.js +23 -0
- package/dist/core/__tests__/dag.js +154 -0
- package/dist/core/__tests__/env-file.test.js +41 -0
- package/dist/core/__tests__/event-aggregator.js +244 -0
- package/dist/core/__tests__/pipeline-loader.js +267 -0
- package/dist/core/__tests__/pipeline-runner.js +257 -0
- package/dist/core/__tests__/state-persistence.js +80 -0
- package/dist/core/__tests__/state.js +58 -0
- package/dist/core/__tests__/step-runner.js +118 -0
- package/dist/core/__tests__/stream-reporter.js +142 -0
- package/dist/core/__tests__/transport.js +50 -0
- package/dist/core/__tests__/utils.js +40 -0
- package/dist/core/bundle.js +130 -0
- package/dist/core/condition.js +11 -0
- package/dist/core/dag.js +143 -0
- package/dist/core/env-file.js +6 -0
- package/dist/core/event-aggregator.js +114 -0
- package/dist/core/index.js +14 -0
- package/dist/core/pipeline-loader.js +81 -0
- package/dist/core/pipeline-runner.js +360 -0
- package/dist/core/reporter.js +11 -0
- package/dist/core/state.js +110 -0
- package/dist/core/step-loader.js +25 -0
- package/dist/core/step-resolver.js +117 -0
- package/dist/core/step-runner.js +225 -0
- package/dist/core/stream-reporter.js +41 -0
- package/dist/core/transport.js +9 -0
- package/dist/core/utils.js +56 -0
- package/dist/engine/__tests__/workspace.js +288 -0
- package/dist/engine/docker-executor.js +10 -2
- package/dist/engine/index.js +1 -0
- package/dist/engine/workspace.js +76 -12
- package/dist/errors.js +122 -0
- package/dist/index.js +3 -0
- package/dist/kits/__tests__/index.js +23 -0
- package/dist/kits/builtin/__tests__/node.js +74 -0
- package/dist/kits/builtin/__tests__/python.js +67 -0
- package/dist/kits/builtin/__tests__/shell.js +74 -0
- package/dist/kits/builtin/node.js +10 -5
- package/dist/kits/builtin/python.js +10 -5
- package/dist/kits/builtin/shell.js +2 -1
- package/dist/kits/index.js +2 -1
- package/package.json +6 -3
- package/dist/cli/types.js +0 -3
- package/dist/engine/docker-runtime.js +0 -65
- package/dist/engine/runtime.js +0 -2
- package/dist/kits/bash.js +0 -19
- package/dist/kits/builtin/bash.js +0 -19
- package/dist/kits/node.js +0 -56
- package/dist/kits/python.js +0 -51
- package/dist/kits/types.js +0 -1
- package/dist/reporter.js +0 -13
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import process from 'node:process';
|
|
2
|
+
import { cp, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { setTimeout } from 'node:timers/promises';
|
|
4
|
+
import { createWriteStream } from 'node:fs';
|
|
5
|
+
import { join, resolve } from 'node:path';
|
|
6
|
+
import { ContainerCrashError, PipexError } from '../errors.js';
|
|
7
|
+
import { StateManager } from './state.js';
|
|
8
|
+
import { dirSize } from './utils.js';
|
|
9
|
+
/**
|
|
10
|
+
* Executes a single step in a workspace.
|
|
11
|
+
* Adapted from PipelineRunner.executeStep() for standalone use.
|
|
12
|
+
*/
|
|
13
|
+
export class StepRunner {
|
|
14
|
+
runtime;
|
|
15
|
+
reporter;
|
|
16
|
+
constructor(runtime, reporter) {
|
|
17
|
+
this.runtime = runtime;
|
|
18
|
+
this.reporter = reporter;
|
|
19
|
+
}
|
|
20
|
+
async run(options) {
|
|
21
|
+
const { workspace, state, step, inputs, pipelineRoot, force, ephemeral } = options;
|
|
22
|
+
const stepRef = { id: step.id, displayName: step.name ?? step.id };
|
|
23
|
+
const resolvedMounts = step.mounts?.map(m => ({
|
|
24
|
+
hostPath: resolve(pipelineRoot, m.host),
|
|
25
|
+
containerPath: m.container
|
|
26
|
+
}));
|
|
27
|
+
const currentFingerprint = this.computeFingerprint(step, inputs, resolvedMounts);
|
|
28
|
+
// Cache check (skip for ephemeral or force)
|
|
29
|
+
if (!force && !ephemeral) {
|
|
30
|
+
const cacheResult = await this.tryUseCache({ workspace, state, stepId: step.id, stepRef, fingerprint: currentFingerprint });
|
|
31
|
+
if (cacheResult) {
|
|
32
|
+
return cacheResult;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
this.reporter.emit({ event: 'STEP_STARTING', workspaceId: workspace.id, step: stepRef });
|
|
36
|
+
return this.executeStep({ workspace, state, step, stepRef, inputs, pipelineRoot, ephemeral, currentFingerprint, resolvedMounts });
|
|
37
|
+
}
|
|
38
|
+
computeFingerprint(step, inputs, resolvedMounts) {
|
|
39
|
+
const inputRunIds = step.inputs
|
|
40
|
+
?.map(i => inputs.get(i.step))
|
|
41
|
+
.filter((id) => id !== undefined);
|
|
42
|
+
return StateManager.fingerprint({
|
|
43
|
+
image: step.image,
|
|
44
|
+
cmd: step.cmd,
|
|
45
|
+
env: step.env,
|
|
46
|
+
inputRunIds,
|
|
47
|
+
mounts: resolvedMounts
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
async tryUseCache({ workspace, state, stepId, stepRef, fingerprint }) {
|
|
51
|
+
const cached = state.getStep(stepId);
|
|
52
|
+
if (cached?.fingerprint === fingerprint) {
|
|
53
|
+
const runs = await workspace.listRuns();
|
|
54
|
+
if (runs.includes(cached.runId)) {
|
|
55
|
+
await workspace.linkRun(stepId, cached.runId);
|
|
56
|
+
this.reporter.emit({ event: 'STEP_SKIPPED', workspaceId: workspace.id, step: stepRef, runId: cached.runId, reason: 'cached' });
|
|
57
|
+
return { runId: cached.runId, exitCode: 0 };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
async executeStep(ctx) {
|
|
63
|
+
const { workspace, step, stepRef, inputs, pipelineRoot, ephemeral } = ctx;
|
|
64
|
+
const runId = workspace.generateRunId();
|
|
65
|
+
const stagingPath = await workspace.prepareRun(runId);
|
|
66
|
+
await this.prepareStagingInputs(workspace, step, runId, inputs);
|
|
67
|
+
await this.prepareCaches(workspace, step);
|
|
68
|
+
const { containerInputs, output, caches, mounts } = this.buildMounts(step, runId, inputs, pipelineRoot);
|
|
69
|
+
const stdoutLog = createWriteStream(join(stagingPath, 'stdout.log'));
|
|
70
|
+
const stderrLog = createWriteStream(join(stagingPath, 'stderr.log'));
|
|
71
|
+
try {
|
|
72
|
+
const result = await this.executeWithRetries({
|
|
73
|
+
ctx, containerInputs, output, caches, mounts, stdoutLog, stderrLog
|
|
74
|
+
});
|
|
75
|
+
this.reporter.result(workspace.id, stepRef, result);
|
|
76
|
+
await closeStream(stdoutLog);
|
|
77
|
+
await closeStream(stderrLog);
|
|
78
|
+
if (ephemeral) {
|
|
79
|
+
await workspace.discardRun(runId);
|
|
80
|
+
this.reporter.emit({ event: 'STEP_FINISHED', workspaceId: workspace.id, step: stepRef, ephemeral: true });
|
|
81
|
+
return { exitCode: result.exitCode };
|
|
82
|
+
}
|
|
83
|
+
return await this.commitOrDiscard({ ...ctx, runId, stagingPath, result });
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
await closeStream(stdoutLog);
|
|
87
|
+
await closeStream(stderrLog);
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async prepareStagingInputs(workspace, step, runId, inputs) {
|
|
92
|
+
if (!step.inputs) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
for (const input of step.inputs) {
|
|
96
|
+
const inputRunId = inputs.get(input.step);
|
|
97
|
+
if (inputRunId && input.copyToOutput) {
|
|
98
|
+
await cp(workspace.runArtifactsPath(inputRunId), workspace.runStagingArtifactsPath(runId), { recursive: true });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async prepareCaches(workspace, step) {
|
|
103
|
+
if (!step.caches) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
for (const cache of step.caches) {
|
|
107
|
+
await workspace.prepareCache(cache.name);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
buildMounts(step, runId, inputs, pipelineRoot) {
|
|
111
|
+
const containerInputs = [];
|
|
112
|
+
if (step.inputs) {
|
|
113
|
+
for (const input of step.inputs) {
|
|
114
|
+
const inputRunId = inputs.get(input.step);
|
|
115
|
+
if (inputRunId) {
|
|
116
|
+
containerInputs.push({ runId: inputRunId, containerPath: `/input/${input.step}` });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const output = { stagingRunId: runId, containerPath: step.outputPath ?? '/output' };
|
|
121
|
+
const caches = step.caches?.map(c => ({ name: c.name, containerPath: c.path }));
|
|
122
|
+
const mounts = step.mounts?.map(m => ({
|
|
123
|
+
hostPath: resolve(pipelineRoot, m.host),
|
|
124
|
+
containerPath: m.container
|
|
125
|
+
}));
|
|
126
|
+
return { containerInputs, output, caches, mounts };
|
|
127
|
+
}
|
|
128
|
+
async executeWithRetries({ ctx, containerInputs, output, caches, mounts, stdoutLog, stderrLog }) {
|
|
129
|
+
const { workspace, step, stepRef, pipelineRoot, ephemeral } = ctx;
|
|
130
|
+
const maxRetries = step.retries ?? 0;
|
|
131
|
+
const retryDelay = step.retryDelayMs ?? 5000;
|
|
132
|
+
let result;
|
|
133
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
134
|
+
try {
|
|
135
|
+
result = await this.runtime.run(workspace, {
|
|
136
|
+
name: `pipex-${workspace.id}-${step.id}-${Date.now()}`,
|
|
137
|
+
image: step.image,
|
|
138
|
+
cmd: step.cmd,
|
|
139
|
+
env: step.env,
|
|
140
|
+
inputs: containerInputs,
|
|
141
|
+
output,
|
|
142
|
+
caches,
|
|
143
|
+
mounts,
|
|
144
|
+
sources: step.sources?.map(m => ({
|
|
145
|
+
hostPath: resolve(pipelineRoot, m.host),
|
|
146
|
+
containerPath: m.container
|
|
147
|
+
})),
|
|
148
|
+
network: step.allowNetwork ? 'bridge' : 'none',
|
|
149
|
+
timeoutSec: step.timeoutSec
|
|
150
|
+
}, ({ stream, line }) => {
|
|
151
|
+
if (stream === 'stdout') {
|
|
152
|
+
stdoutLog.write(line + '\n');
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
stderrLog.write(line + '\n');
|
|
156
|
+
}
|
|
157
|
+
if (ephemeral && stream === 'stdout') {
|
|
158
|
+
process.stdout.write(line + '\n');
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
this.reporter.log(workspace.id, stepRef, stream, line);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
if (error instanceof PipexError && error.transient && attempt < maxRetries) {
|
|
168
|
+
this.reporter.emit({ event: 'STEP_RETRYING', workspaceId: workspace.id, step: stepRef, attempt: attempt + 1, maxRetries });
|
|
169
|
+
await setTimeout(retryDelay);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return result;
|
|
176
|
+
}
|
|
177
|
+
async commitOrDiscard({ workspace, state, step, stepRef, inputs, resolvedMounts, currentFingerprint, runId, stagingPath, result }) {
|
|
178
|
+
const meta = {
|
|
179
|
+
runId,
|
|
180
|
+
stepId: step.id,
|
|
181
|
+
stepName: step.name,
|
|
182
|
+
startedAt: result.startedAt.toISOString(),
|
|
183
|
+
finishedAt: result.finishedAt.toISOString(),
|
|
184
|
+
durationMs: result.finishedAt.getTime() - result.startedAt.getTime(),
|
|
185
|
+
exitCode: result.exitCode,
|
|
186
|
+
image: step.image,
|
|
187
|
+
cmd: step.cmd,
|
|
188
|
+
env: step.env,
|
|
189
|
+
inputs: step.inputs?.map(i => ({
|
|
190
|
+
step: i.step,
|
|
191
|
+
runId: inputs.get(i.step),
|
|
192
|
+
mountedAs: `/input/${i.step}`
|
|
193
|
+
})),
|
|
194
|
+
mounts: resolvedMounts,
|
|
195
|
+
caches: step.caches?.map(c => c.name),
|
|
196
|
+
allowNetwork: step.allowNetwork ?? false,
|
|
197
|
+
fingerprint: currentFingerprint,
|
|
198
|
+
status: result.exitCode === 0 ? 'success' : 'failure'
|
|
199
|
+
};
|
|
200
|
+
await writeFile(join(stagingPath, 'meta.json'), JSON.stringify(meta, null, 2), 'utf8');
|
|
201
|
+
if (result.exitCode === 0 || step.allowFailure) {
|
|
202
|
+
await workspace.commitRun(runId);
|
|
203
|
+
await workspace.linkRun(step.id, runId);
|
|
204
|
+
state.setStep(step.id, runId, currentFingerprint);
|
|
205
|
+
await state.save();
|
|
206
|
+
const durationMs = result.finishedAt.getTime() - result.startedAt.getTime();
|
|
207
|
+
const artifactSize = await dirSize(workspace.runArtifactsPath(runId));
|
|
208
|
+
this.reporter.emit({ event: 'STEP_FINISHED', workspaceId: workspace.id, step: stepRef, runId, durationMs, artifactSize });
|
|
209
|
+
return { runId, exitCode: result.exitCode };
|
|
210
|
+
}
|
|
211
|
+
await workspace.discardRun(runId);
|
|
212
|
+
this.reporter.emit({ event: 'STEP_FAILED', workspaceId: workspace.id, step: stepRef, exitCode: result.exitCode });
|
|
213
|
+
throw new ContainerCrashError(step.id, result.exitCode);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
async function closeStream(stream) {
|
|
217
|
+
if (stream.destroyed) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
return new Promise((resolve, reject) => {
|
|
221
|
+
stream.end(() => {
|
|
222
|
+
resolve();
|
|
223
|
+
});
|
|
224
|
+
stream.on('error', reject);
|
|
225
|
+
});
|
|
226
|
+
}
|
package/dist/cli/utils.js
CHANGED
|
@@ -1,49 +1,3 @@
|
|
|
1
|
-
import { readdir, stat } from 'node:fs/promises';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
1
|
export function getGlobalOptions(cmd) {
|
|
4
2
|
return cmd.optsWithGlobals();
|
|
5
3
|
}
|
|
6
|
-
export async function dirSize(dirPath) {
|
|
7
|
-
let total = 0;
|
|
8
|
-
try {
|
|
9
|
-
const entries = await readdir(dirPath, { withFileTypes: true });
|
|
10
|
-
for (const entry of entries) {
|
|
11
|
-
const fullPath = join(dirPath, entry.name);
|
|
12
|
-
if (entry.isDirectory()) {
|
|
13
|
-
total += await dirSize(fullPath);
|
|
14
|
-
}
|
|
15
|
-
else if (entry.isFile()) {
|
|
16
|
-
const s = await stat(fullPath);
|
|
17
|
-
total += s.size;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
catch {
|
|
22
|
-
// Directory doesn't exist or isn't readable
|
|
23
|
-
}
|
|
24
|
-
return total;
|
|
25
|
-
}
|
|
26
|
-
export function formatSize(bytes) {
|
|
27
|
-
if (bytes < 1024) {
|
|
28
|
-
return `${bytes} B`;
|
|
29
|
-
}
|
|
30
|
-
if (bytes < 1024 * 1024) {
|
|
31
|
-
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
32
|
-
}
|
|
33
|
-
if (bytes < 1024 * 1024 * 1024) {
|
|
34
|
-
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
35
|
-
}
|
|
36
|
-
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
|
37
|
-
}
|
|
38
|
-
export function formatDuration(ms) {
|
|
39
|
-
if (ms < 1000) {
|
|
40
|
-
return `${ms}ms`;
|
|
41
|
-
}
|
|
42
|
-
const seconds = ms / 1000;
|
|
43
|
-
if (seconds < 60) {
|
|
44
|
-
return `${seconds.toFixed(1)}s`;
|
|
45
|
-
}
|
|
46
|
-
const minutes = Math.floor(seconds / 60);
|
|
47
|
-
const remainingSeconds = Math.round(seconds % 60);
|
|
48
|
-
return `${minutes}m ${remainingSeconds}s`;
|
|
49
|
-
}
|