Files
windmill/docs/pipeline-local-dev.md
T
Diego Imbert 7fb8a2e390 fix(parsers): keep s3 asset path suffix verbatim to preserve storage distinction (#10241)
* fix(parsers): keep s3 asset path suffix verbatim to preserve storage distinction

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01An2pTqSmqJd2XwnagvX4kM

* package json

* fix(pipelines): preserve named storage in generated TS/Python S3 URIs

The TS/Python templates emitted `s3:///${s3Key(path)}`, stripping the
leading slash and pinning the URI to default storage. For a named-storage
asset path (`secondary/key`) that produced `s3:///secondary/key`, which
resolves to the default storage with key `secondary/key`, dropping the
named-storage dependency and reading/writing the wrong object.

Emit the path verbatim after `s3://` (matching the DuckDB template) so a
named-storage input/output keeps its storage; identical to the previous
output for default-storage paths. Removes the now-unused `s3Key` helper.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(cli): align bun.lock parser versions with frontend

The PR bumped windmill-parser-wasm-asset (1.749.0→1.753.0) and
windmill-parser-wasm-regex (1.692.0→1.764.0) in package.json and the npm
package-lock.json for both cli and frontend, but cli/bun.lock was left
pinned to the old versions. Sync it so the CLI's wasm asset parser (used
by localGraph inference) matches the frontend and deploy-time parser.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2026-07-21 16:25:06 +00:00

167 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Local development for Data Pipelines
Status: **draft PR, validated end-to-end.** Both the headless CLI paths and the live browser
preview (`pipeline dev``/pipeline_dev`) have been exercised against a running EE + MinIO
stack — headless `pipeline run --local`, browser `Run` / `Run + downstream` cascades writing
real assets, live-reload on save, failure/cascade UX, parameterized run-forms, and the
`--frontend` flag. This document captures the design and the remaining follow-ups.
## What & why
Windmill "data pipelines" are folders of scripts marked with a `// pipeline` comment, wired
together by asset annotations (`// on <asset-uri>`, `// partitioned`, `// schedule`). Until now
they could only be built/run in the browser at `/pipeline/<folder>` (the `PipelineGraphEditor`),
and the CLI only inspected/ran the **deployed** workspace (`wmill pipeline list|show|run`).
This adds the **local edit → preview → run** loop, the pipeline analog of `wmill dev`
(flows/scripts) and `wmill app dev` (raw apps), usable both from a code editor and from an
agentic loop — building a pipeline from working-tree files, seeing the same graph the UI shows,
and running it, all **without deploying**.
## The key design decision (no backend changes)
Full body inference is obtained in the CLI from the **same wasm the frontend uses**:
`windmill-parser-wasm-asset` (`parse_assets_ts | parse_assets_py | parse_assets_sql`). That wasm
returns the entire serialized Rust `ParseAssetsOutput``assets` (with `r`/`w`/`rw` access)
**and** the parsed pipeline annotations (`in_pipeline`, `triggers`, `partition`, …) in one call.
The CLI already loads sibling wasm parsers via `loadParser()`; we just added the `-asset` dep.
Running local content reuses the existing preview API:
`runScriptPreview({ content, language, path, args: { _wmill_skip_asset_dispatch: true }, temp_script_refs })`
per node in topological order. Data flows through real asset storage; `_wmill_skip_asset_dispatch`
makes the client own the whole cascade so the backend dispatcher never double-fires.
⇒ No new backend endpoint, no Rust changes, no TS re-port of the annotation parser.
## Surfaces
### Headless CLI (agentic loop) — `cli/src/commands/pipeline/`
- `pipeline show <folder> --local` — render the DAG from working-tree files (fully offline).
- `pipeline run <folder> --local [--from/--to/--dry-run/--json]` — run the cascade via preview of
local content, reusing the `boundedCascade.ts` topo/lineage engine. Scripts whose only trigger
needs caller input or per-event fanout (`data_upload`/`webhook`/kafka/…) are skipped by default.
- `pipeline run … --upload <script>[:<param>]=<local-file|s3://storage/key>` — bind an object to a
`data_upload`/`webhook` entry point so it (and its downstream) runs: a local path is uploaded to
the workspace store; an `s3://<storage>/<key>` source binds an existing object (authority = named
storage, `s3:///key` for the default store). The target S3Object arg is inferred when the script
declares exactly one (else name it with `:<param>`). Repeatable.
- `pipeline docs <folder> [--local]` — write `PIPELINE.md` + `AGENTS.md`/`CLAUDE.md` pointers
(graph + datatable schemas) so an editor/agent has the same context the UI surfaces.
### Browser live-preview — `pipeline dev` + `/pipeline_dev`
- `pipeline dev [folder]` — folder arg or cwd auto-detect; watches the folder, rebuilds the graph
on each save, pushes `{type:'pipeline', folder, graph, scripts, temp_script_refs}` over a
WebSocket (direct mode, default port 3201), opens the dev page.
- `/pipeline_dev` route → `PipelineDevView.svelte` renders the **same** `PipelineGraphEditor`
(mode `view`) fed by the pushed graph; runs the cascade via preview. Editing stays in the
user's editor; each save live-reloads.
## Files
**New**
- `cli/src/commands/pipeline/localGraph.ts` — the enabler. Walks `f/<folder>` scripts, wasm-infers
each (`parse_assets_<lang>`), assembles the `/assets/graph`-shaped payload (`runnables`,
`assets`, `edges`, `triggers`). Exports `buildLocalPipelineGraph`, `collectScripts`,
`inferScriptAssets`, `workspaceRoot`, and the canonical graph types.
- `cli/src/commands/pipeline/docs.ts``pipeline docs`.
- `cli/src/commands/pipeline/dev.ts``pipeline dev` watcher + WS server.
- `cli/test/pipeline_local_graph_unit.test.ts` — unit tests for the builder.
- `frontend/src/lib/components/assets/AssetGraph/cascadeRun.ts` — reusable run primitives
(`makeLaunch`, `makeWaitJobTerminal`, `runDownstreamCascade`, `runBoundedCascade`) wrapping
`cascadeOrchestrator` + `graphTraversal`.
- `frontend/src/lib/components/assets/AssetGraph/PipelineDevView.svelte` + `frontend/src/routes/pipeline_dev/+page.svelte`.
**Modified**
- `cli/src/commands/pipeline/pipeline.ts``--local` on `show`/`run`; `show` split into graph
acquisition + `renderGraph()` + deployed-only `enrichRootMarkers()`; registers `docs`/`dev`;
graph types moved to `localGraph.ts`.
- `cli/package.json` (+ `windmill-parser-wasm-asset`), `cli/package-lock.json`.
- `system_prompts/auto-generated/*`, `cli/src/guidance/skills.gen.ts` — regenerated via
`python system_prompts/generate.py` (required by AGENTS.md for CLI command changes).
## Language coverage
`inferScriptAssets` mirrors `frontend/src/lib/infer.ts:inferAssets`: ts (bun/deno/nativets),
python3, and SQL dialects all get wasm inference (SQL dialects route to `parse_assets_sql`; its
comment-header annotation scan is dialect-independent). `go`/`bash` have no wasm asset parser, so
they fall back to a minimal `// pipeline` + `// on` scan (annotation-only). Inferred asset paths
must match **exactly** to connect nodes. An S3 asset path is `<storage>/<key>` with an empty
storage segment for the workspace default: `parse_asset_syntax` (shared by the native and wasm
parsers) keeps the URI suffix verbatim, so the SDK object forms — TS `writeS3File({s3:"x"})` and
python `write_s3_file(S3Object(s3="x"))`, which resolve to `s3:///x` — the triple-slash annotation
`// on s3:///x`, and DuckDB `read_csv('s3:///x')` / `COPY ... TO 's3:///x'` all yield path `/x`
(leading slash significant). The named-storage form `s3://secondary/key` yields `secondary/key`
a **different** object and a different node, so the storage distinction is never collapsed. In
practice: use the triple-slash form everywhere for default-storage objects and the producer and
consumer connect regardless of language; mixing in the no-slash form (`s3://x`) names a storage
called `x` and will not connect to a default-storage write.
## How to test
Run the CLI from source (`alias wmilld='bun run /home/rfiszel/windmill/cli/src/main.ts'`).
Headless (works directly against any remote, e.g. internal.windmill.dev / `data-pipelines`):
```
wmilld workspace add internal data-pipelines https://internal.windmill.dev/ # paste a token
# build an example (connected all-DuckDB DAG):
mkdir -p ~/pl-demo/f/demo_pipeline && cd ~/pl-demo && printf 'defaultTs: bun\n' > wmill.yaml
# ingest.duckdb.sql: -- pipeline\nCOPY (SELECT 1 id,'a' name) TO 's3://demo/raw.csv';
# transform.duckdb.sql: -- pipeline\n-- on s3://demo/raw.csv\nCOPY (SELECT * FROM read_csv('s3://demo/raw.csv')) TO 's3://demo/clean.csv';
# report.duckdb.sql: -- pipeline\n-- on s3://demo/clean.csv\nSELECT count(*) FROM read_csv('s3://demo/clean.csv');
wmilld pipeline show demo_pipeline --local
wmilld pipeline run demo_pipeline --local --dry-run
wmilld pipeline run demo_pipeline --local # writes real s3 assets; needs object storage
wmilld pipeline docs demo_pipeline --local # --local: document the working tree (default path queries the deployed graph)
```
Or pull real pipelines: `wmilld sync pull --yes && wmilld pipeline list && wmilld pipeline show <folder> --local`.
Browser preview: against a remote whose deployed frontend predates the `/pipeline_dev` route, the
auto-opened `<remote>/pipeline_dev?…` 404s. Run THIS branch's frontend locally and point the dev
page at it with `--frontend`:
```
cd frontend && REMOTE=https://internal.windmill.dev npm run dev # or a local backend
cd ~/pl-demo && wmilld pipeline dev demo_pipeline --frontend http://localhost:3000
```
`pipeline dev` then opens (and prints) the full URL — **copy that printed URL**, don't hand-write it.
It carries both `wm_token` (workspace auth) and `ws_token` (the per-session WS token); the dev
server rejects the WebSocket without `ws_token`, so the page would sit disconnected:
```
http://localhost:3000/pipeline_dev?workspace=<WS>&wm_token=<TOK>&folder=demo_pipeline&port=3201&ws_token=<WS_TOK>
```
## Validation status
-`pipeline show --local` verified offline against a fixture (renders a connected DAG).
-`localGraph` unit tests (7) + the CLI suite pass; CLI `tsc` clean; frontend `npm run check`
0 errors on changed files; svelte-autofixer clean.
- ✅ CLI agent docs regenerated.
- ✅ Live `pipeline dev``/pipeline_dev` browser preview **exercised against a running EE + MinIO
stack**: graph render + live-reload on save, single-node `Run` and `Run + downstream` cascades
writing real assets, parameterized run-forms, failure/cascade UX, responsive layout, and the
`--frontend` flag. Screenshots in the PR body.
## TODO for the takeover agent
(Items 12 below are **done**; left here for context.)
1. ~~Verify the browser preview end-to-end~~ — done (see Validation status; screenshots in PR).
2. ~~Dev-page route reachability on remotes~~ — done: added `--frontend <origin>` so `pipeline dev`
can open the page on a locally-run frontend while the API/token target the remote.
3. **Route-page dedup**: optionally refactor `frontend/src/routes/(root)/(logged)/pipeline/[folder]/+page.svelte`
to consume `cascadeRun.ts` (its `launchCascadeScript`/`runDraftAwareCascade`/`waitJobTerminal`
are the source of the extraction) — eliminates duplication. Keep behavior identical.
4. **Proxy mode for `pipeline dev`**: currently direct-mode only. Port `dev/dev.ts`'s
`startProxyServer` for embedders that need a localhost origin (e.g. Claude Code preview).
5. **`pipeline dev` editing**: the dev page is view+run only (editing stays in the user's editor).
If in-browser editing with file round-trip is wanted, mirror flow-dev's `handleFlowRoundTrip`.
6. **Asset-path normalization**: the python parser resolves the
`S3Object(s3=…, storage=…?)` constructor / dict-literal forms to the same canonical path as the
TS `{s3, storage}` object form, so SDK writes and reads connect across ts/python. The
SDK-form path keeps its default-storage leading slash (`/x`), matching the triple-slash URI
forms (`s3:///x` in DuckDB and `// on s3:///x`); the no-slash `s3://x` form names a storage
called `x` and is intentionally a distinct node (see Language coverage).
## Plan reference
Original plan: `/home/rfiszel/.claude/plans/tingly-wandering-whistle.md` (local to the author).