@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/README.md
CHANGED
|
@@ -37,13 +37,68 @@ pipex run pipeline.yaml --json
|
|
|
37
37
|
pipex run pipeline.yaml --workdir /tmp/builds
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
+
### Interactive step execution
|
|
41
|
+
|
|
42
|
+
Execute individual steps without a full pipeline file — useful for iterative, exploratory, or agent-driven workflows:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# Create a step file
|
|
46
|
+
cat > step.yaml <<'EOF'
|
|
47
|
+
uses: shell
|
|
48
|
+
with:
|
|
49
|
+
run: "echo hello > /output/greeting.txt"
|
|
50
|
+
EOF
|
|
51
|
+
|
|
52
|
+
# Execute a single step in a workspace
|
|
53
|
+
pipex exec my-workspace -f step.yaml --step greet
|
|
54
|
+
|
|
55
|
+
# Read artifact content
|
|
56
|
+
pipex cat my-workspace greet # list artifacts
|
|
57
|
+
pipex cat my-workspace greet greeting.txt # read a file
|
|
58
|
+
|
|
59
|
+
# Ephemeral mode: stream stdout, don't commit run
|
|
60
|
+
pipex exec my-workspace -f step.yaml --step greet --ephemeral
|
|
61
|
+
|
|
62
|
+
# Chain steps via --input
|
|
63
|
+
pipex exec my-workspace -f process.yaml --step process --input greet
|
|
64
|
+
|
|
65
|
+
# Aliased inputs (mount under /input/data instead of /input/greet)
|
|
66
|
+
pipex exec my-workspace -f process.yaml --step process --input data=greet
|
|
67
|
+
|
|
68
|
+
# Remove a step's run and state entry
|
|
69
|
+
pipex rm-step my-workspace greet
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Inspecting runs
|
|
73
|
+
|
|
74
|
+
Each step execution produces a **run** containing artifacts, logs (stdout/stderr), and metadata:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
# Show all steps and their last run (status, duration, size, date)
|
|
78
|
+
pipex show my-pipeline
|
|
79
|
+
|
|
80
|
+
# Show logs from the last run of a step
|
|
81
|
+
pipex logs my-pipeline download
|
|
82
|
+
pipex logs my-pipeline download --stream stderr
|
|
83
|
+
|
|
84
|
+
# Show execution metadata (image, cmd, duration, exit code, fingerprint…)
|
|
85
|
+
pipex inspect my-pipeline download
|
|
86
|
+
pipex inspect my-pipeline download --json
|
|
87
|
+
|
|
88
|
+
# Export artifacts from a step to the host filesystem
|
|
89
|
+
pipex export my-pipeline download ./output-dir
|
|
90
|
+
```
|
|
91
|
+
|
|
40
92
|
### Managing workspaces
|
|
41
93
|
|
|
42
94
|
```bash
|
|
43
|
-
# List workspaces (with
|
|
95
|
+
# List workspaces (with run/cache counts and disk size)
|
|
44
96
|
pipex list
|
|
45
97
|
pipex ls --json
|
|
46
98
|
|
|
99
|
+
# Remove old runs (keeps only current ones)
|
|
100
|
+
pipex prune my-pipeline
|
|
101
|
+
|
|
47
102
|
# Remove specific workspaces
|
|
48
103
|
pipex rm my-build other-build
|
|
49
104
|
|
|
@@ -56,8 +111,16 @@ pipex clean
|
|
|
56
111
|
| Command | Description |
|
|
57
112
|
|---------|-------------|
|
|
58
113
|
| `run <pipeline>` | Execute a pipeline |
|
|
59
|
-
| `
|
|
114
|
+
| `exec <workspace> -f <step-file>` | Execute a single step in a workspace |
|
|
115
|
+
| `cat <workspace> <step> [path]` | Read or list artifact content from a step's latest run |
|
|
116
|
+
| `show <workspace>` | Show steps and runs in a workspace |
|
|
117
|
+
| `logs <workspace> <step>` | Show stdout/stderr from last run |
|
|
118
|
+
| `inspect <workspace> <step>` | Show run metadata (meta.json) |
|
|
119
|
+
| `export <workspace> <step> <dest>` | Extract artifacts from a step run to the host filesystem |
|
|
120
|
+
| `prune <workspace>` | Remove old runs not referenced by current state |
|
|
121
|
+
| `list` (alias `ls`) | List workspaces (with disk sizes) |
|
|
60
122
|
| `rm <workspace...>` | Remove one or more workspaces |
|
|
123
|
+
| `rm-step <workspace> <step>` | Remove a step's run and state entry |
|
|
61
124
|
| `clean` | Remove all workspaces |
|
|
62
125
|
|
|
63
126
|
### Global Options
|
|
@@ -73,6 +136,22 @@ pipex clean
|
|
|
73
136
|
|--------|-------|-------------|
|
|
74
137
|
| `--workspace <name>` | `-w` | Workspace name for caching |
|
|
75
138
|
| `--force [steps]` | `-f` | Skip cache for all steps, or a comma-separated list |
|
|
139
|
+
| `--dry-run` | | Validate pipeline, compute fingerprints, show what would run without executing |
|
|
140
|
+
| `--target <steps>` | `-t` | Execute only these steps and their dependencies (comma-separated) |
|
|
141
|
+
| `--concurrency <n>` | `-c` | Max parallel step executions (default: CPU count) |
|
|
142
|
+
| `--env-file <path>` | | Load environment variables from a dotenv file for all steps |
|
|
143
|
+
| `--verbose` | | Stream container logs in real-time (interactive mode) |
|
|
144
|
+
|
|
145
|
+
### Exec Options
|
|
146
|
+
|
|
147
|
+
| Option | Alias | Description |
|
|
148
|
+
|--------|-------|-------------|
|
|
149
|
+
| `--file <path>` | `-f` | Step definition file (YAML or JSON, required) |
|
|
150
|
+
| `--step <id>` | | Step ID (overrides file's id/name) |
|
|
151
|
+
| `--input <specs...>` | | Input steps (e.g. `extract` or `data=extract`) |
|
|
152
|
+
| `--ephemeral` | | Stream stdout to terminal and discard the run |
|
|
153
|
+
| `--force` | | Skip cache check |
|
|
154
|
+
| `--verbose` | | Stream container logs in real-time |
|
|
76
155
|
|
|
77
156
|
## Pipeline Format
|
|
78
157
|
|
|
@@ -117,44 +196,57 @@ Kits are reusable templates that generate the image, command, caches, and mounts
|
|
|
117
196
|
```yaml
|
|
118
197
|
name: my-pipeline
|
|
119
198
|
steps:
|
|
120
|
-
- id:
|
|
199
|
+
- id: transform
|
|
121
200
|
uses: node
|
|
122
|
-
with: { script:
|
|
201
|
+
with: { script: transform.js, src: src/app }
|
|
202
|
+
- id: convert
|
|
203
|
+
uses: node
|
|
204
|
+
with: { run: "node /app/convert.js --format csv --output /output/", src: src/app }
|
|
123
205
|
- id: analyze
|
|
124
206
|
uses: python
|
|
125
207
|
with: { script: analyze.py, src: scripts }
|
|
208
|
+
- id: enrich
|
|
209
|
+
uses: python
|
|
210
|
+
with: { run: "python /app/enrich.py --locale fr --input /input/analyze/", src: scripts/ }
|
|
211
|
+
inputs: [{ step: analyze }]
|
|
126
212
|
- id: extract
|
|
127
213
|
uses: shell
|
|
128
|
-
with: { packages: [unzip], run: "unzip /input/
|
|
129
|
-
inputs: [{ step:
|
|
214
|
+
with: { packages: [unzip], run: "unzip /input/transform/archive.zip -d /output/" }
|
|
215
|
+
inputs: [{ step: transform }]
|
|
130
216
|
```
|
|
131
217
|
|
|
132
218
|
`uses` and `image`/`cmd` are mutually exclusive. All other step fields (`env`, `inputs`, `mounts`, `sources`, `caches`, `timeoutSec`, `allowFailure`, `allowNetwork`) remain available and merge with kit defaults (user values take priority). The `src` parameter in `with` copies the host directory into `/app` in the container's writable layer (see [Sources](#sources)).
|
|
133
219
|
|
|
134
220
|
#### Available Kits
|
|
135
221
|
|
|
136
|
-
**`node`** -- Run a Node.js
|
|
222
|
+
**`node`** -- Run a Node.js command with automatic dependency installation.
|
|
137
223
|
|
|
138
224
|
| Parameter | Default | Description |
|
|
139
225
|
|-----------|---------|-------------|
|
|
140
|
-
| `script` |
|
|
226
|
+
| `script` | -- | Script to run (relative to `/app`). Mutually exclusive with `run`. |
|
|
227
|
+
| `run` | -- | Arbitrary shell command. Mutually exclusive with `script`. |
|
|
141
228
|
| `src` | -- | Host directory to copy into `/app` |
|
|
142
229
|
| `version` | `"24"` | Node.js version |
|
|
143
230
|
| `packageManager` | `"npm"` | `"npm"`, `"pnpm"`, or `"yarn"` |
|
|
144
|
-
| `install` | `true` | Run package install before
|
|
231
|
+
| `install` | `true` | Run package install before command |
|
|
145
232
|
| `variant` | `"alpine"` | Image variant |
|
|
146
233
|
|
|
147
|
-
|
|
234
|
+
Exactly one of `script` or `run` is required. `script: transform.js` is shorthand for `run: node /app/transform.js`. Use `run` to pass arguments or call any command available in the container.
|
|
235
|
+
|
|
236
|
+
**`python`** -- Run a Python command with automatic dependency installation from `requirements.txt`.
|
|
148
237
|
|
|
149
238
|
| Parameter | Default | Description |
|
|
150
239
|
|-----------|---------|-------------|
|
|
151
|
-
| `script` |
|
|
240
|
+
| `script` | -- | Script to run (relative to `/app`). Mutually exclusive with `run`. |
|
|
241
|
+
| `run` | -- | Arbitrary shell command. Mutually exclusive with `script`. |
|
|
152
242
|
| `src` | -- | Host directory to copy into `/app` |
|
|
153
243
|
| `version` | `"3.12"` | Python version |
|
|
154
244
|
| `packageManager` | `"pip"` | `"pip"` or `"uv"` |
|
|
155
|
-
| `install` | `true` | Run dependency install before
|
|
245
|
+
| `install` | `true` | Run dependency install before command |
|
|
156
246
|
| `variant` | `"slim"` | Image variant |
|
|
157
247
|
|
|
248
|
+
Exactly one of `script` or `run` is required. `script: analyze.py` is shorthand for `run: python /app/analyze.py`. Use `run` to pass arguments or call any command available in the container.
|
|
249
|
+
|
|
158
250
|
**`shell`** -- Run a shell command in a container, with optional apt package installation.
|
|
159
251
|
|
|
160
252
|
| Parameter | Default | Description |
|
|
@@ -210,15 +302,36 @@ steps:
|
|
|
210
302
|
| `with` | object | Kit parameters |
|
|
211
303
|
| `inputs` | InputSpec[] | Previous steps to mount as read-only |
|
|
212
304
|
| `env` | Record<string, string> | Environment variables |
|
|
305
|
+
| `envFile` | string | Path to a dotenv file (relative to pipeline file) |
|
|
213
306
|
| `outputPath` | string | Output mount point (default: `/output`) |
|
|
214
307
|
| `mounts` | MountSpec[] | Host directories to bind mount (read-only) |
|
|
215
308
|
| `sources` | MountSpec[] | Host directories copied into the container's writable layer |
|
|
216
309
|
| `caches` | CacheSpec[] | Persistent caches to mount |
|
|
310
|
+
| `if` | string | Condition expression — step is skipped when it evaluates to false |
|
|
217
311
|
| `timeoutSec` | number | Execution timeout |
|
|
312
|
+
| `retries` | number | Number of retry attempts on transient failure |
|
|
313
|
+
| `retryDelayMs` | number | Delay between retries (default: 5000) |
|
|
218
314
|
| `allowFailure` | boolean | Continue pipeline if step fails |
|
|
219
315
|
| `allowNetwork` | boolean | Enable network access |
|
|
220
316
|
|
|
221
|
-
###
|
|
317
|
+
### Step Dependencies and Parallel Execution
|
|
318
|
+
|
|
319
|
+
Steps declare dependencies via `inputs`. Pipex automatically determines which steps can run in parallel based on the dependency graph. Steps at the same level (no dependency between them) execute concurrently, up to `--concurrency` (defaults to CPU count).
|
|
320
|
+
|
|
321
|
+
```yaml
|
|
322
|
+
steps:
|
|
323
|
+
- id: download
|
|
324
|
+
# ...
|
|
325
|
+
- id: process-a
|
|
326
|
+
inputs: [{ step: download }] # waits for download
|
|
327
|
+
- id: process-b
|
|
328
|
+
inputs: [{ step: download }] # waits for download
|
|
329
|
+
# process-a and process-b run in parallel
|
|
330
|
+
- id: merge
|
|
331
|
+
inputs: [{ step: process-a }, { step: process-b }] # waits for both
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
#### Inputs
|
|
222
335
|
|
|
223
336
|
Mount previous steps as read-only:
|
|
224
337
|
|
|
@@ -227,10 +340,45 @@ inputs:
|
|
|
227
340
|
- step: step1
|
|
228
341
|
- step: step2
|
|
229
342
|
copyToOutput: true
|
|
343
|
+
- step: step3
|
|
344
|
+
optional: true
|
|
230
345
|
```
|
|
231
346
|
|
|
232
347
|
- Mounted under `/input/{stepName}/`
|
|
233
348
|
- `copyToOutput: true` copies content to output before execution
|
|
349
|
+
- `optional: true` allows the step to run even if the dependency failed or was skipped
|
|
350
|
+
|
|
351
|
+
#### Targeted Execution
|
|
352
|
+
|
|
353
|
+
Use `--target` to execute only specific steps and their dependencies:
|
|
354
|
+
|
|
355
|
+
```bash
|
|
356
|
+
# Only run 'merge' and everything it depends on
|
|
357
|
+
pipex run pipeline.yaml --target merge
|
|
358
|
+
|
|
359
|
+
# Multiple targets
|
|
360
|
+
pipex run pipeline.yaml --target process-a,process-b
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
### Conditional Steps
|
|
364
|
+
|
|
365
|
+
Steps can be conditionally skipped using `if:` with a [JEXL](https://github.com/TomFrost/Jexl) expression. The expression is evaluated against the current environment variables via `env`:
|
|
366
|
+
|
|
367
|
+
```yaml
|
|
368
|
+
- id: notify
|
|
369
|
+
if: env.CI
|
|
370
|
+
uses: shell
|
|
371
|
+
with:
|
|
372
|
+
run: echo "Running in CI"
|
|
373
|
+
|
|
374
|
+
- id: deploy
|
|
375
|
+
if: env.NODE_ENV == "production"
|
|
376
|
+
uses: shell
|
|
377
|
+
with:
|
|
378
|
+
run: echo "Deploying..."
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
When a condition evaluates to false, the step is skipped. Steps that depend on a skipped step with a required (non-optional) input are also skipped.
|
|
234
382
|
|
|
235
383
|
### Host Mounts
|
|
236
384
|
|
|
@@ -298,7 +446,7 @@ Common use cases:
|
|
|
298
446
|
|
|
299
447
|
### Geodata Processing
|
|
300
448
|
|
|
301
|
-
The `examples/geodata/` pipeline downloads a shapefile archive, extracts it, and produces a CSV inventory
|
|
449
|
+
The `examples/geodata/` pipeline downloads a shapefile archive, extracts it, and produces a CSV inventory. The last two steps run in parallel:
|
|
302
450
|
|
|
303
451
|
```
|
|
304
452
|
examples/geodata/
|
|
@@ -311,6 +459,28 @@ Steps: `download` → `extract` → `list-files` / `build-csv`
|
|
|
311
459
|
pipex run examples/geodata/pipeline.yaml
|
|
312
460
|
```
|
|
313
461
|
|
|
462
|
+
### Text Processing
|
|
463
|
+
|
|
464
|
+
The `examples/text-processing/` pipeline demonstrates parallel branches, conditional steps, and optional inputs:
|
|
465
|
+
|
|
466
|
+
```
|
|
467
|
+
examples/text-processing/
|
|
468
|
+
└── pipeline.yaml
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
Steps: `generate` → `stats` + `filter` (parallel) → `report` → `notify` (conditional), with `audit` (conditional, optional input to `notify`)
|
|
472
|
+
|
|
473
|
+
```bash
|
|
474
|
+
# Default: notify and audit are skipped (conditions not met)
|
|
475
|
+
pipex run examples/text-processing/pipeline.yaml
|
|
476
|
+
|
|
477
|
+
# Enable notifications
|
|
478
|
+
NOTIFY=1 pipex run examples/text-processing/pipeline.yaml
|
|
479
|
+
|
|
480
|
+
# Only run stats and its dependencies
|
|
481
|
+
pipex run examples/text-processing/pipeline.yaml --target stats
|
|
482
|
+
```
|
|
483
|
+
|
|
314
484
|
### Multi-Language
|
|
315
485
|
|
|
316
486
|
The `examples/multi-language/` pipeline chains Node.js and Python steps using kits:
|
|
@@ -342,7 +512,7 @@ Workspaces enable caching across runs. The workspace ID is determined by:
|
|
|
342
512
|
1. CLI flag `--workspace` (highest priority)
|
|
343
513
|
2. Pipeline `id` (explicit or derived from `name`)
|
|
344
514
|
|
|
345
|
-
**Cache behavior**: Steps are skipped if image, cmd, env, inputs, and mounts haven't changed. See code documentation for details.
|
|
515
|
+
**Cache behavior**: Steps are skipped if image, cmd, env (including values from `envFile` and `--env-file`), inputs, and mounts haven't changed. See code documentation for details.
|
|
346
516
|
|
|
347
517
|
## Troubleshooting
|
|
348
518
|
|
|
@@ -372,7 +542,7 @@ pipex rm old-workspace-id
|
|
|
372
542
|
pipex clean
|
|
373
543
|
```
|
|
374
544
|
|
|
375
|
-
### Cached step with missing
|
|
545
|
+
### Cached step with missing run
|
|
376
546
|
|
|
377
547
|
Force re-execution:
|
|
378
548
|
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import test from 'ava';
|
|
2
|
+
import { PipexError, DockerError, DockerNotAvailableError, ImagePullError, ContainerTimeoutError, ContainerCrashError, ContainerCleanupError, WorkspaceError, ArtifactNotFoundError, StagingError, PipelineError, ValidationError, CyclicDependencyError, StepNotFoundError, KitError, MissingParameterError } from '../errors.js';
|
|
3
|
+
// -- instanceof chains -------------------------------------------------------
|
|
4
|
+
test('DockerNotAvailableError is instanceof DockerError and PipexError', t => {
|
|
5
|
+
const error = new DockerNotAvailableError();
|
|
6
|
+
t.true(error instanceof DockerNotAvailableError);
|
|
7
|
+
t.true(error instanceof DockerError);
|
|
8
|
+
t.true(error instanceof PipexError);
|
|
9
|
+
t.true(error instanceof Error);
|
|
10
|
+
});
|
|
11
|
+
test('ImagePullError is instanceof DockerError and PipexError', t => {
|
|
12
|
+
const error = new ImagePullError('alpine:latest');
|
|
13
|
+
t.true(error instanceof ImagePullError);
|
|
14
|
+
t.true(error instanceof DockerError);
|
|
15
|
+
t.true(error instanceof PipexError);
|
|
16
|
+
});
|
|
17
|
+
test('ContainerTimeoutError is instanceof DockerError and PipexError', t => {
|
|
18
|
+
const error = new ContainerTimeoutError(30);
|
|
19
|
+
t.true(error instanceof ContainerTimeoutError);
|
|
20
|
+
t.true(error instanceof DockerError);
|
|
21
|
+
t.true(error instanceof PipexError);
|
|
22
|
+
});
|
|
23
|
+
test('ContainerCrashError is instanceof DockerError and PipexError', t => {
|
|
24
|
+
const error = new ContainerCrashError('build', 1);
|
|
25
|
+
t.true(error instanceof ContainerCrashError);
|
|
26
|
+
t.true(error instanceof DockerError);
|
|
27
|
+
t.true(error instanceof PipexError);
|
|
28
|
+
});
|
|
29
|
+
test('ContainerCleanupError is instanceof DockerError and PipexError', t => {
|
|
30
|
+
const error = new ContainerCleanupError();
|
|
31
|
+
t.true(error instanceof ContainerCleanupError);
|
|
32
|
+
t.true(error instanceof DockerError);
|
|
33
|
+
t.true(error instanceof PipexError);
|
|
34
|
+
});
|
|
35
|
+
test('ArtifactNotFoundError is instanceof WorkspaceError and PipexError', t => {
|
|
36
|
+
const error = new ArtifactNotFoundError('missing artifact');
|
|
37
|
+
t.true(error instanceof ArtifactNotFoundError);
|
|
38
|
+
t.true(error instanceof WorkspaceError);
|
|
39
|
+
t.true(error instanceof PipexError);
|
|
40
|
+
});
|
|
41
|
+
test('StagingError is instanceof WorkspaceError and PipexError', t => {
|
|
42
|
+
const error = new StagingError('staging failed');
|
|
43
|
+
t.true(error instanceof StagingError);
|
|
44
|
+
t.true(error instanceof WorkspaceError);
|
|
45
|
+
t.true(error instanceof PipexError);
|
|
46
|
+
});
|
|
47
|
+
test('ValidationError is instanceof PipelineError and PipexError', t => {
|
|
48
|
+
const error = new ValidationError('invalid');
|
|
49
|
+
t.true(error instanceof ValidationError);
|
|
50
|
+
t.true(error instanceof PipelineError);
|
|
51
|
+
t.true(error instanceof PipexError);
|
|
52
|
+
});
|
|
53
|
+
test('CyclicDependencyError is instanceof PipelineError and PipexError', t => {
|
|
54
|
+
const error = new CyclicDependencyError('cycle detected');
|
|
55
|
+
t.true(error instanceof CyclicDependencyError);
|
|
56
|
+
t.true(error instanceof PipelineError);
|
|
57
|
+
t.true(error instanceof PipexError);
|
|
58
|
+
});
|
|
59
|
+
test('StepNotFoundError is instanceof PipelineError and PipexError', t => {
|
|
60
|
+
const error = new StepNotFoundError('build', 'compile');
|
|
61
|
+
t.true(error instanceof StepNotFoundError);
|
|
62
|
+
t.true(error instanceof PipelineError);
|
|
63
|
+
t.true(error instanceof PipexError);
|
|
64
|
+
});
|
|
65
|
+
test('MissingParameterError is instanceof KitError and PipexError', t => {
|
|
66
|
+
const error = new MissingParameterError('node', 'script');
|
|
67
|
+
t.true(error instanceof MissingParameterError);
|
|
68
|
+
t.true(error instanceof KitError);
|
|
69
|
+
t.true(error instanceof PipexError);
|
|
70
|
+
});
|
|
71
|
+
// -- code property -----------------------------------------------------------
|
|
72
|
+
test('DockerNotAvailableError has code DOCKER_NOT_AVAILABLE', t => {
|
|
73
|
+
t.is(new DockerNotAvailableError().code, 'DOCKER_NOT_AVAILABLE');
|
|
74
|
+
});
|
|
75
|
+
test('ImagePullError has code IMAGE_PULL_FAILED', t => {
|
|
76
|
+
t.is(new ImagePullError('alpine').code, 'IMAGE_PULL_FAILED');
|
|
77
|
+
});
|
|
78
|
+
test('ContainerTimeoutError has code CONTAINER_TIMEOUT', t => {
|
|
79
|
+
t.is(new ContainerTimeoutError(30).code, 'CONTAINER_TIMEOUT');
|
|
80
|
+
});
|
|
81
|
+
test('ContainerCrashError has code CONTAINER_CRASH', t => {
|
|
82
|
+
t.is(new ContainerCrashError('build', 1).code, 'CONTAINER_CRASH');
|
|
83
|
+
});
|
|
84
|
+
test('ContainerCleanupError has code CONTAINER_CLEANUP_FAILED', t => {
|
|
85
|
+
t.is(new ContainerCleanupError().code, 'CONTAINER_CLEANUP_FAILED');
|
|
86
|
+
});
|
|
87
|
+
test('ArtifactNotFoundError has code ARTIFACT_NOT_FOUND', t => {
|
|
88
|
+
t.is(new ArtifactNotFoundError('msg').code, 'ARTIFACT_NOT_FOUND');
|
|
89
|
+
});
|
|
90
|
+
test('StagingError has code STAGING_FAILED', t => {
|
|
91
|
+
t.is(new StagingError('msg').code, 'STAGING_FAILED');
|
|
92
|
+
});
|
|
93
|
+
test('ValidationError has code VALIDATION_ERROR', t => {
|
|
94
|
+
t.is(new ValidationError('msg').code, 'VALIDATION_ERROR');
|
|
95
|
+
});
|
|
96
|
+
test('StepNotFoundError has code STEP_NOT_FOUND', t => {
|
|
97
|
+
t.is(new StepNotFoundError('a', 'b').code, 'STEP_NOT_FOUND');
|
|
98
|
+
});
|
|
99
|
+
test('MissingParameterError has code MISSING_PARAMETER', t => {
|
|
100
|
+
t.is(new MissingParameterError('node', 'script').code, 'MISSING_PARAMETER');
|
|
101
|
+
});
|
|
102
|
+
// -- transient flag ----------------------------------------------------------
|
|
103
|
+
test('DockerNotAvailableError is transient', t => {
|
|
104
|
+
t.true(new DockerNotAvailableError().transient);
|
|
105
|
+
});
|
|
106
|
+
test('ImagePullError is transient', t => {
|
|
107
|
+
t.true(new ImagePullError('alpine').transient);
|
|
108
|
+
});
|
|
109
|
+
test('ContainerTimeoutError is not transient', t => {
|
|
110
|
+
t.false(new ContainerTimeoutError(30).transient);
|
|
111
|
+
});
|
|
112
|
+
test('ContainerCrashError is not transient', t => {
|
|
113
|
+
t.false(new ContainerCrashError('build', 1).transient);
|
|
114
|
+
});
|
|
115
|
+
test('ValidationError is not transient', t => {
|
|
116
|
+
t.false(new ValidationError('msg').transient);
|
|
117
|
+
});
|
|
118
|
+
test('StagingError is not transient', t => {
|
|
119
|
+
t.false(new StagingError('msg').transient);
|
|
120
|
+
});
|
|
121
|
+
// -- cause chaining ----------------------------------------------------------
|
|
122
|
+
test('PipexError supports cause chaining', t => {
|
|
123
|
+
const cause = new Error('original');
|
|
124
|
+
const error = new ValidationError('wrapped', { cause });
|
|
125
|
+
t.is(error.cause, cause);
|
|
126
|
+
});
|
|
127
|
+
test('DockerNotAvailableError supports cause chaining', t => {
|
|
128
|
+
const cause = new Error('cannot connect');
|
|
129
|
+
const error = new DockerNotAvailableError({ cause });
|
|
130
|
+
t.is(error.cause, cause);
|
|
131
|
+
});
|
|
132
|
+
test('ImagePullError supports cause chaining', t => {
|
|
133
|
+
const cause = new Error('network error');
|
|
134
|
+
const error = new ImagePullError('alpine', { cause });
|
|
135
|
+
t.is(error.cause, cause);
|
|
136
|
+
});
|
|
137
|
+
// -- message content ---------------------------------------------------------
|
|
138
|
+
test('ContainerCrashError includes step id and exit code', t => {
|
|
139
|
+
const error = new ContainerCrashError('build', 137);
|
|
140
|
+
t.true(error.message.includes('build'));
|
|
141
|
+
t.true(error.message.includes('137'));
|
|
142
|
+
t.is(error.stepId, 'build');
|
|
143
|
+
t.is(error.exitCode, 137);
|
|
144
|
+
});
|
|
145
|
+
test('StepNotFoundError includes step id and referenced step', t => {
|
|
146
|
+
const error = new StepNotFoundError('deploy', 'build');
|
|
147
|
+
t.true(error.message.includes('deploy'));
|
|
148
|
+
t.true(error.message.includes('build'));
|
|
149
|
+
});
|
|
150
|
+
test('MissingParameterError includes kit name and param name', t => {
|
|
151
|
+
const error = new MissingParameterError('node', 'script');
|
|
152
|
+
t.true(error.message.includes('node'));
|
|
153
|
+
t.true(error.message.includes('script'));
|
|
154
|
+
});
|
|
155
|
+
test('ContainerTimeoutError includes timeout value', t => {
|
|
156
|
+
const error = new ContainerTimeoutError(60);
|
|
157
|
+
t.true(error.message.includes('60'));
|
|
158
|
+
});
|
|
159
|
+
test('ImagePullError includes image name', t => {
|
|
160
|
+
const error = new ImagePullError('myregistry/myimage:latest');
|
|
161
|
+
t.true(error.message.includes('myregistry/myimage:latest'));
|
|
162
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import { mkdtemp } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
/**
|
|
6
|
+
* Creates a temporary directory for test isolation.
|
|
7
|
+
* Each test should use its own tmpdir to avoid interference.
|
|
8
|
+
*/
|
|
9
|
+
export async function createTmpDir() {
|
|
10
|
+
return mkdtemp(join(tmpdir(), 'pipex-test-'));
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Silent reporter — all methods are no-ops.
|
|
14
|
+
*/
|
|
15
|
+
export const noopReporter = {
|
|
16
|
+
emit() { }
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Returns a reporter that records emit() calls for assertions.
|
|
20
|
+
*/
|
|
21
|
+
export function recordingReporter() {
|
|
22
|
+
const events = [];
|
|
23
|
+
const reporter = {
|
|
24
|
+
emit(event) {
|
|
25
|
+
events.push(event);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
return { reporter, events };
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Checks if Docker is available on the host (synchronous for use at module level).
|
|
32
|
+
*/
|
|
33
|
+
export function isDockerAvailable() {
|
|
34
|
+
try {
|
|
35
|
+
execSync('docker version', { stdio: 'ignore' });
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import test from 'ava';
|
|
2
|
+
import { isKitStep } from '../types.js';
|
|
3
|
+
test('isKitStep returns true when step has uses', t => {
|
|
4
|
+
t.true(isKitStep({ uses: 'node', name: 'build' }));
|
|
5
|
+
});
|
|
6
|
+
test('isKitStep returns false when step has no uses', t => {
|
|
7
|
+
t.false(isKitStep({ image: 'alpine', cmd: ['echo', 'hi'], name: 'build' }));
|
|
8
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import test from 'ava';
|
|
2
|
+
import { evaluateCondition } from '../condition.js';
|
|
3
|
+
test('env.CI truthy when CI is defined', async (t) => {
|
|
4
|
+
const result = await evaluateCondition('env.CI', { env: { CI: 'true' } });
|
|
5
|
+
t.true(result);
|
|
6
|
+
});
|
|
7
|
+
test('env.CI falsy when CI is absent', async (t) => {
|
|
8
|
+
const result = await evaluateCondition('env.CI', { env: {} });
|
|
9
|
+
t.false(result);
|
|
10
|
+
});
|
|
11
|
+
test('env.NODE_ENV == "production" — equality', async (t) => {
|
|
12
|
+
t.true(await evaluateCondition('env.NODE_ENV == "production"', { env: { NODE_ENV: 'production' } }));
|
|
13
|
+
t.false(await evaluateCondition('env.NODE_ENV == "production"', { env: { NODE_ENV: 'development' } }));
|
|
14
|
+
});
|
|
15
|
+
test('env.CI && !env.STAGING — combined logic', async (t) => {
|
|
16
|
+
t.true(await evaluateCondition('env.CI && !env.STAGING', { env: { CI: 'true' } }));
|
|
17
|
+
t.false(await evaluateCondition('env.CI && !env.STAGING', { env: { CI: 'true', STAGING: 'true' } }));
|
|
18
|
+
t.false(await evaluateCondition('env.CI && !env.STAGING', { env: {} }));
|
|
19
|
+
});
|
|
20
|
+
test('invalid expression returns false (fail-closed)', async (t) => {
|
|
21
|
+
const result = await evaluateCondition(')))invalid(((', { env: {} });
|
|
22
|
+
t.false(result);
|
|
23
|
+
});
|