tubeless 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +106 -0
- package/dist/abort.d.ts +12 -0
- package/dist/abort.d.ts.map +1 -0
- package/dist/abort.js +49 -0
- package/dist/abort.js.map +1 -0
- package/dist/batch.d.ts +22 -0
- package/dist/batch.d.ts.map +1 -0
- package/dist/batch.js +73 -0
- package/dist/batch.js.map +1 -0
- package/dist/checkpoint.d.ts +33 -0
- package/dist/checkpoint.d.ts.map +1 -0
- package/dist/checkpoint.js +71 -0
- package/dist/checkpoint.js.map +1 -0
- package/dist/child-execution.d.ts +30 -0
- package/dist/child-execution.d.ts.map +1 -0
- package/dist/child-execution.js +270 -0
- package/dist/child-execution.js.map +1 -0
- package/dist/cli-command.d.ts +29 -0
- package/dist/cli-command.d.ts.map +1 -0
- package/dist/cli-command.js +314 -0
- package/dist/cli-command.js.map +1 -0
- package/dist/cli-parser.d.ts +28 -0
- package/dist/cli-parser.d.ts.map +1 -0
- package/dist/cli-parser.js +557 -0
- package/dist/cli-parser.js.map +1 -0
- package/dist/cli-pipeline-command.d.ts +84 -0
- package/dist/cli-pipeline-command.d.ts.map +1 -0
- package/dist/cli-pipeline-command.js +185 -0
- package/dist/cli-pipeline-command.js.map +1 -0
- package/dist/cli-types.d.ts +220 -0
- package/dist/cli-types.d.ts.map +1 -0
- package/dist/cli-types.js +19 -0
- package/dist/cli-types.js.map +1 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +6 -0
- package/dist/cli.js.map +1 -0
- package/dist/env.d.ts +2 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +8 -0
- package/dist/env.js.map +1 -0
- package/dist/file-utils.d.ts +4 -0
- package/dist/file-utils.d.ts.map +1 -0
- package/dist/file-utils.js +35 -0
- package/dist/file-utils.js.map +1 -0
- package/dist/interactive-reporter.d.ts +38 -0
- package/dist/interactive-reporter.d.ts.map +1 -0
- package/dist/interactive-reporter.js +308 -0
- package/dist/interactive-reporter.js.map +1 -0
- package/dist/lifecycle.d.ts +17 -0
- package/dist/lifecycle.d.ts.map +1 -0
- package/dist/lifecycle.js +77 -0
- package/dist/lifecycle.js.map +1 -0
- package/dist/live-ticker.d.ts +21 -0
- package/dist/live-ticker.d.ts.map +1 -0
- package/dist/live-ticker.js +377 -0
- package/dist/live-ticker.js.map +1 -0
- package/dist/mapped-child-progress.d.ts +109 -0
- package/dist/mapped-child-progress.d.ts.map +1 -0
- package/dist/mapped-child-progress.js +133 -0
- package/dist/mapped-child-progress.js.map +1 -0
- package/dist/node.d.ts +5 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +5 -0
- package/dist/node.js.map +1 -0
- package/dist/paths.d.ts +9 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +19 -0
- package/dist/paths.js.map +1 -0
- package/dist/pipeline-command-marker.d.ts +5 -0
- package/dist/pipeline-command-marker.d.ts.map +1 -0
- package/dist/pipeline-command-marker.js +21 -0
- package/dist/pipeline-command-marker.js.map +1 -0
- package/dist/pipeline-diagnostics.d.ts +4 -0
- package/dist/pipeline-diagnostics.d.ts.map +1 -0
- package/dist/pipeline-diagnostics.js +17 -0
- package/dist/pipeline-diagnostics.js.map +1 -0
- package/dist/pipeline-module.d.ts +62 -0
- package/dist/pipeline-module.d.ts.map +1 -0
- package/dist/pipeline-module.js +157 -0
- package/dist/pipeline-module.js.map +1 -0
- package/dist/pipeline.d.ts +606 -0
- package/dist/pipeline.d.ts.map +1 -0
- package/dist/pipeline.js +1444 -0
- package/dist/pipeline.js.map +1 -0
- package/dist/progress.d.ts +4 -0
- package/dist/progress.d.ts.map +1 -0
- package/dist/progress.js +8 -0
- package/dist/progress.js.map +1 -0
- package/dist/prompt-select.d.ts +81 -0
- package/dist/prompt-select.d.ts.map +1 -0
- package/dist/prompt-select.js +197 -0
- package/dist/prompt-select.js.map +1 -0
- package/dist/rate-limit.d.ts +7 -0
- package/dist/rate-limit.d.ts.map +1 -0
- package/dist/rate-limit.js +23 -0
- package/dist/rate-limit.js.map +1 -0
- package/dist/render.d.ts +21 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +60 -0
- package/dist/render.js.map +1 -0
- package/dist/reporter-entry.d.ts +3 -0
- package/dist/reporter-entry.d.ts.map +1 -0
- package/dist/reporter-entry.js +3 -0
- package/dist/reporter-entry.js.map +1 -0
- package/dist/reporter.d.ts +50 -0
- package/dist/reporter.d.ts.map +1 -0
- package/dist/reporter.js +167 -0
- package/dist/reporter.js.map +1 -0
- package/dist/retry.d.ts +28 -0
- package/dist/retry.d.ts.map +1 -0
- package/dist/retry.js +59 -0
- package/dist/retry.js.map +1 -0
- package/dist/run-store-sqlite.d.ts +12 -0
- package/dist/run-store-sqlite.d.ts.map +1 -0
- package/dist/run-store-sqlite.js +187 -0
- package/dist/run-store-sqlite.js.map +1 -0
- package/dist/run-store-ui-http.d.ts +5 -0
- package/dist/run-store-ui-http.d.ts.map +1 -0
- package/dist/run-store-ui-http.js +19 -0
- package/dist/run-store-ui-http.js.map +1 -0
- package/dist/run-store-ui-page.d.ts +2 -0
- package/dist/run-store-ui-page.d.ts.map +1 -0
- package/dist/run-store-ui-page.js +759 -0
- package/dist/run-store-ui-page.js.map +1 -0
- package/dist/run-store-ui-protocol.d.ts +28 -0
- package/dist/run-store-ui-protocol.d.ts.map +1 -0
- package/dist/run-store-ui-protocol.js +84 -0
- package/dist/run-store-ui-protocol.js.map +1 -0
- package/dist/run-store-ui-state.d.ts +21 -0
- package/dist/run-store-ui-state.d.ts.map +1 -0
- package/dist/run-store-ui-state.js +65 -0
- package/dist/run-store-ui-state.js.map +1 -0
- package/dist/run-store-ui.d.ts +25 -0
- package/dist/run-store-ui.d.ts.map +1 -0
- package/dist/run-store-ui.js +217 -0
- package/dist/run-store-ui.js.map +1 -0
- package/dist/run-store.d.ts +109 -0
- package/dist/run-store.d.ts.map +1 -0
- package/dist/run-store.js +345 -0
- package/dist/run-store.js.map +1 -0
- package/dist/testing.d.ts +40 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +83 -0
- package/dist/testing.js.map +1 -0
- package/dist/tracing-internal.d.ts +21 -0
- package/dist/tracing-internal.d.ts.map +1 -0
- package/dist/tracing-internal.js +206 -0
- package/dist/tracing-internal.js.map +1 -0
- package/dist/tracing-json.d.ts +12 -0
- package/dist/tracing-json.d.ts.map +1 -0
- package/dist/tracing-json.js +8 -0
- package/dist/tracing-json.js.map +1 -0
- package/dist/tracing-otel.d.ts +36 -0
- package/dist/tracing-otel.d.ts.map +1 -0
- package/dist/tracing-otel.js +60 -0
- package/dist/tracing-otel.js.map +1 -0
- package/dist/tracing.d.ts +48 -0
- package/dist/tracing.d.ts.map +1 -0
- package/dist/tracing.js +2 -0
- package/dist/tracing.js.map +1 -0
- package/dist/workbench-bin.d.ts +3 -0
- package/dist/workbench-bin.d.ts.map +1 -0
- package/dist/workbench-bin.js +8 -0
- package/dist/workbench-bin.js.map +1 -0
- package/dist/workbench-graph.d.ts +3 -0
- package/dist/workbench-graph.d.ts.map +1 -0
- package/dist/workbench-graph.js +64 -0
- package/dist/workbench-graph.js.map +1 -0
- package/dist/workbench-inspect.d.ts +3 -0
- package/dist/workbench-inspect.d.ts.map +1 -0
- package/dist/workbench-inspect.js +65 -0
- package/dist/workbench-inspect.js.map +1 -0
- package/dist/workbench-plan.d.ts +3 -0
- package/dist/workbench-plan.d.ts.map +1 -0
- package/dist/workbench-plan.js +72 -0
- package/dist/workbench-plan.js.map +1 -0
- package/dist/workbench-run.d.ts +13 -0
- package/dist/workbench-run.d.ts.map +1 -0
- package/dist/workbench-run.js +115 -0
- package/dist/workbench-run.js.map +1 -0
- package/dist/workbench-shared.d.ts +67 -0
- package/dist/workbench-shared.d.ts.map +1 -0
- package/dist/workbench-shared.js +163 -0
- package/dist/workbench-shared.js.map +1 -0
- package/dist/workbench-studio.d.ts +26 -0
- package/dist/workbench-studio.d.ts.map +1 -0
- package/dist/workbench-studio.js +57 -0
- package/dist/workbench-studio.js.map +1 -0
- package/dist/workbench-ui.d.ts +3 -0
- package/dist/workbench-ui.d.ts.map +1 -0
- package/dist/workbench-ui.js +262 -0
- package/dist/workbench-ui.js.map +1 -0
- package/dist/workbench.d.ts +5 -0
- package/dist/workbench.d.ts.map +1 -0
- package/dist/workbench.js +43 -0
- package/dist/workbench.js.map +1 -0
- package/docs/README.md +31 -0
- package/docs/agent-guide.md +168 -0
- package/docs/api-reference.md +269 -0
- package/docs/api-report.json +298 -0
- package/docs/child-pipeline-composition.md +185 -0
- package/docs/cli.md +118 -0
- package/docs/concepts.md +502 -0
- package/docs/getting-started.md +162 -0
- package/docs/llms.txt +62 -0
- package/docs/recipes.md +51 -0
- package/docs/studio.md +86 -0
- package/examples/best-effort.ts +42 -0
- package/examples/cancellation-and-testing.ts +45 -0
- package/examples/child-pipeline.ts +51 -0
- package/examples/cli-job.ts +86 -0
- package/examples/conditional-step.ts +41 -0
- package/examples/fan-out-progress.ts +53 -0
- package/examples/local-observability.ts +80 -0
- package/examples/publish-with-gates.ts +56 -0
- package/examples/rendering.ts +34 -0
- package/examples/resumable-enrichment.ts +75 -0
- package/examples/tracing.ts +35 -0
- package/examples/tsconfig.json +15 -0
- package/examples/typed-import.ts +54 -0
- package/examples/validated-boundaries.ts +75 -0
- package/package.json +156 -0
package/docs/llms.txt
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# tubeless
|
|
2
|
+
|
|
3
|
+
> Dependency-free TypeScript primitives for typed, observable data pipelines and their CLI programs.
|
|
4
|
+
|
|
5
|
+
Start:
|
|
6
|
+
|
|
7
|
+
- README: ../README.md
|
|
8
|
+
- First pipeline: ./getting-started.md
|
|
9
|
+
- CLI: ./cli.md
|
|
10
|
+
- Local studio: ./studio.md
|
|
11
|
+
- Choose a pattern: ./recipes.md
|
|
12
|
+
- Execution semantics: ./concepts.md
|
|
13
|
+
|
|
14
|
+
Agent instructions:
|
|
15
|
+
|
|
16
|
+
- Canonical guide: ./agent-guide.md
|
|
17
|
+
- Maintain stable step IDs.
|
|
18
|
+
- Use optional step names only as display overrides; dependencies and selection use IDs.
|
|
19
|
+
- Use pipeline.toMermaid() to generate static graph documentation.
|
|
20
|
+
- Use tubeless inspect with a pipeline module path for a read-only inventory of identity plus the default plan.
|
|
21
|
+
- Use tubeless plan with a pipeline or marked command module to preview target, exact-step, and dry-run selection without executing or supplying domain options. Prefer the marked command when both are exported. Use command.plan() or tubeless plan; do not simulate planning with --plan.
|
|
22
|
+
- Use tubeless graph with a pipeline module path to print Mermaid without a wrapper script.
|
|
23
|
+
- Use tubeless run only with a definePipelineCommand module; put the command's validated application arguments after --.
|
|
24
|
+
- Set dryRun: "skip" on external side effects, or provide a side-effect-free dryRun handler.
|
|
25
|
+
- Use context.log, context.signal, context.sleep, and progress APIs.
|
|
26
|
+
- Use createPipelineTestRuntime from tubeless/testing for deterministic tests with captured logs, statuses, progress, clock, and cancellation.
|
|
27
|
+
- Compile generated agent submissions with bun run eval:agent --; runtime tests, provider invocation, and semantic assessment remain externally controlled.
|
|
28
|
+
- Use definePipelineCommand for pipeline-backed scripts.
|
|
29
|
+
- Omit mapOptions only when validated CLI flags already satisfy same-name pipeline options; map renamed, loaded, or derived values explicitly.
|
|
30
|
+
- Declare public goals with targets: [step] on definePipeline, then select their
|
|
31
|
+
IDs to include required inputs and failure gates. stepIds is an exact filter
|
|
32
|
+
for any step and cannot be combined with targets.
|
|
33
|
+
- Use PipelinePlanStep.selectionReasons for selection explanations; it includes
|
|
34
|
+
target IDs and immediate dependents, so consumers should not rebuild the graph.
|
|
35
|
+
- Branch on PipelineError code/phase/kind, not message text. Original thrown
|
|
36
|
+
codes use sourceCode; failed and dependency-blocked reports expose diagnostic metadata.
|
|
37
|
+
- Use requireOutputs for finalizers that need specific completed step outputs.
|
|
38
|
+
- Use createSteps(optionsSchema), outputSchema, and resultSchema for optional
|
|
39
|
+
dependency-free Standard Schema validation. run supports async schemas;
|
|
40
|
+
synchronous plan does not invoke schemas.
|
|
41
|
+
- Run make check from the package root after package changes.
|
|
42
|
+
|
|
43
|
+
Advanced:
|
|
44
|
+
|
|
45
|
+
- Child pipelines: ./child-pipeline-composition.md
|
|
46
|
+
- Public exports: ./api-reference.md
|
|
47
|
+
- Machine-readable surface: ./api-report.json
|
|
48
|
+
- Agent evaluation cases: ./agent-evaluations.md
|
|
49
|
+
|
|
50
|
+
Executable examples:
|
|
51
|
+
|
|
52
|
+
- ../examples/typed-import.ts
|
|
53
|
+
- ../examples/validated-boundaries.ts
|
|
54
|
+
- ../examples/publish-with-gates.ts
|
|
55
|
+
- ../examples/conditional-step.ts
|
|
56
|
+
- ../examples/best-effort.ts
|
|
57
|
+
- ../examples/child-pipeline.ts
|
|
58
|
+
- ../examples/fan-out-progress.ts
|
|
59
|
+
- ../examples/resumable-enrichment.ts
|
|
60
|
+
- ../examples/cli-job.ts
|
|
61
|
+
- ../examples/cancellation-and-testing.ts
|
|
62
|
+
- ../examples/tracing.ts
|
package/docs/recipes.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Recipe index
|
|
2
|
+
|
|
3
|
+
Every linked TypeScript example is compiled by the package typecheck. Start with
|
|
4
|
+
the smallest example matching the workflow rather than assembling primitives
|
|
5
|
+
from the API inventory.
|
|
6
|
+
|
|
7
|
+
| Intent | Executable recipe | Main primitives |
|
|
8
|
+
| --------------------------------------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------ |
|
|
9
|
+
| Sequential import or ETL | [`typed-import.ts`](../examples/typed-import.ts) | `createSteps`, `dependsOn`, `requireOutputs`, `targets` |
|
|
10
|
+
| Validate options, outputs, and results | [`validated-boundaries.ts`](../examples/validated-boundaries.ts) | Standard Schema, `outputSchema`, `resultSchema` |
|
|
11
|
+
| Inspect, plan, or graph a pipeline module | [`typed-import.ts`](../examples/typed-import.ts) | `tubeless inspect`, `tubeless plan`, `tubeless graph`, `toMermaid` |
|
|
12
|
+
| Safe write/publish preview | [`publish-with-gates.ts`](../examples/publish-with-gates.ts) | `dryRun`, `optionalDependsOn`, `skipAfterFailureOf` |
|
|
13
|
+
| Deliberately omit unnecessary work | [`conditional-step.ts`](../examples/conditional-step.ts) | `step.skippable`, valued skip, skip-aware output typing |
|
|
14
|
+
| Preserve independent work after failure | [`best-effort.ts`](../examples/best-effort.ts) | `continueOnError`, structured `run` result |
|
|
15
|
+
| Compose one reusable workflow | [`child-pipeline.ts`](../examples/child-pipeline.ts) | `fromPipeline`, `mapOptions`, `mapResult` |
|
|
16
|
+
| Fan out over runtime items | [`fan-out-progress.ts`](../examples/fan-out-progress.ts) | `forEachPipeline`, stable keys, concurrency, progress |
|
|
17
|
+
| Show determinate progress | [`fan-out-progress.ts`](../examples/fan-out-progress.ts) | `reportProgress`, mapped-child progress |
|
|
18
|
+
| Retry and rate-limit remote calls | [`resumable-enrichment.ts`](../examples/resumable-enrichment.ts) | `withRetry`, `RateLimiter`, injected sleep and signal |
|
|
19
|
+
| Resume durable long-running work | [`resumable-enrichment.ts`](../examples/resumable-enrichment.ts) | `dryRun`, `openCheckpoint`, `withCheckpointedBatch` |
|
|
20
|
+
| Expose and run a typed command-line program | [`cli-job.ts`](../examples/cli-job.ts) | `definePipelineCommand`, conditional `mapOptions`, `tubeless run` |
|
|
21
|
+
| Render plans and diagnostics | [`rendering.ts`](../examples/rendering.ts) | `renderPipelinePlan`, `renderPipelineError` |
|
|
22
|
+
| Handle cancellation and deterministic testing | [`cancellation-and-testing.ts`](../examples/cancellation-and-testing.ts) | `createPipelineTestRuntime`, captured status/progress |
|
|
23
|
+
| Export JSON or OpenTelemetry lifecycle events | [`tracing.ts`](../examples/tracing.ts) | trace context, attempt events, JSON exporter |
|
|
24
|
+
| Persist, inspect, and explicitly launch runs | [`local-observability.ts`](../examples/local-observability.ts) | SQLite store, `createPipelineRunProjector`, `definePipelineStudio` |
|
|
25
|
+
|
|
26
|
+
## Selection rules
|
|
27
|
+
|
|
28
|
+
1. Use an ordinary step for one unit of domain work.
|
|
29
|
+
2. Set `dryRun: "skip"` or provide a side-effect-free `dryRun` handler before
|
|
30
|
+
exposing side-effecting work through CLI dry-run support.
|
|
31
|
+
3. Use `step.skippable` only for a successful policy decision, never to hide an error.
|
|
32
|
+
4. Use a child pipeline when the child has value independently; use a normal
|
|
33
|
+
helper function when it does not.
|
|
34
|
+
5. Use `forEachPipeline` when every item needs child-pipeline lifecycle and
|
|
35
|
+
reporting. Use `runConcurrent` for lightweight worker functions.
|
|
36
|
+
6. Use `definePipelineCommand` for pipeline scripts; use `defineCommand` only
|
|
37
|
+
when the script is not centered on a pipeline. Preview selection with
|
|
38
|
+
`command.plan()` or `tubeless plan`; do not simulate planning with `--plan`.
|
|
39
|
+
7. Declare public goals with `targets: [step]` on the pipeline, select their IDs
|
|
40
|
+
for goal-oriented execution, and use `stepIds` only for an exact filter. Use
|
|
41
|
+
`requireOutputs` when the final domain result is not meaningful without
|
|
42
|
+
specific step outputs. Read plan `selectionReasons` instead of recreating
|
|
43
|
+
target-closure logic in a CLI or application.
|
|
44
|
+
8. Use `tubeless/render` when plans or diagnostics cross a human or JSON
|
|
45
|
+
presentation boundary; do not duplicate selection-reason or error formatting.
|
|
46
|
+
Use `tubeless/reporter` for optional TTY run reporters; do not import them
|
|
47
|
+
from `tubeless`.
|
|
48
|
+
|
|
49
|
+
For semantics behind these choices, read [core concepts](./concepts.md).
|
|
50
|
+
For workbench commands, read [the CLI](./cli.md). For the local run UI, read
|
|
51
|
+
[the studio](./studio.md).
|
package/docs/studio.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Local studio
|
|
2
|
+
|
|
3
|
+
Normal pipeline and CLI execution is storage-free. The studio is an optional
|
|
4
|
+
local projection of an append-only SQLite event store. Core does not import it.
|
|
5
|
+
|
|
6
|
+
Record one workbench run by placing `--store` before the command file:
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
bunx tubeless run --store .tubeless/runs.sqlite ./scripts/import.ts -- --source rows.txt
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Open the studio only when you want a browser view:
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
bunx tubeless ui --store .tubeless/runs.sqlite
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
That form is read-only. It does not guess executable modules from recorded
|
|
19
|
+
definitions. Register marked `definePipelineCommand` exports to make a
|
|
20
|
+
**Run pipeline** action available:
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
bunx tubeless ui \
|
|
24
|
+
--store .tubeless/runs.sqlite \
|
|
25
|
+
--command ./scripts/import.ts \
|
|
26
|
+
--command ./scripts/publish.ts
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`--export` selects the export when you register exactly one `--command`. Launch
|
|
30
|
+
forms render each command's structured parameter contract: booleans as
|
|
31
|
+
checkboxes, constrained strings as selects, numbers with their bounds, and
|
|
32
|
+
paths or unconstrained values as text fields. Submitted values go through the
|
|
33
|
+
normal typed command parser without a shell. **Preview plan** uses the dry-run
|
|
34
|
+
and step/target values from the same form; it never creates a run.
|
|
35
|
+
|
|
36
|
+
Default bind address is `127.0.0.1`, default port is `4317`, and the default
|
|
37
|
+
store is `.tubeless/runs.sqlite`. Browser-triggered execution requires a
|
|
38
|
+
loopback host (`127.0.0.1`, `::1`, or `localhost`). Non-loopback binding is
|
|
39
|
+
rejected when any command is registered. A non-loopback `--host` without
|
|
40
|
+
commands is allowed and stays read-only: the store is visible to anyone who
|
|
41
|
+
can reach the port, and clear-history is not wired. That bind is a risk you
|
|
42
|
+
enable; see [SECURITY.md](../SECURITY.md).
|
|
43
|
+
|
|
44
|
+
## Checked-in catalog
|
|
45
|
+
|
|
46
|
+
Declare module references once for a repeatable project catalog:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
// tubeless.studio.ts
|
|
50
|
+
import { definePipelineStudio } from "tubeless/workbench/studio";
|
|
51
|
+
|
|
52
|
+
export default definePipelineStudio({
|
|
53
|
+
cwd: ".",
|
|
54
|
+
commands: [
|
|
55
|
+
{ file: "./scripts/import.ts", export: "ImportCommand", name: "Import rows" },
|
|
56
|
+
{ file: "./scripts/publish.ts", export: "PublishCommand" },
|
|
57
|
+
],
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
bunx tubeless ui --store .tubeless/runs.sqlite ./tubeless.studio.ts
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Command paths and `cwd` are relative to the manifest file. Empty or duplicate
|
|
66
|
+
registrations fail before the studio starts. Presentation-name overrides do not
|
|
67
|
+
change run or pipeline identity.
|
|
68
|
+
|
|
69
|
+
## What the UI shows
|
|
70
|
+
|
|
71
|
+
The studio combines active and historical runs in one running-first view. Child
|
|
72
|
+
pipeline executions stay beneath their top-level run. Run details include step
|
|
73
|
+
attempts, progress, logs, and structured errors. Nested pipeline steps are
|
|
74
|
+
labeled with the child pipeline and its declared steps; runtime fan-out is
|
|
75
|
+
identified without guessing its item count.
|
|
76
|
+
|
|
77
|
+
Browser plan, launch, and clear-history requests send
|
|
78
|
+
`x-tubeless-studio-plan`, `x-tubeless-studio-launch`, and
|
|
79
|
+
`x-tubeless-studio-clear-history`. Those requests also require a `Host` header
|
|
80
|
+
that matches the bound authority, and plan/launch require `application/json`.
|
|
81
|
+
The header names are part of the local studio protocol. They are same-origin
|
|
82
|
+
guards, not authentication.
|
|
83
|
+
|
|
84
|
+
Programmatic callers can compose the same pieces from
|
|
85
|
+
`tubeless/run-store/sqlite` and `tubeless/run-store/ui`. See
|
|
86
|
+
[`local-observability.ts`](../examples/local-observability.ts).
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { createSteps, definePipeline } from "tubeless";
|
|
2
|
+
|
|
3
|
+
interface AuditOptions {
|
|
4
|
+
failLinks?: boolean;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const step = createSteps<AuditOptions>();
|
|
8
|
+
|
|
9
|
+
const checkLinks = step("check-links", {
|
|
10
|
+
description: "Validate links independently",
|
|
11
|
+
run: (_inputs, context) => {
|
|
12
|
+
if (context.options.failLinks) throw new Error("broken links found");
|
|
13
|
+
return { checked: 12 };
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const checkMetadata = step("check-metadata", {
|
|
18
|
+
description: "Validate metadata independently",
|
|
19
|
+
run: () => ({ checked: 8 }),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export const BestEffortAuditPipeline = definePipeline({
|
|
23
|
+
id: "best-effort-audit",
|
|
24
|
+
steps: [checkLinks, checkMetadata],
|
|
25
|
+
finalize: (outputs) => ({
|
|
26
|
+
links: outputs["check-links"],
|
|
27
|
+
metadata: outputs["check-metadata"],
|
|
28
|
+
}),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export async function runBestEffortExample() {
|
|
32
|
+
const result = await BestEffortAuditPipeline.run({
|
|
33
|
+
failLinks: true,
|
|
34
|
+
continueOnError: true,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
errors: result.errors,
|
|
39
|
+
metadataStillCompleted: result.value?.metadata?.checked === 8,
|
|
40
|
+
reports: result.steps,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { createSteps, definePipeline } from "tubeless";
|
|
2
|
+
import { createPipelineTestRuntime } from "tubeless/testing";
|
|
3
|
+
|
|
4
|
+
interface PollOptions {
|
|
5
|
+
attempts: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const step = createSteps<PollOptions>();
|
|
9
|
+
|
|
10
|
+
const poll = step("poll", {
|
|
11
|
+
description: "Poll cooperatively with an injected, cancellation-aware sleep",
|
|
12
|
+
run: async (_inputs, context) => {
|
|
13
|
+
for (let attempt = 1; attempt <= context.options.attempts; attempt += 1) {
|
|
14
|
+
await context.sleep(100, context.signal);
|
|
15
|
+
context.reportProgress({ completed: attempt, total: context.options.attempts });
|
|
16
|
+
}
|
|
17
|
+
return context.options.attempts;
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export const PollPipeline = definePipeline({
|
|
22
|
+
id: "poll",
|
|
23
|
+
steps: [poll],
|
|
24
|
+
finalize: (outputs) => outputs.poll ?? 0,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export async function runWithTestRuntime() {
|
|
28
|
+
const test = createPipelineTestRuntime({ cwd: "/workspace" });
|
|
29
|
+
const value: number = await test.runOrThrow(PollPipeline, { attempts: 3 });
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
elapsedMs: test.clock.now(),
|
|
33
|
+
latestProgress: test.latestProgress.get("poll"),
|
|
34
|
+
logs: test.logs,
|
|
35
|
+
statuses: test.statuses,
|
|
36
|
+
value,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function runCancelledTest() {
|
|
41
|
+
const test = createPipelineTestRuntime();
|
|
42
|
+
test.abort("cancel the test run");
|
|
43
|
+
|
|
44
|
+
return test.run(PollPipeline, { attempts: 3 });
|
|
45
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { createSteps, definePipeline } from "tubeless";
|
|
2
|
+
|
|
3
|
+
interface NormalizeOptions {
|
|
4
|
+
rows: readonly string[];
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const normalizeStep = createSteps<NormalizeOptions>();
|
|
8
|
+
|
|
9
|
+
const normalizeRows = normalizeStep("normalize-rows", {
|
|
10
|
+
run: (_inputs, context) =>
|
|
11
|
+
context.options.rows.map((row) => row.trim().toLowerCase()).filter((row) => row.length > 0),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export const NormalizePipeline = definePipeline({
|
|
15
|
+
id: "normalize",
|
|
16
|
+
steps: [normalizeRows],
|
|
17
|
+
targets: [normalizeRows],
|
|
18
|
+
finalize: (outputs) => outputs["normalize-rows"] ?? [],
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
interface ImportOptions {
|
|
22
|
+
lines: readonly string[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const importStep = createSteps<ImportOptions>();
|
|
26
|
+
|
|
27
|
+
const normalizedImport = importStep.fromPipeline("normalized-import", {
|
|
28
|
+
pipeline: NormalizePipeline,
|
|
29
|
+
mapOptions: (_inputs, context) => ({ rows: context.options.lines }),
|
|
30
|
+
mapResult: (rows) => ({ count: rows.length, rows }),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
export const ChildPipelineImport = definePipeline({
|
|
34
|
+
id: "child-pipeline-import",
|
|
35
|
+
steps: [normalizedImport],
|
|
36
|
+
finalize: (outputs) => outputs["normalized-import"],
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
export async function runChildPipelineExample() {
|
|
40
|
+
return ChildPipelineImport.runOrThrow({
|
|
41
|
+
lines: [" Alpha ", "", "Beta"],
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (false) {
|
|
46
|
+
importStep.fromPipeline("invalid-child-selection", {
|
|
47
|
+
pipeline: NormalizePipeline,
|
|
48
|
+
// @ts-expect-error Child run options are checked against its declared target IDs.
|
|
49
|
+
mapOptions: () => ({ rows: [], targets: ["normalise-rows"] }),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import { createSteps, definePipeline, requireOutputs } from "tubeless";
|
|
3
|
+
import { definePipelineCommand } from "tubeless/cli";
|
|
4
|
+
|
|
5
|
+
interface ImportOptions {
|
|
6
|
+
lines: readonly string[];
|
|
7
|
+
limit?: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const step = createSteps<ImportOptions>();
|
|
11
|
+
|
|
12
|
+
const loadRows = step("load-rows", {
|
|
13
|
+
description: "Read raw input records from the caller.",
|
|
14
|
+
run: (_inputs, context) => context.options.lines,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const normalizeRows = step("normalize-rows", {
|
|
18
|
+
dependsOn: [loadRows],
|
|
19
|
+
description: "Trim, lowercase, drop blanks, and apply the optional --limit.",
|
|
20
|
+
run: ({ "load-rows": rows }, context) => {
|
|
21
|
+
const normalized = rows.map((row) => row.trim().toLowerCase()).filter((row) => row.length > 0);
|
|
22
|
+
return context.options.limit === undefined
|
|
23
|
+
? normalized
|
|
24
|
+
: normalized.slice(0, context.options.limit);
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export const ImportPipeline = definePipeline({
|
|
29
|
+
id: "import",
|
|
30
|
+
steps: [loadRows, normalizeRows],
|
|
31
|
+
targets: [normalizeRows],
|
|
32
|
+
finalize: requireOutputs([normalizeRows], (outputs) => ({
|
|
33
|
+
count: outputs["normalize-rows"].length,
|
|
34
|
+
rows: outputs["normalize-rows"],
|
|
35
|
+
})),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The kind of entry point a `scripts/*.ts` file would export: flags become a typed,
|
|
40
|
+
* validated `args` object instead of hand-parsed `process.argv`. `--source`'s `path` type
|
|
41
|
+
* resolves relative to `context.cwd` and confirms the file exists before `run` sees it.
|
|
42
|
+
* Pipeline commands also provide exact `--step`, dependency-aware `--target`
|
|
43
|
+
* for the pipeline's declared goals, and `--continue-on-error`, while
|
|
44
|
+
* forwarding the universal `--dry-run` flag into the pipeline automatically.
|
|
45
|
+
* Plan with `command.plan()` or `tubeless plan`, not `--plan`.
|
|
46
|
+
*/
|
|
47
|
+
export const ImportCommand = definePipelineCommand(ImportPipeline, {
|
|
48
|
+
description: "Normalize a newline-delimited list of input rows.",
|
|
49
|
+
params: {
|
|
50
|
+
source: {
|
|
51
|
+
type: "path",
|
|
52
|
+
description: "File with one row per line.",
|
|
53
|
+
mustExist: true,
|
|
54
|
+
kind: "file",
|
|
55
|
+
},
|
|
56
|
+
limit: {
|
|
57
|
+
type: "number",
|
|
58
|
+
optional: true,
|
|
59
|
+
integer: true,
|
|
60
|
+
min: 1,
|
|
61
|
+
description: "Only keep the first N normalized rows.",
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
mapOptions: (args) => ({
|
|
65
|
+
lines: fs.readFileSync(args.source, "utf8").split("\n"),
|
|
66
|
+
limit: args.limit,
|
|
67
|
+
}),
|
|
68
|
+
summarize: (result) => [`Normalized ${result.count} row(s).`],
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
if (false) {
|
|
72
|
+
const missingLinesParams = { source: { type: "path" } } as const;
|
|
73
|
+
// @ts-expect-error --source does not supply ImportPipeline's required lines option.
|
|
74
|
+
definePipelineCommand(ImportPipeline, { params: missingLinesParams });
|
|
75
|
+
|
|
76
|
+
const renamedOptionalParams = {
|
|
77
|
+
lines: { type: "string", multiple: true },
|
|
78
|
+
max: { type: "number", optional: true },
|
|
79
|
+
} as const;
|
|
80
|
+
// @ts-expect-error --max is not a same-name option on ImportPipeline.
|
|
81
|
+
definePipelineCommand(ImportPipeline, { params: renamedOptionalParams });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (import.meta.main) {
|
|
85
|
+
void ImportCommand.main();
|
|
86
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { createSteps, definePipeline } from "tubeless";
|
|
2
|
+
|
|
3
|
+
interface CacheOptions {
|
|
4
|
+
cachedValue?: string;
|
|
5
|
+
refresh: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const step = createSteps<CacheOptions>();
|
|
9
|
+
|
|
10
|
+
const resolveValue = step.skippable("resolve-value", {
|
|
11
|
+
description: "Reuse a cached value unless a refresh is required",
|
|
12
|
+
skip: (_inputs, context) =>
|
|
13
|
+
!context.options.refresh && context.options.cachedValue !== undefined
|
|
14
|
+
? { reason: "cached value is current", value: context.options.cachedValue }
|
|
15
|
+
: false,
|
|
16
|
+
run: async () => "fresh-value",
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const formatValue = step("format-value", {
|
|
20
|
+
dependsOn: [resolveValue],
|
|
21
|
+
description: "Format the resolved value for the caller",
|
|
22
|
+
run: ({ "resolve-value": value }) => {
|
|
23
|
+
// A skippable step is always typed as T | undefined, even when this
|
|
24
|
+
// policy skip publishes a value. Keep absence handling explicit.
|
|
25
|
+
if (value === undefined) throw new Error("resolve-value produced no value");
|
|
26
|
+
return value.toUpperCase();
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export const ConditionalCachePipeline = definePipeline({
|
|
31
|
+
id: "conditional-cache",
|
|
32
|
+
steps: [resolveValue, formatValue],
|
|
33
|
+
finalize: (outputs) => outputs["format-value"] ?? "",
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export async function runConditionalCacheExample() {
|
|
37
|
+
return ConditionalCachePipeline.runOrThrow({
|
|
38
|
+
cachedValue: "cached-value",
|
|
39
|
+
refresh: false,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { createSteps, definePipeline } from "tubeless";
|
|
2
|
+
|
|
3
|
+
interface ShardOptions {
|
|
4
|
+
records: readonly string[];
|
|
5
|
+
shardId: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const shardStep = createSteps<ShardOptions>();
|
|
9
|
+
|
|
10
|
+
const processRecords = shardStep("process-records", {
|
|
11
|
+
description: "Process every record in one shard",
|
|
12
|
+
run: async (_inputs, context) => {
|
|
13
|
+
const processed: string[] = [];
|
|
14
|
+
for (const [index, record] of context.options.records.entries()) {
|
|
15
|
+
processed.push(record.toUpperCase());
|
|
16
|
+
context.reportProgress({
|
|
17
|
+
completed: index + 1,
|
|
18
|
+
message: record,
|
|
19
|
+
total: context.options.records.length,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
return { processed, shardId: context.options.shardId };
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export const ShardPipeline = definePipeline({
|
|
27
|
+
id: "process-shard",
|
|
28
|
+
steps: [processRecords],
|
|
29
|
+
finalize: (outputs) => outputs["process-records"],
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
interface FanOutOptions {
|
|
33
|
+
concurrency: number;
|
|
34
|
+
shards: readonly { id: string; records: readonly string[] }[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const fanOutStep = createSteps<FanOutOptions>();
|
|
38
|
+
|
|
39
|
+
const processShards = fanOutStep.forEachPipeline("process-shards", {
|
|
40
|
+
pipeline: ShardPipeline,
|
|
41
|
+
description: "Process shards with bounded concurrency and stable identities",
|
|
42
|
+
items: (_inputs, context) => context.options.shards,
|
|
43
|
+
key: (shard) => shard.id,
|
|
44
|
+
concurrency: (_inputs, context) => context.options.concurrency,
|
|
45
|
+
progress: { itemNoun: "shards" },
|
|
46
|
+
mapOptions: (shard) => ({ records: shard.records, shardId: shard.id }),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
export const FanOutPipeline = definePipeline({
|
|
50
|
+
id: "fan-out",
|
|
51
|
+
steps: [processShards],
|
|
52
|
+
finalize: (outputs) => outputs["process-shards"] ?? [],
|
|
53
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { createSteps, defaultPipelineContext, definePipeline, requireOutputs } from "tubeless";
|
|
2
|
+
import { createPipelineRunProjector, type StoredPipelineEvent } from "tubeless/run-store";
|
|
3
|
+
import { openSqlitePipelineRunStore } from "tubeless/run-store/sqlite";
|
|
4
|
+
import { definePipelineStudio } from "tubeless/workbench/studio";
|
|
5
|
+
import {
|
|
6
|
+
startPipelineRunStudio,
|
|
7
|
+
type PipelineRunStudioLauncher,
|
|
8
|
+
type PipelineRunStudioServer,
|
|
9
|
+
} from "tubeless/run-store/ui";
|
|
10
|
+
|
|
11
|
+
const step = createSteps<{ rows: readonly string[] }>();
|
|
12
|
+
const normalize = step("normalize", {
|
|
13
|
+
description: "Normalize imported rows for publication.",
|
|
14
|
+
run: (_inputs, context) => {
|
|
15
|
+
context.log.log("normalizing", context.options.rows.length, "rows");
|
|
16
|
+
return context.options.rows.map((row) => row.trim().toLowerCase());
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export const LocallyObservedPipeline = definePipeline({
|
|
21
|
+
id: "locally-observed-import",
|
|
22
|
+
steps: [normalize],
|
|
23
|
+
targets: [normalize],
|
|
24
|
+
finalize: requireOutputs([normalize], ({ normalize }) => normalize),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
/** A checked-in catalog can register many command modules in one place. */
|
|
28
|
+
export const LocalStudioConfig = definePipelineStudio({
|
|
29
|
+
commands: [{ file: "./cli-job.ts", export: "ImportCommand", name: "Import rows" }],
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Fold paged store events without walking the full history on every refresh.
|
|
34
|
+
* Append only newer pages. Duplicate and out-of-order ids are ignored; `0` is a
|
|
35
|
+
* valid first id. `projectPipelineRunStore` remains the one-shot wrapper.
|
|
36
|
+
*/
|
|
37
|
+
export function snapshotFromPages(pages: readonly (readonly StoredPipelineEvent[])[]) {
|
|
38
|
+
const projector = createPipelineRunProjector();
|
|
39
|
+
for (const page of pages) projector.append(page);
|
|
40
|
+
return projector.snapshot();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Recording is opt-in; ordinary calls to the pipeline remain storage-free. */
|
|
44
|
+
export async function runWithLocalHistory(
|
|
45
|
+
rows: readonly string[],
|
|
46
|
+
filename = ".tubeless/runs.sqlite"
|
|
47
|
+
): Promise<readonly string[]> {
|
|
48
|
+
const store = await openSqlitePipelineRunStore(filename);
|
|
49
|
+
try {
|
|
50
|
+
return await LocallyObservedPipeline.runOrThrow(
|
|
51
|
+
{ rows },
|
|
52
|
+
{
|
|
53
|
+
...defaultPipelineContext(),
|
|
54
|
+
tracing: { exporter: store },
|
|
55
|
+
}
|
|
56
|
+
);
|
|
57
|
+
} finally {
|
|
58
|
+
await store.close();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The UI is another optional reader of the same append-only store. */
|
|
63
|
+
export async function serveLocalStudio(
|
|
64
|
+
filename = ".tubeless/runs.sqlite",
|
|
65
|
+
launcher?: PipelineRunStudioLauncher
|
|
66
|
+
): Promise<PipelineRunStudioServer> {
|
|
67
|
+
const store = await openSqlitePipelineRunStore(filename);
|
|
68
|
+
const server = await startPipelineRunStudio({
|
|
69
|
+
history: { clear: () => store.clearHistory() },
|
|
70
|
+
launcher,
|
|
71
|
+
store,
|
|
72
|
+
});
|
|
73
|
+
return {
|
|
74
|
+
...server,
|
|
75
|
+
close: async () => {
|
|
76
|
+
await server.close();
|
|
77
|
+
await store.close();
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { createSteps, definePipeline } from "tubeless";
|
|
2
|
+
|
|
3
|
+
interface PublishOptions {
|
|
4
|
+
source: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
interface Artifact {
|
|
8
|
+
body: string;
|
|
9
|
+
size: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const step = createSteps<PublishOptions>();
|
|
13
|
+
|
|
14
|
+
const buildArtifact = step("build-artifact", {
|
|
15
|
+
description: "Build an artifact that can be validated and published.",
|
|
16
|
+
run: (_inputs, context): Artifact => ({
|
|
17
|
+
body: context.options.source.trim(),
|
|
18
|
+
size: context.options.source.trim().length,
|
|
19
|
+
}),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const validateArtifact = step("validate-artifact", {
|
|
23
|
+
dependsOn: [buildArtifact],
|
|
24
|
+
description: "Fail before publishing if the artifact is empty.",
|
|
25
|
+
run: ({ "build-artifact": artifact }) => {
|
|
26
|
+
if (artifact.size === 0) {
|
|
27
|
+
throw new Error("artifact is empty");
|
|
28
|
+
}
|
|
29
|
+
return { valid: true };
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const publishArtifact = step("publish-artifact", {
|
|
34
|
+
dependsOn: [buildArtifact],
|
|
35
|
+
optionalDependsOn: [validateArtifact],
|
|
36
|
+
skipAfterFailureOf: [validateArtifact],
|
|
37
|
+
description: "Publish only on real runs, and only after validation has not failed.",
|
|
38
|
+
dryRun: "skip",
|
|
39
|
+
run: ({ "build-artifact": artifact }) => ({
|
|
40
|
+
publishedId: `artifact-${artifact.size}`,
|
|
41
|
+
}),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
export const PublishPipeline = definePipeline({
|
|
45
|
+
id: "publish",
|
|
46
|
+
steps: [buildArtifact, validateArtifact, publishArtifact],
|
|
47
|
+
finalize: (outputs) => ({
|
|
48
|
+
artifact: outputs["build-artifact"],
|
|
49
|
+
publishedId: outputs["publish-artifact"]?.publishedId,
|
|
50
|
+
valid: outputs["validate-artifact"]?.valid === true,
|
|
51
|
+
}),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
export async function runPublishDryRunExample() {
|
|
55
|
+
return PublishPipeline.runOrThrow({ source: "release notes", dryRun: true });
|
|
56
|
+
}
|