Compare commits

..

1 Commits

Author SHA1 Message Date
wendrul
5cc3928726 fix: delete_variable endpoint also deleting resource 2026-03-05 18:00:22 +01:00
812 changed files with 10644 additions and 35192 deletions

View File

@@ -13,10 +13,8 @@ fi
# Check if the file is in the backend directory and is a Rust file # Check if the file is in the backend directory and is a Rust file
if [[ "$FILE_PATH" == *"/backend/"* ]] && [[ "$FILE_PATH" =~ \.rs$ ]]; then if [[ "$FILE_PATH" == *"/backend/"* ]] && [[ "$FILE_PATH" =~ \.rs$ ]]; then
cd "$CLAUDE_PROJECT_DIR/backend" || exit 0 cd "$CLAUDE_PROJECT_DIR/backend" || exit 0
# Run rustfmt, surface errors as context but don't block Claude # Run rustfmt with config from rustfmt.toml (edition=2021)
if rustfmt --config-path rustfmt.toml "$FILE_PATH" 2>&1; then rustfmt --config-path rustfmt.toml "$FILE_PATH" 2>/dev/null || true
echo "Formatted $(basename "$FILE_PATH")"
fi
fi fi
exit 0 exit 0

View File

@@ -15,10 +15,8 @@ if [[ "$FILE_PATH" == *"/frontend/"* ]]; then
# Check if it's a formattable file type # Check if it's a formattable file type
if [[ "$FILE_PATH" =~ \.(ts|js|svelte|json|css|html|md)$ ]]; then if [[ "$FILE_PATH" =~ \.(ts|js|svelte|json|css|html|md)$ ]]; then
cd "$CLAUDE_PROJECT_DIR/frontend" || exit 0 cd "$CLAUDE_PROJECT_DIR/frontend" || exit 0
# Run prettier, surface errors as context but don't block Claude # Run prettier silently, don't fail the hook if prettier fails
if ./node_modules/.bin/prettier --plugin prettier-plugin-svelte --write "$FILE_PATH" 2>&1; then npx prettier --write "$FILE_PATH" 2>/dev/null || true
echo "Formatted $(basename "$FILE_PATH")"
fi
fi fi
fi fi

View File

@@ -28,12 +28,6 @@
"Bash(git show:*)", "Bash(git show:*)",
"Bash(git blame:*)", "Bash(git blame:*)",
"Bash(cargo check:*)", "Bash(cargo check:*)",
"Bash(cargo build --release:*)",
"Bash(sh wm-ts-nav/nav:*)",
"Bash(wm-ts-nav/nav:*)",
"Bash(./wm-ts-nav/nav:*)",
"Bash(wm-ts-nav/target/release/wm-ts-nav:*)",
"Bash(./wm-ts-nav/target/release/wm-ts-nav:*)",
"mcp__ide__getDiagnostics", "mcp__ide__getDiagnostics",
"Bash(npm run generate-backend-client:*)", "Bash(npm run generate-backend-client:*)",
"Bash(npm run check:*)", "Bash(npm run check:*)",

View File

@@ -1,98 +0,0 @@
---
name: local-review
user_invocable: true
description: Code review a pull request for bugs and CLAUDE.md compliance. MUST use when asked to review code.
---
# Local Code Review Skill
Review a pull request for real bugs and CLAUDE.md compliance violations. This review targets HIGH SIGNAL issues only.
## Review Philosophy
- **Only flag issues you are certain about.** If you are not sure an issue is real, do not flag it. False positives erode trust and waste reviewer time.
- Think like a senior engineer doing a final review — flag things that would cause incidents, not things that are merely imperfect.
## What to Flag
- Code that won't compile or parse (syntax errors, type errors, missing imports)
- Code that will definitely produce wrong results regardless of inputs
- Clear, unambiguous CLAUDE.md violations (quote the exact rule being violated)
- Security issues in introduced code (injection, auth bypass, data exposure)
- Incorrect logic that will fail in production
## What NOT to Flag
- Code style or quality concerns
- Potential issues that depend on specific inputs or runtime state
- Subjective suggestions or improvements
- Pre-existing issues not introduced by this PR
- Pedantic nitpicks a senior engineer wouldn't flag
- Issues a linter or type checker will catch
- General quality concerns unless explicitly prohibited in CLAUDE.md
- Issues silenced via lint ignore comments
## Execution Steps
1. **Determine the PR scope**:
- If an argument is provided, use it as the PR number or branch
- Otherwise, detect from the current branch vs main
- Run `gh pr view` if a PR exists, or use `git diff main...HEAD`
2. **Find relevant CLAUDE.md files**:
- Read the root `CLAUDE.md`
- Check for CLAUDE.md files in directories containing changed files
3. **Get the diff and metadata**:
- `gh pr diff` or `git diff main...HEAD` for the full diff
- `gh pr view` or `git log main..HEAD --oneline` for context
4. **Read changed files** where the diff alone is insufficient to understand context
5. **Review for**:
- CLAUDE.md compliance — check each rule against the changed code
- Bugs and logic errors — will this code work correctly?
- Security issues — injection, auth, data exposure in new code
6. **Self-validate each finding**: Before reporting, ask yourself:
- "Is this definitely a real issue, not a false positive?"
- "Would a senior engineer flag this in review?"
- If the answer to either is no, discard the finding
7. **Output findings** to the terminal (default) or post as PR comments (with `--comment` flag)
## Output Format
```
## Code review
Found N issues:
1. <description> (<reason: CLAUDE.md adherence | bug | security>)
<file_path:line_number>
2. <description> (<reason>)
<file_path:line_number>
```
If no issues are found:
```
## Code review
No issues found. Checked for bugs and CLAUDE.md compliance.
```
## Posting Comments (--comment flag)
If the user passes `--comment`, post findings as inline PR comments using:
```bash
gh pr review --comment --body "<summary>"
```
Or for inline comments on specific lines:
```bash
gh api repos/{owner}/{repo}/pulls/{pr}/reviews -f body="<summary>" -f event="COMMENT" -f comments="[...]"
```

View File

@@ -33,7 +33,6 @@ Follow conventional commit format for the PR title:
- Keep under 70 characters - Keep under 70 characters
- Use lowercase, imperative mood - Use lowercase, imperative mood
- No period at the end - No period at the end
- If `*_ee.rs` files were modified, prefix with `[ee]`: `[ee] <type>: <description>`
## PR Body Format ## PR Body Format
@@ -86,25 +85,3 @@ Generated with [Claude Code](https://claude.com/claude-code)
)" )"
``` ```
7. Return the PR URL to the user 7. Return the PR URL to the user
## EE Companion PR (when `*_ee.rs` files were modified)
The `*_ee.rs` files in the windmill repo are **symlinks** to `windmill-ee-private` — changes won't appear in `git diff` of the windmill repo. Instead, check the EE repo for uncommitted or unpushed changes.
Follow the full EE PR workflow in `docs/enterprise.md`. The key PR-specific details:
1. Find the EE repo/worktree: see "Finding the EE Repo" in `docs/enterprise.md`
2. Check for changes: `git -C <ee-path> status --short`
- If there are no changes in the EE repo, skip this entire section
3. Follow steps 15 from the "EE PR Workflow" in `docs/enterprise.md`
4. Create the companion PR (title does NOT get the `[ee]` prefix):
```bash
gh pr create --draft --repo windmill-labs/windmill-ee-private --title "<type>: <description>" --body "$(cat <<'EOF'
Companion PR for windmill-labs/windmill#<PR_NUMBER>
---
Generated with [Claude Code](https://claude.com/claude-code)
EOF
)"
```
5. Commit `ee-repo-ref.txt` and push the updated windmill branch

View File

@@ -119,18 +119,6 @@ jobs:
with: with:
cache-workspaces: backend cache-workspaces: backend
toolchain: 1.93.0 toolchain: 1.93.0
- name: Fix stale v8 build cache
working-directory: ./backend
run: |
# Cargo cache may preserve v8 build fingerprints without the actual
# librusty_v8.a library. Since fingerprints look valid, cargo skips
# build.rs re-run, causing "could not find native static library rusty_v8".
for profile in debug release; do
if [ -d "target/$profile/.fingerprint" ] && [ ! -f "target/$profile/gn_out/obj/librusty_v8.a" ]; then
echo "Cleaning stale v8 build artifacts in target/$profile"
rm -rf "target/$profile/build/v8-"* "target/$profile/.fingerprint/v8-"*
fi
done
- name: cargo check - name: cargo check
timeout-minutes: 16 timeout-minutes: 16
working-directory: ./backend working-directory: ./backend

View File

@@ -1,7 +1,6 @@
name: Backend only integration tests name: Backend only integration tests
on: on:
workflow_dispatch:
push: push:
branches: branches:
- "main" - "main"
@@ -89,18 +88,6 @@ jobs:
with: with:
cache-workspaces: backend cache-workspaces: backend
toolchain: 1.93.0 toolchain: 1.93.0
- name: Fix stale v8 build cache
working-directory: ./backend
run: |
# Cargo cache may preserve v8 build fingerprints without the actual
# librusty_v8.a library. Since fingerprints look valid, cargo skips
# build.rs re-run, causing "could not find native static library rusty_v8".
for profile in debug release; do
if [ -d "target/$profile/.fingerprint" ] && [ ! -f "target/$profile/gn_out/obj/librusty_v8.a" ]; then
echo "Cleaning stale v8 build artifacts in target/$profile"
rm -rf "target/$profile/build/v8-"* "target/$profile/.fingerprint/v8-"*
fi
done
- name: Read EE repo commit hash - name: Read EE repo commit hash
run: | run: |
echo "ee_repo_ref=$(cat ./ee-repo-ref.txt)" >> "$GITHUB_ENV" echo "ee_repo_ref=$(cat ./ee-repo-ref.txt)" >> "$GITHUB_ENV"

View File

@@ -1,37 +0,0 @@
name: Check system prompts freshness
on:
push:
paths:
- "system_prompts/**"
- "typescript-client/**"
- "python-client/wmill/wmill/client.py"
- "openflow.openapi.yaml"
- "backend/windmill-api/openapi.yaml"
- "cli/src/main.ts"
- "cli/src/commands/**"
pull_request:
paths:
- "system_prompts/**"
- "typescript-client/**"
- "python-client/wmill/wmill/client.py"
- "openflow.openapi.yaml"
- "backend/windmill-api/openapi.yaml"
- "cli/src/main.ts"
- "cli/src/commands/**"
jobs:
check-freshness:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
run: pip install pyyaml
- name: Check auto-generated files are up-to-date
run: bash system_prompts/check-freshness.sh

View File

@@ -1,209 +0,0 @@
name: Git Sync Integration Tests
on:
workflow_dispatch:
push:
branches: [main]
paths:
- "backend/windmill-git-sync/**"
- "backend/windmill-api-integration-tests/tests/git_sync*"
- "backend/ee-repo-ref.txt"
- "integration_tests/test/git_sync_test.py"
- ".github/workflows/git-sync-test.yml"
pull_request:
types: [opened, synchronize, reopened]
paths:
- "backend/windmill-git-sync/**"
- "backend/windmill-api-integration-tests/tests/git_sync*"
- "backend/ee-repo-ref.txt"
- "integration_tests/test/git_sync_test.py"
- ".github/workflows/git-sync-test.yml"
concurrency:
group: git-sync-test-${{ github.ref }}
cancel-in-progress: true
jobs:
check-relevance:
runs-on: ubuntu-latest
outputs:
should_run: ${{ steps.check.outputs.should_run }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check if git sync related files changed
id: check
env:
WINDMILL_EE_PRIVATE_ACCESS: ${{ secrets.WINDMILL_EE_PRIVATE_ACCESS }}
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
BASE=${{ github.event.pull_request.base.sha }}
else
BASE=${{ github.event.before }}
fi
CHANGED_FILES=$(git diff --name-only "$BASE"..HEAD 2>/dev/null || echo "")
echo "Changed files:"
echo "$CHANGED_FILES"
# Direct git sync file changes — always relevant
if echo "$CHANGED_FILES" | grep -qE '^(backend/windmill-git-sync/|backend/windmill-api-integration-tests/tests/git_sync|integration_tests/test/git_sync|\.github/workflows/git-sync-test\.yml)'; then
echo "should_run=true" >> "$GITHUB_OUTPUT"
echo "Relevant: direct git sync file changes"
exit 0
fi
# If ee-repo-ref.txt changed, check if the EE diff touches windmill-git-sync/
if echo "$CHANGED_FILES" | grep -q '^backend/ee-repo-ref.txt$'; then
NEW_REF=$(cat backend/ee-repo-ref.txt)
OLD_REF=$(git show "$BASE:backend/ee-repo-ref.txt" 2>/dev/null || echo "")
if [ -n "$OLD_REF" ] && [ "$OLD_REF" != "$NEW_REF" ]; then
# Clone EE repo and check diff
git clone --bare "https://x-access-token:${WINDMILL_EE_PRIVATE_ACCESS}@github.com/windmill-labs/windmill-ee-private.git" /tmp/ee-repo 2>/dev/null
EE_CHANGED=$(git -C /tmp/ee-repo diff --name-only "$OLD_REF".."$NEW_REF" 2>/dev/null || echo "")
echo "EE changed files:"
echo "$EE_CHANGED"
if echo "$EE_CHANGED" | grep -q '^windmill-git-sync/'; then
echo "should_run=true" >> "$GITHUB_OUTPUT"
echo "Relevant: EE git sync files changed"
exit 0
fi
fi
fi
echo "should_run=false" >> "$GITHUB_OUTPUT"
echo "No git sync relevant changes detected, skipping tests"
git_sync_e2e:
needs: [check-relevance]
if: needs.check-relevance.outputs.should_run == 'true'
runs-on: ubicloud-standard-16
services:
postgres:
image: postgres:14
ports:
- 5432:5432
env:
POSTGRES_DB: windmill
POSTGRES_PASSWORD: changeme
options: >-
--health-cmd pg_isready --health-interval 10s --health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.ref }}
fetch-depth: 0
- name: Read EE repo commit hash
run: |
echo "ee_repo_ref=$(cat ./backend/ee-repo-ref.txt)" >> "$GITHUB_ENV"
- uses: actions/checkout@v4
with:
repository: windmill-labs/windmill-ee-private
path: ./windmill-ee-private
ref: ${{ env.ee_repo_ref }}
token: ${{ secrets.WINDMILL_EE_PRIVATE_ACCESS }}
fetch-depth: 0
- name: Substitute EE code
run: |
cd backend && ./substitute_ee_code.sh --copy --dir ./windmill-ee-private
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache-workspaces: backend
toolchain: 1.93.0
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.10
- uses: denoland/setup-deno@v2
with:
deno-version: v2.x
- uses: actions/setup-node@v4
with:
node-version: "20"
- name: Install wmill CLI
run: |
cd cli && bash gen_wm_client.sh && bun install
mkdir -p "$HOME/.local/bin"
printf '#!/bin/sh\nexec bun run "%s/cli/src/main.ts" "$@"\n' "$GITHUB_WORKSPACE" > "$HOME/.local/bin/wmill"
chmod +x "$HOME/.local/bin/wmill"
echo "$HOME/.local/bin" >> $GITHUB_PATH
- name: Build Windmill
working-directory: ./backend
env:
SQLX_OFFLINE: true
CARGO_BUILD_JOBS: 12
RUSTFLAGS: ""
run: |
cargo build --features enterprise,private,license,zip
- name: Start Gitea
run: |
docker run -d --name gitea \
-e GITEA__database__DB_TYPE=sqlite3 \
-e GITEA__security__INSTALL_LOCK=true \
-e GITEA__server__HTTP_PORT=3000 \
-e GITEA__server__ROOT_URL=http://localhost:3000 \
-e GITEA__service__DISABLE_REGISTRATION=false \
-p 3000:3000 \
gitea/gitea:1.22-rootless
echo "Waiting for Gitea to be ready..."
for i in $(seq 1 30); do
if curl -sf http://localhost:3000/api/v1/version > /dev/null 2>&1; then
echo "Gitea is ready"
break
fi
sleep 2
done
curl -sf http://localhost:3000/api/v1/version > /dev/null || { echo "Gitea failed to start"; exit 1; }
- name: Start Windmill
working-directory: ./backend
env:
DATABASE_URL: postgres://postgres:changeme@localhost:5432/windmill
LICENSE_KEY: ${{ secrets.WM_LICENSE_KEY_CI }}
DENO_PATH: deno
BUN_PATH: bun
NODE_BIN_PATH: node
run: |
./target/debug/windmill &
echo "Waiting for Windmill to be ready..."
for i in $(seq 1 60); do
if curl -sf http://localhost:8000/api/version > /dev/null 2>&1; then
echo "Windmill is ready"
break
fi
sleep 2
done
curl -sf http://localhost:8000/api/version > /dev/null || { echo "Windmill failed to start"; exit 1; }
- name: Run git sync E2E tests
timeout-minutes: 10
env:
GITEA_DOCKER_URL: http://localhost:3000
LICENSE_KEY: ${{ secrets.WM_LICENSE_KEY_CI }}
run: |
python3 -m venv .venv
.venv/bin/pip install -r integration_tests/requirements.txt
cd integration_tests && ../.venv/bin/python -m unittest -v test.git_sync_test
- name: Archive logs
uses: actions/upload-artifact@v4
if: always()
with:
name: Git Sync Integration Tests Logs
path: |
integration_tests/logs

View File

@@ -14,7 +14,7 @@ jobs:
with: with:
node-version: "20.x" node-version: "20.x"
registry-url: "https://registry.npmjs.org" registry-url: "https://registry.npmjs.org"
- run: cd typescript-client && ./publish.sh --access public && cd .. - run: cd typescript-client && ./publish.sh && cd ..
env: env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
publish_cli: publish_cli:
@@ -28,6 +28,6 @@ jobs:
- uses: oven-sh/setup-bun@v2 - uses: oven-sh/setup-bun@v2
with: with:
bun-version: latest bun-version: latest
- run: cd cli && ./build.sh && cd npm && npm publish --access public - run: cd cli && ./build.sh && cd npm && npm publish
env: env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

1
.gitignore vendored
View File

@@ -27,4 +27,3 @@ typescript-client/node_modules
frontend/.svelte-kit frontend/.svelte-kit
backend/chrome_profiler.json backend/chrome_profiler.json
.fast-check/ .fast-check/
__pycache__/

View File

@@ -1,105 +0,0 @@
# Project display name in the dashboard
name: Windmill
workspace:
mainBranch: main
worktreeRoot: ../windmill__worktrees
defaultAgent: claude
startupEnvs:
CARGO_FEATURES: "quickjs"
WM_CLONE_DB: false
USE_RUST_PLUGIN: false
lifecycleHooks:
postCreate: bash ./scripts/post-create.sh
preRemove: bash ./scripts/pre-remove.sh
auto_name:
provider: claude
model: haiku
# Each service defines a port env var that webmux injects into pane and agent
# process environments when creating a worktree. Ports are auto-assigned:
# base + (slot x step).
services:
- name: backend
portEnv: BACKEND_PORT
portStart: 8000
portStep: 10
- name: frontend
portEnv: FRONTEND_PORT
portStart: 3000
portStep: 10
profiles:
full:
runtime: host
yolo: true
envPassthrough: []
systemPrompt: >
You are running inside a tmux session with other panes running services.
Pane layout (current window):
- Pane 0: this pane (claude agent)
- Pane 1: backend (cargo watch -x run)
- Pane 2: frontend (npm run dev)
To check logs, use: \`tmux capture-pane -t .1 -p -S -50\` (backend) or \`tmux capture-pane -t .2 -p -S -50\` (frontend).
When restarting backend or frontend, make sure to use ${BACKEND_PORT} and ${FRONTEND_PORT}.
To connect to the database, use this connection string: ${DATABASE_URL}
Because we are running backend with cargo watch, to verify your changes, just check the logs in the backend pane. No need for cargo check.
IMPORTANT: Read docs/autonomous-mode.md before starting any work.
panes:
- id: agent
kind: agent
focus: true
- id: backend
kind: command
split: right
command: ROOT="$(git rev-parse --show-toplevel)"; cd "$ROOT/backend" && PORT=${BACKEND_PORT:-8000} cargo watch -x "run ${CARGO_FEATURES:+--features $CARGO_FEATURES}"
- id: frontend
kind: command
split: bottom
command: ROOT="$(git rev-parse --show-toplevel)"; cd "$ROOT/frontend" && npm run generate-backend-client && REMOTE=${REMOTE:-http://localhost:${BACKEND_PORT:-8000}} npm run dev -- --port ${FRONTEND_PORT:-3000} --host 0.0.0.0
frontendOnly:
runtime: host
yolo: true
envPassthrough: []
systemPrompt: >
You are running inside a tmux session with other panes running services.
Pane layout (current window):
- Pane 0: this pane (claude agent)
- Pane 1: frontend (npm run dev)
To check logs, use: \`tmux capture-pane -t .1 -p -S -50\` (frontend).
When restarting frontend, make sure to use ${FRONTEND_PORT}.
To connect to the database, use this connection string: ${DATABASE_URL}
Because we are running frontend with npm run dev, to verify your changes, just check the logs in the frontend pane. No need for npm run build.
IMPORTANT: Read docs/autonomous-mode.md before starting any work.
panes:
- id: agent
kind: agent
focus: true
- id: frontend
kind: command
split: right
command: ROOT="$(git rev-parse --show-toplevel)"; cd "$ROOT/frontend" && npm run generate-backend-client && npm run dev -- --port ${FRONTEND_PORT:-3000} --host 0.0.0.0
agentOnly:
runtime: host
yolo: true
envPassthrough: []
systemPrompt: >
IMPORTANT: Read docs/autonomous-mode.md before starting any work.
panes:
- id: agent
kind: agent
focus: true
integrations:
github:
linkedRepos:
- repo: windmill-labs/windmill-ee-private
alias: ee-private
dir: ../windmill-ee-private__worktrees
linear:
enabled: true

113
.wmdev.yaml Normal file
View File

@@ -0,0 +1,113 @@
name: Windmill
startupEnvs:
CARGO_FEATURES: "quickjs"
WM_CLONE_DB: false
USE_RUST_PLUGIN: false
services:
- name: BE
portEnv: BACKEND_PORT
- name: FE
portEnv: FRONTEND_PORT
profiles:
default:
name: default
sandbox:
name: sandbox
image: windmill-sandbox
envPassthrough:
- AWS_ACCESS_KEY_ID
- AWS_SECRET_ACCESS_KEY
- R2_ENDPOINT
- R2_BUCKET
- R2_PUBLIC_URL
extraMounts:
- hostPath: ~/.ssh
guestPath: /root/.ssh
writable: true
- hostPath: ~/.codex
guestPath: /root/.codex
writable: true
- hostPath: ~/windmill-ee-private
writable: true
- hostPath: ~/windmill-ee-private__worktrees
writable: true
systemPrompt: >
You are running inside a sandboxed container with full permissions.
This worktree is configured with the following ports:
- Backend: port ${BACKEND_PORT}.
Start with: cd backend && PORT=${BACKEND_PORT}
DATABASE_URL=postgres://postgres:changeme@localhost:5432/windmill
cargo watch -x run
- Frontend: port ${FRONTEND_PORT}.
Start with: cd frontend && REMOTE=http://localhost:${BACKEND_PORT}
npm run dev -- --port ${FRONTEND_PORT} --host 0.0.0.0
--- Screenshots ---
You can take screenshots of the frontend UI and upload them to R2
for use in PR descriptions.
1) Take a screenshot:
bunx playwright screenshot --browser chromium
http://localhost:${FRONTEND_PORT}/path/to/page /tmp/screenshot.png
2) Upload to R2:
aws s3 cp /tmp/screenshot.png
"s3://$(printenv R2_BUCKET)/$(git rev-parse --abbrev-ref HEAD)/screenshot.png"
--endpoint-url "$(printenv R2_ENDPOINT)"
3) The public URL will be:
$(printenv R2_PUBLIC_URL)/<branch>/screenshot.png
4) Include in PR descriptions using markdown image syntax.
--- Terminal Recordings (asciinema) ---
You can record terminal sessions and upload them for sharing.
asciinema is available on PATH.
1) Write a shell script with the commands to demo. Add sleep
delays for readable pacing:
- 0.5s after printing a "$ command" line (lets viewer read it)
- 1.5-2s after command output (lets viewer absorb the result)
- Set GIT_PAGER=cat and PAGER=cat to prevent pager hangs
2) Record headlessly:
asciinema rec --headless --overwrite \
-c "bash /tmp/demo.sh" \
--window-size 120x50 \
--title "Description of demo" \
/tmp/demo.cast
3) Upload to asciinema.org:
XDG_DATA_HOME=/tmp/.local/share \
asciinema upload --server-url https://asciinema.org /tmp/demo.cast
--- Mermaid Diagrams ---
You can render Mermaid diagrams to SVG using the pre-installed mmdc CLI.
The puppeteer config (no-sandbox + Chromium path) is at /root/.puppeteerrc.json.
1) Write a .mmd file with your diagram:
cat > /tmp/diagram.mmd << 'EOF'
graph TD
A[Start] --> B[End]
EOF
2) Render to SVG (the -p flag is required):
mmdc -i /tmp/diagram.mmd -o /tmp/diagram.svg -p /root/.puppeteerrc.json
3) Upload to R2:
aws s3 cp /tmp/diagram.svg
"s3://$(printenv R2_BUCKET)/$(git rev-parse --abbrev-ref HEAD)/diagram.svg"
--endpoint-url "$(printenv R2_ENDPOINT)"
4) The public URL will be:
$(printenv R2_PUBLIC_URL)/<branch>/diagram.svg
5) Include in PR descriptions using markdown image syntax.
IMPORTANT: Read docs/autonomous-mode.md before starting any work.
linkedRepos:
- repo: windmill-labs/windmill-ee-private
alias: ee

View File

@@ -67,7 +67,6 @@ files:
copy: copy:
- backend/.env - backend/.env
- scripts/ - scripts/
- wm-ts-nav/target/release/wm-ts-nav
sandbox: sandbox:
enabled: false enabled: false

View File

@@ -1,122 +1,5 @@
# Changelog # Changelog
## [1.657.0](https://github.com/windmill-labs/windmill/compare/v1.656.0...v1.657.0) (2026-03-14)
### Features
* add datatable config support to CLI settings sync and backend export ([#8024](https://github.com/windmill-labs/windmill/issues/8024)) ([5df37fb](https://github.com/windmill-labs/windmill/commit/5df37fb0dbf9190a430f066cf2d3c48914782e53))
## [1.656.0](https://github.com/windmill-labs/windmill/compare/v1.655.0...v1.656.0) (2026-03-13)
### Features
* add GitHub Enterprise Server (GHES) support for GitHub App git sync ([#8344](https://github.com/windmill-labs/windmill/issues/8344)) ([2e430c4](https://github.com/windmill-labs/windmill/commit/2e430c4c0b8540df7b6997434a7a9f9134858026))
* **cli:** add unified generate-metadata command ([#8335](https://github.com/windmill-labs/windmill/issues/8335)) ([4c2c165](https://github.com/windmill-labs/windmill/commit/4c2c165a5b757bd5f2f49074bb290407bce3b2fb))
### Bug Fixes
* **ci:** add NODE_AUTH_TOKEN for npm publish authentication ([2a8e276](https://github.com/windmill-labs/windmill/commit/2a8e276b6d2761bb2798b6bc5f8d90ab34fbb403))
* **ci:** remove provenance flag and use NPM_TOKEN for npm publish ([44dd3ee](https://github.com/windmill-labs/windmill/commit/44dd3ee8cd05d288828d1d46c84cbcdf40f8fa78))
* **cli:** exclude raw app backend files from script metadata generation ([#8362](https://github.com/windmill-labs/windmill/issues/8362)) ([060687b](https://github.com/windmill-labs/windmill/commit/060687b1fa6b627a7b06fbdc4b3f4eb0b63411c0))
* **cli:** normalize path separators in generate-metadata folder filter for Windows ([#8358](https://github.com/windmill-labs/windmill/issues/8358)) ([404ae09](https://github.com/windmill-labs/windmill/commit/404ae09d429fb545610ba17d747e1903c542d4a3))
* **cli:** suppress verbose lock generation messages in generate-metadata ([#8357](https://github.com/windmill-labs/windmill/issues/8357)) ([51933be](https://github.com/windmill-labs/windmill/commit/51933be3cabd853960d384cd358c7bcaef6bfa86))
* **frontend:** collapse flow topbar buttons to icon-only in narrow panes ([#8322](https://github.com/windmill-labs/windmill/issues/8322)) ([b585dee](https://github.com/windmill-labs/windmill/commit/b585dee64dfd63d20812ca969b17ff9ee9989493))
* **frontend:** filter webhook/email tokens by scope instead of label ([#8363](https://github.com/windmill-labs/windmill/issues/8363)) ([0d31c35](https://github.com/windmill-labs/windmill/commit/0d31c35f3e12d637c757a95fe350294002cbf640))
* **frontend:** improve native mode alert message and fix workspaced tag detection ([#8361](https://github.com/windmill-labs/windmill/issues/8361)) ([fb12b31](https://github.com/windmill-labs/windmill/commit/fb12b31df081b2f1ac63becea6e6538ca80f8c46))
* **frontend:** prevent duplicate and reserved agent tool names ([#8367](https://github.com/windmill-labs/windmill/issues/8367)) ([c431053](https://github.com/windmill-labs/windmill/commit/c431053a1e24ef29cd551a86de4d013fd7f158be))
* graceful shutdown instead of panic on job completion channel failure ([#8345](https://github.com/windmill-labs/windmill/issues/8345)) ([724d135](https://github.com/windmill-labs/windmill/commit/724d1350d070fcf078034a52166d3048fb74e6f3))
* Linked resources and vars not triggering both sync jobs on delete ([#8342](https://github.com/windmill-labs/windmill/issues/8342)) ([8e3b8bd](https://github.com/windmill-labs/windmill/commit/8e3b8bdfd2ded9652bc7e876c6bcd0ac2cfae148))
* lower default indexer memory/batch settings to prevent OOM ([#8347](https://github.com/windmill-labs/windmill/issues/8347)) ([d9d45cf](https://github.com/windmill-labs/windmill/commit/d9d45cf2f9235b0e7118d0fc97ccdc0776ca9726))
## [1.655.0](https://github.com/windmill-labs/windmill/compare/v1.654.0...v1.655.0) (2026-03-12)
### Features
* add auto_commit option to Kafka triggers with advanced UI badges ([#8317](https://github.com/windmill-labs/windmill/issues/8317)) ([ec20d76](https://github.com/windmill-labs/windmill/commit/ec20d76216492086842c4f5e4e3b36727a5631e9))
* partition audit log table by day with configurable retention ([#8292](https://github.com/windmill-labs/windmill/issues/8292)) ([2aef01d](https://github.com/windmill-labs/windmill/commit/2aef01d18c0723aedcc626f4f3991195620774ab))
* support minimal telemetry mode ([#8243](https://github.com/windmill-labs/windmill/issues/8243)) ([fe1519f](https://github.com/windmill-labs/windmill/commit/fe1519f1284aadd67d5dce46cf0cb52ab351f789))
### Bug Fixes
* **cli:** instruct agent to tell user about generate-metadata and sync push instead of running them ([#8318](https://github.com/windmill-labs/windmill/issues/8318)) ([7fb729c](https://github.com/windmill-labs/windmill/commit/7fb729cc8483a2e6966a8e8995678929f4d451a0))
* fix saved inputs popover infinite loop ([#8311](https://github.com/windmill-labs/windmill/issues/8311)) ([425a75e](https://github.com/windmill-labs/windmill/commit/425a75e030b15fe65676169f9069fbb7da19828e))
* native mode now properly sets DB pool size and sleep queue ([#8332](https://github.com/windmill-labs/windmill/issues/8332)) ([d8b4132](https://github.com/windmill-labs/windmill/commit/d8b4132b9ae90af759c6655f4f69479f6738e60a))
* prevent zombie jobs from looping forever ([#8313](https://github.com/windmill-labs/windmill/issues/8313)) ([48bc3e2](https://github.com/windmill-labs/windmill/commit/48bc3e244558dccb1f08f455b299600861788b0d))
* set min_connections(0) to prevent sqlx pool spin loop ([#8334](https://github.com/windmill-labs/windmill/issues/8334)) ([bf4340f](https://github.com/windmill-labs/windmill/commit/bf4340f40c1eb9cacee4c32e07ba44f2c92bf7c4))
* show diff editor content for resources without a language ([#8331](https://github.com/windmill-labs/windmill/issues/8331)) ([cbc7e78](https://github.com/windmill-labs/windmill/commit/cbc7e78f8a60bff1d8730a6183cdbc9125d8e2b1))
* skip python preinstall on native workers ([#8329](https://github.com/windmill-labs/windmill/issues/8329)) ([4306c9e](https://github.com/windmill-labs/windmill/commit/4306c9e4fef317e298a76924edb4f20aa7ced105))
* skip token expiry notifications for debugger and mcp-oauth tokens ([#8316](https://github.com/windmill-labs/windmill/issues/8316)) ([8667329](https://github.com/windmill-labs/windmill/commit/86673291100fd16aaf216ed33ca9b648b8a2b7a5))
* use !inline ref for scripts inside flows (preproc, error, ai tool) ([#8319](https://github.com/windmill-labs/windmill/issues/8319)) ([ca8a627](https://github.com/windmill-labs/windmill/commit/ca8a6274bc81ad49fa0c6166694ae4d65a4048cb))
## [1.654.0](https://github.com/windmill-labs/windmill/compare/v1.653.0...v1.654.0) (2026-03-10)
### Features
* add git sync support for workspace dependencies ([#8144](https://github.com/windmill-labs/windmill/issues/8144)) ([4f29e05](https://github.com/windmill-labs/windmill/commit/4f29e05e3ae725e0be7ab797f8fa2186d8c5c0a5))
* add kafka trigger offset reset and auto.offset.reset config ([#8283](https://github.com/windmill-labs/windmill/issues/8283)) ([b02f9e5](https://github.com/windmill-labs/windmill/commit/b02f9e5c2426bff2356e1aaaa18e05b18c5efc6b))
* add preprocessor support for dedicated workers and bunnative scripts ([#8284](https://github.com/windmill-labs/windmill/issues/8284)) ([dc0e59f](https://github.com/windmill-labs/windmill/commit/dc0e59f432a0e3a53606adb8ac76d2dd2d365ace))
* add Vertex AI support for Google Gemini models ([#8303](https://github.com/windmill-labs/windmill/issues/8303)) ([cb349cb](https://github.com/windmill-labs/windmill/commit/cb349cb3d1b7561fb70a8c23fa83dc1c9441821c))
* **frontend:** replace flat sugiyama with recursive compound layout for flow graph ([#8204](https://github.com/windmill-labs/windmill/issues/8204)) ([cad4436](https://github.com/windmill-labs/windmill/commit/cad44365ac17029a2257f12cef061219b0265570))
### Bug Fixes
* **cli:** fail when passing an invalid --workspace arg ([#8294](https://github.com/windmill-labs/windmill/issues/8294)) ([f291b1c](https://github.com/windmill-labs/windmill/commit/f291b1cc19689e69e7aa008c19ce747e9c56240e))
* debounce webhook arg accumulation with max_count/max_time limits ([#8307](https://github.com/windmill-labs/windmill/issues/8307)) ([83be59e](https://github.com/windmill-labs/windmill/commit/83be59e0e866ebd091f1e27c0571710a989fd2e4))
* delete debounce_key on post-preprocessing limit exceeded ([#8299](https://github.com/windmill-labs/windmill/issues/8299)) ([438f609](https://github.com/windmill-labs/windmill/commit/438f609a78325ee5c2493079ca27bf587fa0d5ff))
* explicilty fail when --base-url --token --workspace are invalid ([#8302](https://github.com/windmill-labs/windmill/issues/8302)) ([5baeb8c](https://github.com/windmill-labs/windmill/commit/5baeb8c842a392c21457b7561e30b385e02a6a48))
* handle missing schema in RunnableByPath during wmill.d.ts generation ([#8300](https://github.com/windmill-labs/windmill/issues/8300)) ([b841e0a](https://github.com/windmill-labs/windmill/commit/b841e0a0384941079f37374f8fbbe2dd7fb51897))
* optimize flow lock generation and add rt.d.ts guidance for TS resource types ([#8295](https://github.com/windmill-labs/windmill/issues/8295)) ([b40cf80](https://github.com/windmill-labs/windmill/commit/b40cf80fdd62cbc31db0872ada551ce213b9dac8))
* preserve teams oauth tenant on settings page reload ([#8308](https://github.com/windmill-labs/windmill/issues/8308)) ([dbfa271](https://github.com/windmill-labs/windmill/commit/dbfa271b8962fe7b3d2aa8bf494e9557047fc8b3))
* resync custom_instance_user password on startup ([#8297](https://github.com/windmill-labs/windmill/issues/8297)) ([53ac43f](https://github.com/windmill-labs/windmill/commit/53ac43f5ee34570a9bb7b3441c73095e23690300))
* show meaningful error messages in database manager schema fetch ([#8296](https://github.com/windmill-labs/windmill/issues/8296)) ([cda8439](https://github.com/windmill-labs/windmill/commit/cda843922dcfd9a02ef9926751cbf8f544d2d4b6))
* skip loading flow preview history for new flows ([#8293](https://github.com/windmill-labs/windmill/issues/8293)) ([ac8c668](https://github.com/windmill-labs/windmill/commit/ac8c668cb93e56bc2a247bbdbbec14e5608125d2))
* teams selection not sticking in workspace settings ([#8309](https://github.com/windmill-labs/windmill/issues/8309)) ([fefc8c6](https://github.com/windmill-labs/windmill/commit/fefc8c62a00fe7a39f3104091e08087cd7c37afb))
## [1.653.0](https://github.com/windmill-labs/windmill/compare/v1.652.0...v1.653.0) (2026-03-10)
### Features
* add indexer time window setting (default 7 days) ([#8290](https://github.com/windmill-labs/windmill/issues/8290)) ([0c4d72c](https://github.com/windmill-labs/windmill/commit/0c4d72cfe38d61cf3f6e9bc31056005f1adb494d))
* add slack connection fields to workspace settings export/import ([#8287](https://github.com/windmill-labs/windmill/issues/8287)) ([39e77ec](https://github.com/windmill-labs/windmill/commit/39e77ecd002b41630fa8d146ee0f15369656acda))
### Performance Improvements
* optimize job_stats storage for timestamps and zero-memory jobs ([#8289](https://github.com/windmill-labs/windmill/issues/8289)) ([2d8335d](https://github.com/windmill-labs/windmill/commit/2d8335dc43a7cb182eb5a058119d8b0be067cdfd))
## [1.652.0](https://github.com/windmill-labs/windmill/compare/v1.651.1...v1.652.0) (2026-03-09)
### Features
* add secretKeyRef support for package registry and storage credentials ([#8275](https://github.com/windmill-labs/windmill/issues/8275)) ([73d27e9](https://github.com/windmill-labs/windmill/commit/73d27e92dd6ced1602f6328f245fec0fa96860e1))
* expose OTEL trace context as env vars in job execution ([#8277](https://github.com/windmill-labs/windmill/issues/8277)) ([93f75ad](https://github.com/windmill-labs/windmill/commit/93f75ada5e49036f0d998e3d3d53de4dc2c2e83f))
* workflow-as-code (WAC) v2 ([#8172](https://github.com/windmill-labs/windmill/issues/8172)) ([a6d4390](https://github.com/windmill-labs/windmill/commit/a6d4390790d21d535df1e9d525bffd577c50d8dc))
### Bug Fixes
* cli: support deleting linked resources-variables without throwing ([#8248](https://github.com/windmill-labs/windmill/issues/8248)) ([7859bca](https://github.com/windmill-labs/windmill/commit/7859bca6ae80d32a73a46910960afc6812e64115))
* Database studio fixes ([#8251](https://github.com/windmill-labs/windmill/issues/8251)) ([1d78589](https://github.com/windmill-labs/windmill/commit/1d785899404e8636a206cda9a2914df32a1a5269))
* **frontend:** unsaved changes dialog when flow already saved ([#8259](https://github.com/windmill-labs/windmill/issues/8259)) ([0330993](https://github.com/windmill-labs/windmill/commit/0330993cb66cdabffcd6e552a0f85a9a3931c62d))
* gracefully handle uninitialized OTEL tracing proxy port ([#8274](https://github.com/windmill-labs/windmill/issues/8274)) ([8b1fe8f](https://github.com/windmill-labs/windmill/commit/8b1fe8f9de7b0c03655558d0c46cfff71a4b2047))
* guard iteration picker VirtualList against empty items array ([#8273](https://github.com/windmill-labs/windmill/issues/8273)) ([c97cf60](https://github.com/windmill-labs/windmill/commit/c97cf604ab4a902d89fe873b90dbeb9dabc940eb)), closes [#8272](https://github.com/windmill-labs/windmill/issues/8272)
* mask secrets in OAuth config debug/log output ([#8269](https://github.com/windmill-labs/windmill/issues/8269)) ([e75763d](https://github.com/windmill-labs/windmill/commit/e75763dbe5ffe08e6cde082203596d510c2c3b29))
* parallel branchall hang on bad stop_after_all_iters_if + results.x.length null ([#8276](https://github.com/windmill-labs/windmill/issues/8276)) ([41e523f](https://github.com/windmill-labs/windmill/commit/41e523f827c4e3d5db525a1f14e24936b0b8af46))
* redact secrets in set_global_setting log line ([#8270](https://github.com/windmill-labs/windmill/issues/8270)) ([6a0473c](https://github.com/windmill-labs/windmill/commit/6a0473c5783dc0fef2ae82dc5345a5f0596f124d))
* remove $bindable() fallback values causing props_invalid_value error in oauth settings ([#8265](https://github.com/windmill-labs/windmill/issues/8265)) ([037035e](https://github.com/windmill-labs/windmill/commit/037035e094937827305dad29bd76a495d78bc46f))
* skip down migrations in potentially_stale checksum comparison ([#8271](https://github.com/windmill-labs/windmill/issues/8271)) ([5ba4029](https://github.com/windmill-labs/windmill/commit/5ba4029d8692b2e6054fca7f45ed4cfded4738ef))
* sql input horizontal scroll missing after switching flow steps ([#8249](https://github.com/windmill-labs/windmill/issues/8249)) ([ce8ac9c](https://github.com/windmill-labs/windmill/commit/ce8ac9cf52dc17061673b9b72556279c48c26f8e))
* wmill workspace whoami output ([#8246](https://github.com/windmill-labs/windmill/issues/8246)) ([1ac391a](https://github.com/windmill-labs/windmill/commit/1ac391a795585747fe5911ac41b157556569fedb))
## [1.651.1](https://github.com/windmill-labs/windmill/compare/v1.651.0...v1.651.1) (2026-03-05) ## [1.651.1](https://github.com/windmill-labs/windmill/compare/v1.651.0...v1.651.1) (2026-03-05)

View File

@@ -4,7 +4,7 @@ Open-source platform for internal tools, workflows, API integrations, background
## Workflow ## Workflow
1. **Understand**: Before coding, explore the codebase (see Code Navigation below). Use `outline` to understand file structure, `body` to read specific symbols, `def`/`callers`/`callees` to trace code, `Grep` to find usages. Read `docs/` for domain context. 1. **Understand**: Before coding, read relevant docs from `docs/` to understand the area you're changing
2. **Plan**: For non-trivial changes, use plan mode. For large features, break into reviewable stages 2. **Plan**: For non-trivial changes, use plan mode. For large features, break into reviewable stages
3. **Execute**: Follow coding patterns from skills (`rust-backend`, `svelte-frontend`) 3. **Execute**: Follow coding patterns from skills (`rust-backend`, `svelte-frontend`)
4. **Validate**: After every change, run the appropriate checks per `docs/validation.md` 4. **Validate**: After every change, run the appropriate checks per `docs/validation.md`
@@ -14,8 +14,7 @@ Open-source platform for internal tools, workflows, API integrations, background
- **Validation**: `docs/validation.md` — what checks to run based on what you changed - **Validation**: `docs/validation.md` — what checks to run based on what you changed
- **Enterprise**: `docs/enterprise.md` — EE file conventions and PR workflow - **Enterprise**: `docs/enterprise.md` — EE file conventions and PR workflow
- **Backend patterns**: use the `rust-backend` skill when writing Rust code - **Backend patterns**: use the `rust-backend` skill when writing Rust code
- **Frontend patterns**: use the `svelte-frontend` skill when writing Svelte code. Do NOT edit svelte files unless you have read that skill. - **Frontend patterns**: use the `svelte-frontend` skill when writing Svelte code
- **Code review**: use `/local-review` to review a PR for bugs and CLAUDE.md compliance
- **Domain guides**: `.claude/skills/native-trigger/` and `frontend/tutorial-system-guide.mdc` - **Domain guides**: `.claude/skills/native-trigger/` and `frontend/tutorial-system-guide.mdc`
- **Brand/UI guidelines**: `frontend/brand-guidelines.md` - **Brand/UI guidelines**: `frontend/brand-guidelines.md`
@@ -27,58 +26,8 @@ Open-source platform for internal tools, workflows, API integrations, background
- **Login**: `admin@windmill.dev` / `changeme` - **Login**: `admin@windmill.dev` / `changeme`
- **Instance settings**: navigate to `/#superadmin-settings` - **Instance settings**: navigate to `/#superadmin-settings`
## Banned Patterns
### `$bindable(default_value)` on optional props
Using `$bindable(default_value)` on props that can be `undefined` is **banned**. This pattern causes subtle bugs because the default value masks the `undefined` state.
**Bad:**
```svelte
let { my_prop = $bindable(default_value) }: { my_prop?: string } = $props()
```
**Correct alternatives:**
1. **Use `$derived` with nullish coalescing** — handle the potential `undefined` at the usage site:
```svelte
let { my_prop = $bindable() }: { my_prop?: string } = $props()
let effective_value = $derived(my_prop ?? default_value)
```
2. **Create a `useMyPropState()` helper** — encapsulate the undefined-handling logic in a reusable function and call it higher in the component tree, so the child component always receives a defined value.
## Code Navigation
`wm-ts-nav` is an AST-aware code navigator. Use **wm-ts-nav** for structural queries — it skips comments/strings and understands symbol boundaries.
**MUST use `outline` before `Read`** on unfamiliar files — a 500-line file costs ~500 lines of context, while `outline` costs ~20. Then **MUST use `body "X"`** instead of reading a full file to see one function/struct. Use `Read` with offset/limit only when you need surrounding context that `body` doesn't capture.
- `refs "X" --caller` instead of reading files to find which function contains each reference
- `callers "X"` / `callees "X"` for call-graph questions
```bash
NAV="sh wm-ts-nav/nav"
# Use --root backend for Rust, --root frontend/src for TS/Svelte
$NAV --root backend outline backend/path/to/file.rs # file structure
$NAV --root backend def "ServiceName" # find definition
$NAV --root backend body "decrypt_oauth_data" # extract source code
$NAV --root backend search "%" --parent ServiceName # methods on a type
$NAV --root backend search "Trigger" --kind struct # find by kind
$NAV --root backend refs "X" --file handler.rs --caller # scoped refs with caller
$NAV --root backend callers "X" # who calls X?
$NAV --root backend callees "X" # what does X call?
```
**Limitations** — syntax-level analysis, no type inference. Use **Grep** instead when completeness matters (finding all usages, exhaustiveness checks):
- `refs`/`callers`/`callees` can't follow re-exports, glob imports, or different import paths to the same symbol
- Trait impls, macro-generated symbols (`sqlx::FromRow`), and namespace member access (`ns.X`) are invisible
- `callees` shows all identifiers in a function body, not just actual calls
## Core Principles ## Core Principles
- **MUST `outline` before `Read`** on unfamiliar files — then `body` or `Read` with offset/limit for specifics
- Search for existing code to reuse before writing new code - Search for existing code to reuse before writing new code
- Follow established patterns in the codebase - Follow established patterns in the codebase
- Keep changes focused — don't refactor beyond what's asked - Keep changes focused — don't refactor beyond what's asked

View File

@@ -268,11 +268,11 @@ RUN bun install -g windmill-cli \
RUN curl -fsSL https://claude.ai/install.sh | bash \ RUN curl -fsSL https://claude.ai/install.sh | bash \
&& cp /root/.local/share/claude/versions/* /usr/bin/claude && cp /root/.local/share/claude/versions/* /usr/bin/claude
COPY --from=php:8.3.30-cli /usr/local/bin/php /usr/bin/php COPY --from=php:8.3.7-cli /usr/local/bin/php /usr/bin/php
COPY --from=composer:2.9.5 /usr/bin/composer /usr/bin/composer COPY --from=composer:2.7.6 /usr/bin/composer /usr/bin/composer
# add the docker client to call docker from a worker if enabled # add the docker client to call docker from a worker if enabled
COPY --from=docker:29-dind /usr/local/bin/docker /usr/local/bin/ COPY --from=docker:dind /usr/local/bin/docker /usr/local/bin/
ENV RUSTUP_HOME="/tmp/windmill/cache/rustup" ENV RUSTUP_HOME="/tmp/windmill/cache/rustup"
ENV CARGO_HOME="/tmp/windmill/cache/cargo" ENV CARGO_HOME="/tmp/windmill/cache/cargo"

View File

@@ -192,6 +192,70 @@ sandbox:
This mounts both the main EE repo (used by the main worktree) and the EE worktrees directory (used by feature worktrees) into every sandbox container. This mounts both the main EE repo (used by the main worktree) and the EE worktrees directory (used by feature worktrees) into every sandbox container.
## Cursor SSH Integration (`wmc`)
`wm-cursor` (aliased as `wmc`) gives each worktree its own Cursor SSH remote window with an independently-focused tmux session. All windows are visible in the status bar across all Cursor terminals, but each one is focused on its own worktree.
This uses **grouped tmux sessions** — multiple sessions that share the same window list but track focus independently:
```
tmux session: main <-- your main Cursor terminal
tmux session: cursor-feat-a <-- Cursor window for feat-a (focused on wm-feat-a)
tmux session: cursor-feat-b <-- Cursor window for feat-b (focused on wm-feat-b)
\__ all three share the same windows in the status bar
```
### Setup
Run once from inside tmux on the remote:
```bash
./scripts/wm-cursor setup /home/hugo/projects/windmill
```
This:
1. **Merges `.vscode/settings.json`** — adds the `wm-tmux` terminal profile (auto-attaches to the `main` tmux session), disables auto port forwarding, configures forwarding for ports 8000/3000/5432, and stops rust-analyzer from auto-starting. Existing settings are preserved.
2. **Creates `.vscode/tasks.json`** — auto-starts the dev database (`start-dev-db.sh`) when the folder opens.
3. **Adds `wmc` alias to `~/.zshrc`** — so you can use `wmc` from any tmux window.
4. **Adds `eval "$(wmc completions)"`** to `~/.zshrc` — provides tab-completion for subcommands and worktree names (for `open`, `open-ee`, and `close`).
After setup, reopen Cursor's terminal to pick up the new profile.
### Usage
All commands run from inside a tmux session (i.e., from Cursor's integrated terminal after setup).
**Create a new worktree + open Cursor:**
```bash
wmc add -A -p "implement feature X"
```
This runs `workmux add`, creates a grouped tmux session, writes `.vscode/settings.json` in the worktree (with port forwarding matching the worktree's assigned ports), and opens a new Cursor window.
**Open Cursor for an existing worktree:**
```bash
wmc open my-feature
```
**Open the EE worktree in Cursor (no tmux session):**
```bash
wmc open-ee my-feature
```
This finds the matching `windmill-ee-private__worktrees/<name>` directory and opens it in a new Cursor window.
**Close a worktree's Cursor window and tmux window (keeps the worktree):**
```bash
wmc close my-feature
```
This kills the grouped tmux session and calls `workmux close` to close the tmux window. The worktree and branch are preserved. Grouped sessions are also automatically cleaned up when you `workmux rm` a worktree (via `scripts/worktree-cleanup`).
## Cargo Features ## Cargo Features
To build the backend with specific Cargo features (e.g., `enterprise`, `parquet`), pass them via `CARGO_FEATURES`. The backend pane reads this from `.env.local` and appends `--features <value>` to the `cargo watch` command. To build the backend with specific Cargo features (e.g., `enterprise`, `parquet`), pass them via `CARGO_FEATURES`. The backend pane reads this from `.env.local` and appends `--features <value>` to the `cargo watch` command.
@@ -206,6 +270,20 @@ CARGO_FEATURES="enterprise,parquet" wm add my-feature
This gets written to `.env.local` by the `post_create` hook (`scripts/worktree-env`), and the backend pane picks it up automatically. This gets written to `.env.local` by the `post_create` hook (`scripts/worktree-env`), and the backend pane picks it up automatically.
**With `wmc` (wm-cursor):**
Use the `--features` flag:
```bash
# Create a new worktree with features
wmc add --features "enterprise,parquet" -A -p "implement feature X"
# Open an existing worktree with different features
wmc open my-feature --features "enterprise,parquet"
```
The `--features` flag exports `CARGO_FEATURES` so the `post_create` hook writes it to `.env.local`. When using `wmc open`, it updates the existing `.env.local` with the new features.
## Login ## Login
Default credentials: `admin@windmill.dev` / `changeme` Default credentials: `admin@windmill.dev` / `changeme`

View File

@@ -1,41 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT id, topic, partition, \"offset\" FROM kafka_pending_commits\n WHERE workspace_id = $1 AND kafka_trigger_path = $2\n ORDER BY id",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "topic",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "partition",
"type_info": "Int4"
},
{
"ordinal": 3,
"name": "offset",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false,
false,
false,
false
]
},
"hash": "038d2fde90fa9e99e30d15161777fa3ab402e33edfca46daa95b52e525424586"
}

View File

@@ -46,11 +46,11 @@
] ]
}, },
"nullable": [ "nullable": [
true, false,
true, false,
true, false,
true, false,
true, false,
true, true,
true true
] ]

View File

@@ -1,29 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE kafka_trigger\n SET\n kafka_resource_path = $1,\n group_id = $2,\n topics = $3,\n filters = $4,\n auto_offset_reset = $5,\n auto_commit = $6,\n script_path = $7,\n path = $8,\n is_flow = $9,\n edited_by = $10,\n email = $11,\n edited_at = now(),\n server_id = NULL,\n error = NULL,\n error_handler_path = $14,\n error_handler_args = $15,\n retry = $16\n WHERE\n workspace_id = $12 AND path = $13\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"VarcharArray",
"JsonbArray",
"Varchar",
"Bool",
"Varchar",
"Varchar",
"Bool",
"Varchar",
"Varchar",
"Text",
"Text",
"Varchar",
"Jsonb",
"Jsonb"
]
},
"nullable": []
},
"hash": "072e5ab78f929c6b7264f98c1588cb24cc635836276ee6faa2438f494bfbce04"
}

View File

@@ -1,17 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE job_stats SET offsets_cs = array_append(offsets_cs, (EXTRACT(EPOCH FROM (now() - timeseries_start)) * 100)::int), timeseries_int = array_append(timeseries_int, $4) WHERE workspace_id = $1 AND job_id = $2 AND metric_id = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Uuid",
"Text",
"Int4"
]
},
"nullable": []
},
"hash": "0af0e0a1dddeee2021ba060e390e1b60caa3752669636e9fb0817a68121a9451"
}

View File

@@ -1,16 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE v2_job_queue SET canceled_by = $2, canceled_reason = $3 WHERE id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Varchar",
"Text"
]
},
"nullable": []
},
"hash": "0cd9cad7109340edc81a5a40620b6efdae570e3416ec6c2493cc04f75c32a699"
}

View File

@@ -1,40 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT j.id, j.runnable_path, j.args, j.kind::text AS \"kind!\"\n FROM v2_job j\n JOIN v2_job_queue q ON j.id = q.id\n WHERE j.runnable_path = $1\n AND j.kind = 'deploymentcallback'\n ORDER BY j.created_at DESC\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "runnable_path",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "args",
"type_info": "Jsonb"
},
{
"ordinal": 3,
"name": "kind!",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
true,
true,
null
]
},
"hash": "0d4f28ca0c5697c96711ca7225a9a4013e6ccabb495c371471c9d1287defda8f"
}

View File

@@ -1,6 +1,6 @@
{ {
"db_name": "PostgreSQL", "db_name": "PostgreSQL",
"query": "\n SELECT\n (elem->>'installation_id')::bigint as installation_id,\n elem->>'account_id' as account_id,\n elem->>'github_base_url' as github_base_url\n FROM workspace_settings,\n LATERAL jsonb_array_elements(git_app_installations) AS elem\n WHERE workspace_id = $1\n ", "query": "\n SELECT\n (elem->>'installation_id')::bigint as installation_id,\n elem->>'account_id' as account_id\n FROM workspace_settings,\n LATERAL jsonb_array_elements(git_app_installations) AS elem\n WHERE workspace_id = $1\n ",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@@ -12,11 +12,6 @@
"ordinal": 1, "ordinal": 1,
"name": "account_id", "name": "account_id",
"type_info": "Text" "type_info": "Text"
},
{
"ordinal": 2,
"name": "github_base_url",
"type_info": "Text"
} }
], ],
"parameters": { "parameters": {
@@ -25,10 +20,9 @@
] ]
}, },
"nullable": [ "nullable": [
null,
null, null,
null null
] ]
}, },
"hash": "ae7adc583cdd3f876164ed60569ed531b05eaa17fccc599306eb1a96a65ee761" "hash": "0ee14619dd81df460b2b8cc6df2b89646279f77469c35deffca8e17a11d7f6c8"
} }

View File

@@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE v2_job_queue SET suspend = $2, suspend_until = now() + interval '14 day' WHERE id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Int4"
]
},
"nullable": []
},
"hash": "10af387fce25f6ea7af275e8e93b7ab1f2fc29a2ba79a39576551bdf66b592b6"
}

View File

@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "\n WITH _ AS (\n UPDATE debounce_key\n SET debounced_times = 0, -- reset debounced_times\n first_started_at = now(), -- rest\n previous_job_id = NULL\n WHERE job_id = $1\n )\n UPDATE v2_job_debounce_batch \n SET debounce_batch = nextval('debounce_batch_seq') -- move to new batch\n WHERE id = $1\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": []
},
"hash": "16c96166ffa6b9aec65c6072b204b52b87e3c2f3d76e47eb173fc78721355066"
}

View File

@@ -1,6 +1,6 @@
{ {
"db_name": "PostgreSQL", "db_name": "PostgreSQL",
"query": "\n WITH job_info AS (\n SELECT id, kind::text AS kind, parent_job\n FROM v2_job\n WHERE id = $1\n )\n SELECT\n q.id AS \"id!\",\n s.flow_status,\n q.suspend AS \"suspend!\",\n j.runnable_path AS script_path,\n j.permissioned_as_email AS email,\n (ji.kind IN ('flow', 'flowpreview')) AS \"is_flow_level!\",\n (ji.kind NOT IN ('flow', 'flowpreview') AND q.id = ji.id) AS \"is_wac!\"\n FROM job_info ji\n JOIN v2_job_queue q ON q.id = CASE\n WHEN ji.kind IN ('flow', 'flowpreview') THEN ji.id\n ELSE COALESCE(ji.parent_job, ji.id)\n END\n JOIN v2_job j ON j.id = q.id\n LEFT JOIN v2_job_status s ON s.id = q.id\n FOR UPDATE OF q\n ", "query": "\n WITH job_info AS (\n SELECT id, kind::text AS kind, parent_job\n FROM v2_job\n WHERE id = $1\n )\n SELECT\n q.id AS \"id!\",\n s.flow_status,\n q.suspend AS \"suspend!\",\n j.runnable_path AS script_path,\n j.permissioned_as_email AS email,\n (ji.kind IN ('flow', 'flowpreview')) AS \"is_flow_level!\"\n FROM job_info ji\n JOIN v2_job_queue q ON q.id = CASE\n WHEN ji.kind IN ('flow', 'flowpreview') THEN ji.id\n ELSE ji.parent_job\n END\n JOIN v2_job j ON j.id = q.id\n JOIN v2_job_status s ON s.id = q.id\n FOR UPDATE OF q\n ",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@@ -32,11 +32,6 @@
"ordinal": 5, "ordinal": 5,
"name": "is_flow_level!", "name": "is_flow_level!",
"type_info": "Bool" "type_info": "Bool"
},
{
"ordinal": 6,
"name": "is_wac!",
"type_info": "Bool"
} }
], ],
"parameters": { "parameters": {
@@ -50,9 +45,8 @@
false, false,
true, true,
false, false,
null,
null null
] ]
}, },
"hash": "dbc7e74e259b502e700491ee0248e0c9c8c61e1bf609be60ac5dc5d438189353" "hash": "1a0ab65bbf2751f702fc696c1e32a7dd9524cdd806be1ad8e9ab88d4c88d3f82"
} }

View File

@@ -0,0 +1,17 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE job_stats SET timestamps = array_append(timestamps, now()), timeseries_int = array_append(timeseries_int, $4) WHERE workspace_id = $1 AND job_id = $2 AND metric_id = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Uuid",
"Text",
"Int4"
]
},
"nullable": []
},
"hash": "1db82007445ff5f644bb607aa28f5747cb50d193475fff5fcfdde37d1bc74636"
}

View File

@@ -1,23 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT reset_offset FROM kafka_trigger WHERE workspace_id = $1 AND path = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "reset_offset",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false
]
},
"hash": "1df610a583e86edb70c374fd66c68554a6a4291426c09dd5b04fd832f9d31208"
}

View File

@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT EXISTS(SELECT 1 FROM v2_job_queue WHERE id = $1)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "exists",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
null
]
},
"hash": "2c8b8ed14647332491846ae3fa8b0ab8113d52ae8ae613a810c2b452e0972d05"
}

View File

@@ -1,23 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM variable WHERE path = $1 AND workspace_id = $2 RETURNING path",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "path",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false
]
},
"hash": "3317484a9c09c07c2c9db9debaecc4a4d518093ab48e79365dbb808068e0b8ff"
}

View File

@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT (config->>'native_mode')::boolean FROM config WHERE name = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "bool",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null
]
},
"hash": "36b95bc7956eb7bba7cd6fa9cd829980a0bf4970b919cabad1daab16627404fc"
}

View File

@@ -1,23 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO kafka_trigger (\n path, kafka_resource_path, topics, group_id, script_path,\n is_flow, workspace_id, edited_by, email, auto_commit\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"VarcharArray",
"Varchar",
"Varchar",
"Bool",
"Varchar",
"Varchar",
"Varchar",
"Bool"
]
},
"nullable": []
},
"hash": "45fc21026fa76e5d69f00a68a7be81abb3ec627578f2d14f0ce33896dc6ab4cf"
}

View File

@@ -1,16 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE workspace_settings\n SET git_app_installations = (\n SELECT jsonb_agg(\n CASE\n WHEN (elem->>'installation_id')::bigint = $2 THEN $1::jsonb\n ELSE elem\n END\n )\n FROM jsonb_array_elements(git_app_installations) AS elem\n )\n WHERE workspace_id = $3\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Jsonb",
"Int8",
"Text"
]
},
"nullable": []
},
"hash": "48b394bd9ca63d33a7ea97113b0096bd0777da52c05e23262572089e0c3c6c46"
}

View File

@@ -1,23 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT auto_commit FROM kafka_trigger WHERE workspace_id = $1 AND path = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "auto_commit",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false
]
},
"hash": "4b2a29b3ef7ec4802d81ec4b706623b991c938e40d0db25290b03dc0577c2740"
}

View File

@@ -1,23 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT auto_commit\n FROM kafka_trigger\n WHERE workspace_id = $1 AND path = $2\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "auto_commit",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false
]
},
"hash": "50807b807bb901a380926798be655c13a18dfd26e237a8218d3006e2898b5aa3"
}

View File

@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE v2_job_status SET\n workflow_as_code_status = jsonb_set(\n jsonb_set(\n COALESCE(workflow_as_code_status, '{}'::jsonb),\n array[$1],\n COALESCE(workflow_as_code_status->$1, '{}'::jsonb)\n ),\n array[$1, 'duration_ms'],\n to_jsonb($2::bigint)\n )\n WHERE id = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Int8",
"Uuid"
]
},
"nullable": []
},
"hash": "56f7325e3b0316866714e76d94b50d9d258c288883b2b5b0ab286f5cb50850b5"
}

View File

@@ -15,7 +15,7 @@
] ]
}, },
"nullable": [ "nullable": [
null true
] ]
}, },
"hash": "5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55" "hash": "5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55"

View File

@@ -1,6 +1,6 @@
{ {
"db_name": "PostgreSQL", "db_name": "PostgreSQL",
"query": "WITH active_users AS (SELECT distinct username as email FROM (SELECT username, timestamp, operation FROM audit_partitioned UNION ALL SELECT username, timestamp, operation FROM audit) AS a WHERE timestamp > NOW() - INTERVAL '1 month' AND (operation = 'users.login' OR operation = 'oauth.login' OR operation = 'users.token.refresh')),\n authors as (SELECT distinct email FROM usr WHERE usr.operator IS false)\n SELECT email, email NOT IN (SELECT email FROM authors) as operator_only, login_type::text, verified, super_admin, devops, name, company, username, first_time_user\n FROM password\n WHERE email IN (SELECT email FROM active_users)\n ORDER BY super_admin DESC, devops DESC\n LIMIT $1 OFFSET $2", "query": "WITH active_users AS (SELECT distinct username as email FROM audit WHERE timestamp > NOW() - INTERVAL '1 month' AND (operation = 'users.login' OR operation = 'oauth.login' OR operation = 'users.token.refresh')),\n authors as (SELECT distinct email FROM usr WHERE usr.operator IS false)\n SELECT email, email NOT IN (SELECT email FROM authors) as operator_only, login_type::text, verified, super_admin, devops, name, company, username, first_time_user\n FROM password\n WHERE email IN (SELECT email FROM active_users)\n ORDER BY super_admin DESC, devops DESC\n LIMIT $1 OFFSET $2",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@@ -73,5 +73,5 @@
false false
] ]
}, },
"hash": "9229d9a9ff389cf26e480b604b83900e2d362ee934ef27284ef39f4eed440e59" "hash": "72d3ebb05ac1ffeb0e8d0a3146d95bb5b90e7c4d1dc2c8a6ef06eddf6678f230"
} }

View File

@@ -1,6 +1,6 @@
{ {
"db_name": "PostgreSQL", "db_name": "PostgreSQL",
"query": "\n SELECT kafka_resource_path, topics, group_id, mode AS \"mode: String\",\n auto_offset_reset, auto_commit, reset_offset\n FROM kafka_trigger\n WHERE workspace_id = $1 AND path = $2\n ", "query": "\n SELECT kafka_resource_path, topics, group_id, mode AS \"mode: String\"\n FROM kafka_trigger\n WHERE workspace_id = $1 AND path = $2\n ",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@@ -33,21 +33,6 @@
} }
} }
} }
},
{
"ordinal": 4,
"name": "auto_offset_reset",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "auto_commit",
"type_info": "Bool"
},
{
"ordinal": 6,
"name": "reset_offset",
"type_info": "Bool"
} }
], ],
"parameters": { "parameters": {
@@ -57,14 +42,11 @@
] ]
}, },
"nullable": [ "nullable": [
false,
false,
false,
false, false,
false, false,
false, false,
false false
] ]
}, },
"hash": "4cf4be7a981173d3f242887d9313c7e60d23e9827f23c0de5b546ed56697d54a" "hash": "7e3bfb33fb771aec39b43a7550091ce7c9b1261b52d10f4a7f3273fed3c916df"
} }

View File

@@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM kafka_pending_commits WHERE id = ANY($1)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8Array"
]
},
"nullable": []
},
"hash": "80bad96cbec6b5eca57a6380e7515565490a271050dcc4b5aac2b730ae3a55b9"
}

View File

@@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE v2_job_queue SET suspend = GREATEST(suspend - 1, 0) WHERE id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": []
},
"hash": "a35164456ade8e79cb8f5418c8fe82c45be45409f881292f0d4a3316362ba1f4"
}

View File

@@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE v2_job_queue SET running = false, started_at = null WHERE id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": []
},
"hash": "a684f160d1a366c1928fef27c613e6e08f808f423c8f2d58b9c849aba7d176f5"
}

View File

@@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE v2_job_queue SET suspend = 0, suspend_until = NULL WHERE id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": []
},
"hash": "a72081cb042f09034338dcb49381e91093233cf16af0dae666b4743f3878b22e"
}

View File

@@ -1,17 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE job_stats SET offsets_cs = array_append(offsets_cs, (EXTRACT(EPOCH FROM (now() - timeseries_start)) * 100)::int), timeseries_float = array_append(timeseries_float, $4) WHERE workspace_id = $1 AND job_id = $2 AND metric_id = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Uuid",
"Text",
"Float4"
]
},
"nullable": []
},
"hash": "a837494a58ab58bfa18c0385350a861076a5fc4f7eefceb1c0de5cf55293f327"
}

View File

@@ -1,28 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n (elem->>'installation_id')::bigint as installation_id,\n elem->>'github_base_url' as github_base_url\n FROM workspace_settings,\n LATERAL jsonb_array_elements(git_app_installations) AS elem\n WHERE workspace_id = $1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "installation_id",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "github_base_url",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null,
null
]
},
"hash": "ad5fc9212a123a8328397496ef3b5eea1780226698e419ac59ba55012296913d"
}

View File

@@ -1,6 +1,6 @@
{ {
"db_name": "PostgreSQL", "db_name": "PostgreSQL",
"query": "INSERT INTO audit_partitioned\n (workspace_id, username, operation, action_kind, resource, parameters, email, span)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", "query": "INSERT INTO audit\n (workspace_id, username, operation, action_kind, resource, parameters, email, span)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8)",
"describe": { "describe": {
"columns": [], "columns": [],
"parameters": { "parameters": {
@@ -29,5 +29,5 @@
}, },
"nullable": [] "nullable": []
}, },
"hash": "fbccafe6d34093a723b9d5a6ee8d618a80ceba6de2d39202e6293ef5207c31f6" "hash": "ad8487a797713b3a6c10fb399c9fb8dcd940bb92e998145e250f28ccfe1c7033"
} }

View File

@@ -1,6 +1,6 @@
{ {
"db_name": "PostgreSQL", "db_name": "PostgreSQL",
"query": "\n INSERT INTO kafka_trigger (\n workspace_id,\n path,\n kafka_resource_path,\n group_id,\n topics,\n filters,\n auto_offset_reset,\n auto_commit,\n script_path,\n is_flow,\n mode,\n edited_by,\n email,\n edited_at,\n error_handler_path,\n error_handler_args,\n retry\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, now(), $14, $15, $16\n )\n ", "query": "\n INSERT INTO kafka_trigger (\n workspace_id,\n path,\n kafka_resource_path,\n group_id,\n topics,\n filters,\n script_path,\n is_flow,\n mode,\n edited_by,\n email,\n edited_at,\n error_handler_path,\n error_handler_args,\n retry\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, now(), $12, $13, $14\n )\n ",
"describe": { "describe": {
"columns": [], "columns": [],
"parameters": { "parameters": {
@@ -13,8 +13,6 @@
"JsonbArray", "JsonbArray",
"Varchar", "Varchar",
"Bool", "Bool",
"Varchar",
"Bool",
{ {
"Custom": { "Custom": {
"name": "trigger_mode", "name": "trigger_mode",
@@ -36,5 +34,5 @@
}, },
"nullable": [] "nullable": []
}, },
"hash": "5dd6315ec270c268e905262e4b0a920837354d91a0ae16b1236c1267da71765f" "hash": "aed5439aa6dad950e505f9f8f6914fa5ca21319c501b2822c9bc751ddfc9a0a4"
} }

View File

@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE workspace_settings\n SET git_app_installations = (\n SELECT jsonb_agg(\n CASE\n WHEN (elem->>'installation_id')::bigint = $2 THEN $1::jsonb\n ELSE elem\n END\n )\n FROM jsonb_array_elements(git_app_installations) AS elem\n )\n WHERE workspace_id = $3\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Jsonb",
"Int8",
"Text"
]
},
"nullable": []
},
"hash": "b4d48c820bf41619bffa8f62367e98369e1d93514e1618723a34bf96080d4ebc"
}

View File

@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE v2_job_queue SET suspend = GREATEST(suspend - 1, 0) WHERE id = $1 RETURNING suspend",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "suspend",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false
]
},
"hash": "b663e6baf2f8da00c6d94e5b8e35be9d9f51071978c0e48df4284d8b90000a4a"
}

View File

@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n (elem->>'installation_id')::bigint as installation_id\n FROM workspace_settings,\n LATERAL jsonb_array_elements(git_app_installations) AS elem\n WHERE workspace_id = $1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "installation_id",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null
]
},
"hash": "be00ac55e8668a0ed3befda7d8595c7cda0cba0b119d4fdb8a0dea1b28a1d560"
}

View File

@@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO v2_job_completed\n (workspace_id, id, started_at, duration_ms, result, memory_peak, status, worker)\n SELECT q.workspace_id, q.id, q.started_at,\n COALESCE((EXTRACT('epoch' FROM now()) - EXTRACT('epoch' FROM COALESCE(q.started_at, now()))) * 1000, 0)::bigint,\n $2::jsonb, r.memory_peak, 'failure'::job_status, q.worker\n FROM v2_job_queue q\n LEFT JOIN v2_job_runtime r ON r.id = q.id\n WHERE q.id = $1\n ON CONFLICT (id) DO UPDATE SET status = 'failure', result = $2::jsonb",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Jsonb"
]
},
"nullable": []
},
"hash": "beecb176df512e4a94771d0d73c4c597e07e53d499131b57e4d6441fd0af09cb"
}

View File

@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM resource WHERE path = $1 AND workspace_id = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": []
},
"hash": "bf2aeb9a1e649106d2a084c1d628690a44573c1869a206474811215714ba97c2"
}

View File

@@ -1,23 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE kafka_trigger SET reset_offset = true, server_id = NULL WHERE workspace_id = $1 AND path = $2 RETURNING true",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "?column?",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
null
]
},
"hash": "c2f38c9e09aac73d10e8f327715927c07832badb2c9145d5996b829163bdf7d9"
}

View File

@@ -1,24 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE v2_job_status SET\n workflow_as_code_status = jsonb_set(\n jsonb_set(\n workflow_as_code_status,\n array[$1],\n COALESCE(workflow_as_code_status->$1, '{}'::jsonb)\n ),\n array[$1, 'duration_ms'],\n to_jsonb($2::bigint)\n )\n WHERE id = $3 AND workflow_as_code_status IS NOT NULL\n RETURNING workflow_as_code_status->'_checkpoint'->'pending_steps'->'job_ids' AS \"job_ids: serde_json::Value\"",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "job_ids: serde_json::Value",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Text",
"Int8",
"Uuid"
]
},
"nullable": [
null
]
},
"hash": "c331609952e0b98d36f605bd5d2933aa54523bf44d63e304440e53b9eadd5340"
}

View File

@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT args FROM v2_job WHERE id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "args",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
true
]
},
"hash": "c944e384c4b4c6455b431978adc54d176f294b661675392cf92561c0e6e02e6e"
}

View File

@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "\n WITH _ AS (\n UPDATE debounce_key\n SET debounced_times = 0,\n first_started_at = now(),\n previous_job_id = NULL\n WHERE job_id = $1\n )\n UPDATE v2_job_debounce_batch\n SET debounce_batch = nextval('debounce_batch_seq')\n WHERE id = $1\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": []
},
"hash": "c9530931f670eab1208c4a284a55afdc3fcbb0eb5f98fd63e2ec89442becbfaa"
}

View File

@@ -0,0 +1,17 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE job_stats SET timestamps = array_append(timestamps, now()), timeseries_float = array_append(timeseries_float, $4) WHERE workspace_id = $1 AND job_id = $2 AND metric_id = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Uuid",
"Text",
"Float4"
]
},
"nullable": []
},
"hash": "d44c37882150532383d1058639f4d3af288a346c50f29809dbc55a34088a0abc"
}

View File

@@ -0,0 +1,27 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE kafka_trigger\n SET\n kafka_resource_path = $1,\n group_id = $2,\n topics = $3,\n filters = $4,\n script_path = $5,\n path = $6,\n is_flow = $7,\n edited_by = $8,\n email = $9,\n edited_at = now(),\n server_id = NULL,\n error = NULL,\n error_handler_path = $12,\n error_handler_args = $13,\n retry = $14\n WHERE\n workspace_id = $10 AND path = $11\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"VarcharArray",
"JsonbArray",
"Varchar",
"Varchar",
"Bool",
"Varchar",
"Varchar",
"Text",
"Text",
"Varchar",
"Jsonb",
"Jsonb"
]
},
"nullable": []
},
"hash": "e2921e44c70cf6c76c55177f2b56985e84c59ecb3e1a13fcf27d5f7ae5f8d84c"
}

View File

@@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE kafka_trigger SET reset_offset = false WHERE workspace_id = $1 AND path = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": []
},
"hash": "ef15599f532fab2cbb487542ffec047cf3b7ce22ce868db1b1a63e6c10d0d12b"
}

View File

@@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE v2_job_queue SET suspend = 1, suspend_until = now() + make_interval(secs => $2) WHERE id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Float8"
]
},
"nullable": []
},
"hash": "f56c58fea9f27d2e55d33720e032808e90cb068d1048717f82992f476377cc20"
}

View File

@@ -1,18 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO kafka_pending_commits (workspace_id, kafka_trigger_path, topic, partition, \"offset\")\n VALUES ($1, $2, $3, $4, $5)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Varchar",
"Int4",
"Int8"
]
},
"nullable": []
},
"hash": "f67e5c96eb9cb35953d4c3e83e0fcbb5b647737e0366529a2f418218b1a74679"
}

313
backend/Cargo.lock generated
View File

@@ -169,9 +169,9 @@ dependencies = [
[[package]] [[package]]
name = "anstream" name = "anstream"
version = "1.0.0" version = "0.6.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
dependencies = [ dependencies = [
"anstyle", "anstyle",
"anstyle-parse", "anstyle-parse",
@@ -184,15 +184,15 @@ dependencies = [
[[package]] [[package]]
name = "anstyle" name = "anstyle"
version = "1.0.14" version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
[[package]] [[package]]
name = "anstyle-parse" name = "anstyle-parse"
version = "1.0.0" version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
dependencies = [ dependencies = [
"utf8parse", "utf8parse",
] ]
@@ -1860,9 +1860,9 @@ dependencies = [
[[package]] [[package]]
name = "bon" name = "bon"
version = "3.9.1" version = "3.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f47dbe92550676ee653353c310dfb9cf6ba17ee70396e1f7cf0a2020ad49b2fe" checksum = "2d13a61f2963b88eef9c1be03df65d42f6996dfeac1054870d950fcf66686f83"
dependencies = [ dependencies = [
"bon-macros", "bon-macros",
"rustversion", "rustversion",
@@ -1870,9 +1870,9 @@ dependencies = [
[[package]] [[package]]
name = "bon-macros" name = "bon-macros"
version = "3.9.1" version = "3.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "519bd3116aeeb42d5372c29d982d16d0170d3d4a5ed85fc7dd91642ffff3c67c" checksum = "d314cc62af2b6b0c65780555abb4d02a03dd3b799cd42419044f0c38d99738c0"
dependencies = [ dependencies = [
"darling 0.23.0", "darling 0.23.0",
"ident_case", "ident_case",
@@ -2208,9 +2208,9 @@ dependencies = [
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.57" version = "1.2.56"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2"
dependencies = [ dependencies = [
"find-msvc-tools", "find-msvc-tools",
"jobserver", "jobserver",
@@ -2323,9 +2323,9 @@ dependencies = [
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.6.0" version = "4.5.60"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive", "clap_derive",
@@ -2333,9 +2333,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_builder" name = "clap_builder"
version = "4.6.0" version = "4.5.60"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876"
dependencies = [ dependencies = [
"anstream", "anstream",
"anstyle", "anstyle",
@@ -2345,9 +2345,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_derive" name = "clap_derive"
version = "4.6.0" version = "4.5.55"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5"
dependencies = [ dependencies = [
"heck 0.5.0", "heck 0.5.0",
"proc-macro2", "proc-macro2",
@@ -2357,9 +2357,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_lex" name = "clap_lex"
version = "1.1.0" version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831"
[[package]] [[package]]
name = "clipboard-win" name = "clipboard-win"
@@ -2418,9 +2418,9 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]] [[package]]
name = "colorchoice" name = "colorchoice"
version = "1.0.5" version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
[[package]] [[package]]
name = "combine" name = "combine"
@@ -7103,7 +7103,7 @@ dependencies = [
"libc", "libc",
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"socket2 0.6.3", "socket2 0.6.2",
"system-configuration", "system-configuration",
"tokio", "tokio",
"tower-service", "tower-service",
@@ -7974,9 +7974,9 @@ dependencies = [
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.183" version = "0.2.182"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
[[package]] [[package]]
name = "libffi" name = "libffi"
@@ -8102,9 +8102,9 @@ dependencies = [
[[package]] [[package]]
name = "libz-sys" name = "libz-sys"
version = "1.1.25" version = "1.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d52f4c29e2a68ac30c9087e1b772dc9f44a2b66ed44edf2266cf2be9b03dafc1" checksum = "4735e9cbde5aac84a5ce588f6b23a90b9b0b528f6c5a8db8a4aff300463a0839"
dependencies = [ dependencies = [
"cc", "cc",
"libc", "libc",
@@ -9383,9 +9383,9 @@ checksum = "80adb31078122c880307e9cdfd4e3361e6545c319f9b9dcafcb03acd3b51a575"
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.21.4" version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]] [[package]]
name = "once_cell_polyfill" name = "once_cell_polyfill"
@@ -9460,9 +9460,9 @@ dependencies = [
[[package]] [[package]]
name = "openssl" name = "openssl"
version = "0.10.76" version = "0.10.75"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328"
dependencies = [ dependencies = [
"bitflags 2.9.4", "bitflags 2.9.4",
"cfg-if", "cfg-if",
@@ -9507,9 +9507,9 @@ dependencies = [
[[package]] [[package]]
name = "openssl-sys" name = "openssl-sys"
version = "0.9.112" version = "0.9.111"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321"
dependencies = [ dependencies = [
"cc", "cc",
"libc", "libc",
@@ -10641,9 +10641,9 @@ dependencies = [
[[package]] [[package]]
name = "quick_cache" name = "quick_cache"
version = "0.6.19" version = "0.6.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "530e84778a55de0f52645a51d4e3b9554978acd6a1e7cd50b6a6784692b3029e" checksum = "7ada44a88ef953a3294f6eb55d2007ba44646015e18613d2f213016379203ef3"
dependencies = [ dependencies = [
"ahash 0.8.12", "ahash 0.8.12",
"equivalent", "equivalent",
@@ -10664,7 +10664,7 @@ dependencies = [
"quinn-udp", "quinn-udp",
"rustc-hash 2.1.1", "rustc-hash 2.1.1",
"rustls 0.23.35", "rustls 0.23.35",
"socket2 0.6.3", "socket2 0.6.2",
"thiserror 2.0.18", "thiserror 2.0.18",
"tokio", "tokio",
"tracing", "tracing",
@@ -10673,9 +10673,9 @@ dependencies = [
[[package]] [[package]]
name = "quinn-proto" name = "quinn-proto"
version = "0.11.14" version = "0.11.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31"
dependencies = [ dependencies = [
"aws-lc-rs", "aws-lc-rs",
"bytes", "bytes",
@@ -10702,7 +10702,7 @@ dependencies = [
"cfg_aliases 0.2.1", "cfg_aliases 0.2.1",
"libc", "libc",
"once_cell", "once_cell",
"socket2 0.6.3", "socket2 0.6.2",
"tracing", "tracing",
"windows-sys 0.60.2", "windows-sys 0.60.2",
] ]
@@ -11987,9 +11987,9 @@ checksum = "ece8e78b2f38ec51c51f5d475df0a7187ba5111b2a28bdc761ee05b075d40a71"
[[package]] [[package]]
name = "schannel" name = "schannel"
version = "0.1.29" version = "0.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1"
dependencies = [ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
@@ -12677,12 +12677,12 @@ dependencies = [
[[package]] [[package]]
name = "socket2" name = "socket2"
version = "0.6.3" version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.61.2", "windows-sys 0.60.2",
] ]
[[package]] [[package]]
@@ -13854,9 +13854,9 @@ dependencies = [
[[package]] [[package]]
name = "tempfile" name = "tempfile"
version = "3.27.0" version = "3.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0"
dependencies = [ dependencies = [
"fastrand", "fastrand",
"getrandom 0.4.2", "getrandom 0.4.2",
@@ -14462,7 +14462,7 @@ dependencies = [
"indexmap 2.11.1", "indexmap 2.11.1",
"toml_datetime 0.7.0", "toml_datetime 0.7.0",
"toml_parser", "toml_parser",
"winnow 0.7.15", "winnow 0.7.14",
] ]
[[package]] [[package]]
@@ -14471,7 +14471,7 @@ version = "1.0.9+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4"
dependencies = [ dependencies = [
"winnow 0.7.15", "winnow 0.7.14",
] ]
[[package]] [[package]]
@@ -14697,9 +14697,9 @@ dependencies = [
[[package]] [[package]]
name = "tracing-subscriber" name = "tracing-subscriber"
version = "0.3.23" version = "0.3.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e"
dependencies = [ dependencies = [
"matchers", "matchers",
"nu-ansi-term", "nu-ansi-term",
@@ -15741,7 +15741,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill" name = "windmill"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-nats", "async-nats",
@@ -15808,7 +15808,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-alerting" name = "windmill-alerting"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"axum 0.7.9", "axum 0.7.9",
"chrono", "chrono",
@@ -15821,7 +15821,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-api" name = "windmill-api"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"argon2", "argon2",
@@ -15849,7 +15849,6 @@ dependencies = [
"dashmap 6.1.0", "dashmap 6.1.0",
"datafusion", "datafusion",
"ed25519-dalek", "ed25519-dalek",
"eventsource-stream",
"flate2", "flate2",
"futures", "futures",
"git-version", "git-version",
@@ -15941,7 +15940,6 @@ dependencies = [
"windmill-parser-py", "windmill-parser-py",
"windmill-parser-py-imports", "windmill-parser-py-imports",
"windmill-parser-sql", "windmill-parser-sql",
"windmill-parser-sql-asset",
"windmill-parser-ts", "windmill-parser-ts",
"windmill-queue", "windmill-queue",
"windmill-store", "windmill-store",
@@ -15962,7 +15960,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-api-agent-workers" name = "windmill-api-agent-workers"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"axum 0.7.9", "axum 0.7.9",
"chrono", "chrono",
@@ -15985,7 +15983,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-api-assets" name = "windmill-api-assets"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"axum 0.7.9", "axum 0.7.9",
"chrono", "chrono",
@@ -15998,7 +15996,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-api-auth" name = "windmill-api-auth"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"axum 0.7.9", "axum 0.7.9",
@@ -16024,7 +16022,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-api-client" name = "windmill-api-client"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"reqwest 0.12.28", "reqwest 0.12.28",
"serde", "serde",
@@ -16034,7 +16032,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-api-configs" name = "windmill-api-configs"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"axum 0.7.9", "axum 0.7.9",
"chrono", "chrono",
@@ -16051,7 +16049,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-api-debug" name = "windmill-api-debug"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"axum 0.7.9", "axum 0.7.9",
"base64 0.22.1", "base64 0.22.1",
@@ -16074,7 +16072,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-api-embeddings" name = "windmill-api-embeddings"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"axum 0.7.9", "axum 0.7.9",
@@ -16097,7 +16095,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-api-flow-conversations" name = "windmill-api-flow-conversations"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"axum 0.7.9", "axum 0.7.9",
"chrono", "chrono",
@@ -16113,7 +16111,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-api-flows" name = "windmill-api-flows"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"axum 0.7.9", "axum 0.7.9",
"chrono", "chrono",
@@ -16133,7 +16131,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-api-groups" name = "windmill-api-groups"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"axum 0.7.9", "axum 0.7.9",
"chrono", "chrono",
@@ -16153,7 +16151,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-api-inputs" name = "windmill-api-inputs"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"axum 0.7.9", "axum 0.7.9",
"chrono", "chrono",
@@ -16167,7 +16165,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-api-integration-tests" name = "windmill-api-integration-tests"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-nats", "async-nats",
@@ -16187,7 +16185,6 @@ dependencies = [
"windmill-api-auth", "windmill-api-auth",
"windmill-api-client", "windmill-api-client",
"windmill-common", "windmill-common",
"windmill-git-sync",
"windmill-native-triggers", "windmill-native-triggers",
"windmill-test-utils", "windmill-test-utils",
"windmill-worker", "windmill-worker",
@@ -16195,7 +16192,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-api-jobs" name = "windmill-api-jobs"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"axum 0.7.9", "axum 0.7.9",
@@ -16220,7 +16217,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-api-npm-proxy" name = "windmill-api-npm-proxy"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"axum 0.7.9", "axum 0.7.9",
"flate2", "flate2",
@@ -16238,7 +16235,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-api-openapi" name = "windmill-api-openapi"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"axum 0.7.9", "axum 0.7.9",
@@ -16259,7 +16256,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-api-schedule" name = "windmill-api-schedule"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"axum 0.7.9", "axum 0.7.9",
"chrono", "chrono",
@@ -16279,7 +16276,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-api-scripts" name = "windmill-api-scripts"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"axum 0.7.9", "axum 0.7.9",
"chrono", "chrono",
@@ -16309,7 +16306,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-api-settings" name = "windmill-api-settings"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"axum 0.7.9", "axum 0.7.9",
@@ -16336,7 +16333,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-api-sse" name = "windmill-api-sse"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"lazy_static", "lazy_static",
"serde", "serde",
@@ -16348,7 +16345,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-api-users" name = "windmill-api-users"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"argon2", "argon2",
"axum 0.7.9", "axum 0.7.9",
@@ -16371,7 +16368,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-api-workers" name = "windmill-api-workers"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"axum 0.7.9", "axum 0.7.9",
"chrono", "chrono",
@@ -16385,7 +16382,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-api-workspaces" name = "windmill-api-workspaces"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"axum 0.7.9", "axum 0.7.9",
"chrono", "chrono",
@@ -16416,7 +16413,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-audit" name = "windmill-audit"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"chrono", "chrono",
"lazy_static", "lazy_static",
@@ -16430,7 +16427,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-autoscaling" name = "windmill-autoscaling"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"axum 0.7.9", "axum 0.7.9",
@@ -16449,7 +16446,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-common" name = "windmill-common"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"aes-gcm", "aes-gcm",
"anyhow", "anyhow",
@@ -16548,7 +16545,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-dep-map" name = "windmill-dep-map"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"chrono", "chrono",
"itertools 0.14.0", "itertools 0.14.0",
@@ -16567,7 +16564,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-git-sync" name = "windmill-git-sync"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"regex", "regex",
"serde", "serde",
@@ -16582,7 +16579,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-indexer" name = "windmill-indexer"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"astral-tokio-tar", "astral-tokio-tar",
@@ -16606,7 +16603,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-jseval" name = "windmill-jseval"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"futures", "futures",
@@ -16623,7 +16620,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-macros" name = "windmill-macros"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"itertools 0.14.0", "itertools 0.14.0",
"lazy_static", "lazy_static",
@@ -16639,7 +16636,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-mcp" name = "windmill-mcp"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@@ -16660,7 +16657,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-native-triggers" name = "windmill-native-triggers"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@@ -16691,7 +16688,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-oauth" name = "windmill-oauth"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-oauth2", "async-oauth2",
@@ -16715,7 +16712,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-object-store" name = "windmill-object-store"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-stream", "async-stream",
@@ -16749,7 +16746,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-operator" name = "windmill-operator"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"futures", "futures",
@@ -16767,7 +16764,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-parser" name = "windmill-parser"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"convert_case 0.6.0", "convert_case 0.6.0",
"serde", "serde",
@@ -16776,7 +16773,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-parser-bash" name = "windmill-parser-bash"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"lazy_static", "lazy_static",
@@ -16788,7 +16785,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-parser-csharp" name = "windmill-parser-csharp"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"serde_json", "serde_json",
@@ -16800,7 +16797,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-parser-go" name = "windmill-parser-go"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"gosyn", "gosyn",
@@ -16812,7 +16809,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-parser-graphql" name = "windmill-parser-graphql"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"lazy_static", "lazy_static",
@@ -16824,7 +16821,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-parser-java" name = "windmill-parser-java"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"serde_json", "serde_json",
@@ -16836,7 +16833,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-parser-nu" name = "windmill-parser-nu"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"nu-parser", "nu-parser",
@@ -16847,7 +16844,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-parser-php" name = "windmill-parser-php"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"itertools 0.14.0", "itertools 0.14.0",
@@ -16858,7 +16855,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-parser-py" name = "windmill-parser-py"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"itertools 0.14.0", "itertools 0.14.0",
@@ -16866,22 +16863,12 @@ dependencies = [
"rustpython-parser", "rustpython-parser",
"serde_json", "serde_json",
"windmill-parser", "windmill-parser",
] "windmill-parser-sql",
[[package]]
name = "windmill-parser-py-asset"
version = "1.657.0"
dependencies = [
"anyhow",
"rustpython-ast",
"rustpython-parser",
"windmill-parser",
"windmill-parser-sql-asset",
] ]
[[package]] [[package]]
name = "windmill-parser-py-imports" name = "windmill-parser-py-imports"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-recursion", "async-recursion",
@@ -16905,7 +16892,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-parser-ruby" name = "windmill-parser-ruby"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"lazy_static", "lazy_static",
@@ -16919,7 +16906,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-parser-rust" name = "windmill-parser-rust"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"convert_case 0.6.0", "convert_case 0.6.0",
@@ -16936,7 +16923,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-parser-sql" name = "windmill-parser-sql"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"lazy_static", "lazy_static",
@@ -16944,17 +16931,6 @@ dependencies = [
"regex-lite", "regex-lite",
"serde", "serde",
"serde_json", "serde_json",
"windmill-parser",
"windmill-types",
]
[[package]]
name = "windmill-parser-sql-asset"
version = "1.657.0"
dependencies = [
"anyhow",
"serde",
"serde_json",
"sqlparser 0.59.0", "sqlparser 0.59.0",
"windmill-parser", "windmill-parser",
"windmill-types", "windmill-types",
@@ -16962,7 +16938,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-parser-ts" name = "windmill-parser-ts"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"lazy_static", "lazy_static",
@@ -16976,43 +16952,12 @@ dependencies = [
"triomphe", "triomphe",
"wasm-bindgen", "wasm-bindgen",
"windmill-parser", "windmill-parser",
] "windmill-parser-sql",
[[package]]
name = "windmill-parser-ts-asset"
version = "1.657.0"
dependencies = [
"anyhow",
"serde-wasm-bindgen",
"swc_common",
"swc_ecma_ast",
"swc_ecma_parser",
"swc_ecma_visit",
"triomphe",
"wasm-bindgen",
"windmill-parser",
"windmill-parser-sql-asset",
]
[[package]]
name = "windmill-parser-wac"
version = "1.657.0"
dependencies = [
"anyhow",
"rustpython-ast",
"rustpython-parser",
"serde",
"serde_json",
"sha2 0.10.9",
"swc_common",
"swc_ecma_ast",
"swc_ecma_parser",
"swc_ecma_visit",
] ]
[[package]] [[package]]
name = "windmill-parser-yaml" name = "windmill-parser-yaml"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"serde", "serde",
@@ -17023,7 +16968,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-queue" name = "windmill-queue"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-recursion", "async-recursion",
@@ -17060,7 +17005,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-runtime-nativets" name = "windmill-runtime-nativets"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"const_format", "const_format",
@@ -17098,7 +17043,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-sql-datatype-parser-wasm" name = "windmill-sql-datatype-parser-wasm"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"getrandom 0.3.4", "getrandom 0.3.4",
"wasm-bindgen", "wasm-bindgen",
@@ -17109,7 +17054,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-store" name = "windmill-store"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-recursion", "async-recursion",
@@ -17138,7 +17083,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-test-utils" name = "windmill-test-utils"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"axum 0.7.9", "axum 0.7.9",
@@ -17161,7 +17106,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-trigger" name = "windmill-trigger"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@@ -17194,7 +17139,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-trigger-email" name = "windmill-trigger-email"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@@ -17214,7 +17159,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-trigger-gcp" name = "windmill-trigger-gcp"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@@ -17248,7 +17193,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-trigger-http" name = "windmill-trigger-http"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@@ -17283,7 +17228,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-trigger-kafka" name = "windmill-trigger-kafka"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@@ -17306,7 +17251,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-trigger-mqtt" name = "windmill-trigger-mqtt"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@@ -17330,7 +17275,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-trigger-nats" name = "windmill-trigger-nats"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-nats", "async-nats",
@@ -17354,7 +17299,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-trigger-postgres" name = "windmill-trigger-postgres"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@@ -17389,7 +17334,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-trigger-sqs" name = "windmill-trigger-sqs"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@@ -17417,7 +17362,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-trigger-websocket" name = "windmill-trigger-websocket"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@@ -17440,7 +17385,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-types" name = "windmill-types"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bitflags 2.9.4", "bitflags 2.9.4",
@@ -17458,7 +17403,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-worker" name = "windmill-worker"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-once-cell", "async-once-cell",
@@ -17564,7 +17509,7 @@ dependencies = [
[[package]] [[package]]
name = "windmill-worker-volumes" name = "windmill-worker-volumes"
version = "1.657.0" version = "1.651.1"
dependencies = [ dependencies = [
"bytes", "bytes",
"futures", "futures",
@@ -18164,9 +18109,9 @@ dependencies = [
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "0.7.15" version = "0.7.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
@@ -18447,18 +18392,18 @@ dependencies = [
[[package]] [[package]]
name = "zerocopy" name = "zerocopy"
version = "0.8.42" version = "0.8.40"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3" checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5"
dependencies = [ dependencies = [
"zerocopy-derive", "zerocopy-derive",
] ]
[[package]] [[package]]
name = "zerocopy-derive" name = "zerocopy-derive"
version = "0.8.42" version = "0.8.40"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f" checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "windmill" name = "windmill"
version = "1.657.0" version = "1.651.1"
authors.workspace = true authors.workspace = true
edition.workspace = true edition.workspace = true
@@ -59,7 +59,6 @@ members = [
"./windmill-oauth", "./windmill-oauth",
"./parsers/windmill-parser", "./parsers/windmill-parser",
"./parsers/windmill-parser-ts", "./parsers/windmill-parser-ts",
"./parsers/windmill-parser-ts-asset",
"./parsers/windmill-parser-go", "./parsers/windmill-parser-go",
"./parsers/windmill-parser-rust", "./parsers/windmill-parser-rust",
"./parsers/windmill-parser-csharp", "./parsers/windmill-parser-csharp",
@@ -68,11 +67,7 @@ members = [
"./parsers/windmill-parser-ruby", "./parsers/windmill-parser-ruby",
"./parsers/windmill-parser-bash", "./parsers/windmill-parser-bash",
"./parsers/windmill-parser-py", "./parsers/windmill-parser-py",
"./parsers/windmill-parser-py-asset",
"./parsers/windmill-parser-py-imports", "./parsers/windmill-parser-py-imports",
"./parsers/windmill-parser-wac",
"./parsers/windmill-parser-sql",
"./parsers/windmill-parser-sql-asset",
"./parsers/windmill-sql-datatype-parser-wasm", "./parsers/windmill-sql-datatype-parser-wasm",
"./parsers/windmill-parser-yaml", "windmill-macros", "parsers/windmill-parser-nu", "./parsers/windmill-parser-yaml", "windmill-macros", "parsers/windmill-parser-nu",
"./windmill-worker-volumes", "./windmill-worker-volumes",
@@ -82,7 +77,7 @@ members = [
exclude = ["./windmill-duckdb-ffi-internal"] exclude = ["./windmill-duckdb-ffi-internal"]
[workspace.package] [workspace.package]
version = "1.657.0" version = "1.651.1"
authors = ["Ruben Fiszel <ruben@windmill.dev>"] authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021" edition = "2021"
@@ -324,9 +319,7 @@ windmill-api-workers = { path = "./windmill-api-workers" }
windmill-store = { path = "./windmill-store" } windmill-store = { path = "./windmill-store" }
windmill-parser = { path = "./parsers/windmill-parser" } windmill-parser = { path = "./parsers/windmill-parser" }
windmill-parser-ts = { path = "./parsers/windmill-parser-ts" } windmill-parser-ts = { path = "./parsers/windmill-parser-ts" }
windmill-parser-ts-asset = { path = "./parsers/windmill-parser-ts-asset" }
windmill-parser-py = { path = "./parsers/windmill-parser-py" } windmill-parser-py = { path = "./parsers/windmill-parser-py" }
windmill-parser-py-asset = { path = "./parsers/windmill-parser-py-asset" }
windmill-parser-py-imports = { path = "./parsers/windmill-parser-py-imports" } windmill-parser-py-imports = { path = "./parsers/windmill-parser-py-imports" }
windmill-parser-go = { path = "./parsers/windmill-parser-go" } windmill-parser-go = { path = "./parsers/windmill-parser-go" }
windmill-parser-rust = { path = "./parsers/windmill-parser-rust" } windmill-parser-rust = { path = "./parsers/windmill-parser-rust" }
@@ -337,10 +330,8 @@ windmill-parser-ruby = { path = "./parsers/windmill-parser-ruby" }
windmill-parser-nu = { path = "./parsers/windmill-parser-nu" } windmill-parser-nu = { path = "./parsers/windmill-parser-nu" }
windmill-parser-bash = { path = "./parsers/windmill-parser-bash" } windmill-parser-bash = { path = "./parsers/windmill-parser-bash" }
windmill-parser-sql = { path = "./parsers/windmill-parser-sql" } windmill-parser-sql = { path = "./parsers/windmill-parser-sql" }
windmill-parser-sql-asset = { path = "./parsers/windmill-parser-sql-asset" }
windmill-parser-graphql = { path = "./parsers/windmill-parser-graphql" } windmill-parser-graphql = { path = "./parsers/windmill-parser-graphql" }
windmill-parser-php = { path = "./parsers/windmill-parser-php" } windmill-parser-php = { path = "./parsers/windmill-parser-php" }
windmill-parser-wac = { path = "./parsers/windmill-parser-wac" }
windmill-jseval = { path = "./windmill-jseval" } windmill-jseval = { path = "./windmill-jseval" }
windmill-runtime-nativets = { path = "./windmill-runtime-nativets" } windmill-runtime-nativets = { path = "./windmill-runtime-nativets" }
windmill-api-client = { path = "./windmill-api-client" } windmill-api-client = { path = "./windmill-api-client" }

View File

@@ -1 +1 @@
c74c86b78a66b976fd9968b21f77903723e668ec f9549c813b3dba5324ea9d1edacc8756a6d699bf

View File

@@ -1,2 +0,0 @@
ALTER TABLE kafka_trigger DROP COLUMN auto_offset_reset;
ALTER TABLE kafka_trigger DROP COLUMN reset_offset;

View File

@@ -1,2 +0,0 @@
ALTER TABLE kafka_trigger ADD COLUMN auto_offset_reset VARCHAR(10) NOT NULL DEFAULT 'latest';
ALTER TABLE kafka_trigger ADD COLUMN reset_offset BOOLEAN NOT NULL DEFAULT FALSE;

View File

@@ -1,2 +0,0 @@
ALTER TABLE job_stats DROP COLUMN IF EXISTS timeseries_start;
ALTER TABLE job_stats DROP COLUMN IF EXISTS offsets_cs;

View File

@@ -1,5 +0,0 @@
-- Store timeseries timestamps as a start time + integer centisecond offsets
-- instead of full TIMESTAMPTZ[] arrays. Saves ~4 bytes per data point.
-- i32 centiseconds gives ~248 days of range with 10ms precision.
ALTER TABLE job_stats ADD COLUMN IF NOT EXISTS timeseries_start TIMESTAMPTZ;
ALTER TABLE job_stats ADD COLUMN IF NOT EXISTS offsets_cs INTEGER[];

View File

@@ -1 +0,0 @@
DROP TABLE IF EXISTS audit_partitioned CASCADE;

View File

@@ -1,58 +0,0 @@
-- Create a new daily-partitioned audit table alongside the existing one.
-- New inserts go to audit_partitioned; reads UNION ALL both tables.
-- The old audit table empties out naturally via retention cleanup.
CREATE TABLE audit_partitioned (
workspace_id VARCHAR(50) NOT NULL,
id BIGINT NOT NULL DEFAULT nextval('audit_id_seq'),
timestamp TIMESTAMPTZ NOT NULL DEFAULT now(),
username VARCHAR(255) NOT NULL,
operation VARCHAR(50) NOT NULL,
action_kind ACTION_KIND NOT NULL,
resource VARCHAR(255),
parameters JSONB,
email VARCHAR(255),
span VARCHAR(255),
PRIMARY KEY (id, timestamp)
) PARTITION BY RANGE (timestamp);
-- Create daily partitions for today + 3 days
DO $$
DECLARE
curr_date DATE := CURRENT_DATE;
end_date DATE := CURRENT_DATE + INTERVAL '3 days';
BEGIN
WHILE curr_date <= end_date LOOP
EXECUTE format(
'CREATE TABLE %I PARTITION OF audit_partitioned FOR VALUES FROM (%L) TO (%L)',
'audit_' || to_char(curr_date, 'YYYYMMDD'),
curr_date,
curr_date + INTERVAL '1 day'
);
curr_date := curr_date + INTERVAL '1 day';
END LOOP;
END $$;
-- Indexes (auto-propagated to all current and future partitions)
CREATE INDEX ix_audit_partitioned_timestamps ON audit_partitioned (timestamp DESC);
CREATE INDEX idx_audit_partitioned_workspace ON audit_partitioned (workspace_id, timestamp DESC);
CREATE INDEX idx_audit_partitioned_recent_login_activities
ON audit_partitioned (timestamp, username)
WHERE operation IN ('users.login', 'oauth.login', 'users.token.refresh');
-- Grants (match the old audit table)
GRANT ALL ON audit_partitioned TO windmill_user;
GRANT ALL ON audit_partitioned TO windmill_admin;
-- RLS (match the old audit table)
ALTER TABLE audit_partitioned ENABLE ROW LEVEL SECURITY;
CREATE POLICY admin_policy ON audit_partitioned FOR ALL TO windmill_admin USING (true);
CREATE POLICY see_own ON audit_partitioned FOR ALL TO windmill_user
USING ((username)::text = current_setting('session.user'::text));
CREATE POLICY schedule ON audit_partitioned FOR INSERT TO windmill_user
WITH CHECK ((username)::text ~~ 'schedule-%'::text);
CREATE POLICY schedule_audit ON audit_partitioned FOR INSERT TO windmill_user
WITH CHECK ((parameters ->> 'end_user'::text) ~~ 'schedule-%'::text);
CREATE POLICY webhook ON audit_partitioned FOR INSERT TO windmill_user
WITH CHECK ((username)::text ~~ 'webhook-%'::text);

View File

@@ -1,2 +0,0 @@
DROP TABLE IF EXISTS kafka_pending_commits;
ALTER TABLE kafka_trigger DROP COLUMN auto_commit;

View File

@@ -1,14 +0,0 @@
ALTER TABLE kafka_trigger ADD COLUMN auto_commit BOOLEAN NOT NULL DEFAULT TRUE;
CREATE TABLE kafka_pending_commits (
id BIGSERIAL PRIMARY KEY,
workspace_id VARCHAR(50) NOT NULL,
kafka_trigger_path VARCHAR(255) NOT NULL,
topic VARCHAR(255) NOT NULL,
partition INTEGER NOT NULL,
"offset" BIGINT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
FOREIGN KEY (workspace_id, kafka_trigger_path) REFERENCES kafka_trigger(workspace_id, path) ON DELETE CASCADE
);
CREATE INDEX idx_kafka_pending_commits_trigger ON kafka_pending_commits (workspace_id, kafka_trigger_path);

View File

@@ -1,16 +0,0 @@
[package]
name = "windmill-parser-py-asset"
version.workspace = true
edition.workspace = true
authors.workspace = true
[lib]
name = "windmill_parser_py_asset"
path = "./src/lib.rs"
[dependencies]
windmill-parser.workspace = true
windmill-parser-sql-asset.workspace = true
rustpython-parser.workspace = true
rustpython-ast = { version = "0.4.0", features = ["visitor"] }
anyhow.workspace = true

View File

@@ -10,6 +10,7 @@ path = "./src/lib.rs"
[dependencies] [dependencies]
windmill-parser.workspace = true windmill-parser.workspace = true
windmill-parser-sql.workspace = true
rustpython-parser.workspace = true rustpython-parser.workspace = true
itertools.workspace = true itertools.workspace = true
serde_json.workspace = true serde_json.workspace = true

View File

@@ -215,7 +215,7 @@ impl AssetsFinder {
_ => return Err(()), _ => return Err(()),
}; };
// We use the SQL parser to detect RW, specific tables, etc. // We use the SQL parser to detect RW, specific tables, etc.
let sql_assets = windmill_parser_sql_asset::parse_wmill_sdk_sql_assets( let sql_assets = windmill_parser_sql::parse_wmill_sdk_sql_assets(
*kind, *kind,
path, path,
schema.as_deref(), schema.as_deref(),

View File

@@ -21,54 +21,13 @@ use rustpython_parser::{
Parse, Parse,
}; };
pub mod asset_parser;
pub mod pydantic_parser; pub mod pydantic_parser;
pub use asset_parser::parse_assets;
const FUNCTION_CALL: &str = "<function call>"; const FUNCTION_CALL: &str = "<function call>";
/// Get the simple type name from an expression (e.g. `str`, `int`).
fn simple_type_name(e: &Expr) -> Option<&str> {
match e {
Expr::Name(ExprName { id, .. }) => Some(id.as_ref()),
_ => None,
}
}
/// If `e` is `list[T]` or `List[T]`, return the inner expression `T`.
fn list_elem_expr(e: &Expr) -> Option<&Expr> {
match e {
Expr::Subscript(x) => match x.value.as_ref() {
Expr::Name(ExprName { id, .. }) if id == "list" || id == "List" => {
Some(x.slice.as_ref())
}
_ => None,
},
_ => None,
}
}
/// Detect `T | list[T]` or `list[T] | T` union patterns.
/// Returns the original type string (e.g. "str | list[str]") for use as `otyp`.
fn detect_py_union_array_otyp(e: &Expr) -> Option<String> {
let Expr::BinOp(x) = e else { return None };
// T | list[T]
if let (Some(scalar), Some(elem)) = (simple_type_name(&x.left), list_elem_expr(&x.right)) {
if let Some(elem_name) = simple_type_name(elem) {
if scalar == elem_name {
return Some(format!("{} | list[{}]", scalar, elem_name));
}
}
}
// list[T] | T
if let (Some(elem), Some(scalar)) = (list_elem_expr(&x.left), simple_type_name(&x.right)) {
if let Some(elem_name) = simple_type_name(elem) {
if scalar == elem_name {
return Some(format!("list[{}] | {}", elem_name, scalar));
}
}
}
None
}
/// Cheap string-based check to see if code might contain Pydantic models or dataclasses. /// Cheap string-based check to see if code might contain Pydantic models or dataclasses.
/// Returns true if we should do full AST parsing for type detection, false otherwise. /// Returns true if we should do full AST parsing for type detection, false otherwise.
/// This avoids expensive parsing for the common case where scripts don't use these features. /// This avoids expensive parsing for the common case where scripts don't use these features.
@@ -337,14 +296,11 @@ pub fn parse_python_signature(
// Check if main function was found // Check if main function was found
if params.is_none() { if params.is_none() {
let is_wac_v2 = (code.contains("@workflow") || code.contains("workflow("))
&& (code.contains("@task") || code.contains("task("))
&& (code.contains("import wmill") || code.contains("from wmill"));
return Ok(MainArgSignature { return Ok(MainArgSignature {
star_args: false, star_args: false,
star_kwargs: false, star_kwargs: false,
args: vec![], args: vec![],
no_main_func: Some(!is_wac_v2), no_main_func: Some(true),
has_preprocessor: Some(has_preprocessor), has_preprocessor: Some(has_preprocessor),
}); });
} }
@@ -434,19 +390,8 @@ pub fn parse_python_signature(
_ => {} _ => {}
} }
// Detect T | list[T] union types and set otyp for
// debounce accumulation support. Falls back to docstring
// description if no union array pattern is found.
let union_otyp = params.args[i]
.as_arg()
.annotation
.as_ref()
.and_then(|ann| detect_py_union_array_otyp(ann.as_ref()));
Arg { Arg {
otyp: union_otyp.or_else(|| { otyp: metadata.descriptions.get(&arg_name).map(|d| d.to_string()),
metadata.descriptions.get(&arg_name).map(|d| d.to_string())
}),
name: arg_name, name: arg_name,
typ, typ,
has_default: has_default || default.is_some(), has_default: has_default || default.is_some(),
@@ -496,9 +441,6 @@ fn parse_expr(
Expr::Constant(ExprConstant { value: Constant::None, .. }) Expr::Constant(ExprConstant { value: Constant::None, .. })
) { ) {
(parse_expr(&x.left, enums, module).0, true) (parse_expr(&x.left, enums, module).0, true)
} else if detect_py_union_array_otyp(e.as_ref()).is_some() {
// T | list[T] — parsed type is Unknown; otyp is set separately
(Typ::Unknown, false)
} else { } else {
(Typ::Unknown, false) (Typ::Unknown, false)
} }
@@ -1104,33 +1046,6 @@ def main(a: str, b: Optional[str], c: str | None): return
Ok(()) Ok(())
} }
#[test]
fn test_parse_python_union_array_type() -> anyhow::Result<()> {
let code = r#"
def main(items: str | list[str], numbers: list[int] | int, plain: str):
pass
"#;
let result = parse_python_signature(code, None, false)?;
assert_eq!(result.args.len(), 3);
// str | list[str] → otyp set, typ Unknown
assert_eq!(result.args[0].name, "items");
assert_eq!(result.args[0].otyp, Some("str | list[str]".to_string()));
assert_eq!(result.args[0].typ, Typ::Unknown);
// list[int] | int → otyp set, typ Unknown
assert_eq!(result.args[1].name, "numbers");
assert_eq!(result.args[1].otyp, Some("list[int] | int".to_string()));
assert_eq!(result.args[1].typ, Typ::Unknown);
// plain str → no otyp
assert_eq!(result.args[2].name, "plain");
assert_eq!(result.args[2].otyp, None);
assert_eq!(result.args[2].typ, Typ::Str(None));
Ok(())
}
#[test] #[test]
fn test_parse_python_sig_enum() -> anyhow::Result<()> { fn test_parse_python_sig_enum() -> anyhow::Result<()> {
let code = r#" let code = r#"

View File

@@ -1,17 +0,0 @@
[package]
name = "windmill-parser-sql-asset"
version.workspace = true
edition.workspace = true
authors.workspace = true
[lib]
name = "windmill_parser_sql_asset"
path = "./src/lib.rs"
[dependencies]
windmill-parser.workspace = true
windmill-types.workspace = true
anyhow.workspace = true
serde_json.workspace = true
serde.workspace = true
sqlparser = { version = "0.59.0", features = ["visitor"] }

View File

@@ -1,4 +0,0 @@
mod asset_parser;
mod asset_parser_utils;
pub use asset_parser::parse_assets;
pub use asset_parser_utils::parse_wmill_sdk_sql_assets;

View File

@@ -20,4 +20,5 @@ windmill-types.workspace = true
anyhow.workspace = true anyhow.workspace = true
lazy_static.workspace = true lazy_static.workspace = true
serde_json.workspace = true serde_json.workspace = true
serde.workspace = true serde.workspace = true
sqlparser = { version = "0.59.0", features = ["visitor"] }

View File

@@ -20,6 +20,11 @@ pub use windmill_parser::{Arg, MainArgSignature, ObjectType, Typ};
pub const SANITIZED_ENUM_STR: &str = "__sanitized_enum__"; pub const SANITIZED_ENUM_STR: &str = "__sanitized_enum__";
pub const SANITIZED_RAW_STRING_STR: &str = "__sanitized_raw_string__"; pub const SANITIZED_RAW_STRING_STR: &str = "__sanitized_raw_string__";
mod asset_parser;
mod asset_parser_utils;
pub use asset_parser::parse_assets;
pub use asset_parser_utils::parse_wmill_sdk_sql_assets;
pub fn parse_mysql_sig(code: &str) -> anyhow::Result<MainArgSignature> { pub fn parse_mysql_sig(code: &str) -> anyhow::Result<MainArgSignature> {
let parsed = parse_mysql_file(&code)?; let parsed = parse_mysql_file(&code)?;
if let Some(x) = parsed { if let Some(x) = parsed {
@@ -233,7 +238,7 @@ lazy_static::lazy_static! {
// used for `unsafe` sql interpolation // used for `unsafe` sql interpolation
// -- %%name%% (type) = default // -- %%name%% (type) = default
static ref RE_ARG_SQL_INTERPOLATION: Regex = Regex::new(r#"(?m)^--\s*%%([a-z_][a-z0-9_]*)%%[ \t]*([\w][\w \t\/]*)?(?: ?\= ?(.+))? *(?:\r|\n|$)"#).unwrap(); static ref RE_ARG_SQL_INTERPOLATION: Regex = Regex::new(r#"(?m)^--\s*%%([a-z_][a-z0-9_]*)%%\s*([\s\w\/]+)?(?: ?\= ?(.+))? *(?:\r|\n|$)"#).unwrap();
} }
fn parsed_default(parsed_typ: &Typ, default: String) -> Option<serde_json::Value> { fn parsed_default(parsed_typ: &Typ, default: String) -> Option<serde_json::Value> {
@@ -1542,36 +1547,4 @@ SELECT $1::integer;
Ok(()) Ok(())
} }
#[test]
fn test_parse_pgsql_safe_interpolated_args() -> anyhow::Result<()> {
// There was a bug where enum would be "angrycreative"/"bishop"/"test SELECT x"
let code = r#"
-- %%table_name%% angrycreative/bishop/test
SELECT x
"#;
assert_eq!(
parse_pgsql_sig(code)?,
MainArgSignature {
star_args: false,
star_kwargs: false,
args: vec![Arg {
otyp: Some("__sanitized_enum__".to_string()),
name: "table_name".to_string(),
typ: Typ::Str(Some(vec![
"angrycreative".to_string(),
"bishop".to_string(),
"test".to_string()
])),
default: None,
has_default: false,
oidx: None,
},],
no_main_func: None,
has_preprocessor: None
}
);
Ok(())
}
} }

View File

@@ -1,23 +0,0 @@
[package]
name = "windmill-parser-ts-asset"
version.workspace = true
edition.workspace = true
authors.workspace = true
[lib]
name = "windmill_parser_ts_asset"
path = "./src/lib.rs"
[target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-bindgen.workspace = true
serde-wasm-bindgen.workspace = true
[dependencies]
windmill-parser.workspace = true
windmill-parser-sql-asset.workspace = true
swc_common.workspace = true
triomphe.workspace = true
swc_ecma_parser.workspace = true
swc_ecma_ast.workspace = true
swc_ecma_visit.workspace = true
anyhow.workspace = true

View File

@@ -15,6 +15,7 @@ serde-wasm-bindgen.workspace = true
[dependencies] [dependencies]
windmill-parser.workspace = true windmill-parser.workspace = true
windmill-parser-sql.workspace = true
swc_common.workspace = true swc_common.workspace = true
triomphe.workspace = true triomphe.workspace = true
swc_ecma_parser.workspace = true swc_ecma_parser.workspace = true

View File

@@ -259,7 +259,7 @@ impl Visit for AssetsFinder {
}); });
// We use the SQL parser to detect RW, specific tables, etc. // We use the SQL parser to detect RW, specific tables, etc.
let sql_assets = windmill_parser_sql_asset::parse_wmill_sdk_sql_assets( let sql_assets = windmill_parser_sql::parse_wmill_sdk_sql_assets(
*kind, *kind,
asset_name, asset_name,
schema.as_deref(), schema.as_deref(),

View File

@@ -211,6 +211,8 @@ pub enum TypeDecl {
Interface(TsInterfaceDecl), Interface(TsInterfaceDecl),
Alias(TsTypeAliasDecl), Alias(TsTypeAliasDecl),
} }
pub mod asset_parser;
pub use asset_parser::parse_assets;
/// skip_params is a micro optimization for when we just want to find the main /// skip_params is a micro optimization for when we just want to find the main
/// function without parsing all the params. /// function without parsing all the params.
@@ -259,9 +261,7 @@ pub fn parse_deno_signature(
for specifier in &named_export.specifiers { for specifier in &named_export.specifiers {
if let swc_ecma_ast::ExportSpecifier::Named(spec) = specifier { if let swc_ecma_ast::ExportSpecifier::Named(spec) = specifier {
let export_name = match &spec.exported { let export_name = match &spec.exported {
Some(swc_ecma_ast::ModuleExportName::Ident(ident)) => { Some(swc_ecma_ast::ModuleExportName::Ident(ident)) => ident.sym.as_ref(),
ident.sym.as_ref()
}
Some(swc_ecma_ast::ModuleExportName::Str(s)) => s.value.as_ref(), Some(swc_ecma_ast::ModuleExportName::Str(s)) => s.value.as_ref(),
None => match &spec.orig { None => match &spec.orig {
swc_ecma_ast::ModuleExportName::Ident(ident) => ident.sym.as_ref(), swc_ecma_ast::ModuleExportName::Ident(ident) => ident.sym.as_ref(),
@@ -315,11 +315,7 @@ pub fn parse_deno_signature(
let mut c: u16 = 0; let mut c: u16 = 0;
let is_wac_v2 = entrypoint_params.is_none() let no_main_func = entrypoint_params.is_none();
&& code.contains("workflow(")
&& code.contains("task(")
&& code.contains("windmill-client");
let no_main_func = entrypoint_params.is_none() && !is_wac_v2;
let mut type_resolver = HashMap::new(); let mut type_resolver = HashMap::new();
let r = MainArgSignature { let r = MainArgSignature {
star_args: false, star_args: false,
@@ -363,12 +359,8 @@ fn parse_param(
let r = match param.pat { let r = match param.pat {
Pat::Ident(ident) => { Pat::Ident(ident) => {
let (name, typ, nullable) = binding_ident_to_arg(symbol_table, type_resolver, &ident); let (name, typ, nullable) = binding_ident_to_arg(symbol_table, type_resolver, &ident);
let otyp = ident
.type_ann
.as_ref()
.and_then(|ta| detect_union_array_otyp(&ta.type_ann));
Ok(Arg { Ok(Arg {
otyp, otyp: None,
name, name,
typ, typ,
default: None, default: None,
@@ -378,21 +370,13 @@ fn parse_param(
} }
// Pat::Object(ObjectPat { ... }) = todo!() // Pat::Object(ObjectPat { ... }) = todo!()
Pat::Assign(AssignPat { left, right, .. }) => { Pat::Assign(AssignPat { left, right, .. }) => {
let (name, mut typ, _nullable, otyp) = match *left { let (name, mut typ, _nullable) = match *left {
Pat::Ident(ident) => { Pat::Ident(ident) => binding_ident_to_arg(symbol_table, type_resolver, &ident),
let otyp = ident
.type_ann
.as_ref()
.and_then(|ta| detect_union_array_otyp(&ta.type_ann));
let (name, typ, nullable) =
binding_ident_to_arg(symbol_table, type_resolver, &ident);
(name, typ, nullable, otyp)
}
Pat::Object(ObjectPat { type_ann, .. }) => { Pat::Object(ObjectPat { type_ann, .. }) => {
let (typ, nullable) = eval_type_ann(symbol_table, type_resolver, &type_ann); let (typ, nullable) = eval_type_ann(symbol_table, type_resolver, &type_ann);
*counter += 1; *counter += 1;
let name = format!("anon{}", counter); let name = format!("anon{}", counter);
(name, typ, nullable, None) (name, typ, nullable)
} }
_ => { _ => {
return Err(anyhow::anyhow!( return Err(anyhow::anyhow!(
@@ -428,7 +412,7 @@ fn parse_param(
if typ == Typ::Unknown && dflt.is_some() { if typ == Typ::Unknown && dflt.is_some() {
typ = json_to_typ(dflt.as_ref().unwrap(), false); typ = json_to_typ(dflt.as_ref().unwrap(), false);
} }
Ok(Arg { otyp, name, typ, default: dflt, has_default: true, oidx: None }) Ok(Arg { otyp: None, name, typ, default: dflt, has_default: true, oidx: None })
} }
Pat::Object(ObjectPat { type_ann, .. }) => { Pat::Object(ObjectPat { type_ann, .. }) => {
let (typ, nullable) = eval_type_ann(symbol_table, type_resolver, &type_ann); let (typ, nullable) = eval_type_ann(symbol_table, type_resolver, &type_ann);
@@ -849,9 +833,7 @@ fn tstype_to_typ(
false, false,
), ),
symbol @ _ if symbol.starts_with("DynMultiselect_") => ( symbol @ _ if symbol.starts_with("DynMultiselect_") => (
Typ::DynMultiselect( Typ::DynMultiselect(symbol.strip_prefix("DynMultiselect_").unwrap().to_string()),
symbol.strip_prefix("DynMultiselect_").unwrap().to_string(),
),
false, false,
), ),
symbol @ _ => { symbol @ _ => {
@@ -973,75 +955,6 @@ fn one_of_properties(
.collect() .collect()
} }
fn ts_type_to_string(ts_type: &TsType) -> Option<String> {
match ts_type {
TsType::TsKeywordType(t) => Some(
match t.kind {
TsKeywordTypeKind::TsStringKeyword => "string",
TsKeywordTypeKind::TsNumberKeyword => "number",
TsKeywordTypeKind::TsBooleanKeyword => "boolean",
TsKeywordTypeKind::TsObjectKeyword => "object",
TsKeywordTypeKind::TsBigIntKeyword => "bigint",
TsKeywordTypeKind::TsAnyKeyword => "any",
_ => return None,
}
.to_string(),
),
TsType::TsTypeRef(TsTypeRef { type_name, .. }) => match type_name {
TsEntityName::Ident(Ident { sym, .. }) => Some(sym.to_string()),
_ => None,
},
_ => None,
}
}
fn get_array_elem_type(ts_type: &TsType) -> Option<&TsType> {
match ts_type {
TsType::TsArrayType(TsArrayType { elem_type, .. }) => Some(elem_type),
_ => None,
}
}
/// Detects union types of the form `T | T[]` or `T[] | T` and returns
/// the original type string (e.g. "string | string[]").
fn detect_union_array_otyp(ts_type: &TsType) -> Option<String> {
let TsType::TsUnionOrIntersectionType(TsUnionOrIntersectionType::TsUnionType(TsUnionType {
types,
..
})) = ts_type
else {
return None;
};
if types.len() != 2 {
return None;
}
// Check pattern: T | T[]
if let (Some(scalar_name), Some(array_elem)) =
(ts_type_to_string(&types[0]), get_array_elem_type(&types[1]))
{
if let Some(elem_name) = ts_type_to_string(array_elem) {
if scalar_name == elem_name {
return Some(format!("{} | {}[]", scalar_name, elem_name));
}
}
}
// Check pattern: T[] | T
if let (Some(array_elem), Some(scalar_name)) =
(get_array_elem_type(&types[0]), ts_type_to_string(&types[1]))
{
if let Some(elem_name) = ts_type_to_string(array_elem) {
if scalar_name == elem_name {
return Some(format!("{}[] | {}", elem_name, scalar_name));
}
}
}
None
}
fn find_undefined(types: &Vec<Box<TsType>>) -> Option<usize> { fn find_undefined(types: &Vec<Box<TsType>>) -> Option<usize> {
types.into_iter().position(|x| match **x { types.into_iter().position(|x| match **x {
TsType::TsKeywordType(TsKeywordType { kind, .. }) => { TsType::TsKeywordType(TsKeywordType { kind, .. }) => {

View File

@@ -646,55 +646,6 @@ mod tests {
); );
} }
#[test]
fn test_parse_union_array_type() {
let code = r#"
export async function main(
items: string | string[],
numbers: number[] | number,
plain: string
) {
return { items, numbers, plain };
}
"#;
let sig = parse_deno_signature(code, false, false, None).unwrap();
assert_eq!(
sig,
MainArgSignature {
star_args: false,
star_kwargs: false,
args: vec![
Arg {
name: "items".to_string(),
otyp: Some("string | string[]".to_string()),
typ: Typ::Unknown,
default: None,
has_default: false,
oidx: None,
},
Arg {
name: "numbers".to_string(),
otyp: Some("number[] | number".to_string()),
typ: Typ::Unknown,
default: None,
has_default: false,
oidx: None,
},
Arg {
name: "plain".to_string(),
otyp: None,
typ: Typ::Str(None),
default: None,
has_default: false,
oidx: None,
},
],
no_main_func: Some(false),
has_preprocessor: Some(false),
}
);
}
#[test] #[test]
fn test_parse_invalid_typescript() { fn test_parse_invalid_typescript() {
let code = r#" let code = r#"

View File

@@ -1,21 +0,0 @@
[package]
name = "windmill-parser-wac"
version.workspace = true
edition.workspace = true
authors.workspace = true
[lib]
name = "windmill_parser_wac"
path = "./src/lib.rs"
[dependencies]
rustpython-parser.workspace = true
rustpython-ast = { version = "0.4.0", features = ["visitor"] }
swc_common.workspace = true
swc_ecma_parser.workspace = true
swc_ecma_ast.workspace = true
swc_ecma_visit.workspace = true
serde.workspace = true
serde_json.workspace = true
anyhow.workspace = true
sha2.workspace = true

View File

@@ -1,44 +0,0 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct WorkflowDag {
pub nodes: Vec<DagNode>,
pub edges: Vec<DagEdge>,
pub params: Vec<Param>,
pub source_hash: String,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Param {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub typ: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct DagNode {
pub id: String,
pub node_type: DagNodeType,
pub label: String,
pub line: usize,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(tag = "type")]
pub enum DagNodeType {
Step { name: String, script: String },
Branch { condition_source: String },
ParallelStart,
ParallelEnd,
LoopStart { iter_source: String },
LoopEnd,
Return,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct DagEdge {
pub from: String,
pub to: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
}

View File

@@ -1,32 +0,0 @@
pub mod dag;
pub mod python;
pub mod typescript;
pub mod validation;
use dag::WorkflowDag;
use validation::CompileError;
#[derive(Debug, serde::Serialize)]
#[serde(tag = "type")]
pub enum ParseResult {
#[serde(rename = "success")]
Success(WorkflowDag),
#[serde(rename = "error")]
Error { errors: Vec<CompileError> },
}
pub fn parse_workflow(code: &str, language: &str) -> ParseResult {
let result = match language {
"python" | "python3" | "py" => python::parse_python_workflow(code),
"typescript" | "ts" | "deno" | "bun" => typescript::parse_ts_workflow(code),
_ => Err(vec![CompileError {
message: format!("Unsupported language: {language}"),
line: 0,
}]),
};
match result {
Ok(dag) => ParseResult::Success(dag),
Err(errors) => ParseResult::Error { errors },
}
}

View File

@@ -1,717 +0,0 @@
use std::collections::HashMap;
use rustpython_parser::{
ast::{
Expr, ExprAwait, ExprCall, ExprName, Stmt, StmtExpr, StmtFor, StmtIf, StmtReturn, StmtTry,
StmtTryStar, StmtWhile,
},
Parse,
};
use crate::dag::{DagEdge, DagNode, DagNodeType, Param, WorkflowDag};
use crate::validation::{self, CompileError};
struct LineIndex {
newline_offsets: Vec<usize>,
}
impl LineIndex {
fn new(source: &str) -> Self {
let mut offsets = vec![0];
for (i, c) in source.char_indices() {
if c == '\n' {
offsets.push(i + 1);
}
}
Self { newline_offsets: offsets }
}
fn line_of(&self, byte_offset: usize) -> usize {
match self.newline_offsets.binary_search(&byte_offset) {
Ok(line) => line + 1,
Err(line) => line,
}
}
}
/// Maps task function name → optional external path (from `@task(path="...")`)
type TaskFunctions = HashMap<String, Option<String>>;
/// First pass: scan top-level `@task async def foo(...)` declarations.
fn collect_task_functions(stmts: &[Stmt]) -> TaskFunctions {
let mut tasks = HashMap::new();
for stmt in stmts {
if let Stmt::AsyncFunctionDef(func) = stmt {
for dec in &func.decorator_list {
match dec {
// @task (bare decorator)
Expr::Name(ExprName { id, .. }) if id.as_str() == "task" => {
tasks.insert(func.name.to_string(), None);
}
// @task(path="...")
Expr::Call(call) => {
if let Expr::Name(ExprName { id, .. }) = call.func.as_ref() {
if id.as_str() == "task" {
let path = extract_task_path_kwarg(call);
tasks.insert(func.name.to_string(), path);
}
}
}
_ => {}
}
}
}
}
tasks
}
/// Extract the `path=` keyword argument from a `@task(path="...")` call.
fn extract_task_path_kwarg(call: &ExprCall) -> Option<String> {
for kw in &call.keywords {
if let Some(ref arg) = kw.arg {
if arg.as_str() == "path" {
if let Expr::Constant(c) = &kw.value {
if let rustpython_parser::ast::Constant::Str(s) = &c.value {
return Some(s.to_string());
}
}
}
}
}
None
}
struct WacWalker {
nodes: Vec<DagNode>,
edges: Vec<DagEdge>,
errors: Vec<CompileError>,
node_counter: usize,
line_index: LineIndex,
task_functions: TaskFunctions,
in_try: bool,
in_while: bool,
in_nested_func: bool,
in_comprehension: bool,
}
impl WacWalker {
fn new(source: &str, task_functions: TaskFunctions) -> Self {
Self {
nodes: Vec::new(),
edges: Vec::new(),
errors: Vec::new(),
node_counter: 0,
line_index: LineIndex::new(source),
task_functions,
in_try: false,
in_while: false,
in_nested_func: false,
in_comprehension: false,
}
}
fn next_id(&mut self) -> String {
let id = format!("step_{}", self.node_counter);
self.node_counter += 1;
id
}
fn add_node(&mut self, node: DagNode) -> String {
let id = node.id.clone();
self.nodes.push(node);
id
}
fn add_edge(&mut self, from: &str, to: &str, label: Option<String>) {
self.edges
.push(DagEdge { from: from.to_string(), to: to.to_string(), label });
}
fn line_of_expr(&self, expr: &Expr) -> usize {
let offset = match expr {
Expr::Call(c) => c.range.start().to_usize(),
Expr::Await(a) => a.range.start().to_usize(),
Expr::Attribute(a) => a.range.start().to_usize(),
Expr::Name(n) => n.range.start().to_usize(),
_ => 0,
};
self.line_index.line_of(offset)
}
fn line_of_stmt(&self, stmt: &Stmt) -> usize {
let offset = match stmt {
Stmt::If(s) => s.range.start().to_usize(),
Stmt::For(s) => s.range.start().to_usize(),
Stmt::While(s) => s.range.start().to_usize(),
Stmt::Return(s) => s.range.start().to_usize(),
Stmt::Expr(s) => s.range.start().to_usize(),
Stmt::Try(s) => s.range.start().to_usize(),
Stmt::TryStar(s) => s.range.start().to_usize(),
Stmt::Assign(s) => s.range.start().to_usize(),
Stmt::AnnAssign(s) => s.range.start().to_usize(),
Stmt::FunctionDef(s) => s.range.start().to_usize(),
Stmt::AsyncFunctionDef(s) => s.range.start().to_usize(),
_ => 0,
};
self.line_index.line_of(offset)
}
/// Check if an expression is a call to a known @task function
fn is_task_fn_call(&self, expr: &Expr) -> bool {
if let Expr::Call(call) = expr {
if let Expr::Name(ExprName { id, .. }) = call.func.as_ref() {
return self.task_functions.contains_key(id.as_str());
}
}
false
}
/// Check if an expression is `asyncio.gather(...)` call
fn is_asyncio_gather_call(expr: &Expr) -> bool {
if let Expr::Call(call) = expr {
if let Expr::Attribute(rustpython_parser::ast::ExprAttribute { value, attr, .. }) =
call.func.as_ref()
{
if attr.as_str() == "gather" {
if let Expr::Name(ExprName { id, .. }) = value.as_ref() {
return id.as_str() == "asyncio";
}
}
}
}
false
}
/// Extract step name and script from a task function call.
/// Name = function name, script = task_path or function name.
fn extract_step_info_from_task_call(&self, call: &ExprCall) -> Option<(String, String)> {
if let Expr::Name(ExprName { id, .. }) = call.func.as_ref() {
let name = id.to_string();
let script = self
.task_functions
.get(id.as_str())
.and_then(|p| p.clone())
.unwrap_or_else(|| name.clone());
Some((name, script))
} else {
None
}
}
fn expr_to_source(expr: &Expr) -> String {
match expr {
Expr::Compare(c) => {
let left = Self::expr_to_source(&c.left);
if let Some(comparator) = c.comparators.first() {
let right = Self::expr_to_source(comparator);
let op = match c.ops.first() {
Some(rustpython_parser::ast::CmpOp::Gt) => ">",
Some(rustpython_parser::ast::CmpOp::Lt) => "<",
Some(rustpython_parser::ast::CmpOp::GtE) => ">=",
Some(rustpython_parser::ast::CmpOp::LtE) => "<=",
Some(rustpython_parser::ast::CmpOp::Eq) => "==",
Some(rustpython_parser::ast::CmpOp::NotEq) => "!=",
Some(rustpython_parser::ast::CmpOp::In) => "in",
Some(rustpython_parser::ast::CmpOp::NotIn) => "not in",
Some(rustpython_parser::ast::CmpOp::Is) => "is",
Some(rustpython_parser::ast::CmpOp::IsNot) => "is not",
None => "?",
};
format!("{left} {op} {right}")
} else {
left
}
}
Expr::Subscript(s) => {
let value = Self::expr_to_source(&s.value);
let slice = Self::expr_to_source(&s.slice);
format!("{value}[{slice}]")
}
Expr::Attribute(a) => {
let value = Self::expr_to_source(&a.value);
format!("{value}.{}", a.attr)
}
Expr::Name(n) => n.id.to_string(),
Expr::Constant(c) => match &c.value {
rustpython_parser::ast::Constant::Str(s) => format!("\"{s}\""),
rustpython_parser::ast::Constant::Int(i) => i.to_string(),
rustpython_parser::ast::Constant::Float(f) => f.to_string(),
rustpython_parser::ast::Constant::Bool(b) => b.to_string(),
rustpython_parser::ast::Constant::None => "None".to_string(),
_ => "...".to_string(),
},
_ => "...".to_string(),
}
}
/// Check if a statement body contains any task function calls (recursively)
fn body_contains_step(&self, body: &[Stmt]) -> bool {
for stmt in body {
if self.stmt_contains_step(stmt) {
return true;
}
}
false
}
fn stmt_contains_step(&self, stmt: &Stmt) -> bool {
match stmt {
Stmt::Expr(StmtExpr { value, .. }) => self.expr_contains_step(value),
Stmt::Assign(a) => self.expr_contains_step(&a.value),
Stmt::If(s) => self.body_contains_step(&s.body) || self.body_contains_step(&s.orelse),
Stmt::For(s) => self.body_contains_step(&s.body) || self.body_contains_step(&s.orelse),
Stmt::While(s) => {
self.body_contains_step(&s.body) || self.body_contains_step(&s.orelse)
}
Stmt::Try(s) => {
self.body_contains_step(&s.body)
|| self.body_contains_step(&s.orelse)
|| self.body_contains_step(&s.finalbody)
|| s.handlers.iter().any(|h| match h {
rustpython_parser::ast::ExceptHandler::ExceptHandler(eh) => {
self.body_contains_step(&eh.body)
}
})
}
Stmt::TryStar(s) => {
self.body_contains_step(&s.body)
|| self.body_contains_step(&s.orelse)
|| self.body_contains_step(&s.finalbody)
|| s.handlers.iter().any(|h| match h {
rustpython_parser::ast::ExceptHandler::ExceptHandler(eh) => {
self.body_contains_step(&eh.body)
}
})
}
Stmt::Return(_) => false,
_ => false,
}
}
fn expr_contains_step(&self, expr: &Expr) -> bool {
if self.is_task_fn_call(expr) {
return true;
}
match expr {
Expr::Await(ExprAwait { value, .. }) => self.expr_contains_step(value),
Expr::Call(call) => {
if self.is_task_fn_call(&Expr::Call(call.clone())) {
return true;
}
if Self::is_asyncio_gather_call(&Expr::Call(call.clone())) {
return call.args.iter().any(|a| self.expr_contains_step(a));
}
false
}
_ => false,
}
}
/// Walk a list of statements, returning (first_node_id, last_node_id)
fn walk_body(&mut self, body: &[Stmt]) -> Option<(String, String)> {
let mut first_id: Option<String> = None;
let mut prev_id: Option<String> = None;
for stmt in body {
if let Some((stmt_first, stmt_last)) = self.walk_stmt(stmt) {
if let Some(ref prev) = prev_id {
self.add_edge(prev, &stmt_first, None);
}
if first_id.is_none() {
first_id = Some(stmt_first);
}
prev_id = Some(stmt_last);
}
}
match (first_id, prev_id) {
(Some(f), Some(l)) => Some((f, l)),
_ => None,
}
}
fn walk_stmt(&mut self, stmt: &Stmt) -> Option<(String, String)> {
match stmt {
Stmt::Expr(StmtExpr { value, .. }) => self.walk_expr_stmt(value),
Stmt::Assign(a) => self.walk_expr_stmt(&a.value),
Stmt::If(if_stmt) => self.walk_if(if_stmt),
Stmt::For(for_stmt) => self.walk_for(for_stmt),
Stmt::While(while_stmt) => self.walk_while(while_stmt),
Stmt::Try(try_stmt) => self.walk_try(try_stmt),
Stmt::TryStar(try_stmt) => self.walk_try_star(try_stmt),
Stmt::Return(ret) => self.walk_return(ret),
Stmt::FunctionDef(_) | Stmt::AsyncFunctionDef(_) => {
if self.stmt_contains_step(stmt) {
self.errors.push(validation::error_step_in_nested_function(
self.line_of_stmt(stmt),
));
}
None
}
_ => None,
}
}
fn walk_expr_stmt(&mut self, expr: &Expr) -> Option<(String, String)> {
// await task_fn(...)
if let Expr::Await(ExprAwait { value, .. }) = expr {
// await task_fn(...)
if let Expr::Call(call) = value.as_ref() {
if self.is_task_fn_call(&Expr::Call(call.clone())) {
return self.emit_step(call, expr);
}
}
// await asyncio.gather(task_fn(...), task_fn(...), ...)
if Self::is_asyncio_gather_call(value) {
if let Expr::Call(gather_call) = value.as_ref() {
return self.emit_parallel(gather_call, expr);
}
}
}
// Bare task_fn() without await — validation error
if self.is_task_fn_call(expr) {
self.errors
.push(validation::error_missing_await(self.line_of_expr(expr)));
}
None
}
fn emit_step(&mut self, call: &ExprCall, expr: &Expr) -> Option<(String, String)> {
if self.in_try {
self.errors
.push(validation::error_step_in_try(self.line_of_expr(expr)));
return None;
}
if self.in_while {
self.errors
.push(validation::error_step_in_while(self.line_of_expr(expr)));
return None;
}
if self.in_nested_func {
self.errors.push(validation::error_step_in_nested_function(
self.line_of_expr(expr),
));
return None;
}
if self.in_comprehension {
self.errors.push(validation::error_step_in_comprehension(
self.line_of_expr(expr),
));
return None;
}
let (name, script) = self
.extract_step_info_from_task_call(call)
.unwrap_or(("unknown".into(), "unknown".into()));
let id = self.next_id();
let node_id = self.add_node(DagNode {
id: id.clone(),
node_type: DagNodeType::Step { name: name.clone(), script },
label: name,
line: self.line_of_expr(expr),
});
Some((node_id.clone(), node_id))
}
fn emit_parallel(&mut self, gather_call: &ExprCall, expr: &Expr) -> Option<(String, String)> {
if self.in_try {
self.errors
.push(validation::error_step_in_try(self.line_of_expr(expr)));
return None;
}
if self.in_while {
self.errors
.push(validation::error_step_in_while(self.line_of_expr(expr)));
return None;
}
let line = self.line_of_expr(expr);
let start_id = self.next_id();
let start_node_id = self.add_node(DagNode {
id: start_id.clone(),
node_type: DagNodeType::ParallelStart,
label: "parallel".to_string(),
line,
});
let mut step_ids = Vec::new();
for arg in &gather_call.args {
// Each arg should be task_fn(...)
if let Expr::Call(call) = arg {
if self.is_task_fn_call(&Expr::Call(call.clone())) {
let (name, script) = self
.extract_step_info_from_task_call(call)
.unwrap_or(("unknown".into(), "unknown".into()));
let step_id = self.next_id();
let node_id = self.add_node(DagNode {
id: step_id.clone(),
node_type: DagNodeType::Step { name: name.clone(), script },
label: name,
line: self.line_of_expr(arg),
});
self.add_edge(&start_node_id, &node_id, None);
step_ids.push(node_id);
}
}
}
let end_id = self.next_id();
let end_node_id = self.add_node(DagNode {
id: end_id.clone(),
node_type: DagNodeType::ParallelEnd,
label: "join".to_string(),
line,
});
for step_id in &step_ids {
self.add_edge(step_id, &end_node_id, None);
}
Some((start_node_id, end_node_id))
}
fn walk_if(&mut self, if_stmt: &StmtIf) -> Option<(String, String)> {
let has_steps_in_body = self.body_contains_step(&if_stmt.body);
let has_steps_in_else = self.body_contains_step(&if_stmt.orelse);
if !has_steps_in_body && !has_steps_in_else {
return None;
}
let line = self.line_index.line_of(if_stmt.range.start().to_usize());
let condition_source = Self::expr_to_source(&if_stmt.test);
let branch_id = self.next_id();
let branch_node_id = self.add_node(DagNode {
id: branch_id.clone(),
node_type: DagNodeType::Branch { condition_source },
label: "if".to_string(),
line,
});
let merge_id = format!("{branch_id}_merge");
let mut last_ids = Vec::new();
if let Some((true_first, true_last)) = self.walk_body(&if_stmt.body) {
self.add_edge(&branch_node_id, &true_first, Some("true".to_string()));
last_ids.push(true_last);
} else {
last_ids.push(branch_node_id.clone());
}
if !if_stmt.orelse.is_empty() {
if let Some((else_first, else_last)) = self.walk_body(&if_stmt.orelse) {
self.add_edge(&branch_node_id, &else_first, Some("false".to_string()));
last_ids.push(else_last);
} else {
last_ids.push(branch_node_id.clone());
}
}
if last_ids.len() == 1 {
Some((branch_node_id, last_ids.into_iter().next().unwrap()))
} else {
Some((branch_node_id, merge_id))
}
}
fn walk_for(&mut self, for_stmt: &StmtFor) -> Option<(String, String)> {
if !self.body_contains_step(&for_stmt.body) {
return None;
}
let line = self.line_index.line_of(for_stmt.range.start().to_usize());
let iter_source = Self::expr_to_source(&for_stmt.iter);
let start_id = self.next_id();
let start_node_id = self.add_node(DagNode {
id: start_id.clone(),
node_type: DagNodeType::LoopStart { iter_source },
label: "for".to_string(),
line,
});
if let Some((body_first, body_last)) = self.walk_body(&for_stmt.body) {
self.add_edge(&start_node_id, &body_first, None);
self.add_edge(&body_last, &start_node_id, Some("next".to_string()));
}
let end_id = self.next_id();
let end_node_id = self.add_node(DagNode {
id: end_id.clone(),
node_type: DagNodeType::LoopEnd,
label: "end for".to_string(),
line,
});
self.add_edge(&start_node_id, &end_node_id, Some("done".to_string()));
Some((start_node_id, end_node_id))
}
fn walk_while(&mut self, while_stmt: &StmtWhile) -> Option<(String, String)> {
if self.body_contains_step(&while_stmt.body) {
let line = self.line_index.line_of(while_stmt.range.start().to_usize());
self.errors.push(validation::error_step_in_while(line));
}
None
}
fn walk_try(&mut self, try_stmt: &StmtTry) -> Option<(String, String)> {
let has_steps = self.body_contains_step(&try_stmt.body)
|| self.body_contains_step(&try_stmt.orelse)
|| self.body_contains_step(&try_stmt.finalbody)
|| try_stmt.handlers.iter().any(|h| match h {
rustpython_parser::ast::ExceptHandler::ExceptHandler(eh) => {
self.body_contains_step(&eh.body)
}
});
if has_steps {
let line = self.line_index.line_of(try_stmt.range.start().to_usize());
self.errors.push(validation::error_step_in_try(line));
}
None
}
fn walk_try_star(&mut self, try_stmt: &StmtTryStar) -> Option<(String, String)> {
let has_steps = self.body_contains_step(&try_stmt.body)
|| self.body_contains_step(&try_stmt.orelse)
|| self.body_contains_step(&try_stmt.finalbody)
|| try_stmt.handlers.iter().any(|h| match h {
rustpython_parser::ast::ExceptHandler::ExceptHandler(eh) => {
self.body_contains_step(&eh.body)
}
});
if has_steps {
let line = self.line_index.line_of(try_stmt.range.start().to_usize());
self.errors.push(validation::error_step_in_try(line));
}
None
}
fn walk_return(&mut self, ret: &StmtReturn) -> Option<(String, String)> {
let line = self.line_index.line_of(ret.range.start().to_usize());
let id = self.next_id();
let node_id = self.add_node(DagNode {
id: id.clone(),
node_type: DagNodeType::Return,
label: "return".to_string(),
line,
});
Some((node_id.clone(), node_id))
}
}
/// Extract workflow function parameters (no longer skips ctx)
fn extract_params(args: &rustpython_parser::ast::Arguments) -> Vec<Param> {
let mut params = Vec::new();
for arg_with_default in args.args.iter().chain(args.posonlyargs.iter()) {
let name = arg_with_default.def.arg.to_string();
let typ = arg_with_default
.def
.annotation
.as_ref()
.map(|ann| WacWalker::expr_to_source(ann));
params.push(Param { name, typ });
}
params
}
pub fn parse_python_workflow(code: &str) -> Result<WorkflowDag, Vec<CompileError>> {
let ast = rustpython_parser::ast::Suite::parse(code, "<workflow>")
.map_err(|e| vec![CompileError { message: format!("Parse error: {e}"), line: 0 }])?;
// First pass: collect @task functions
let task_functions = collect_task_functions(&ast);
// Find the @workflow async def
let workflow_fn = ast.iter().find_map(|stmt| {
if let Stmt::AsyncFunctionDef(func) = stmt {
let has_workflow_decorator = func.decorator_list.iter().any(|dec| {
if let Expr::Name(ExprName { id, .. }) = dec {
id.as_str() == "workflow"
} else {
false
}
});
if has_workflow_decorator {
return Some(func);
}
}
// Also check non-async for error reporting
if let Stmt::FunctionDef(func) = stmt {
let has_workflow_decorator = func.decorator_list.iter().any(|dec| {
if let Expr::Name(ExprName { id, .. }) = dec {
id.as_str() == "workflow"
} else {
false
}
});
if has_workflow_decorator {
return None; // Will be reported as not-async below
}
}
None
});
// Check for non-async workflow function
let non_async_workflow = ast.iter().find_map(|stmt| {
if let Stmt::FunctionDef(func) = stmt {
let has_workflow_decorator = func.decorator_list.iter().any(|dec| {
if let Expr::Name(ExprName { id, .. }) = dec {
id.as_str() == "workflow"
} else {
false
}
});
if has_workflow_decorator {
let line_index = LineIndex::new(code);
return Some(line_index.line_of(func.range.start().to_usize()));
}
}
None
});
if let Some(line) = non_async_workflow {
if workflow_fn.is_none() {
return Err(vec![validation::error_not_async(line)]);
}
}
let workflow_fn = workflow_fn.ok_or_else(|| {
vec![CompileError { message: "No @workflow async function found.".to_string(), line: 0 }]
})?;
let params = extract_params(&workflow_fn.args);
let source_hash = compute_source_hash(code);
let mut walker = WacWalker::new(code, task_functions);
walker.walk_body(&workflow_fn.body);
if !walker.errors.is_empty() {
return Err(walker.errors);
}
Ok(WorkflowDag { nodes: walker.nodes, edges: walker.edges, params, source_hash })
}
fn compute_source_hash(code: &str) -> String {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(code.as_bytes());
format!("{:x}", hasher.finalize())
}
trait ToUsize {
fn to_usize(self) -> usize;
}
impl ToUsize for rustpython_parser::text_size::TextSize {
fn to_usize(self) -> usize {
u32::from(self) as usize
}
}

View File

@@ -1,739 +0,0 @@
use std::collections::HashMap;
use swc_common::{sync::Lrc, FileName, SourceMap, SourceMapper, Spanned};
use swc_ecma_ast::*;
use swc_ecma_parser::{lexer::Lexer, Parser, StringInput, Syntax, TsSyntax};
use crate::dag::{DagEdge, DagNode, DagNodeType, Param, WorkflowDag};
use crate::validation::{self, CompileError};
/// Maps task function name → optional external path (from `task("f/path", ...)`)
type TaskFunctions = HashMap<String, Option<String>>;
/// First pass: scan top-level `const foo = task(async (...) => {})` or
/// `const foo = task("f/path", async (...) => {})` declarations.
fn collect_task_functions(module: &Module) -> TaskFunctions {
let mut tasks = HashMap::new();
for item in &module.body {
// const foo = task(async (...) => { ... })
// const foo = task("f/path", async (...) => { ... })
if let ModuleItem::Stmt(Stmt::Decl(Decl::Var(var_decl))) = item {
for decl in &var_decl.decls {
if let (Some(name), Some(init)) = (extract_var_name(&decl.name), &decl.init) {
if let Some(path) = extract_task_call_info(init) {
tasks.insert(name, path);
}
}
}
}
// export const foo = task(...)
if let ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(export)) = item {
if let Decl::Var(var_decl) = &export.decl {
for decl in &var_decl.decls {
if let (Some(name), Some(init)) = (extract_var_name(&decl.name), &decl.init) {
if let Some(path) = extract_task_call_info(init) {
tasks.insert(name, path);
}
}
}
}
}
}
tasks
}
/// Extract variable name from a pattern (simple ident case)
fn extract_var_name(pat: &Pat) -> Option<String> {
if let Pat::Ident(BindingIdent { id, .. }) = pat {
Some(id.sym.to_string())
} else {
None
}
}
/// Check if expr is `task(async fn)` or `task("path", async fn)`.
/// Returns Some(optional_path) if it is a task() call.
fn extract_task_call_info(expr: &Expr) -> Option<Option<String>> {
if let Expr::Call(call) = expr {
if let Callee::Expr(callee) = &call.callee {
if let Expr::Ident(ident) = callee.as_ref() {
if ident.sym.as_ref() == "task" {
// task("f/path", async fn) or task(async fn)
if call.args.len() == 2 {
// task("f/path", async fn)
let path = extract_string_lit(&call.args[0].expr);
return Some(path);
} else if call.args.len() == 1 {
// task(async fn)
return Some(None);
}
}
}
}
}
None
}
struct TsWacWalker {
nodes: Vec<DagNode>,
edges: Vec<DagEdge>,
errors: Vec<CompileError>,
node_counter: usize,
cm: Lrc<SourceMap>,
task_functions: TaskFunctions,
in_try: bool,
in_while: bool,
in_nested_func: bool,
}
impl TsWacWalker {
fn new(cm: Lrc<SourceMap>, task_functions: TaskFunctions) -> Self {
Self {
nodes: Vec::new(),
edges: Vec::new(),
errors: Vec::new(),
node_counter: 0,
cm,
task_functions,
in_try: false,
in_while: false,
in_nested_func: false,
}
}
fn next_id(&mut self) -> String {
let id = format!("step_{}", self.node_counter);
self.node_counter += 1;
id
}
fn add_node(&mut self, node: DagNode) -> String {
let id = node.id.clone();
self.nodes.push(node);
id
}
fn add_edge(&mut self, from: &str, to: &str, label: Option<String>) {
self.edges
.push(DagEdge { from: from.to_string(), to: to.to_string(), label });
}
fn span_line(&self, span: swc_common::Span) -> usize {
let loc = self.cm.lookup_char_pos(span.lo);
loc.line
}
/// Check if expr is a call to a known task function
fn is_task_call(&self, expr: &Expr) -> bool {
if let Expr::Call(call) = expr {
if let Callee::Expr(callee) = &call.callee {
if let Expr::Ident(ident) = callee.as_ref() {
return self.task_functions.contains_key(ident.sym.as_ref());
}
}
}
false
}
/// Check if expr is `Promise.all([...])`
fn is_promise_all(expr: &Expr) -> bool {
if let Expr::Call(call) = expr {
if let Callee::Expr(callee) = &call.callee {
if let Expr::Member(MemberExpr { obj, prop: MemberProp::Ident(prop), .. }) =
callee.as_ref()
{
if prop.sym.as_ref() == "all" {
if let Expr::Ident(ident) = obj.as_ref() {
return ident.sym.as_ref() == "Promise";
}
}
}
}
}
false
}
/// Extract step name and script from a task function call.
/// Name = function name, script = task_path or function name.
fn extract_step_info_from_task_call(&self, call: &CallExpr) -> Option<(String, String)> {
if let Callee::Expr(callee) = &call.callee {
if let Expr::Ident(ident) = callee.as_ref() {
let name = ident.sym.to_string();
let script = self
.task_functions
.get(ident.sym.as_ref())
.and_then(|p| p.clone())
.unwrap_or_else(|| name.clone());
return Some((name, script));
}
}
None
}
fn expr_to_source(&self, expr: &Expr) -> String {
let span = expr.span();
self.cm
.span_to_snippet(span)
.unwrap_or_else(|_| "...".to_string())
}
fn body_contains_step(&self, stmts: &[Stmt]) -> bool {
stmts.iter().any(|s| self.stmt_contains_step(s))
}
fn stmt_contains_step(&self, stmt: &Stmt) -> bool {
match stmt {
Stmt::Expr(expr_stmt) => self.expr_contains_step(&expr_stmt.expr),
Stmt::Decl(Decl::Var(var_decl)) => var_decl.decls.iter().any(|d| {
d.init
.as_ref()
.map_or(false, |init| self.expr_contains_step(init))
}),
Stmt::If(if_stmt) => {
self.stmt_contains_step(&if_stmt.cons)
|| if_stmt
.alt
.as_ref()
.map_or(false, |alt| self.stmt_contains_step(alt))
}
Stmt::Block(block) => self.body_contains_step(&block.stmts),
Stmt::For(for_stmt) => self.stmt_contains_step(&for_stmt.body),
Stmt::ForIn(for_in) => self.stmt_contains_step(&for_in.body),
Stmt::ForOf(for_of) => self.stmt_contains_step(&for_of.body),
Stmt::While(while_stmt) => self.stmt_contains_step(&while_stmt.body),
Stmt::Try(try_stmt) => {
self.body_contains_step(&try_stmt.block.stmts)
|| try_stmt
.handler
.as_ref()
.map_or(false, |h| self.body_contains_step(&h.body.stmts))
|| try_stmt
.finalizer
.as_ref()
.map_or(false, |f| self.body_contains_step(&f.stmts))
}
Stmt::Return(ret) => ret
.arg
.as_ref()
.map_or(false, |arg| self.expr_contains_step(arg)),
_ => false,
}
}
fn expr_contains_step(&self, expr: &Expr) -> bool {
if self.is_task_call(expr) {
return true;
}
match expr {
Expr::Await(await_expr) => self.expr_contains_step(&await_expr.arg),
Expr::Call(call) => {
if Self::is_promise_all(&Expr::Call(call.clone())) {
return call.args.iter().any(|a| self.expr_contains_step(&a.expr));
}
false
}
Expr::Paren(p) => self.expr_contains_step(&p.expr),
_ => false,
}
}
fn walk_body(&mut self, stmts: &[Stmt]) -> Option<(String, String)> {
let mut first_id: Option<String> = None;
let mut prev_id: Option<String> = None;
for stmt in stmts {
if let Some((stmt_first, stmt_last)) = self.walk_stmt(stmt) {
if let Some(ref prev) = prev_id {
self.add_edge(prev, &stmt_first, None);
}
if first_id.is_none() {
first_id = Some(stmt_first);
}
prev_id = Some(stmt_last);
}
}
match (first_id, prev_id) {
(Some(f), Some(l)) => Some((f, l)),
_ => None,
}
}
fn walk_stmt(&mut self, stmt: &Stmt) -> Option<(String, String)> {
match stmt {
Stmt::Expr(expr_stmt) => self.walk_expr_stmt(&expr_stmt.expr),
Stmt::Decl(Decl::Var(var_decl)) => {
// const result = await task_fn(...)
for decl in &var_decl.decls {
if let Some(init) = &decl.init {
if let Some(result) = self.walk_expr_stmt(init) {
return Some(result);
}
}
}
None
}
Stmt::If(if_stmt) => self.walk_if(if_stmt),
Stmt::For(for_stmt) => self.walk_for_stmt(for_stmt),
Stmt::ForIn(for_in) => self.walk_for_in(for_in),
Stmt::ForOf(for_of) => self.walk_for_of(for_of),
Stmt::While(while_stmt) => self.walk_while(while_stmt),
Stmt::Try(try_stmt) => self.walk_try(try_stmt),
Stmt::Block(block) => self.walk_body(&block.stmts),
Stmt::Return(ret) => self.walk_return(ret),
Stmt::Decl(Decl::Fn(_)) => {
if self.stmt_contains_step(stmt) {
self.errors.push(validation::error_step_in_nested_function(
self.span_line(stmt.span()),
));
}
None
}
_ => None,
}
}
fn walk_expr_stmt(&mut self, expr: &Expr) -> Option<(String, String)> {
// await task_fn(...)
if let Expr::Await(await_expr) = expr {
if let Expr::Call(call) = await_expr.arg.as_ref() {
if self.is_task_call(&Expr::Call(call.clone())) {
return self.emit_step(call, expr);
}
}
// await Promise.all([task_fn(...), ...])
if Self::is_promise_all(&await_expr.arg) {
if let Expr::Call(promise_call) = await_expr.arg.as_ref() {
return self.emit_parallel(promise_call, expr);
}
}
}
// Bare task_fn() without await
if self.is_task_call(expr) {
self.errors
.push(validation::error_missing_await(self.span_line(expr.span())));
}
None
}
fn emit_step(&mut self, call: &CallExpr, expr: &Expr) -> Option<(String, String)> {
if self.in_try {
self.errors
.push(validation::error_step_in_catch(self.span_line(expr.span())));
return None;
}
if self.in_while {
self.errors
.push(validation::error_step_in_while(self.span_line(expr.span())));
return None;
}
if self.in_nested_func {
self.errors.push(validation::error_step_in_nested_function(
self.span_line(expr.span()),
));
return None;
}
let (name, script) = self
.extract_step_info_from_task_call(call)
.unwrap_or(("unknown".into(), "unknown".into()));
let id = self.next_id();
let node_id = self.add_node(DagNode {
id: id.clone(),
node_type: DagNodeType::Step { name: name.clone(), script },
label: name,
line: self.span_line(expr.span()),
});
Some((node_id.clone(), node_id))
}
fn emit_parallel(&mut self, promise_call: &CallExpr, expr: &Expr) -> Option<(String, String)> {
if self.in_try {
self.errors
.push(validation::error_step_in_catch(self.span_line(expr.span())));
return None;
}
if self.in_while {
self.errors
.push(validation::error_step_in_while(self.span_line(expr.span())));
return None;
}
let line = self.span_line(expr.span());
let start_id = self.next_id();
let start_node_id = self.add_node(DagNode {
id: start_id.clone(),
node_type: DagNodeType::ParallelStart,
label: "parallel".to_string(),
line,
});
let mut step_ids = Vec::new();
// Promise.all takes an array as first argument
if let Some(first_arg) = promise_call.args.first() {
if let Expr::Array(ArrayLit { elems, .. }) = first_arg.expr.as_ref() {
for elem in elems.iter().flatten() {
if let Expr::Call(call) = elem.expr.as_ref() {
if self.is_task_call(&Expr::Call(call.clone())) {
let (name, script) = self
.extract_step_info_from_task_call(call)
.unwrap_or(("unknown".into(), "unknown".into()));
let step_id = self.next_id();
let node_id = self.add_node(DagNode {
id: step_id.clone(),
node_type: DagNodeType::Step { name: name.clone(), script },
label: name,
line: self.span_line(elem.expr.span()),
});
self.add_edge(&start_node_id, &node_id, None);
step_ids.push(node_id);
}
}
}
}
}
let end_id = self.next_id();
let end_node_id = self.add_node(DagNode {
id: end_id.clone(),
node_type: DagNodeType::ParallelEnd,
label: "join".to_string(),
line,
});
for step_id in &step_ids {
self.add_edge(step_id, &end_node_id, None);
}
Some((start_node_id, end_node_id))
}
fn walk_if(&mut self, if_stmt: &IfStmt) -> Option<(String, String)> {
let has_steps_cons = self.stmt_contains_step(&if_stmt.cons);
let has_steps_alt = if_stmt
.alt
.as_ref()
.map_or(false, |a| self.stmt_contains_step(a));
if !has_steps_cons && !has_steps_alt {
return None;
}
let line = self.span_line(if_stmt.span);
let condition_source = self.expr_to_source(&if_stmt.test);
let branch_id = self.next_id();
let branch_node_id = self.add_node(DagNode {
id: branch_id.clone(),
node_type: DagNodeType::Branch { condition_source },
label: "if".to_string(),
line,
});
let mut last_ids = Vec::new();
// True branch
if let Some((true_first, true_last)) = self.walk_stmt(&if_stmt.cons) {
self.add_edge(&branch_node_id, &true_first, Some("true".to_string()));
last_ids.push(true_last);
} else {
last_ids.push(branch_node_id.clone());
}
// False branch
if let Some(alt) = &if_stmt.alt {
if let Some((else_first, else_last)) = self.walk_stmt(alt) {
self.add_edge(&branch_node_id, &else_first, Some("false".to_string()));
last_ids.push(else_last);
} else {
last_ids.push(branch_node_id.clone());
}
}
if last_ids.len() == 1 {
Some((branch_node_id, last_ids.into_iter().next().unwrap()))
} else {
let merge_id = format!("{branch_id}_merge");
Some((branch_node_id, merge_id))
}
}
fn walk_for_stmt(&mut self, for_stmt: &ForStmt) -> Option<(String, String)> {
if !self.stmt_contains_step(&for_stmt.body) {
return None;
}
self.walk_loop_body(&for_stmt.body, for_stmt.span, "for")
}
fn walk_for_in(&mut self, for_in: &ForInStmt) -> Option<(String, String)> {
if !self.stmt_contains_step(&for_in.body) {
return None;
}
let iter_source = self.expr_to_source(&for_in.right);
self.walk_loop_body_with_iter(&for_in.body, for_in.span, &iter_source)
}
fn walk_for_of(&mut self, for_of: &ForOfStmt) -> Option<(String, String)> {
if !self.stmt_contains_step(&for_of.body) {
return None;
}
let iter_source = self.expr_to_source(&for_of.right);
self.walk_loop_body_with_iter(&for_of.body, for_of.span, &iter_source)
}
fn walk_loop_body(
&mut self,
body: &Stmt,
span: swc_common::Span,
_label: &str,
) -> Option<(String, String)> {
self.walk_loop_body_with_iter(body, span, "...")
}
fn walk_loop_body_with_iter(
&mut self,
body: &Stmt,
span: swc_common::Span,
iter_source: &str,
) -> Option<(String, String)> {
let line = self.span_line(span);
let start_id = self.next_id();
let start_node_id = self.add_node(DagNode {
id: start_id.clone(),
node_type: DagNodeType::LoopStart { iter_source: iter_source.to_string() },
label: "for".to_string(),
line,
});
if let Some((body_first, body_last)) = self.walk_stmt(body) {
self.add_edge(&start_node_id, &body_first, None);
self.add_edge(&body_last, &start_node_id, Some("next".to_string()));
}
let end_id = self.next_id();
let end_node_id = self.add_node(DagNode {
id: end_id.clone(),
node_type: DagNodeType::LoopEnd,
label: "end for".to_string(),
line,
});
self.add_edge(&start_node_id, &end_node_id, Some("done".to_string()));
Some((start_node_id, end_node_id))
}
fn walk_while(&mut self, while_stmt: &WhileStmt) -> Option<(String, String)> {
if self.stmt_contains_step(&while_stmt.body) {
self.errors.push(validation::error_step_in_while(
self.span_line(while_stmt.span),
));
}
None
}
fn walk_try(&mut self, try_stmt: &TryStmt) -> Option<(String, String)> {
let has_steps = self.body_contains_step(&try_stmt.block.stmts)
|| try_stmt
.handler
.as_ref()
.map_or(false, |h| self.body_contains_step(&h.body.stmts))
|| try_stmt
.finalizer
.as_ref()
.map_or(false, |f| self.body_contains_step(&f.stmts));
if has_steps {
self.errors.push(validation::error_step_in_catch(
self.span_line(try_stmt.span),
));
}
None
}
fn walk_return(&mut self, ret: &ReturnStmt) -> Option<(String, String)> {
let line = self.span_line(ret.span);
let id = self.next_id();
let node_id = self.add_node(DagNode {
id: id.clone(),
node_type: DagNodeType::Return,
label: "return".to_string(),
line,
});
Some((node_id.clone(), node_id))
}
}
/// Extract workflow function params (no longer skips ctx)
fn extract_ts_params(params: &[swc_ecma_ast::Param], cm: &Lrc<SourceMap>) -> Vec<Param> {
let mut result = Vec::new();
for param in params {
let (name, typ) = match &param.pat {
Pat::Ident(BindingIdent { id, type_ann, .. }) => {
let name = id.sym.to_string();
let typ = type_ann.as_ref().map(|ann| {
cm.span_to_snippet(ann.type_ann.span())
.unwrap_or_else(|_| "unknown".to_string())
});
(name, typ)
}
_ => continue,
};
result.push(Param { name, typ });
}
result
}
pub fn parse_ts_workflow(code: &str) -> Result<WorkflowDag, Vec<CompileError>> {
let cm: Lrc<SourceMap> = Default::default();
let fm = cm.new_source_file(FileName::Custom("workflow.ts".into()).into(), code.into());
let lexer = Lexer::new(
Syntax::Typescript(TsSyntax::default()),
Default::default(),
StringInput::from(&*fm),
None,
);
let mut parser = Parser::new_from(lexer);
let module = parser
.parse_module()
.map_err(|e| vec![CompileError { message: format!("Parse error: {e:?}"), line: 0 }])?;
// First pass: collect task functions
let task_functions = collect_task_functions(&module);
// Find: export default workflow(async (...) => { ... })
// or: export default workflow(async function(...) { ... })
let mut workflow_body: Option<(&[Stmt], Vec<Param>)> = None;
for item in &module.body {
// export default workflow(async (...) => { ... })
if let ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr(export)) = item {
if let Some(result) = find_workflow_call(&export.expr, &cm) {
workflow_body = Some(result);
break;
}
}
// const wf = workflow(async (...) => { ... }); export default wf;
if let ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultDecl(export)) = item {
if let DefaultDecl::Fn(_) = &export.decl {
// `export default async function(...) { ... }` — not wrapped in workflow(), skip
}
}
if let ModuleItem::Stmt(Stmt::Decl(Decl::Var(var_decl))) = item {
for decl in &var_decl.decls {
if let Some(init) = &decl.init {
if let Some(result) = find_workflow_call(init, &cm) {
workflow_body = Some(result);
break;
}
}
}
}
}
let (stmts, params) = workflow_body.ok_or_else(|| {
vec![CompileError {
message: "No workflow() wrapped async function found.".to_string(),
line: 0,
}]
})?;
let source_hash = compute_source_hash(code);
let mut walker = TsWacWalker::new(cm, task_functions);
walker.walk_body(stmts);
if !walker.errors.is_empty() {
return Err(walker.errors);
}
Ok(WorkflowDag { nodes: walker.nodes, edges: walker.edges, params, source_hash })
}
/// Find workflow(async (...) => { ... }) or workflow(async function(...) { ... })
fn find_workflow_call<'a>(expr: &'a Expr, cm: &Lrc<SourceMap>) -> Option<(&'a [Stmt], Vec<Param>)> {
if let Expr::Call(call) = expr {
// Check if callee is `workflow`
let is_workflow = match &call.callee {
Callee::Expr(callee_expr) => {
if let Expr::Ident(ident) = callee_expr.as_ref() {
ident.sym.as_ref() == "workflow"
} else {
false
}
}
_ => false,
};
if is_workflow {
if let Some(first_arg) = call.args.first() {
return extract_async_fn_body(&first_arg.expr, cm);
}
}
}
None
}
fn extract_async_fn_body<'a>(
expr: &'a Expr,
cm: &Lrc<SourceMap>,
) -> Option<(&'a [Stmt], Vec<Param>)> {
match expr {
Expr::Arrow(arrow) if arrow.is_async => {
let params = extract_arrow_params(&arrow.params, cm);
match &*arrow.body {
BlockStmtOrExpr::BlockStmt(block) => Some((&block.stmts, params)),
_ => None,
}
}
Expr::Fn(fn_expr) if fn_expr.function.is_async => {
let params = extract_ts_params(&fn_expr.function.params, cm);
fn_expr
.function
.body
.as_ref()
.map(|body| (body.stmts.as_slice(), params))
}
Expr::Paren(p) => extract_async_fn_body(&p.expr, cm),
_ => None,
}
}
/// Extract arrow function params (no longer skips ctx)
fn extract_arrow_params(pats: &[Pat], cm: &Lrc<SourceMap>) -> Vec<Param> {
let mut result = Vec::new();
for pat in pats {
match pat {
Pat::Ident(BindingIdent { id, type_ann, .. }) => {
let name = id.sym.to_string();
let typ = type_ann.as_ref().map(|ann| {
cm.span_to_snippet(ann.type_ann.span())
.unwrap_or_else(|_| "unknown".to_string())
});
result.push(Param { name, typ });
}
_ => {}
}
}
result
}
fn extract_string_lit(expr: &Expr) -> Option<String> {
match expr {
Expr::Lit(Lit::Str(s)) => Some(s.value.to_string()),
Expr::Tpl(tpl) if tpl.exprs.is_empty() && tpl.quasis.len() == 1 => {
tpl.quasis.first().map(|q| q.raw.to_string())
}
_ => None,
}
}
fn compute_source_hash(code: &str) -> String {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(code.as_bytes());
format!("{:x}", hasher.finalize())
}

View File

@@ -1,64 +0,0 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct CompileError {
pub message: String,
pub line: usize,
}
impl std::fmt::Display for CompileError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "line {}: {}", self.line, self.message)
}
}
pub fn error_step_in_try(line: usize) -> CompileError {
CompileError {
message:
"Task calls inside try/except are not allowed. Steps have built-in error handling."
.to_string(),
line,
}
}
pub fn error_step_in_while(line: usize) -> CompileError {
CompileError {
message: "Task calls inside while loops are not allowed. Use for loops instead."
.to_string(),
line,
}
}
pub fn error_step_in_nested_function(line: usize) -> CompileError {
CompileError {
message: "Task calls inside nested functions, closures, or lambdas are not allowed."
.to_string(),
line,
}
}
pub fn error_step_in_comprehension(line: usize) -> CompileError {
CompileError { message: "Task calls inside comprehensions are not allowed.".to_string(), line }
}
pub fn error_not_async(line: usize) -> CompileError {
CompileError { message: "Workflow function must be async.".to_string(), line }
}
pub fn error_missing_await(line: usize) -> CompileError {
CompileError {
message:
"Task calls must be awaited directly or used inside asyncio.gather()/Promise.all()."
.to_string(),
line,
}
}
pub fn error_step_in_catch(line: usize) -> CompileError {
CompileError {
message:
"Task calls inside catch blocks are not allowed. Steps have built-in error handling."
.to_string(),
line,
}
}

View File

@@ -1,266 +0,0 @@
use windmill_parser_wac::dag::DagNodeType;
use windmill_parser_wac::python::parse_python_workflow;
#[test]
fn test_simple_sequential_workflow() {
let code = r#"
import asyncio
from wmill import workflow, task
@task
async def extract_data(url: str): ...
@task
async def load_data(data: list): ...
@workflow
async def my_etl(url: str):
raw = await extract_data(url=url)
await load_data(data=raw)
return {"status": "done"}
"#;
let dag = parse_python_workflow(code).expect("should parse");
assert_eq!(dag.nodes.len(), 3); // 2 steps + 1 return
assert_eq!(dag.edges.len(), 2); // step0->step1, step1->return
// Check params (url — no ctx to skip)
assert_eq!(dag.params.len(), 1);
assert_eq!(dag.params[0].name, "url");
assert_eq!(dag.params[0].typ.as_deref(), Some("str"));
// Check first step
match &dag.nodes[0].node_type {
DagNodeType::Step { name, script } => {
assert_eq!(name, "extract_data");
assert_eq!(script, "extract_data");
}
_ => panic!("expected Step node"),
}
// Check second step
match &dag.nodes[1].node_type {
DagNodeType::Step { name, script } => {
assert_eq!(name, "load_data");
assert_eq!(script, "load_data");
}
_ => panic!("expected Step node"),
}
// Check return
assert!(matches!(dag.nodes[2].node_type, DagNodeType::Return));
// Check source hash is non-empty
assert!(!dag.source_hash.is_empty());
}
#[test]
fn test_parallel_workflow() {
let code = r#"
import asyncio
from wmill import workflow, task
@task
async def extract_data(url: str): ...
@task
async def clean_data(data: list): ...
@task
async def compute_stats(data: list): ...
@task
async def load_to_warehouse(rows: list): ...
@workflow
async def my_etl(url: str):
raw = await extract_data(url=url)
cleaned, stats = await asyncio.gather(
clean_data(data=raw),
compute_stats(data=raw),
)
await load_to_warehouse(rows=cleaned)
return {"status": "done"}
"#;
let dag = parse_python_workflow(code).expect("should parse");
// extract, ParallelStart, clean, stats, ParallelEnd, load, return = 7
assert_eq!(dag.nodes.len(), 7);
assert!(matches!(dag.nodes[0].node_type, DagNodeType::Step { .. }));
assert!(matches!(dag.nodes[1].node_type, DagNodeType::ParallelStart));
assert!(matches!(dag.nodes[2].node_type, DagNodeType::Step { .. }));
assert!(matches!(dag.nodes[3].node_type, DagNodeType::Step { .. }));
assert!(matches!(dag.nodes[4].node_type, DagNodeType::ParallelEnd));
assert!(matches!(dag.nodes[5].node_type, DagNodeType::Step { .. }));
assert!(matches!(dag.nodes[6].node_type, DagNodeType::Return));
}
#[test]
fn test_conditional_workflow() {
let code = r#"
import asyncio
from wmill import workflow, task
@task
async def send_alert(msg: str): ...
@task
async def load_data(): ...
@workflow
async def my_etl(count: int):
if count > 100:
await send_alert(msg="large")
await load_data()
return {"done": True}
"#;
let dag = parse_python_workflow(code).expect("should parse");
// Branch, notify step, load step, return = 4
assert_eq!(dag.nodes.len(), 4);
assert!(matches!(dag.nodes[0].node_type, DagNodeType::Branch { .. }));
assert!(matches!(dag.nodes[1].node_type, DagNodeType::Step { .. }));
}
#[test]
fn test_for_loop_workflow() {
let code = r#"
import asyncio
from wmill import workflow, task
@task
async def process_item(item: str): ...
@workflow
async def my_etl(items: list):
for item in items:
await process_item(item=item)
return {"done": True}
"#;
let dag = parse_python_workflow(code).expect("should parse");
// LoopStart, step, LoopEnd, return = 4
assert_eq!(dag.nodes.len(), 4);
assert!(matches!(
dag.nodes[0].node_type,
DagNodeType::LoopStart { .. }
));
assert!(matches!(dag.nodes[1].node_type, DagNodeType::Step { .. }));
assert!(matches!(dag.nodes[2].node_type, DagNodeType::LoopEnd));
}
#[test]
fn test_reject_step_in_try() {
let code = r#"
import asyncio
from wmill import workflow, task
@task
async def extract_data(): ...
@workflow
async def my_etl():
try:
await extract_data()
except Exception:
pass
"#;
let result = parse_python_workflow(code);
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors[0].message.contains("try/except"));
}
#[test]
fn test_reject_step_in_while() {
let code = r#"
import asyncio
from wmill import workflow, task
@task
async def extract_data(): ...
@workflow
async def my_etl():
while True:
await extract_data()
"#;
let result = parse_python_workflow(code);
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors[0].message.contains("while"));
}
#[test]
fn test_reject_non_async() {
let code = r#"
from wmill import workflow
@workflow
def my_etl():
pass
"#;
let result = parse_python_workflow(code);
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors[0].message.contains("async"));
}
#[test]
fn test_reject_missing_await() {
let code = r#"
import asyncio
from wmill import workflow, task
@task
async def extract_data(): ...
@workflow
async def my_etl():
extract_data()
"#;
let result = parse_python_workflow(code);
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors[0].message.contains("awaited"));
}
#[test]
fn test_no_workflow_function() {
let code = r#"
async def my_func():
pass
"#;
let result = parse_python_workflow(code);
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors[0].message.contains("No @workflow"));
}
#[test]
fn test_task_with_external_path() {
let code = r#"
import asyncio
from wmill import workflow, task
@task(path="f/external_script")
async def run_external(x: int): ...
@workflow
async def my_wf(x: int):
result = await run_external(x=x)
return result
"#;
let dag = parse_python_workflow(code).expect("should parse");
assert_eq!(dag.nodes.len(), 2); // 1 step + 1 return (bare `return` is not a step node but walk_return creates one)
match &dag.nodes[0].node_type {
DagNodeType::Step { name, script } => {
assert_eq!(name, "run_external");
assert_eq!(script, "f/external_script");
}
_ => panic!("expected Step node"),
}
}

View File

@@ -1,245 +0,0 @@
use windmill_parser_wac::dag::DagNodeType;
use windmill_parser_wac::typescript::parse_ts_workflow;
#[test]
fn test_simple_sequential_ts_workflow() {
let code = r#"
import { workflow, task } from "windmill-client";
const extract_data = task(async (url: string) => {});
const load_data = task(async (data: any) => {});
export default workflow(async (url: string) => {
const raw = await extract_data(url);
await load_data(raw);
return { status: "done" };
});
"#;
let dag = parse_ts_workflow(code).expect("should parse");
assert_eq!(dag.nodes.len(), 3); // 2 steps + 1 return
assert_eq!(dag.edges.len(), 2);
// Check params (url — no ctx to skip)
assert_eq!(dag.params.len(), 1);
assert_eq!(dag.params[0].name, "url");
assert_eq!(dag.params[0].typ.as_deref(), Some("string"));
match &dag.nodes[0].node_type {
DagNodeType::Step { name, script } => {
assert_eq!(name, "extract_data");
assert_eq!(script, "extract_data");
}
_ => panic!("expected Step node"),
}
match &dag.nodes[1].node_type {
DagNodeType::Step { name, script } => {
assert_eq!(name, "load_data");
assert_eq!(script, "load_data");
}
_ => panic!("expected Step node"),
}
assert!(matches!(dag.nodes[2].node_type, DagNodeType::Return));
assert!(!dag.source_hash.is_empty());
}
#[test]
fn test_parallel_ts_workflow() {
let code = r#"
import { workflow, task } from "windmill-client";
const extract_data = task(async (url: string) => {});
const clean_data = task(async (data: any) => {});
const compute_stats = task(async (data: any) => {});
const load_to_warehouse = task(async (rows: any) => {});
export default workflow(async (url: string) => {
const raw = await extract_data(url);
const [cleaned, stats] = await Promise.all([
clean_data(raw),
compute_stats(raw),
]);
await load_to_warehouse(cleaned);
return { status: "done", rows: stats.rowCount };
});
"#;
let dag = parse_ts_workflow(code).expect("should parse");
// extract, ParallelStart, clean, stats, ParallelEnd, load, return = 7
assert_eq!(dag.nodes.len(), 7);
assert!(matches!(dag.nodes[0].node_type, DagNodeType::Step { .. }));
assert!(matches!(dag.nodes[1].node_type, DagNodeType::ParallelStart));
assert!(matches!(dag.nodes[2].node_type, DagNodeType::Step { .. }));
assert!(matches!(dag.nodes[3].node_type, DagNodeType::Step { .. }));
assert!(matches!(dag.nodes[4].node_type, DagNodeType::ParallelEnd));
assert!(matches!(dag.nodes[5].node_type, DagNodeType::Step { .. }));
assert!(matches!(dag.nodes[6].node_type, DagNodeType::Return));
}
#[test]
fn test_conditional_ts_workflow() {
let code = r#"
import { workflow, task } from "windmill-client";
const send_alert = task(async (msg: string) => {});
const load_data = task(async () => {});
export default workflow(async (count: number) => {
if (count > 100) {
await send_alert("large");
}
await load_data();
return { done: true };
});
"#;
let dag = parse_ts_workflow(code).expect("should parse");
// Branch, notify, load, return = 4
assert_eq!(dag.nodes.len(), 4);
assert!(matches!(dag.nodes[0].node_type, DagNodeType::Branch { .. }));
}
#[test]
fn test_for_of_ts_workflow() {
let code = r#"
import { workflow, task } from "windmill-client";
const process_item = task(async (item: string) => {});
export default workflow(async (items: string[]) => {
for (const item of items) {
await process_item(item);
}
return { done: true };
});
"#;
let dag = parse_ts_workflow(code).expect("should parse");
// LoopStart, step, LoopEnd, return = 4
assert_eq!(dag.nodes.len(), 4);
assert!(matches!(
dag.nodes[0].node_type,
DagNodeType::LoopStart { .. }
));
}
#[test]
fn test_reject_step_in_try_catch() {
let code = r#"
import { workflow, task } from "windmill-client";
const extract_data = task(async () => {});
export default workflow(async () => {
try {
await extract_data();
} catch (e) {
console.log(e);
}
});
"#;
let result = parse_ts_workflow(code);
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors[0].message.contains("catch"));
}
#[test]
fn test_reject_step_in_while_ts() {
let code = r#"
import { workflow, task } from "windmill-client";
const extract_data = task(async () => {});
export default workflow(async () => {
while (true) {
await extract_data();
}
});
"#;
let result = parse_ts_workflow(code);
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors[0].message.contains("while"));
}
#[test]
fn test_reject_missing_await_ts() {
let code = r#"
import { workflow, task } from "windmill-client";
const extract_data = task(async () => {});
export default workflow(async () => {
extract_data();
});
"#;
let result = parse_ts_workflow(code);
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors[0].message.contains("awaited"));
}
#[test]
fn test_no_workflow_wrapper() {
let code = r#"
export default async function main(ctx: any) {
return {};
}
"#;
let result = parse_ts_workflow(code);
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors[0].message.contains("No workflow()"));
}
#[test]
fn test_variable_declaration_with_step() {
let code = r#"
import { workflow, task } from "windmill-client";
const compute = task(async () => {});
export default workflow(async () => {
const result = await compute();
return result;
});
"#;
let dag = parse_ts_workflow(code).expect("should parse");
assert_eq!(dag.nodes.len(), 2); // step + return
assert!(matches!(dag.nodes[0].node_type, DagNodeType::Step { .. }));
}
#[test]
fn test_task_with_external_path() {
let code = r#"
import { workflow, task } from "windmill-client";
const run_external = task("f/external_script", async (x: number) => {});
export default workflow(async (x: number) => {
const result = await run_external(x);
return result;
});
"#;
let dag = parse_ts_workflow(code).expect("should parse");
assert_eq!(dag.nodes.len(), 2); // step + return
match &dag.nodes[0].node_type {
DagNodeType::Step { name, script } => {
assert_eq!(name, "run_external");
assert_eq!(script, "f/external_script");
}
_ => panic!("expected Step node"),
}
}

View File

@@ -38,8 +38,6 @@ csharp-parser = [ "dep:windmill-parser-csharp"]
nu-parser = [ "dep:windmill-parser-nu"] nu-parser = [ "dep:windmill-parser-nu"]
java-parser = [ "dep:windmill-parser-java"] java-parser = [ "dep:windmill-parser-java"]
ruby-parser = [ "dep:windmill-parser-ruby"] ruby-parser = [ "dep:windmill-parser-ruby"]
wac-parser = [ "dep:windmill-parser-wac"]
asset-parser = [ "dep:windmill-parser-ts-asset", "dep:windmill-parser-py-asset", "dep:windmill-parser-sql-asset"]
[dependencies] [dependencies]
anyhow.workspace = true anyhow.workspace = true
@@ -57,10 +55,6 @@ windmill-parser-csharp = { workspace = true, optional = true }
windmill-parser-nu = { workspace = true, optional = true } windmill-parser-nu = { workspace = true, optional = true }
windmill-parser-java = { workspace = true, optional = true } windmill-parser-java = { workspace = true, optional = true }
windmill-parser-ruby = { workspace = true, optional = true } windmill-parser-ruby = { workspace = true, optional = true }
windmill-parser-wac = { workspace = true, optional = true }
windmill-parser-ts-asset = { workspace = true, optional = true }
windmill-parser-py-asset = { workspace = true, optional = true }
windmill-parser-sql-asset = { workspace = true, optional = true }
wasm-bindgen.workspace = true wasm-bindgen.workspace = true
serde_json.workspace = true serde_json.workspace = true

View File

@@ -56,17 +56,6 @@ const targets = [
features: "ruby-parser", features: "ruby-parser",
env: "tree-sitter", env: "tree-sitter",
}, },
{
ident: "wac",
desc: "Workflow-as-Code",
features: "wac-parser",
env: "default",
}, {
ident: "asset",
desc: "Asset parsers (TS, Python, SQL) with SQL AST",
features: "asset-parser",
env: "default",
},
# ^^^ Add new entry here ^^^ # ^^^ Add new entry here ^^^
]; ];
# NOTE: This is legacy command for building all, but it is not more used # NOTE: This is legacy command for building all, but it is not more used

View File

@@ -33,6 +33,3 @@ popd
pushd "pkg-java" && npm publish ${args} pushd "pkg-java" && npm publish ${args}
popd popd
pushd "pkg-asset" && npm publish ${args}
popd

Some files were not shown because too many files have changed in this diff Show More