@livingdata/pipex 0.0.8 → 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 +186 -16
- 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/clean.js +22 -0
- package/dist/cli/commands/exec.js +89 -0
- package/dist/cli/commands/export.js +32 -0
- package/dist/cli/commands/inspect.js +58 -0
- package/dist/cli/commands/list.js +39 -0
- package/dist/cli/commands/logs.js +54 -0
- package/dist/cli/commands/prune.js +26 -0
- package/dist/cli/commands/rm-step.js +41 -0
- package/dist/cli/commands/rm.js +27 -0
- package/dist/cli/commands/run-bundle.js +59 -0
- package/dist/cli/commands/run.js +44 -0
- package/dist/cli/commands/show.js +108 -0
- package/dist/cli/condition.js +11 -0
- package/dist/cli/dag.js +143 -0
- package/dist/cli/index.js +24 -105
- package/dist/cli/interactive-reporter.js +227 -0
- package/dist/cli/pipeline-loader.js +10 -110
- package/dist/cli/pipeline-runner.js +256 -111
- package/dist/cli/reporter.js +2 -107
- package/dist/cli/state.js +30 -9
- 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 +3 -0
- 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 +32 -6
- package/dist/engine/index.js +1 -0
- package/dist/engine/workspace.js +164 -66
- 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
package/dist/engine/workspace.js
CHANGED
|
@@ -1,23 +1,28 @@
|
|
|
1
|
-
import { access, mkdir, readdir, rename, rm, symlink } from 'node:fs/promises';
|
|
1
|
+
import { access, mkdir, readdir, readFile, rename, rm, symlink, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
+
import { WorkspaceError, StagingError } from '../errors.js';
|
|
4
5
|
/**
|
|
5
6
|
* Isolated execution environment for container runs.
|
|
6
7
|
*
|
|
7
8
|
* A workspace provides:
|
|
8
9
|
* - **staging/**: Temporary write location during execution
|
|
9
|
-
* - **
|
|
10
|
+
* - **runs/**: Committed run outputs (immutable, read-only)
|
|
10
11
|
* - **caches/**: Persistent read-write caches (shared across steps)
|
|
11
12
|
* - **state.json**: Managed by orchestration layer (e.g., CLI) for caching
|
|
12
13
|
*
|
|
13
|
-
* ##
|
|
14
|
+
* ## Run Lifecycle
|
|
14
15
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* 3. Success: `commitArtifact()` atomically moves to `artifacts/{artifactId}/`
|
|
18
|
-
* OR Failure: `discardArtifact()` deletes `staging/{artifactId}/`
|
|
16
|
+
* Each step execution produces a **run**, a structured directory containing
|
|
17
|
+
* artifacts (files produced by the step), logs (stdout/stderr), and metadata.
|
|
19
18
|
*
|
|
20
|
-
*
|
|
19
|
+
* 1. `prepareRun()` creates `staging/{runId}/` with `artifacts/` subdirectory
|
|
20
|
+
* 2. Container writes to `staging/{runId}/artifacts/` (mounted as `/output`)
|
|
21
|
+
* 3. Orchestration layer writes logs and metadata to `staging/{runId}/`
|
|
22
|
+
* 4. Success: `commitRun()` atomically moves to `runs/{runId}/`
|
|
23
|
+
* OR Failure: `discardRun()` deletes `staging/{runId}/`
|
|
24
|
+
*
|
|
25
|
+
* Runs are immutable once committed.
|
|
21
26
|
*
|
|
22
27
|
* ## Cache Lifecycle
|
|
23
28
|
*
|
|
@@ -30,12 +35,12 @@ import { join } from 'node:path';
|
|
|
30
35
|
* @example
|
|
31
36
|
* ```typescript
|
|
32
37
|
* const ws = await Workspace.create('/tmp/workdir', 'my-workspace')
|
|
33
|
-
* const
|
|
34
|
-
* await ws.
|
|
38
|
+
* const runId = ws.generateRunId()
|
|
39
|
+
* await ws.prepareRun(runId)
|
|
35
40
|
* await ws.prepareCache('pnpm-store')
|
|
36
41
|
* // ... container execution ...
|
|
37
|
-
* await ws.
|
|
38
|
-
* // OR await ws.
|
|
42
|
+
* await ws.commitRun(runId) // On success
|
|
43
|
+
* // OR await ws.discardRun(runId) // On failure
|
|
39
44
|
* ```
|
|
40
45
|
*/
|
|
41
46
|
export class Workspace {
|
|
@@ -49,7 +54,7 @@ export class Workspace {
|
|
|
49
54
|
return Workspace.generateId();
|
|
50
55
|
}
|
|
51
56
|
/**
|
|
52
|
-
* Creates a new workspace with staging,
|
|
57
|
+
* Creates a new workspace with staging, runs, and caches directories.
|
|
53
58
|
* @param workdirRoot - Root directory for all workspaces
|
|
54
59
|
* @param id - Optional workspace ID (auto-generated if omitted)
|
|
55
60
|
* @returns Newly created workspace
|
|
@@ -58,7 +63,7 @@ export class Workspace {
|
|
|
58
63
|
const workspaceId = id ?? Workspace.generateWorkspaceId();
|
|
59
64
|
const root = join(workdirRoot, workspaceId);
|
|
60
65
|
await mkdir(join(root, 'staging'), { recursive: true });
|
|
61
|
-
await mkdir(join(root, '
|
|
66
|
+
await mkdir(join(root, 'runs'), { recursive: true });
|
|
62
67
|
await mkdir(join(root, 'caches'), { recursive: true });
|
|
63
68
|
return new Workspace(workspaceId, root);
|
|
64
69
|
}
|
|
@@ -96,7 +101,7 @@ export class Workspace {
|
|
|
96
101
|
*/
|
|
97
102
|
static async remove(workdirRoot, id) {
|
|
98
103
|
if (id.includes('..') || id.includes('/')) {
|
|
99
|
-
throw new
|
|
104
|
+
throw new WorkspaceError('INVALID_WORKSPACE_ID', `Invalid workspace ID: ${id}`);
|
|
100
105
|
}
|
|
101
106
|
await rm(join(workdirRoot, id), { recursive: true, force: true });
|
|
102
107
|
}
|
|
@@ -113,75 +118,104 @@ export class Workspace {
|
|
|
113
118
|
this.root = root;
|
|
114
119
|
}
|
|
115
120
|
/**
|
|
116
|
-
* Generates a unique
|
|
117
|
-
* @returns
|
|
121
|
+
* Generates a unique run identifier.
|
|
122
|
+
* @returns Run ID in format: `{timestamp}-{uuid-prefix}`
|
|
118
123
|
*/
|
|
119
|
-
|
|
124
|
+
generateRunId() {
|
|
120
125
|
return Workspace.generateId();
|
|
121
126
|
}
|
|
122
127
|
/**
|
|
123
|
-
* Returns the staging directory path for
|
|
124
|
-
*
|
|
125
|
-
* @param artifactId - Artifact identifier
|
|
128
|
+
* Returns the staging directory path for a run.
|
|
129
|
+
* @param runId - Run identifier
|
|
126
130
|
* @returns Absolute path to staging directory
|
|
127
|
-
* @throws If artifact ID is invalid
|
|
128
131
|
*/
|
|
129
|
-
|
|
130
|
-
this.
|
|
131
|
-
return join(this.root, 'staging',
|
|
132
|
+
runStagingPath(runId) {
|
|
133
|
+
this.validateRunId(runId);
|
|
134
|
+
return join(this.root, 'staging', runId);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Returns the staging artifacts directory path for a run.
|
|
138
|
+
* @param runId - Run identifier
|
|
139
|
+
* @returns Absolute path to staging artifacts directory
|
|
140
|
+
*/
|
|
141
|
+
runStagingArtifactsPath(runId) {
|
|
142
|
+
return join(this.runStagingPath(runId), 'artifacts');
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Returns the committed run directory path.
|
|
146
|
+
* @param runId - Run identifier
|
|
147
|
+
* @returns Absolute path to run directory
|
|
148
|
+
*/
|
|
149
|
+
runPath(runId) {
|
|
150
|
+
this.validateRunId(runId);
|
|
151
|
+
return join(this.root, 'runs', runId);
|
|
132
152
|
}
|
|
133
153
|
/**
|
|
134
|
-
* Returns the
|
|
135
|
-
*
|
|
136
|
-
* @
|
|
137
|
-
* @returns Absolute path to artifact directory
|
|
138
|
-
* @throws If artifact ID is invalid
|
|
154
|
+
* Returns the artifacts directory path within a committed run.
|
|
155
|
+
* @param runId - Run identifier
|
|
156
|
+
* @returns Absolute path to run artifacts directory
|
|
139
157
|
*/
|
|
140
|
-
|
|
141
|
-
this.
|
|
142
|
-
return join(this.root, 'artifacts', artifactId);
|
|
158
|
+
runArtifactsPath(runId) {
|
|
159
|
+
return join(this.runPath(runId), 'artifacts');
|
|
143
160
|
}
|
|
144
161
|
/**
|
|
145
|
-
* Prepares a staging directory for a new
|
|
146
|
-
*
|
|
162
|
+
* Prepares a staging directory for a new run.
|
|
163
|
+
* Creates both the run directory and its artifacts subdirectory.
|
|
164
|
+
* @param runId - Run identifier
|
|
147
165
|
* @returns Absolute path to the created staging directory
|
|
148
166
|
*/
|
|
149
|
-
async
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
167
|
+
async prepareRun(runId) {
|
|
168
|
+
try {
|
|
169
|
+
const path = this.runStagingPath(runId);
|
|
170
|
+
await mkdir(path, { recursive: true });
|
|
171
|
+
await mkdir(join(path, 'artifacts'), { recursive: true });
|
|
172
|
+
return path;
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
throw new StagingError(`Failed to prepare run ${runId}`, { cause: error });
|
|
176
|
+
}
|
|
153
177
|
}
|
|
154
178
|
/**
|
|
155
|
-
* Commits a staging
|
|
179
|
+
* Commits a staging run to the runs directory.
|
|
156
180
|
* Uses atomic rename operation for consistency.
|
|
157
|
-
* @param
|
|
181
|
+
* @param runId - Run identifier
|
|
158
182
|
*/
|
|
159
|
-
async
|
|
160
|
-
|
|
183
|
+
async commitRun(runId) {
|
|
184
|
+
try {
|
|
185
|
+
await rename(this.runStagingPath(runId), this.runPath(runId));
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
throw new StagingError(`Failed to commit run ${runId}`, { cause: error });
|
|
189
|
+
}
|
|
161
190
|
}
|
|
162
191
|
/**
|
|
163
|
-
* Creates a symlink from `step-
|
|
192
|
+
* Creates a symlink from `step-runs/{stepId}` to the committed run.
|
|
164
193
|
* Replaces any existing symlink for the same step.
|
|
165
194
|
* @param stepId - Step identifier
|
|
166
|
-
* @param
|
|
195
|
+
* @param runId - Committed run identifier
|
|
167
196
|
*/
|
|
168
|
-
async
|
|
169
|
-
const dir = join(this.root, 'step-
|
|
197
|
+
async linkRun(stepId, runId) {
|
|
198
|
+
const dir = join(this.root, 'step-runs');
|
|
170
199
|
await mkdir(dir, { recursive: true });
|
|
171
200
|
const linkPath = join(dir, stepId);
|
|
172
201
|
await rm(linkPath, { force: true });
|
|
173
|
-
await symlink(join('..', '
|
|
202
|
+
await symlink(join('..', 'runs', runId), linkPath);
|
|
174
203
|
}
|
|
175
204
|
/**
|
|
176
|
-
* Discards a staging
|
|
177
|
-
* @param
|
|
205
|
+
* Discards a staging run (on execution failure).
|
|
206
|
+
* @param runId - Run identifier
|
|
178
207
|
*/
|
|
179
|
-
async
|
|
180
|
-
|
|
208
|
+
async discardRun(runId) {
|
|
209
|
+
try {
|
|
210
|
+
await rm(this.runStagingPath(runId), { recursive: true, force: true });
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
throw new StagingError(`Failed to discard run ${runId}`, { cause: error });
|
|
214
|
+
}
|
|
181
215
|
}
|
|
182
216
|
/**
|
|
183
217
|
* Removes all staging directories.
|
|
184
|
-
* Should be called on workspace initialization to clean up incomplete
|
|
218
|
+
* Should be called on workspace initialization to clean up incomplete runs.
|
|
185
219
|
*/
|
|
186
220
|
async cleanupStaging() {
|
|
187
221
|
const stagingDir = join(this.root, 'staging');
|
|
@@ -198,18 +232,34 @@ export class Workspace {
|
|
|
198
232
|
}
|
|
199
233
|
}
|
|
200
234
|
/**
|
|
201
|
-
* Lists all committed
|
|
202
|
-
* @returns Array of
|
|
235
|
+
* Lists all committed run IDs in this workspace.
|
|
236
|
+
* @returns Array of run IDs (directory names in runs/)
|
|
203
237
|
*/
|
|
204
|
-
async
|
|
238
|
+
async listRuns() {
|
|
205
239
|
try {
|
|
206
|
-
const entries = await readdir(join(this.root, '
|
|
240
|
+
const entries = await readdir(join(this.root, 'runs'), { withFileTypes: true });
|
|
207
241
|
return entries.filter(e => e.isDirectory()).map(e => e.name);
|
|
208
242
|
}
|
|
209
243
|
catch {
|
|
210
244
|
return [];
|
|
211
245
|
}
|
|
212
246
|
}
|
|
247
|
+
/**
|
|
248
|
+
* Removes runs not in the given set of active run IDs.
|
|
249
|
+
* @param activeRunIds - Set of run IDs to keep
|
|
250
|
+
* @returns Number of runs removed
|
|
251
|
+
*/
|
|
252
|
+
async pruneRuns(activeRunIds) {
|
|
253
|
+
const allRuns = await this.listRuns();
|
|
254
|
+
let removed = 0;
|
|
255
|
+
for (const runId of allRuns) {
|
|
256
|
+
if (!activeRunIds.has(runId)) {
|
|
257
|
+
await rm(join(this.root, 'runs', runId), { recursive: true, force: true });
|
|
258
|
+
removed++;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return removed;
|
|
262
|
+
}
|
|
213
263
|
/**
|
|
214
264
|
* Returns the cache directory path.
|
|
215
265
|
* Caches are persistent read-write directories shared across steps.
|
|
@@ -246,32 +296,80 @@ export class Workspace {
|
|
|
246
296
|
}
|
|
247
297
|
}
|
|
248
298
|
/**
|
|
249
|
-
*
|
|
250
|
-
* @param
|
|
251
|
-
* @
|
|
299
|
+
* Marks a step as currently running by writing a marker file.
|
|
300
|
+
* @param stepId - Step identifier
|
|
301
|
+
* @param meta - Metadata about the running step
|
|
302
|
+
*/
|
|
303
|
+
async markStepRunning(stepId, meta) {
|
|
304
|
+
const dir = join(this.root, 'running');
|
|
305
|
+
await mkdir(dir, { recursive: true });
|
|
306
|
+
await writeFile(join(dir, stepId), JSON.stringify(meta), 'utf8');
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Removes the running marker for a step.
|
|
310
|
+
* @param stepId - Step identifier
|
|
311
|
+
*/
|
|
312
|
+
async markStepDone(stepId) {
|
|
313
|
+
await rm(join(this.root, 'running', stepId), { force: true });
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Lists all steps currently marked as running.
|
|
317
|
+
* @returns Array of running step metadata
|
|
318
|
+
*/
|
|
319
|
+
async listRunningSteps() {
|
|
320
|
+
const dir = join(this.root, 'running');
|
|
321
|
+
try {
|
|
322
|
+
const entries = await readdir(dir);
|
|
323
|
+
const results = [];
|
|
324
|
+
for (const entry of entries) {
|
|
325
|
+
try {
|
|
326
|
+
const content = await readFile(join(dir, entry), 'utf8');
|
|
327
|
+
const meta = JSON.parse(content);
|
|
328
|
+
results.push({ stepId: entry, ...meta });
|
|
329
|
+
}
|
|
330
|
+
catch {
|
|
331
|
+
// Ignore malformed entries
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return results;
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
return [];
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Removes all running markers.
|
|
342
|
+
*/
|
|
343
|
+
async cleanupRunning() {
|
|
344
|
+
await rm(join(this.root, 'running'), { recursive: true, force: true });
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Validates a run ID to prevent path traversal attacks.
|
|
348
|
+
* @param id - Run identifier to validate
|
|
349
|
+
* @throws If the run ID contains invalid characters or path traversal attempts
|
|
252
350
|
* @internal
|
|
253
351
|
*/
|
|
254
|
-
|
|
352
|
+
validateRunId(id) {
|
|
255
353
|
if (!/^[\w-]+$/.test(id)) {
|
|
256
|
-
throw new
|
|
354
|
+
throw new WorkspaceError('INVALID_RUN_ID', `Invalid run ID: ${id}. Must contain only alphanumeric characters, dashes, and underscores.`);
|
|
257
355
|
}
|
|
258
356
|
if (id.includes('..')) {
|
|
259
|
-
throw new
|
|
357
|
+
throw new WorkspaceError('INVALID_RUN_ID', `Invalid run ID: ${id}. Path traversal is not allowed.`);
|
|
260
358
|
}
|
|
261
359
|
}
|
|
262
360
|
/**
|
|
263
361
|
* Validates a cache name to prevent path traversal attacks.
|
|
264
|
-
* Same rules as
|
|
362
|
+
* Same rules as run IDs: alphanumeric, dashes, underscores only.
|
|
265
363
|
* @param name - Cache name to validate
|
|
266
364
|
* @throws If the cache name contains invalid characters or path traversal attempts
|
|
267
365
|
* @internal
|
|
268
366
|
*/
|
|
269
367
|
validateCacheName(name) {
|
|
270
368
|
if (!/^[\w-]+$/.test(name)) {
|
|
271
|
-
throw new
|
|
369
|
+
throw new WorkspaceError('INVALID_CACHE_NAME', `Invalid cache name: ${name}. Must contain only alphanumeric characters, dashes, and underscores.`);
|
|
272
370
|
}
|
|
273
371
|
if (name.includes('..')) {
|
|
274
|
-
throw new
|
|
372
|
+
throw new WorkspaceError('INVALID_CACHE_NAME', `Invalid cache name: ${name}. Path traversal is not allowed.`);
|
|
275
373
|
}
|
|
276
374
|
}
|
|
277
375
|
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
export class PipexError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
constructor(code, message, options) {
|
|
4
|
+
super(message, options);
|
|
5
|
+
this.code = code;
|
|
6
|
+
this.name = 'PipexError';
|
|
7
|
+
}
|
|
8
|
+
get transient() {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
// -- Docker errors -----------------------------------------------------------
|
|
13
|
+
export class DockerError extends PipexError {
|
|
14
|
+
constructor(code, message, options) {
|
|
15
|
+
super(code, message, options);
|
|
16
|
+
this.name = 'DockerError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export class DockerNotAvailableError extends DockerError {
|
|
20
|
+
constructor(options) {
|
|
21
|
+
super('DOCKER_NOT_AVAILABLE', 'Docker CLI not found. Please install Docker.', options);
|
|
22
|
+
this.name = 'DockerNotAvailableError';
|
|
23
|
+
}
|
|
24
|
+
get transient() {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export class ImagePullError extends DockerError {
|
|
29
|
+
constructor(image, options) {
|
|
30
|
+
super('IMAGE_PULL_FAILED', `Failed to pull image "${image}"`, options);
|
|
31
|
+
this.name = 'ImagePullError';
|
|
32
|
+
}
|
|
33
|
+
get transient() {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export class ContainerTimeoutError extends DockerError {
|
|
38
|
+
constructor(timeoutSec, options) {
|
|
39
|
+
super('CONTAINER_TIMEOUT', `Container exceeded timeout of ${timeoutSec}s`, options);
|
|
40
|
+
this.name = 'ContainerTimeoutError';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export class ContainerCrashError extends DockerError {
|
|
44
|
+
stepId;
|
|
45
|
+
exitCode;
|
|
46
|
+
constructor(stepId, exitCode, options) {
|
|
47
|
+
super('CONTAINER_CRASH', `Step ${stepId} failed with exit code ${exitCode}`, options);
|
|
48
|
+
this.stepId = stepId;
|
|
49
|
+
this.exitCode = exitCode;
|
|
50
|
+
this.name = 'ContainerCrashError';
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export class ContainerCleanupError extends DockerError {
|
|
54
|
+
constructor(options) {
|
|
55
|
+
super('CONTAINER_CLEANUP_FAILED', 'Failed to clean up container', options);
|
|
56
|
+
this.name = 'ContainerCleanupError';
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// -- Workspace errors --------------------------------------------------------
|
|
60
|
+
export class WorkspaceError extends PipexError {
|
|
61
|
+
constructor(code, message, options) {
|
|
62
|
+
super(code, message, options);
|
|
63
|
+
this.name = 'WorkspaceError';
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export class ArtifactNotFoundError extends WorkspaceError {
|
|
67
|
+
constructor(message, options) {
|
|
68
|
+
super('ARTIFACT_NOT_FOUND', message, options);
|
|
69
|
+
this.name = 'ArtifactNotFoundError';
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export class StagingError extends WorkspaceError {
|
|
73
|
+
constructor(message, options) {
|
|
74
|
+
super('STAGING_FAILED', message, options);
|
|
75
|
+
this.name = 'StagingError';
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// -- Pipeline errors ---------------------------------------------------------
|
|
79
|
+
export class PipelineError extends PipexError {
|
|
80
|
+
constructor(code, message, options) {
|
|
81
|
+
super(code, message, options);
|
|
82
|
+
this.name = 'PipelineError';
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export class ValidationError extends PipelineError {
|
|
86
|
+
constructor(message, options) {
|
|
87
|
+
super('VALIDATION_ERROR', message, options);
|
|
88
|
+
this.name = 'ValidationError';
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
export class CyclicDependencyError extends PipelineError {
|
|
92
|
+
constructor(message, options) {
|
|
93
|
+
super('CYCLIC_DEPENDENCY', message, options);
|
|
94
|
+
this.name = 'CyclicDependencyError';
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
export class StepNotFoundError extends PipelineError {
|
|
98
|
+
constructor(stepId, referencedStep, options) {
|
|
99
|
+
super('STEP_NOT_FOUND', `Step ${stepId}: input step '${referencedStep}' not found or not yet executed`, options);
|
|
100
|
+
this.name = 'StepNotFoundError';
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// -- Bundle errors -----------------------------------------------------------
|
|
104
|
+
export class BundleError extends PipexError {
|
|
105
|
+
constructor(message, options) {
|
|
106
|
+
super('BUNDLE_ERROR', message, options);
|
|
107
|
+
this.name = 'BundleError';
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// -- Kit errors --------------------------------------------------------------
|
|
111
|
+
export class KitError extends PipexError {
|
|
112
|
+
constructor(code, message, options) {
|
|
113
|
+
super(code, message, options);
|
|
114
|
+
this.name = 'KitError';
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
export class MissingParameterError extends KitError {
|
|
118
|
+
constructor(kitName, paramName, options) {
|
|
119
|
+
super('MISSING_PARAMETER', `Kit "${kitName}": "${paramName}" parameter is required`, options);
|
|
120
|
+
this.name = 'MissingParameterError';
|
|
121
|
+
}
|
|
122
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -38,3 +38,6 @@
|
|
|
38
38
|
*/
|
|
39
39
|
// Export engine layer for programmatic use
|
|
40
40
|
export { Workspace, ContainerExecutor, DockerCliExecutor } from './engine/index.js';
|
|
41
|
+
// Export core layer for pipeline orchestration
|
|
42
|
+
export { PipelineRunner, StepRunner, PipelineLoader, StateManager, ConsoleReporter, StreamReporter, CompositeReporter, InMemoryTransport, EventAggregator, buildGraph, topologicalLevels, subgraph, leafNodes, evaluateCondition, dirSize, formatSize, formatDuration } from './core/index.js';
|
|
43
|
+
export { PipexError, DockerError, DockerNotAvailableError, ImagePullError, ContainerTimeoutError, ContainerCrashError, ContainerCleanupError, WorkspaceError, ArtifactNotFoundError, StagingError, PipelineError, ValidationError, CyclicDependencyError, StepNotFoundError, KitError, MissingParameterError } from './errors.js';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import test from 'ava';
|
|
2
|
+
import { KitError } from '../../errors.js';
|
|
3
|
+
import { getKit } from '../index.js';
|
|
4
|
+
test('getKit returns node kit', t => {
|
|
5
|
+
const kit = getKit('node');
|
|
6
|
+
t.is(kit.name, 'node');
|
|
7
|
+
});
|
|
8
|
+
test('getKit returns python kit', t => {
|
|
9
|
+
const kit = getKit('python');
|
|
10
|
+
t.is(kit.name, 'python');
|
|
11
|
+
});
|
|
12
|
+
test('getKit returns shell kit', t => {
|
|
13
|
+
const kit = getKit('shell');
|
|
14
|
+
t.is(kit.name, 'shell');
|
|
15
|
+
});
|
|
16
|
+
test('getKit throws KitError on unknown kit with available list', t => {
|
|
17
|
+
const error = t.throws(() => getKit('unknown'));
|
|
18
|
+
t.true(error instanceof KitError);
|
|
19
|
+
t.truthy(error?.message.includes('Unknown kit'));
|
|
20
|
+
t.truthy(error?.message.includes('node'));
|
|
21
|
+
t.truthy(error?.message.includes('python'));
|
|
22
|
+
t.truthy(error?.message.includes('shell'));
|
|
23
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import test from 'ava';
|
|
2
|
+
import { KitError, MissingParameterError } from '../../../errors.js';
|
|
3
|
+
import { nodeKit } from '../node.js';
|
|
4
|
+
test('resolve with minimal params (script only)', t => {
|
|
5
|
+
const result = nodeKit.resolve({ script: 'index.js' });
|
|
6
|
+
t.is(result.image, 'node:24-alpine');
|
|
7
|
+
t.truthy(result.cmd[2].includes('node /app/index.js'));
|
|
8
|
+
t.is(result.sources, undefined);
|
|
9
|
+
});
|
|
10
|
+
test('resolve uses default version and variant', t => {
|
|
11
|
+
const result = nodeKit.resolve({ script: 'app.js' });
|
|
12
|
+
t.is(result.image, 'node:24-alpine');
|
|
13
|
+
});
|
|
14
|
+
test('resolve with npm package manager', t => {
|
|
15
|
+
const result = nodeKit.resolve({ script: 'app.js', packageManager: 'npm' });
|
|
16
|
+
t.truthy(result.cmd[2].includes('cd /app && npm install'));
|
|
17
|
+
t.deepEqual(result.caches, [{ name: 'npm-cache', path: '/root/.npm' }]);
|
|
18
|
+
});
|
|
19
|
+
test('resolve with pnpm package manager', t => {
|
|
20
|
+
const result = nodeKit.resolve({ script: 'app.js', packageManager: 'pnpm' });
|
|
21
|
+
t.truthy(result.cmd[2].includes('pnpm install'));
|
|
22
|
+
t.deepEqual(result.caches, [{ name: 'pnpm-store', path: '/root/.local/share/pnpm/store' }]);
|
|
23
|
+
});
|
|
24
|
+
test('resolve with yarn package manager', t => {
|
|
25
|
+
const result = nodeKit.resolve({ script: 'app.js', packageManager: 'yarn' });
|
|
26
|
+
t.truthy(result.cmd[2].includes('yarn install'));
|
|
27
|
+
t.deepEqual(result.caches, [{ name: 'yarn-cache', path: '/usr/local/share/.cache/yarn' }]);
|
|
28
|
+
});
|
|
29
|
+
test('resolve with install=false skips install command', t => {
|
|
30
|
+
const result = nodeKit.resolve({ script: 'app.js', install: false });
|
|
31
|
+
t.falsy(result.cmd[2].includes('npm install'));
|
|
32
|
+
t.truthy(result.cmd[2].includes('node /app/app.js'));
|
|
33
|
+
});
|
|
34
|
+
test('resolve with src adds source', t => {
|
|
35
|
+
const result = nodeKit.resolve({ script: 'app.js', src: 'myapp' });
|
|
36
|
+
t.deepEqual(result.sources, [{ host: 'myapp', container: '/app' }]);
|
|
37
|
+
t.is(result.mounts, undefined);
|
|
38
|
+
});
|
|
39
|
+
test('resolve sets allowNetwork to true', t => {
|
|
40
|
+
const result = nodeKit.resolve({ script: 'app.js' });
|
|
41
|
+
t.true(result.allowNetwork);
|
|
42
|
+
});
|
|
43
|
+
test('resolve throws KitError on unsupported packageManager', t => {
|
|
44
|
+
const error = t.throws(() => nodeKit.resolve({ script: 'app.js', packageManager: 'bun' }), {
|
|
45
|
+
message: /unsupported packageManager/
|
|
46
|
+
});
|
|
47
|
+
t.true(error instanceof KitError);
|
|
48
|
+
});
|
|
49
|
+
test('resolve throws MissingParameterError without script or run', t => {
|
|
50
|
+
const error = t.throws(() => nodeKit.resolve({}), { message: /required/i });
|
|
51
|
+
t.true(error instanceof MissingParameterError);
|
|
52
|
+
});
|
|
53
|
+
test('resolve throws KitError when both script and run are provided', t => {
|
|
54
|
+
const error = t.throws(() => nodeKit.resolve({ script: 'app.js', run: 'npm run build' }), {
|
|
55
|
+
message: /mutually exclusive/
|
|
56
|
+
});
|
|
57
|
+
t.true(error instanceof KitError);
|
|
58
|
+
});
|
|
59
|
+
// -- run parameter ------------------------------------------------------------
|
|
60
|
+
test('resolve with run uses the command directly', t => {
|
|
61
|
+
const result = nodeKit.resolve({ run: 'npm run build --prefix /app' });
|
|
62
|
+
t.truthy(result.cmd[2].includes('npm run build --prefix /app'));
|
|
63
|
+
t.falsy(result.cmd[2].includes('node /app/'));
|
|
64
|
+
});
|
|
65
|
+
test('resolve with run still runs install by default', t => {
|
|
66
|
+
const result = nodeKit.resolve({ run: 'npx eslint /app/src' });
|
|
67
|
+
t.truthy(result.cmd[2].includes('npm install'));
|
|
68
|
+
t.truthy(result.cmd[2].includes('npx eslint /app/src'));
|
|
69
|
+
});
|
|
70
|
+
test('resolve with run and install=false skips install', t => {
|
|
71
|
+
const result = nodeKit.resolve({ run: 'node --version', install: false });
|
|
72
|
+
t.falsy(result.cmd[2].includes('npm install'));
|
|
73
|
+
t.is(result.cmd[2], 'node --version');
|
|
74
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import test from 'ava';
|
|
2
|
+
import { KitError, MissingParameterError } from '../../../errors.js';
|
|
3
|
+
import { pythonKit } from '../python.js';
|
|
4
|
+
test('resolve with minimal params (script only)', t => {
|
|
5
|
+
const result = pythonKit.resolve({ script: 'main.py' });
|
|
6
|
+
t.is(result.image, 'python:3.12-slim');
|
|
7
|
+
t.truthy(result.cmd[2].includes('python /app/main.py'));
|
|
8
|
+
});
|
|
9
|
+
test('resolve uses default version and variant', t => {
|
|
10
|
+
const result = pythonKit.resolve({ script: 'app.py' });
|
|
11
|
+
t.is(result.image, 'python:3.12-slim');
|
|
12
|
+
});
|
|
13
|
+
test('resolve with pip package manager', t => {
|
|
14
|
+
const result = pythonKit.resolve({ script: 'app.py', packageManager: 'pip' });
|
|
15
|
+
t.truthy(result.cmd[2].includes('pip install'));
|
|
16
|
+
t.deepEqual(result.caches, [{ name: 'pip-cache', path: '/root/.cache/pip' }]);
|
|
17
|
+
});
|
|
18
|
+
test('resolve with uv package manager', t => {
|
|
19
|
+
const result = pythonKit.resolve({ script: 'app.py', packageManager: 'uv' });
|
|
20
|
+
t.truthy(result.cmd[2].includes('uv pip install'));
|
|
21
|
+
t.deepEqual(result.caches, [{ name: 'uv-cache', path: '/root/.cache/uv' }]);
|
|
22
|
+
});
|
|
23
|
+
test('resolve with install=false skips install command', t => {
|
|
24
|
+
const result = pythonKit.resolve({ script: 'app.py', install: false });
|
|
25
|
+
t.falsy(result.cmd[2].includes('pip install'));
|
|
26
|
+
t.truthy(result.cmd[2].includes('python /app/app.py'));
|
|
27
|
+
});
|
|
28
|
+
test('resolve with src adds mount', t => {
|
|
29
|
+
const result = pythonKit.resolve({ script: 'app.py', src: 'myproject' });
|
|
30
|
+
t.deepEqual(result.mounts, [{ host: 'myproject', container: '/app' }]);
|
|
31
|
+
});
|
|
32
|
+
test('resolve sets allowNetwork to true', t => {
|
|
33
|
+
const result = pythonKit.resolve({ script: 'app.py' });
|
|
34
|
+
t.true(result.allowNetwork);
|
|
35
|
+
});
|
|
36
|
+
test('resolve throws KitError on unsupported packageManager', t => {
|
|
37
|
+
const error = t.throws(() => pythonKit.resolve({ script: 'app.py', packageManager: 'conda' }), {
|
|
38
|
+
message: /unsupported packageManager/
|
|
39
|
+
});
|
|
40
|
+
t.true(error instanceof KitError);
|
|
41
|
+
});
|
|
42
|
+
test('resolve throws MissingParameterError without script or run', t => {
|
|
43
|
+
const error = t.throws(() => pythonKit.resolve({}), { message: /required/i });
|
|
44
|
+
t.true(error instanceof MissingParameterError);
|
|
45
|
+
});
|
|
46
|
+
test('resolve throws KitError when both script and run are provided', t => {
|
|
47
|
+
const error = t.throws(() => pythonKit.resolve({ script: 'app.py', run: 'pytest /app/tests' }), {
|
|
48
|
+
message: /mutually exclusive/
|
|
49
|
+
});
|
|
50
|
+
t.true(error instanceof KitError);
|
|
51
|
+
});
|
|
52
|
+
// -- run parameter ------------------------------------------------------------
|
|
53
|
+
test('resolve with run uses the command directly', t => {
|
|
54
|
+
const result = pythonKit.resolve({ run: 'pytest /app/tests -v' });
|
|
55
|
+
t.truthy(result.cmd[2].includes('pytest /app/tests -v'));
|
|
56
|
+
t.falsy(result.cmd[2].includes('python /app/'));
|
|
57
|
+
});
|
|
58
|
+
test('resolve with run still runs install by default', t => {
|
|
59
|
+
const result = pythonKit.resolve({ run: 'pytest /app/tests' });
|
|
60
|
+
t.truthy(result.cmd[2].includes('pip install'));
|
|
61
|
+
t.truthy(result.cmd[2].includes('pytest /app/tests'));
|
|
62
|
+
});
|
|
63
|
+
test('resolve with run and install=false skips install', t => {
|
|
64
|
+
const result = pythonKit.resolve({ run: 'python --version', install: false });
|
|
65
|
+
t.falsy(result.cmd[2].includes('pip install'));
|
|
66
|
+
t.is(result.cmd[2], 'python --version');
|
|
67
|
+
});
|