Compare commits

..

79 Commits

Author SHA1 Message Date
centdix
36aadbebec Merge branch 'main' into frontdev 2026-02-23 13:29:18 +00:00
centdix
bedba3b75e fix: move pane zoom into attach cmd chain for reliable initial zoom
- Pass initialPane to attach() so zoom runs inside the shell command
  chain where tmux is guaranteed to exist (no external race)
- Send initialPane in the first resize WS message (atomic, single msg)
- Remove pendingPane from WsData (dead code from iterative patching)
- Fix unzoom: use shell conditional instead of broken tmux if-shell

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 00:41:23 +00:00
centdix
771d67c849 fix: pane zoom race condition and desktop unzoom on attach
- Bun processes async WS messages sequentially, so selectPane always
  arrives after attach completes — call selectPane() directly when
  attached instead of only queueing
- Always unzoom on attach via tmux if-shell #{window_zoomed_flag} so
  desktop never starts with a pane zoomed from a previous mobile session
- Remove unreliable setTimeout approach, send selectPane from client
  immediately after resize

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 00:27:14 +00:00
centdix
46d486960a feat: make dev-dashboard mobile-friendly
Collapsible sidebar (full-screen overlay on mobile), hamburger menu,
bottom pane switcher bar for full (Claude/Backend/Frontend) and sandbox
(Claude/Shell) profiles, auto-zoom into Claude pane on mobile connect,
larger terminal font on mobile, and iOS overscroll prevention.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 00:04:03 +00:00
centdix
ca73267cbb complete readme 2026-02-22 23:56:12 +00:00
centdix
6eabb8db63 nit 2026-02-22 16:59:53 +00:00
centdix
5cc8f20cd2 docs: add Cursor IDE new window tip to dev-dashboard README
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 16:00:07 +00:00
centdix
ea5312e940 fix: prevent stale proc exit from deleting active terminal session
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 14:44:27 +00:00
centdix
c62ba73ce4 fix: select claude pane on attach and restart tmux if dead
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 14:41:22 +00:00
centdix
98ac164ac8 merge 2026-02-22 14:11:44 +00:00
centdix
ed6aaeeea3 fix: use WM_WORKTREE_PATH in worktree-cleanup and add debug logs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 14:07:47 +00:00
centdix
5e415c0a12 playwirght 2026-02-22 14:03:49 +00:00
centdix
35dded1347 feat: sanitize worktree name input to valid git branch name
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-22 13:51:26 +00:00
centdix
aedf012c84 fix: restore dialog focus using programmatic focus instead of autofocus
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 13:51:19 +00:00
centdix
62fea97547 feat: add Cmd+M keyboard shortcut for merge
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 13:48:35 +00:00
centdix
96c2d88d91 feat: add merge endpoint and button, remove unused close/send endpoints
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 13:46:59 +00:00
centdix
5eb308ad35 nit 2026-02-22 13:42:55 +00:00
centdix
cfa04e1188 kill process on clenaup 2026-02-22 13:42:46 +00:00
centdix
0d520f730b 0.0.0.0 2026-02-22 13:19:52 +00:00
centdix
8cdc7d6e9e nit 2026-02-22 01:55:41 +00:00
centdix
51077245c7 feat: move SSH settings to gear icon next to Cursor button
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-22 01:50:47 +00:00
centdix
9e387c3559 fix: make playwright browsers dir writable for sandbox user
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 01:48:03 +00:00
centdix
7aea965803 style: reduce terminal font size to 11
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-22 01:46:59 +00:00
centdix
8446e3b551 refactor: overhaul sandbox Dockerfile and fix screenshot instructions
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 01:41:04 +00:00
centdix
a6d6136d57 fix: increase Bun.serve idleTimeout to prevent worktree removal timeout
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-22 01:40:17 +00:00
centdix
c93b2e287c feat: add screenshot capture and R2 upload support for sandbox agents
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 00:36:17 +00:00
centdix
93f927c1c1 fix: add EE repo mount for sandbox and remove agent key listener
Add ~/windmill-ee-private mount to sandbox extra_mounts (needed for
.git access) and remove ArrowLeft/ArrowRight agent-switching keydown
handler from CreateWorktreeDialog.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 21:35:53 +00:00
centdix
83f21510f3 feat: add open in Cursor button with SSH remote support
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 21:19:55 +00:00
centdix
5c1f69ddcd nit 2026-02-21 21:19:52 +00:00
centdix
3f2bd424c7 docs: add EE sandbox extra_mounts setup instructions
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-21 21:14:53 +00:00
centdix
0d5f42e89e refactor: replace symlink hooks with additionalDirectories for EE access
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-21 21:14:53 +00:00
centdix
dfad07881d rm 2026-02-21 21:14:47 +00:00
centdix
7b6ba7093a feat: remove agent-only profile, add dialog shortcuts, auto-focus terminal
- Remove agent-only profile (only full and agent-yolo remain)
- Arrow up/down in create dialog cycles profiles, left/right cycles agents
- Auto-focus terminal when switching worktrees

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 20:43:57 +00:00
centdix
8042e33c38 nit 2026-02-21 20:38:44 +00:00
centdix
57d23c92c5 feat: show port status in worktree sidebar list
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 20:34:24 +00:00
centdix
f21140f7cd feat: add run.sh, agent name in sidebar, and vite preview proxy
- run.sh builds frontend then serves in production mode
- Persist AGENT in .env.local and show it in worktree list
- Add preview proxy config so production mode routes API/WS correctly

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 20:34:02 +00:00
centdix
2800226bd4 nit 2026-02-21 20:22:46 +00:00
centdix
2ae82796fc feat: show worktree profile tag in sidebar
Persist profile to .env.local on worktree creation and display it
(full, agent-only, agent-yolo) in the worktree list.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 20:22:33 +00:00
centdix
d1290ba777 feat: add dev.sh script and document keyboard shortcuts
Single script to start both backend and frontend with prefixed logs.
Updated README with dev.sh usage and keyboard shortcut reference.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 20:03:40 +00:00
centdix
9de0060884 feat: add keyboard shortcuts for worktree navigation in dev-dashboard
Cmd+Up/Down to switch worktrees, Cmd+K to create new, Cmd+D to remove.
xterm.js passthrough ensures shortcuts bubble up from terminal. Shortcut
hints displayed in a fixed panel at the bottom of the sidebar.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 20:01:45 +00:00
centdix
3df8964fc6 feat: add tmux pane layout to agent system prompt in workmux config
Agent now knows pane 1 is backend (cargo) and pane 2 is frontend (npm),
so "check backend logs" maps directly to the right capture-pane command.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 19:51:37 +00:00
centdix
fb0b2234ba docs: add dev-dashboard README with architecture and quick start
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 19:36:03 +00:00
centdix
4064ec3a3d nit 2026-02-21 19:28:31 +00:00
centdix
0db6cbd10c feat: add codex agent support to sandbox container
- Install @openai/codex in Dockerfile.sandbox
- Pass developer_instructions via -c flag with proper shell escaping
- Use --yolo flag for sandbox profile (container is the sandbox)
- Mount ~/.codex into container via workmux extra_mounts config

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 19:27:30 +00:00
centdix
ab91f78017 nit 2026-02-21 19:12:57 +00:00
centdix
1e0245ca9a feat: add codex as agent choice in worktree creation dialog
Add agent selector (Claude/Codex) to the create worktree dialog,
orthogonal to the profile choice. Selection is persisted alongside
the profile default.

Backend builds the appropriate command per agent: codex uses
--full-auto for sandbox, claude uses --dangerously-skip-permissions
with --append-system-prompt.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 19:11:26 +00:00
centdix
bead746bb8 fix: socat zombie reaping, reconcile filter, and polling logs
- Consume proc.exited promise to prevent zombie socat processes
- Use container name prefix filter instead of ancestor (matches
  containers from older image builds)
- Improve polling logs to show retry count and waiting state

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 19:06:48 +00:00
centdix
268b5ee2a8 feat: add save as default checkbox for worktree profile selection
Persist the selected profile to localStorage when checked, and
preselect it on subsequent dialog opens.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 19:04:58 +00:00
centdix
24f2571b37 fix: use PORT env var and cargo watch for backend in system prompt
Change backend start command to use PORT= instead of --port flag and
cargo watch for auto-reload. Install cargo-watch in sandbox container.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 18:59:11 +00:00
centdix
ed1a655317 fix: make cargo registry world-writable in sandbox container
The sqlx-cli install populates /opt/cargo/registry as root. Add
chmod -R a+rwX after the install so the sandbox user can write
to the registry when building.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 18:50:02 +00:00
centdix
a4b440a20d feat: add socat port forwarding for sandbox containers
When a worktree runs inside a Docker sandbox, its ports are only
reachable via the container's bridge IP. socat forwards host ports to
the container so the browser (over SSH) can reach them.

- New socat.ts module manages forwarding lifecycle (start/stop/reconcile)
- Polls for container after creation (non-blocking, up to 30s)
- Kills orphaned socat on startup before re-establishing forwards
- Cleans up on worktree removal and SIGINT/SIGTERM
- Extract readEnvLocal to env.ts to break circular import
- Change isPortListening to HTTP fetch (avoids socat false positives)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 18:49:59 +00:00
centdix
b550be8711 fix: install Node 22 and bootstrap frontend in sandbox container
Replace Debian's Node 18 with NodeSource Node 22. Run npm install and
generate-backend-client in the entrypoint so the frontend is ready.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 18:42:21 +00:00
centdix
071129f03b feat: show clickable port badges in top bar with live status
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-21 18:02:40 +00:00
centdix
e9ac1ce9eb feat: add logging for worktree lifecycle and port assignments
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-21 17:53:54 +00:00
centdix
587142ddac fix: assign worktree port slots by scanning existing .env.local files
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-21 17:53:49 +00:00
centdix
d9ed0c318f fix: run sqlx migrations on sandbox startup and fix cargo permissions
The sandbox entrypoint now runs `sqlx migrate run` after creating the
database so that sqlx compile-time query checks work immediately. Also
makes /opt/cargo world-writable so arbitrary-UID sandbox users can write
to the cargo git cache and registry.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 17:37:39 +00:00
centdix
4959a0553a feat: append environment-aware system prompt to claude in worktree agents
Read .env.local from the worktree to get port assignments and build a
system prompt informing Claude of backend/frontend ports and startup
commands. For sandbox profiles, double-escape quotes to survive the
extra shell layer inside the container.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 17:36:06 +00:00
centdix
c4323e40c1 fix: enable clipboard support and suppress browser context menu in web terminal
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-21 17:22:43 +00:00
centdix
696b8de1ed fix: assign worktree ports by index instead of scanning for free ports
Determine the slot from the worktree's position in workmux list rather
than probing ports. Keeps the port-in-use safety check. Removes unused
find_port helper.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 15:45:24 +00:00
centdix
5f6dda9060 feat: add shell pane alongside agent in agent-only/yolo profiles
Split a 33% width shell pane on the right, using the worktree
directory from pane 0 so it starts in the correct path.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 15:34:02 +00:00
centdix
8490f4435d feat: extract CreateWorktreeDialog component and fix Enter to confirm
Extract inline create dialog into its own component. Wrap both dialogs
in forms so Enter submits, and autofocus the confirm button in
ConfirmDialog so Enter triggers confirm instead of cancel.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 15:27:43 +00:00
centdix
fa2f65e512 feat: add optional name input to worktree creation dialog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 15:23:55 +00:00
centdix
b9dec43d2a feat: add postgres + sqlx-cli to sandbox container
Start PostgreSQL (owned by postgres user) in entrypoint.sh with a unix
socket in /tmp so the agent can use DATABASE_URL=postgres:///windmill?host=/tmp
for sqlx migrations and cargo check.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 15:04:23 +00:00
centdix
5227b76c2f fix: run agent-yolo profile inside sandbox container
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 13:56:34 +00:00
centdix
1643d654a8 feat: add custom sandbox Dockerfile with sudo support
- Add Dockerfile.sandbox with sudo, writable passwd/shadow, and
  entrypoint that registers dynamic UIDs for full root access inside container
- Remove playwright MCP server (npx not available in sandbox)
- Move sandbox host_commands/image config to global workmux config
- Remove git from host_commands to prevent infinite fork bomb via shims

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 13:55:59 +00:00
centdix
65a8789dfd fix: close confirm dialog immediately and gray out removing worktrees
Dismiss the confirmation dialog as soon as the user confirms instead
of waiting for the API call. Show the item grayed out with
pointer-events disabled while deletion is in progress. Auto-select
the previous (or next) worktree when the selected one is removed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 02:35:07 +00:00
centdix
6299e7a36a fix: ensure tmux server is running on dashboard startup
Start a detached tmux session if none exists, so worktree
operations don't fail when tmux hasn't been started yet.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 02:03:25 +00:00
centdix
bcbfe4659d feat: add remove button to each worktree in sidebar list
Show an × button on hover for non-main worktrees. Replace the
boolean showConfirmRemove with a removeBranch string so the
confirm dialog works from both sidebar and top bar.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 02:03:06 +00:00
centdix
b2fac069df feat: enable sandbox mode for agent-yolo profile
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-21 01:49:44 +00:00
centdix
4ab08cb6a1 feat: add agent-yolo profile and hide worktrees without tmux window
Add "Agent (skip permissions)" profile that runs claude with
--dangerously-skip-permissions. Filter worktrees without a tmux
window from the sidebar list instead of showing a disabled entry.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 01:40:51 +00:00
centdix
d6d4d85d8f fix: enable tmux mouse mode and update gitignore for split layout
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-21 01:29:39 +00:00
centdix
81d386d365 feat: add profile selector for worktree creation
Support "agent-only" and "full" profiles when creating worktrees.
Agent-only skips default pane commands, kills extra panes, and starts
only claude. Full uses the default workmux pane layout. Profile is
selected via a centered dialog in the frontend.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 01:10:09 +00:00
centdix
a1b878842f refactor: simplify session view to remove-only with confirmation modal
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-21 01:04:01 +00:00
centdix
00c3e9baf0 fix: only connect terminal when worktree has a tmux window
Check mux status before attempting terminal connection. Worktrees
without a tmux window (mux !== "✓") show an informational message
instead of failing with "can't find window".

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 00:45:25 +00:00
centdix
118dcb59af fix: defer terminal spawn until client reports actual dimensions
Instead of spawning with hardcoded 120x30 on WebSocket open, wait for
the client's first resize message with real fitted dimensions. Fixes
terminal not taking full width/height since script+pipes PTY can't be
resized after creation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 00:35:17 +00:00
centdix
bd583be239 fix: clean up stale tmux sessions on dashboard startup
Server crashes/restarts left orphaned wm-dash-* grouped tmux sessions,
causing "duplicate session" errors on subsequent connections. Now cleans
up stale sessions on startup and pre-emptively before each attach.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 00:32:18 +00:00
centdix
c1f7cb5d42 fix: use script+pipes for terminal PTY and fix frontend sizing
Bun's terminal option data callback doesn't fire inside Bun.serve
context (Bun 1.3.9 bug). Switch to script(1) for PTY allocation with
piped stdin/stdout. Fix terminal not taking full width/height by adding
min-h-0, min-w-0 and width: 100% for proper flex layout.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 00:04:50 +00:00
centdix
d502ef5029 refactor: split dev-dashboard into separate backend and svelte frontend
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-20 23:33:16 +00:00
centdix
bc7ca9982b feat: add dev-dashboard — browser-based workmux frontend
Web dashboard (Bun + xterm.js) that wraps workmux CLI commands and
renders tmux windows in embedded browser terminals. Replaces direct
tmux navigation with a sidebar-based UI at localhost:5111.

- Bun HTTP server with REST API for worktree CRUD (add/rm/open/close/send)
- Bun.Terminal PTY API to attach to tmux grouped sessions per worktree
- xterm.js frontend with WebSocket bridge for real-time terminal I/O
- Scrollback buffer for reconnection, ResizeObserver for dynamic fitting
- Add direnv allow to worktree-env post-create hook for nix devshell

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 23:07:32 +00:00
432 changed files with 13640 additions and 22429 deletions

View File

@@ -1,21 +0,0 @@
#!/usr/bin/env bash
# PreToolUse hook: block destructive git operations when on the main branch.
# Non-git tool calls and read-only git commands pass through silently.
set -euo pipefail
input="$(cat)"
tool_name="$(echo "$input" | jq -r '.tool_name // empty')"
# Only care about Bash tool calls
[[ "$tool_name" == "Bash" ]] || exit 0
command="$(echo "$input" | jq -r '.tool_input.command // empty')"
# Only care about git write commands
if [[ "$command" =~ ^git\ (push|reset|revert|checkout|merge|rebase|commit|add) ]]; then
branch="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
if [[ "$branch" == "main" ]]; then
echo "BLOCK: You are on the main branch. Create or switch to a feature branch first."
fi
fi

View File

@@ -30,15 +30,7 @@
"Bash(cargo check:*)",
"mcp__ide__getDiagnostics",
"Bash(npm run generate-backend-client:*)",
"Bash(npm run check:*)",
"Bash(git push:*)",
"Bash(git reset:*)",
"Bash(git revert:*)",
"Bash(git checkout:*)",
"Bash(git merge:*)",
"Bash(git rebase:*)",
"Bash(git add:*)",
"Bash(git commit:*)"
"Bash(npm run check:*)"
],
"deny": [
"Read(.env)",
@@ -63,23 +55,17 @@
"Bash(chown:*)",
"Bash(truncate:*)",
"Bash(shred:*)",
"Bash(unlink:*)"
"Bash(unlink:*)",
"Bash(git push:*)",
"Bash(git reset:*)",
"Bash(git revert:*)",
"Bash(git checkout:*)",
"Bash(git merge:*)",
"Bash(git rebase:*)"
]
},
"enableAllProjectMcpServers": true,
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/guard-main-branch.sh",
"timeout": 5
}
]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write",
@@ -114,4 +100,4 @@
"typescript-lsp@claude-plugins-official": true,
"code-review@claude-plugins-official": true
}
}
}

View File

@@ -1,39 +0,0 @@
---
name: refine
user_invocable: true
description: End-of-session reflection. Reviews friction encountered during the session and proposes updates to docs/ to capture lessons learned.
---
# Refine Skill
Reflect on the current session and update documentation with lessons learned.
## Instructions
1. **Identify friction**: Review what happened in this session:
- Run `git diff main...HEAD --stat` to see what files were touched
- Think about: what was slow, what failed, what required multiple attempts, what information was missing or hard to find
2. **Read current docs**: Read the docs that were relevant to this session:
- `docs/validation.md`
- `docs/enterprise.md`
- `docs/autonomous-mode.md`
- Any skills that were invoked
3. **Propose updates**: For each piece of friction, decide if it warrants a doc update:
- **Missing knowledge**: Information you had to discover that should be documented
- **Wrong guidance**: Instructions that led you astray
- **Missing validation rule**: A check that should be in the validation matrix
- **New pattern**: A codebase pattern worth capturing for next time
4. **Apply updates**: Edit the relevant `docs/` files. Keep changes minimal and specific — add only what would have saved time this session.
5. **Report**: Summarize what was added/changed and why.
## Rules
- Only add knowledge confirmed by this session — no speculative additions
- Keep docs concise — add a line or two, not a paragraph
- If a whole new doc is needed, create it in `docs/` and add a pointer in `CLAUDE.md`
- Don't update skills unless a coding pattern was genuinely wrong
- Don't add things Claude already knows — only Windmill-specific knowledge

View File

@@ -3,105 +3,493 @@ name: rust-backend
description: Rust coding guidelines for the Windmill backend. MUST use when writing or modifying Rust code in the backend directory.
---
# Windmill Rust Patterns
# Rust Backend Coding Guidelines
Apply these Windmill-specific patterns when writing Rust code in `backend/`.
Apply these patterns when writing or modifying Rust code in the `backend/` directory.
## Data Structure Design
Choose between `struct`, `enum`, or `newtype` based on domain needs:
- Use `enum` for state machines instead of boolean flags or loosely related fields
- Model invariants explicitly using types (e.g., `NonZeroU32`, `Duration`, custom enums)
- Consider ownership of each field:
- Use `&str` vs `String`, slices vs vectors
- Use `Arc<T>` when sharing across threads
- Use `Cow<'a, T>` for flexible ownership
```rust
// State machine with enum
enum JobState {
Pending { scheduled_for: DateTime<Utc> },
Running { started_at: DateTime<Utc>, worker: String },
Completed { result: JobResult, duration_ms: i64 },
Failed { error: String, retries: u32 },
}
// Avoid multiple booleans
struct Job {
is_pending: bool, // Don't do this
is_running: bool,
is_completed: bool,
}
```
## Impl Block Organization
Place `impl` blocks immediately below the struct/enum they modify. Group methods logically:
```rust
struct JobQueue {
jobs: Vec<Job>,
capacity: usize,
}
impl JobQueue {
// Constructors first
pub fn new(capacity: usize) -> Self { ... }
pub fn with_jobs(jobs: Vec<Job>) -> Self { ... }
// Getters
pub fn len(&self) -> usize { ... }
pub fn is_empty(&self) -> bool { ... }
// Mutation methods
pub fn push(&mut self, job: Job) -> Result<()> { ... }
pub fn pop(&mut self) -> Option<Job> { ... }
// Domain logic
pub fn next_scheduled(&self) -> Option<&Job> { ... }
}
```
## Iterator Chains Over For-Loops
Prefer functional iterator chains (`.filter().map().collect()`) over imperative for-loops:
```rust
// Preferred
let results: Vec<_> = items
.iter()
.filter(|item| item.is_valid())
.map(|item| item.transform())
.collect();
// Avoid
let mut results = Vec::new();
for item in items.iter() {
if item.is_valid() {
results.push(item.transform());
}
}
```
## Error Handling
Use `Error` from `windmill_common::error`. Return `Result<T, Error>` or `JsonResult<T>`:
Use the `Error` type from `windmill_common::error`. Return `Result<T, Error>` or `JsonResult<T>` for fallible functions:
```rust
use windmill_common::error::{Error, Result};
// Use ? operator for propagation
pub async fn get_job(db: &DB, id: Uuid) -> Result<Job> {
sqlx::query_as!(Job, "SELECT id, workspace_id FROM v2_job WHERE id = $1", id)
let job = sqlx::query_as!(Job, "SELECT ... WHERE id = $1", id)
.fetch_optional(db)
.await?
.ok_or_else(|| Error::NotFound("job not found".to_string()))?;
Ok(job)
}
```
Never panic in library code. Reserve `.unwrap()` for compile-time guarantees.
## SQLx Patterns
**Never use `SELECT *`** — always list columns explicitly. Critical for backwards compatibility when workers lag behind API version:
Prefer `if let` for optional handling. Use `let...else` when early return makes code clearer:
```rust
// Correct
sqlx::query_as!(Job, "SELECT id, workspace_id, path FROM v2_job WHERE id = $1", id)
// Wrong — breaks when columns are added
sqlx::query_as!(Job, "SELECT * FROM v2_job WHERE id = $1", id)
let Some(config) = get_config() else {
return Err(Error::MissingConfig);
};
```
Use batch operations to avoid N+1:
Never panic in library code. Reserve `.unwrap()` for cases with compile-time guarantees. Keep functions short to help lifetime inference and clarity.
## Early Returns
Return early to avoid deep nesting. Handle error cases and edge conditions first:
```rust
// Preferred — single query with IN clause
sqlx::query!("SELECT ... WHERE id = ANY($1)", &ids[..]).fetch_all(db).await?
```
// Preferred - early returns
fn process_job(job: Option<Job>) -> Result<Output> {
let Some(job) = job else {
return Ok(Output::default());
};
Use transactions for multi-step operations. Parameterize all queries.
if !job.is_valid() {
return Err(Error::InvalidJob);
}
## JSON Handling
if job.is_cached() {
return Ok(job.cached_result());
}
Prefer `Box<serde_json::value::RawValue>` over `serde_json::Value` when storing/passing JSON without inspection:
// Main logic at the end, not nested
execute_job(job)
}
```rust
pub struct Job {
pub args: Option<Box<serde_json::value::RawValue>>,
// Avoid - deep nesting
fn process_job(job: Option<Job>) -> Result<Output> {
if let Some(job) = job {
if job.is_valid() {
if !job.is_cached() {
execute_job(job)
} else {
Ok(job.cached_result())
}
} else {
Err(Error::InvalidJob)
}
} else {
Ok(Output::default())
}
}
```
Only use `serde_json::Value` when you need to inspect or modify the JSON.
## Variable Shadowing
## Serde Optimizations
Shadow variables instead of creating new names with prefixes:
```rust
#[derive(Serialize, Deserialize)]
pub struct Job {
#[serde(skip_serializing_if = "Option::is_none")]
pub parent_job: Option<Uuid>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub tags: Vec<String>,
#[serde(default)]
pub priority: i32,
// Preferred
let data = fetch_raw_data();
let data = parse(data);
let data = validate(data)?;
// Avoid
let raw_data = fetch_raw_data();
let parsed_data = parse(raw_data);
let validated_data = validate(parsed_data)?;
```
## Minimal Comments
- No inline comments explaining obvious code
- No TODO/FIXME comments in committed code
- Doc comments (`///`) only on public items
- Let code be self-documenting through clear naming
## Type Safety
Use enums over boolean flags for clarity:
```rust
// Preferred
enum JobStatus {
Pending,
Running,
Completed,
}
// Avoid
struct Job {
is_running: bool,
is_completed: bool,
}
```
## Async & Concurrency
## Pattern Matching
Never block the async runtime. Use `spawn_blocking` for CPU-intensive work:
Prefer explicit matching. Use wildcards strategically for fallback cases or ignored fields:
```rust
let result = tokio::task::spawn_blocking(move || expensive_computation(&data)).await?;
// Explicit matching preferred
match status {
JobStatus::Pending => handle_pending(),
JobStatus::Running => handle_running(),
JobStatus::Completed => handle_completed(),
}
// Wildcards OK for fallback
match result {
Ok(value) => process(value),
Err(_) => return default_value(),
}
// Wildcards OK for ignoring fields in destructuring
let Point { x, y, .. } = point;
```
**Mutex selection**: Prefer `std::sync::Mutex` (or `parking_lot::Mutex`) for data protection. Only use `tokio::sync::Mutex` when holding locks across `.await` points.
## Destructuring in Function Signatures
Use `tokio::sync::mpsc` (bounded) for channels. Avoid `std::thread::sleep` in async contexts.
## Module Structure & Visibility
- Use `pub(crate)` instead of `pub` when possible
- Place new code in the appropriate crate based on functionality
- API endpoints go in `windmill-api/src/` organized by domain
- Shared functionality goes in `windmill-common/src/`
## Code Navigation
Always use rust-analyzer LSP for go-to-definition, find-references, and type info. Do not guess at module paths.
## Axum Handlers
Destructure extractors directly in function signatures:
Destructure structs directly in function parameters:
```rust
// Preferred
async fn process_job(
Extension(db): Extension<DB>,
Path((workspace, job_id)): Path<(String, Uuid)>,
Query(pagination): Query<Pagination>,
) -> Result<Json<Job>> { ... }
) -> Result<Json<Job>> {
// ...
}
// Avoid
async fn process_job(
db_ext: Extension<DB>,
path: Path<(String, Uuid)>,
query: Query<Pagination>,
) -> Result<Json<Job>> {
let Extension(db) = db_ext;
let Path((workspace, job_id)) = path;
// ...
}
```
## Trait Implementations
Use standard trait implementations to simplify conversions and reduce boilerplate:
```rust
// Implement From/Into for type conversions
impl From<DbJob> for ApiJob {
fn from(db: DbJob) -> Self {
ApiJob {
id: db.id,
status: db.status.into(),
}
}
}
// Use TryFrom for fallible conversions
impl TryFrom<String> for JobKind {
type Error = Error;
fn try_from(s: String) -> Result<Self, Self::Error> { ... }
}
```
Apply `derive` macros to reduce boilerplate:
```rust
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Job { ... }
```
## Module Structure
- Use `pub(crate)` instead of `pub` when possible; expose only what needs exposing
- Keep APIs small and expressive; avoid leaking internal types
- Organize code into modules reflecting ownership and domain boundaries
```rust
// Prefer restricted visibility
pub(crate) fn internal_helper() { ... }
// Only pub for external API
pub fn create_job(...) -> Result<Job> { ... }
```
## Code Navigation
Always use rust-analyzer LSP for:
- Go to definition
- Find references
- Type information
- Import resolution
Do not guess at module paths or type definitions.
## JSON Handling
Prefer `Box<serde_json::value::RawValue>` over `serde_json::Value` when:
- Storing JSON in the database (JSONB columns)
- Passing JSON through without modification
- The JSON structure doesn't need inspection
```rust
// Preferred - avoids parsing/serialization overhead
pub struct Job {
pub id: Uuid,
pub args: Option<Box<serde_json::value::RawValue>>,
}
// Only use Value when you need to inspect/modify JSON
let value: serde_json::Value = serde_json::from_str(&json)?;
if let Some(field) = value.get("field") {
// modify or inspect
}
```
## Serde Optimizations
Use serde attributes to optimize serialization:
```rust
#[derive(Serialize, Deserialize)]
pub struct Job {
#[serde(rename = "jobId")]
pub id: Uuid,
#[serde(default)]
pub priority: i32,
#[serde(skip_serializing_if = "Option::is_none")]
pub parent_job: Option<Uuid>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub tags: Vec<String>,
}
```
Prefer borrowing for zero-copy deserialization when lifetimes allow:
```rust
#[derive(Deserialize)]
pub struct JobInput<'a> {
#[serde(borrow)]
pub workspace_id: Cow<'a, str>,
#[serde(borrow)]
pub script_path: &'a str,
}
```
## SQLx Patterns
**Never use `SELECT *`** - always list columns explicitly. This is critical for backwards compatibility when workers run behind the API server version:
```rust
// Preferred - explicit columns
sqlx::query_as!(
Job,
"SELECT id, workspace_id, path, created_at FROM v2_job WHERE id = $1",
job_id
)
// Avoid - breaks when columns are added
sqlx::query_as!(Job, "SELECT * FROM v2_job WHERE id = $1", job_id)
```
Use batch operations to minimize round trips:
```rust
// Preferred - single query with multiple values
sqlx::query!(
"INSERT INTO job_logs (job_id, logs) VALUES ($1, $2), ($3, $4)",
id1, log1, id2, log2
)
// Avoid N+1 queries
for id in ids {
sqlx::query!("SELECT ... WHERE id = $1", id).fetch_one(db).await?;
}
// Preferred - single query with IN clause
sqlx::query!("SELECT ... WHERE id = ANY($1)", &ids[..]).fetch_all(db).await?
```
Use transactions for multi-step operations and parameterize all queries.
## Async & Tokio Patterns
Never block the async runtime. Use `spawn_blocking` for CPU-intensive or blocking I/O:
```rust
// Preferred - offload blocking work
let result = tokio::task::spawn_blocking(move || {
expensive_computation(&data)
}).await?;
// Avoid - blocks the runtime
let result = expensive_computation(&data); // Don't do this in async
```
Use tokio primitives for sleep and channels:
```rust
use tokio::sync::mpsc;
use tokio::time::sleep;
// Avoid in async contexts
use std::thread::sleep; // Blocks the runtime
```
Use bounded channels for backpressure:
```rust
// Preferred - bounded channel prevents overwhelming
let (tx, rx) = tokio::sync::mpsc::channel(100);
// Be careful with unbounded
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
```
## Mutex Selection in Async Code
**Prefer `std::sync::Mutex` (or `parking_lot::Mutex`) over `tokio::sync::Mutex`** for protecting data in async code. The async mutex is more expensive and only needed when holding locks across `.await` points.
```rust
// Preferred for data protection - std mutex is faster
use std::sync::Mutex;
struct Cache {
data: Mutex<HashMap<String, Value>>,
}
impl Cache {
fn get(&self, key: &str) -> Option<Value> {
self.data.lock().unwrap().get(key).cloned()
}
fn insert(&self, key: String, value: Value) {
self.data.lock().unwrap().insert(key, value);
}
}
```
**Use `tokio::sync::Mutex` only when you must hold the lock across `.await` points**, typically for IO resources like database connections:
```rust
use tokio::sync::Mutex;
use std::sync::Arc;
// Async mutex for IO resources held across await points
let conn = Arc::new(Mutex::new(db_connection));
async fn execute_query(conn: Arc<Mutex<DbConn>>, query: &str) {
let mut lock = conn.lock().await;
lock.execute(query).await; // Lock held across .await
}
```
**Common pattern**: Wrap `Arc<Mutex<...>>` in a struct with non-async methods that lock internally, keeping lock scope minimal:
```rust
struct SharedState {
inner: std::sync::Mutex<StateInner>,
}
impl SharedState {
fn update(&self, value: i32) {
self.inner.lock().unwrap().value = value;
}
fn get(&self) -> i32 {
self.inner.lock().unwrap().value
}
}
```
**Alternative for IO resources**: Spawn a dedicated task to manage the resource and communicate via message passing:
```rust
let (tx, mut rx) = tokio::sync::mpsc::channel(32);
tokio::spawn(async move {
while let Some(cmd) = rx.recv().await {
handle_io_command(&mut resource, cmd).await;
}
});
```
## Build & Tooling
Build speed tips:
- Use `cargo check` during rapid iteration over `cargo build`
- Minimize unnecessary dependencies and feature flags

View File

@@ -3,78 +3,227 @@ name: svelte-frontend
description: Svelte coding guidelines for the Windmill frontend. MUST use when writing or modifying code in the frontend directory.
---
# Windmill Svelte Patterns
# Svelte 5 Best Practices
Apply these Windmill-specific patterns when writing Svelte code in `frontend/`. For general Svelte 5 syntax (runes, snippets, event handling), use the Svelte MCP server.
This guide outlines best practices for developing with Svelte 5, incorporating the new Runes API and other modern Svelte features. These rules MUST NOT be applied on svelte 4 files unless explicitly asked to do so.
## Windmill UI Components (MUST use)
## Reactivity with Runes
Always use Windmill's design-system components. Never use raw HTML elements.
Svelte 5 introduces Runes for more explicit and flexible reactivity.
### Buttons — `<Button>`
1. **Embrace Runes for State Management**:
* Use `$state` for reactive local component state.
```svelte
<script>
let count = $state(0);
```svelte
<script>
import { Button } from '$lib/components/common'
import { ChevronLeft } from 'lucide-svelte'
</script>
function increment() {
count += 1;
}
</script>
<Button variant="default" onclick={handleClick}>Label</Button>
<Button startIcon={{ icon: ChevronLeft }} iconOnly onclick={prev} />
```
<button onclick={increment}>
Clicked {count} {count === 1 ? 'time' : 'times'}
</button>
```
* Use `$derived` for computed values based on other reactive state.
```svelte
<script>
let count = $state(0);
const doubled = $derived(count * 2);
</script>
Props: `variant?: 'accent' | 'accent-secondary' | 'default' | 'subtle'`, `unifiedSize?: 'sm' | 'md' | 'lg'`, `startIcon?: { icon: SvelteComponent }`, `iconOnly?: boolean`, `disabled?: boolean`
<p>{count} * 2 = {doubled}</p>
```
* Use `$effect` for side effects that need to run when reactive values change (e.g., logging, manual DOM manipulation, data fetching). Remember `$effect` does not run on the server.
```svelte
<script>
let count = $state(0);
### Text inputs — `<TextInput>`
$effect(() => {
console.log('The count is now', count);
if (count > 5) {
alert('Count is too high!');
}
});
</script>
```
```svelte
<script>
import { TextInput } from '$lib/components/common'
</script>
2. **Props with `$props`**:
* Declare component props using `$props()`. This offers better clarity and flexibility compared to `export let`.
```svelte
<script>
// ChildComponent.svelte
let { name, age = $state(30) } = $props();
</script>
<TextInput bind:value={val} placeholder="Enter value" />
```
<p>Name: {name}</p>
<p>Age: {age}</p>
```
* For bindable props, use `$bindable`.
```svelte
<script>
// MyInput.svelte
let { value = $bindable() } = $props();
</script>
Props: `value?: string | number` (bindable), `placeholder?: string`, `disabled?: boolean`, `error?: string | boolean`, `size?: 'sm' | 'md' | 'lg'`
<input bind:value />
```
### Selects — `<Select>`
## Event Handling
```svelte
<script>
import Select from '$lib/components/select/Select.svelte'
</script>
* **Use direct event attributes**: Svelte 5 moves away from `on:` directives for DOM events.
* **Do**: `<button onclick={handleClick}>...</button>`
* **Don't**: `<button on:click={handleClick}>...</button>`
* **For component events, prefer callback props**: Instead of `createEventDispatcher`, pass functions as props.
```svelte
<!-- Parent.svelte -->
<script>
import Child from './Child.svelte';
let message = $state('');
function handleChildEvent(detail) {
message = detail;
}
</script>
<Child onCustomEvent={handleChildEvent} />
<p>Message from child: {message}</p>
<Select items={[{ label: 'Jan', value: 1 }]} bind:value={selected} />
```
<!-- Child.svelte -->
<script>
let { onCustomEvent } = $props();
function emitEvent() {
onCustomEvent('Hello from child!');
}
</script>
<button onclick={emitEvent}>Send Event</button>
```
Props: `items?: Array<{ label?: string; value: any }>`, `value` (bindable), `placeholder?: string`, `clearable?: boolean`, `size?: 'sm' | 'md' | 'lg'`
## Snippets for Content Projection
### Icons — `lucide-svelte`
* **Use `{#snippet ...}` and `{@render ...}` instead of slots**: Snippets are more powerful and flexible.
```svelte
<!-- Parent.svelte -->
<script>
import Card from './Card.svelte';
</script>
Never write inline SVGs. Import from `lucide-svelte`:
<Card>
{#snippet title()}
My Awesome Title
{/snippet}
{#snippet content()}
<p>Some interesting content here.</p>
{/snippet}
</Card>
```svelte
<script>
import { ChevronLeft, X } from 'lucide-svelte'
</script>
<ChevronLeft size={16} />
```
<!-- Card.svelte -->
<script>
let { title, content } = $props();
</script>
## Form Components
<article>
<header>{@render title()}</header>
<div>{@render content()}</div>
</article>
```
* Default content is passed via the `children` prop (which is a snippet).
```svelte
<!-- Wrapper.svelte -->
<script>
let { children } = $props();
</script>
<div>
{@render children?.()}
</div>
```
Form components (TextInput, Toggle, Select, etc.) should use the unified size system when placed together.
## Component Design
## Styling
1. **Create Small, Reusable Components**: Break down complex UIs into smaller, focused components. Each component should have a single responsibility. This also aids performance by limiting the scope of reactivity updates.
2. **Descriptive Naming**: Use clear and descriptive names for variables, functions, and components.
3. **Minimize Logic in Components**: Move complex business logic to utility functions or services. Keep components focused on presentation and interaction.
- Use Tailwind CSS for all styling — no custom CSS
- Use Windmill's theming classes for colors/surfaces (see `frontend/brand-guidelines.md`)
- Read component props JSDoc before using them
## State Management (Stores)
## Svelte MCP Server
1. **Segment Stores**: Avoid a single global store. Create multiple stores, each responsible for a specific piece of global state (e.g., `userStore.js`, `themeStore.js`). This can help limit reactivity updates to only the parts of the UI that depend on specific state segments.
2. **Use Custom Stores for Complex Logic**: For stores with related methods, create custom stores.
```javascript
// counterStore.js
import { writable } from 'svelte/store';
Use the Svelte MCP tools when working on Svelte code:
function createCounter() {
const { subscribe, set, update } = writable(0);
1. **list-sections**: Call first to discover available docs
2. **get-documentation**: Fetch relevant sections based on use_cases
3. **svelte-autofixer**: MUST use on all Svelte code before finalizing — keep calling until no issues
4. **playground-link**: Only after user confirms and code was NOT written to project files
return {
subscribe,
increment: () => update(n => n + 1),
decrement: () => update(n => n - 1),
reset: () => set(0)
};
}
export const counter = createCounter();
```
3. **Use Context API for Localized State**: For state shared within a component subtree, consider Svelte's context API (`setContext`, `getContext`) instead of global stores when the state doesn't need to be truly global.
## Performance Optimizations (Svelte 5)
When generating Svelte 5 code, prioritize frontend performance by applying the following principles:
### General Svelte 5 Principles
- **Leverage the Compiler:** Trust Svelte's compiler to generate optimized JavaScript. Avoid manual DOM manipulation (`document.querySelector`, etc.) unless absolutely necessary for integrating third-party libraries that lack Svelte adapters.
- **Keep Components Small and Focused:** Reinforcing from Component Design, smaller components lead to less complex reactivity graphs and more targeted, efficient updates.
### Reactivity & State Management
- **Optimize Computations with `$derived`:** Always use `$derived` for computed values that depend on other state. This ensures the computation only runs when its specific dependencies change, avoiding unnecessary work compared to recomputing derived values in `$effect` or less efficient methods.
- **Minimize `$effect` Usage:** Use `$effect` sparingly and only for true side effects that interact with the outside world or non-Svelte state. Avoid putting complex logic or state updates *within* an `$effect` unless those updates are explicitly intended as a reaction to external changes or non-Svelte state. Excessive or complex effects can impact rendering performance.
- **Structure State for Fine-Grained Updates:** Design your `$state` objects or variables such that updates affect only the necessary parts of the UI. Avoid putting too much unrelated state into a single large object that gets frequently updated, as this can potentially trigger broader updates than necessary. Consider normalizing complex, nested state.
### List Rendering (`{#each}`)
- **Mandate `key` Attribute:** Always use a `key` attribute (`{#each items as item (item.id)}`) that refers to a unique, stable identifier for each item in a list. This is critical for allowing Svelte to efficiently update, reorder, add, or remove list items without destroying and re-creating unnecessary DOM elements and component instances.
### Component Loading & Bundling
- **Implement Lazy Loading/Code Splitting:** For routes, components, or modules that are not immediately needed on page load, use dynamic imports (`import(...)`) to split the code bundle. SvelteKit handles this automatically for routes, but it can be applied manually to components using helper patterns if needed.
- **Be Mindful of Third-Party Libraries:** When incorporating external libraries, import only the necessary functions or components to minimize the final bundle size. Prefer libraries designed to be tree-shakeable.
### Rendering & DOM
- **Use CSS for Animations/Transitions:** Prefer CSS animations or transitions where possible for performance. Svelte's built-in `transition:` directive is also highly optimized and should be used for complex state-driven transitions, but simple cases can often use plain CSS.
- **Optimize Image Loading:** Implement best practices for images: use optimized formats (WebP, AVIF), lazy loading (`loading="lazy"`), and responsive images (`<picture>`, `srcset`) to avoid loading unnecessarily large images.
### Server-Side Rendering (SSR) & Hydration
- **Ensure SSR Compatibility:** Write components that can be rendered on the server for faster initial page loads. Avoid relying on browser-specific APIs (like `window` or `document`) in the main `<script>` context. If necessary, use `$effect` or check `if (browser)` inside effects to run browser-specific code only on the client.
- **Minimize Work During Hydration:** Structure components and data fetching such that minimal complex setup or computation is required when the client-side Svelte code takes over from the server-rendered HTML. Heavy synchronous work during hydration can block the main thread.
## General Clean Code Practices
1. **Organized File Structure**: Group related files together. A common structure:
```
/src
|-- /routes // Page components (if using a router like SvelteKit)
|-- /lib // Utility functions, services, constants (SvelteKit often uses this)
| |-- /stores
| |-- /utils
| |-- /services
| |-- /components // Reusable UI components
|-- App.svelte
|-- main.js (or main.ts)
```
2. **Scoped Styles**: Keep CSS scoped to components to avoid unintended side effects and improve maintainability. Avoid `:global` where possible.
3. **Immutability**: With Svelte 5 and `$state`, direct assignments to properties of `$state` objects (`obj.prop = value;`) are generally fine as Svelte's reactivity system handles updates. However, for non-rune state or when interacting with other systems, understanding and sometimes preferring immutable updates (creating new objects/arrays) can still be relevant.
4. **Use `class:` and `style:` directives**: For dynamic classes and styles, use Svelte's built-in directives for cleaner templates and potentially optimized updates.
```svelte
<script>
let isActive = $state(true);
let color = $state('blue');
</script>
<div class:active={isActive} style:color={color}>
Hello
</div>
```
5. **Stay Updated**: Keep Svelte and its related packages up to date to benefit from the latest features, performance improvements, and security fixes.

View File

@@ -42,7 +42,7 @@ RUN wget https://www.python.org/ftp/python/${PYTHON_VERSION}/Python-${PYTHON_VER
RUN /usr/local/bin/python3 -m pip install pip-tools
# Bun
COPY --from=oven/bun:1.3.10 /usr/local/bin/bun /usr/bin/bun
COPY --from=oven/bun:1.3.8 /usr/local/bin/bun /usr/bin/bun
# Install windmill CLI
RUN bun install -g windmill-cli \

View File

@@ -15,8 +15,11 @@ sed -i '' -e "/\"version\": /s/: .*,/: \"$VERSION\",/" ${root_dirpath}/typescrip
sed -i '' -e "/\"version\": /s/: .*,/: \"$VERSION\",/" ${root_dirpath}/frontend/package.json
sed -i '' -e "/^version =/s/= .*/= \"$VERSION\"/" ${root_dirpath}/python-client/wmill/pyproject.toml
sed -i '' -e "/^windmill-api =/s/= .*/= \"\\^$VERSION\"/" ${root_dirpath}/python-client/wmill/pyproject.toml
sed -i '' -e "/^version =/s/= .*/= \"$VERSION\"/" ${root_dirpath}/python-client/wmill_pg/pyproject.toml
sed -i '' -e "/^[[:space:]]*ModuleVersion[[:space:]]*=/s/= .*/= '$VERSION'/" ${root_dirpath}/powershell-client/WindmillClient/WindmillClient.psd1
# sed -i '' -e "/^wmill =/s/= .*/= \"\\^$VERSION\"/" python-client/wmill_pg/pyproject.toml
sed -i '' -e "/^wmill =/s/= .*/= \">=$VERSION\"/" ${root_dirpath}/lsp/Pipfile
sed -i '' -e "/^wmill_pg =/s/= .*/= \">=$VERSION\"/" ${root_dirpath}/lsp/Pipfile
sed -i '' -E "s/name = \"windmill\"\nversion = \"[^\"]*\"\\n(.*)/name = \"windmill\"\nversion = \"$VERSION\"\\n\\1/" ${root_dirpath}/backend/Cargo.lock

View File

@@ -16,8 +16,11 @@ sed -i -e "/\"version\": /s/: .*,/: \"$VERSION\",/" ${root_dirpath}/typescript-c
sed -i -e "/\"version\": /s/: .*,/: \"$VERSION\",/" ${root_dirpath}/frontend/package.json
sed -i -e "/^version =/s/= .*/= \"$VERSION\"/" ${root_dirpath}/python-client/wmill/pyproject.toml
sed -i -e "/^windmill-api =/s/= .*/= \"\\^$VERSION\"/" ${root_dirpath}/python-client/wmill/pyproject.toml
sed -i -e "/^version =/s/= .*/= \"$VERSION\"/" ${root_dirpath}/python-client/wmill_pg/pyproject.toml
sed -i -e "/^[[:space:]]*ModuleVersion[[:space:]]*=/s/= .*/= '$VERSION'/" ${root_dirpath}/powershell-client/WindmillClient/WindmillClient.psd1
# sed -i -e "/^wmill =/s/= .*/= \"\\^$VERSION\"/" ${root_dirpath}/python-client/wmill_pg/pyproject.toml
sed -i -e "/^wmill =/s/= .*/= \">=$VERSION\"/" ${root_dirpath}/lsp/Pipfile
sed -i -e "/^wmill_pg =/s/= .*/= \">=$VERSION\"/" ${root_dirpath}/lsp/Pipfile
sed -i -zE "s/name = \"windmill\"\nversion = \"[^\"]*\"\\n(.*)/name = \"windmill\"\nversion = \"$VERSION\"\\n\\1/" ${root_dirpath}/backend/Cargo.lock

View File

@@ -31,3 +31,9 @@ updates:
directory: "/python-client/wmill"
schedule:
interval: "weekly"
# Maintain dependencies for wmill_pg python client
- package-ecosystem: "pip"
directory: "/python-client/wmill_pg"
schedule:
interval: "weekly"

View File

@@ -19,7 +19,7 @@ defaults:
jobs:
cargo_test:
runs-on: ubicloud-standard-16
runs-on: blacksmith-16vcpu-ubuntu-2404
services:
postgres:
image: postgres
@@ -55,7 +55,7 @@ jobs:
go-version: 1.21.5
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.10
bun-version: 1.3.8
- uses: actions/setup-node@v4
with:
node-version: "20"
@@ -86,8 +86,22 @@ jobs:
working-directory: /
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache-workspaces: backend
cache: false
toolchain: 1.93.0
- name: Cache cargo target directory
uses: useblacksmith/stickydisk@v1
with:
key: cargo-target
path: ./backend/target
- name: Cache cargo registry
uses: useblacksmith/cache@v1
with:
path: |
~/.cargo/registry
~/.cargo/git
key: cargo-registry-${{ hashFiles('backend/Cargo.lock') }}
restore-keys: |
cargo-registry-
- name: Read EE repo commit hash
run: |
echo "ee_repo_ref=$(cat ./ee-repo-ref.txt)" >> "$GITHUB_ENV"
@@ -215,7 +229,7 @@ jobs:
fi
echo "Verified: Package requires authentication for @windmill-test/private-pkg"
- name: Cache DuckDB FFI module build
uses: actions/cache@v3
uses: useblacksmith/cache@v1
with:
path: ./backend/windmill-duckdb-ffi-internal/target
key: ${{ runner.os }}-duckdb-ffi-${{ hashFiles('./backend/windmill-duckdb-ffi-internal/src/**/*.rs', './backend/windmill-duckdb-ffi-internal/Cargo.toml', './backend/windmill-duckdb-ffi-internal/Cargo.lock') }}
@@ -231,6 +245,7 @@ jobs:
RUST_LOG_STYLE: never
CARGO_NET_GIT_FETCH_WITH_CLI: true
CARGO_BUILD_JOBS: 12
CARGO_INCREMENTAL: 1
WMDEBUG_FORCE_V0_WORKSPACE_DEPENDENCIES: 1
WMDEBUG_FORCE_RUNNABLE_SETTINGS_V0: 1
WMDEBUG_FORCE_NO_LEGACY_DEBOUNCING_COMPAT: 1
@@ -238,4 +253,4 @@ jobs:
run: |
deno --version && bun -v && node --version && go version && python3 --version && php --version && ruby --version && pwsh --version && dotnet --version
cd windmill-duckdb-ffi-internal && ./build_dev.sh && cd ..
DENO_PATH=$(which deno) BUN_PATH=$(which bun) NODE_BIN_PATH=$(which node) GO_PATH=$(which go) UV_PATH=$(which uv) PHP_PATH=$(which php) COMPOSER_PATH=$(which composer) RUBY_PATH=$(which ruby) RUBY_BUNDLE_PATH=$(which bundle) RUBY_GEM_PATH=$(which gem) POWERSHELL_PATH=$(which pwsh) DOTNET_PATH=$(which dotnet) cargo test --features enterprise,deno_core,duckdb,license,python,rust,scoped_cache,parquet,private,private_registry_test,csharp,php,ruby,mysql,quickjs,mcp,run_inline --all -- --nocapture --test-threads=10
DENO_PATH=$(which deno) BUN_PATH=$(which bun) NODE_BIN_PATH=$(which node) GO_PATH=$(which go) UV_PATH=$(which uv) PHP_PATH=$(which php) COMPOSER_PATH=$(which composer) RUBY_PATH=$(which ruby) RUBY_BUNDLE_PATH=$(which bundle) RUBY_GEM_PATH=$(which gem) POWERSHELL_PATH=$(which pwsh) DOTNET_PATH=$(which dotnet) cargo test --features enterprise,deno_core,duckdb,license,python,rust,scoped_cache,parquet,private,private_registry_test,csharp,php,ruby,mysql,quickjs,mcp --all -- --nocapture --test-threads=10

View File

@@ -9,7 +9,9 @@ on:
issue_comment:
types:
- created
- edited
pull_request_review_comment:
types:
- created
jobs:
notify_discord_when_pr_opened:
@@ -51,7 +53,23 @@ jobs:
COMMENT_BODY: ${{ github.event.comment.body }}
COMMENT_AUTHOR: ${{ github.event.comment.user.login }}
COMMENT_URL: ${{ github.event.comment.html_url }}
COMMENT_IS_EDIT: ${{ github.event.action == 'edited' }}
DISCORD_CHANNEL_ID: "1372204995868491786"
DISCORD_GUILD_ID: "930051556043276338"
secrets:
DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_AI_BOT_TOKEN }}
notify_discord_on_review_comment:
if: >
github.event_name == 'pull_request_review_comment'
&& github.event.comment.user.login != 'cloudflare-workers-and-pages[bot]'
&& github.event.comment.user.login != 'ellipsis-dev[bot]'
uses: ./.github/workflows/shareable-discord-notification.yml
with:
PR_STATUS: "comment"
PR_NUMBER: ${{ github.event.pull_request.number }}
COMMENT_BODY: ${{ github.event.comment.body }}
COMMENT_AUTHOR: ${{ github.event.comment.user.login }}
COMMENT_URL: ${{ github.event.comment.html_url }}
DISCORD_CHANNEL_ID: "1372204995868491786"
DISCORD_GUILD_ID: "930051556043276338"
secrets:

View File

@@ -36,10 +36,6 @@ on:
description: "The comment URL"
type: string
default: ""
COMMENT_IS_EDIT:
description: "Whether this is an edit of an existing comment"
type: string
default: "false"
secrets:
DISCORD_WEBHOOK_URL:
description: "Discord Webhook URL"
@@ -139,7 +135,7 @@ jobs:
runs-on: ubuntu-latest
if: ${{ inputs.PR_STATUS == 'comment' }}
steps:
- name: Post or update comment in Discord thread
- name: Post comment to Discord thread
env:
BOT_TOKEN: ${{ secrets.DISCORD_BOT_TOKEN }}
CHANNEL_ID: ${{ inputs.DISCORD_CHANNEL_ID }}
@@ -148,7 +144,6 @@ jobs:
COMMENT_BODY: ${{ inputs.COMMENT_BODY }}
COMMENT_AUTHOR: ${{ inputs.COMMENT_AUTHOR }}
COMMENT_URL: ${{ inputs.COMMENT_URL }}
COMMENT_IS_EDIT: ${{ inputs.COMMENT_IS_EDIT }}
run: |
# 1) Find the thread by PR number
threads=$(curl -s -H "Authorization: Bot $BOT_TOKEN" \
@@ -177,36 +172,10 @@ jobs:
truncated_body="$COMMENT_BODY"
fi
# 3) Build the message content
if [ "$COMMENT_IS_EDIT" = "true" ]; then
message=$(printf '**%s** [edited comment](%s):\n%s' "$COMMENT_AUTHOR" "$COMMENT_URL" "$truncated_body")
else
message=$(printf '**%s** [commented](%s):\n%s' "$COMMENT_AUTHOR" "$COMMENT_URL" "$truncated_body")
fi
# 3) Post the comment to the thread
message=$(printf '**%s** [commented](%s):\n%s' "$COMMENT_AUTHOR" "$COMMENT_URL" "$truncated_body")
payload=$(jq -n --arg content "$message" '{content: $content, flags: 4, allowed_mentions: {parse: []}}')
# 4) If this is an edit, try to find and update the existing Discord message
if [ "$COMMENT_IS_EDIT" = "true" ]; then
# Search recent messages in the thread for one containing the comment URL
messages=$(curl -s -H "Authorization: Bot $BOT_TOKEN" \
"https://discord.com/api/v10/channels/${thread_id}/messages?limit=100")
existing_msg_id=$(echo "$messages" | jq -r \
--arg url "$COMMENT_URL" \
'[.[] | select(.content | contains($url))] | first | .id // empty')
if [ -n "$existing_msg_id" ]; then
echo "Updating existing Discord message $existing_msg_id"
curl -s -X PATCH \
-H "Authorization: Bot $BOT_TOKEN" \
-H "Content-Type: application/json" \
-d "$payload" \
"https://discord.com/api/v10/channels/${thread_id}/messages/${existing_msg_id}"
exit 0
fi
echo "Original Discord message not found, posting as new message"
fi
# 5) Post a new message to the thread
curl -s -X POST \
-H "Authorization: Bot $BOT_TOKEN" \
-H "Content-Type: application/json" \

View File

@@ -4,5 +4,11 @@
"type": "http",
"url": "https://mcp.svelte.dev/mcp"
}
},
"playwright": {
"command": "npx",
"args": [
"@playwright/mcp@latest"
]
}
}

View File

@@ -1,106 +0,0 @@
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

@@ -11,7 +11,7 @@ worktree_prefix: ""
window_prefix: "wm-"
auto_name:
model: "gemini-2.5-flash-lite"
model: "claude-sonnet-4.6"
system_prompt: |
Generate a concise git branch name based on the task description.
@@ -47,7 +47,7 @@ pre_remove:
panes:
- command: >-
claude --dangerously-skip-permissions --append-system-prompt
claude --append-system-prompt
"You are running inside a tmux session with other panes running services.\n
Pane layout (current window):\n
- Pane 0: this pane (claude agent)\n
@@ -55,12 +55,11 @@ panes:
- Pane 2: frontend (npm run dev)\n\n
To check logs, use: \`tmux capture-pane -t .1 -p -S -50\` (backend) or \`tmux capture-pane -t .2 -p -S -50\` (frontend).\n
When restarting backend or frontend, make sure to use the ports listed in .env.local.\n
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.\n\n
IMPORTANT: Read docs/autonomous-mode.md before starting any work."
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."
focus: true
- command: 'ROOT="$(git rev-parse --show-toplevel)"; [ -f "$ROOT/.env.local" ] && source "$ROOT/.env.local"; cd "$ROOT/backend" && PORT=${BACKEND_PORT:-8000} cargo watch -x "run ${CARGO_FEATURES:+--features $CARGO_FEATURES}"'
- command: 'ROOT="$(git rev-parse --show-toplevel)"; [ -f "$ROOT/.env.local" ] && source "$ROOT/.env.local"; cd "$ROOT/backend" && PORT=${BACKEND_PORT:-8000} cargo watch -x run'
split: horizontal
- command: 'ROOT="$(git rev-parse --show-toplevel)"; [ -f "$ROOT/.env.local" ] && source "$ROOT/.env.local"; 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'
- command: 'ROOT="$(git rev-parse --show-toplevel)"; [ -f "$ROOT/.env.local" ] && source "$ROOT/.env.local"; cd "$ROOT/frontend" && npm install && npm run generate-backend-client && REMOTE=${REMOTE:-http://localhost:${BACKEND_PORT:-8000}} npm run dev -- --port ${FRONTEND_PORT:-3000} --host 0.0.0.0'
split: vertical
files:
@@ -71,3 +70,6 @@ files:
sandbox:
enabled: false
toolchain: off
# image, host_commands, and extra_mounts configured in global
# ~/.config/workmux/config.yaml — see README_WORKMUX_DEV.md for required
# extra_mounts (windmill-ee-private access in sandbox)

View File

@@ -1,132 +1,5 @@
# Changelog
## [1.647.2](https://github.com/windmill-labs/windmill/compare/v1.647.1...v1.647.2) (2026-03-02)
### Bug Fixes
* update oracle instant client arm64 download url ([#8179](https://github.com/windmill-labs/windmill/issues/8179)) ([758b35f](https://github.com/windmill-labs/windmill/commit/758b35f8ebbf78e1473a8fd83dbc795d58b23b80))
## [1.647.1](https://github.com/windmill-labs/windmill/compare/v1.647.0...v1.647.1) (2026-03-02)
### Bug Fixes
* add missing display_name and tenant fields to instance config OAuthClient ([#8176](https://github.com/windmill-labs/windmill/issues/8176)) ([db44b8b](https://github.com/windmill-labs/windmill/commit/db44b8be74e1709dbf759dd391bdb3861b3c711b))
* add missing grant_types field to instance config OAuth structs ([#8175](https://github.com/windmill-labs/windmill/issues/8175)) ([fca94f8](https://github.com/windmill-labs/windmill/commit/fca94f88dd796db66e0c5bd0225e23b92efce4a7))
* show sync endpoint timeout setting on all instances ([#8170](https://github.com/windmill-labs/windmill/issues/8170)) ([c70307d](https://github.com/windmill-labs/windmill/commit/c70307d3f2dfe61a0250dd12234470a25baf2d1b))
## [1.647.0](https://github.com/windmill-labs/windmill/compare/v1.646.0...v1.647.0) (2026-03-01)
### Features
* populate baseUrl and userId in Nextcloud resource from OAuth ([#8132](https://github.com/windmill-labs/windmill/issues/8132)) ([5d58a87](https://github.com/windmill-labs/windmill/commit/5d58a87a7f02c4f7775bd02c885071495a5f686d))
* runScript inline for path and hash ([#8019](https://github.com/windmill-labs/windmill/issues/8019)) ([7d9d16a](https://github.com/windmill-labs/windmill/commit/7d9d16a6a3357981e5692023982ca1e670acfaae))
* slow stream warnings, batch size control, and fix result/skipped filters ([#8154](https://github.com/windmill-labs/windmill/issues/8154)) ([7a32abe](https://github.com/windmill-labs/windmill/commit/7a32abec96124f96a1dbac11e03162cca68f3286))
### Bug Fixes
* : persist show schedules and show future jobs toggles in local storage ([#8125](https://github.com/windmill-labs/windmill/issues/8125)) ([f1d8568](https://github.com/windmill-labs/windmill/commit/f1d8568831bf69ee790def4f90df8f32c59a94e0)), closes [#8123](https://github.com/windmill-labs/windmill/issues/8123)
* add partial index for fast failure filtering on runs page ([#8150](https://github.com/windmill-labs/windmill/issues/8150)) ([d4673c2](https://github.com/windmill-labs/windmill/commit/d4673c2e91168dcdb0aca9d6c039df0d9c52bb28))
* copy deps and remove user auto-add on workspace fork ([#8142](https://github.com/windmill-labs/windmill/issues/8142)) ([0776de6](https://github.com/windmill-labs/windmill/commit/0776de6b2173075f533fd59a49efb111000da5df))
* fix custom TS Monaco worker not reloading on file uri change ([#8130](https://github.com/windmill-labs/windmill/issues/8130)) ([b68ff96](https://github.com/windmill-labs/windmill/commit/b68ff965dd4f67046fae7e8cf756c8b3e15c2643))
* Handle CTEs and local tables in SQL asset parser ([#8131](https://github.com/windmill-labs/windmill/issues/8131)) ([0955051](https://github.com/windmill-labs/windmill/commit/095505136c2b3e03f656ace20a5c1bbe142fa63f))
* prevent wm-cursor from hanging on stale cursor IPC sockets ([b9e3e05](https://github.com/windmill-labs/windmill/commit/b9e3e053e4914e753bbb806e6b748c791edb92d2))
* process deletes before adds in CLI sync push to avoid conflicts ([#8148](https://github.com/windmill-labs/windmill/issues/8148)) ([278983c](https://github.com/windmill-labs/windmill/commit/278983c4fd38d67a14a8c208178c04db05ee1880))
* remove review comments from discord notifications and support comment edits ([cdc0543](https://github.com/windmill-labs/windmill/commit/cdc0543747680267e30974037a2eb180a19062d9))
* restore email domain (MX) setting in instance settings UI ([#8152](https://github.com/windmill-labs/windmill/issues/8152)) ([13daebf](https://github.com/windmill-labs/windmill/commit/13daebf88ac1abcb833646490073f922ac7c050e))
* sync flow on_behalf_of_email on load ([#8149](https://github.com/windmill-labs/windmill/issues/8149)) ([faf190f](https://github.com/windmill-labs/windmill/commit/faf190f12d96cd75ba9eda10ab3e6f26d2eed813))
* validate tarball URL host against registry to prevent SSRF and token exfiltration ([#8153](https://github.com/windmill-labs/windmill/issues/8153)) ([86182ed](https://github.com/windmill-labs/windmill/commit/86182ed2e999f018fc72343308e7df8e9de6c189))
### Performance Improvements
* batch large job list requests and fix loadExtraJobs cursor ([#8151](https://github.com/windmill-labs/windmill/issues/8151)) ([4f5a804](https://github.com/windmill-labs/windmill/commit/4f5a8040912e18f34401a6e3a95dea6f97d1d24c))
* lazy-load heavy deps (graphql, openapi-parser, sha256) ([#8145](https://github.com/windmill-labs/windmill/issues/8145)) ([ba48d70](https://github.com/windmill-labs/windmill/commit/ba48d7015741eb6bbbe04088a957c37499cd8471))
* lazy-load markdown in Tooltip components ([#8143](https://github.com/windmill-labs/windmill/issues/8143)) ([bd9ff03](https://github.com/windmill-labs/windmill/commit/bd9ff03010f75557dcc315d10e9208b4e9cafece))
## [1.646.0](https://github.com/windmill-labs/windmill/compare/v1.645.0...v1.646.0) (2026-02-26)
### Features
* add force_branch parameter to git sync settings ([#8089](https://github.com/windmill-labs/windmill/issues/8089)) ([4e1ae27](https://github.com/windmill-labs/windmill/commit/4e1ae276b006992e06ae755ec9315dbfadf4f838))
* add wmill docs CLI command for querying documentation ([#8114](https://github.com/windmill-labs/windmill/issues/8114)) ([01c7270](https://github.com/windmill-labs/windmill/commit/01c7270cdaa0d5dbee2e15aa5dd08551cff60c70))
* Broad filters for search ([#8112](https://github.com/windmill-labs/windmill/issues/8112)) ([16a6d5e](https://github.com/windmill-labs/windmill/commit/16a6d5e7afe9323b2f2c7a93828518f5d924cc69))
* change on behalf selector to allow picking any user + select value in target by default if possible ([#8113](https://github.com/windmill-labs/windmill/issues/8113)) ([408c5af](https://github.com/windmill-labs/windmill/commit/408c5af6d8352f1e205e4543772ce5d060556ffc))
### Bug Fixes
* remove duplicate job loading on chart zoom ([#8121](https://github.com/windmill-labs/windmill/issues/8121)) ([99c01bc](https://github.com/windmill-labs/windmill/commit/99c01bca3863ac9b2882948bb5914f051a7716a4))
* runs page date picker query parameter handling ([#8120](https://github.com/windmill-labs/windmill/issues/8120)) ([427bc64](https://github.com/windmill-labs/windmill/commit/427bc6410be7fda132fc91991164e9b38b32c7e3))
## [1.645.0](https://github.com/windmill-labs/windmill/compare/v1.644.0...v1.645.0) (2026-02-26)
### Features
* add resume and cancel button text options to Slack approval API + formatted args + typo ([#8095](https://github.com/windmill-labs/windmill/issues/8095)) ([c7c828b](https://github.com/windmill-labs/windmill/commit/c7c828b56e7a5f877ef0a78498018ed930bccb23))
* Data table as pg resource / trigger ([#8088](https://github.com/windmill-labs/windmill/issues/8088)) ([8e7ba9b](https://github.com/windmill-labs/windmill/commit/8e7ba9b33da2ddba0eba8341219b9a3576a9d95d))
* option to preserve on_behalf_of and edited_by for admins and users in the new wm_deployers group ([#8079](https://github.com/windmill-labs/windmill/issues/8079)) ([7ac93f6](https://github.com/windmill-labs/windmill/commit/7ac93f6ee30eb8dfa6ddb9c19697cde93bf7e134))
* per-worktree database isolation and Claude Code auto-trust ([09970cd](https://github.com/windmill-labs/windmill/commit/09970cd22b8f19c6d01351f9a9bf4aac170116c2))
* show triggers in fork deploy to parent UI. ([#8094](https://github.com/windmill-labs/windmill/issues/8094)) ([935b005](https://github.com/windmill-labs/windmill/commit/935b0058e2b8056e07f8dd8f80ef6de78ca8331f))
### Bug Fixes
* **backend:** fix skip check crash when flow-level skip_expr triggers on first module with skip_if ([#8111](https://github.com/windmill-labs/windmill/issues/8111)) ([7bb450e](https://github.com/windmill-labs/windmill/commit/7bb450edbfccd5c21dc5dbc1e7bf2f2ecc4c779c))
* **backend:** pass parent_path for trigger renames in git sync ([#8059](https://github.com/windmill-labs/windmill/issues/8059)) ([5730009](https://github.com/windmill-labs/windmill/commit/5730009404171cbffb67d0296baf9c0aa2858816))
* correct asset node x offset inside loops and branches ([#8093](https://github.com/windmill-labs/windmill/issues/8093)) ([1c9ac97](https://github.com/windmill-labs/windmill/commit/1c9ac97f876a82c6ce3b18e30ffdeea79ccd4481))
* delete non-session tokens on workspace archive and reject token creation for archived workspaces ([#8082](https://github.com/windmill-labs/windmill/issues/8082)) ([bc67255](https://github.com/windmill-labs/windmill/commit/bc672555a77f3b78ff324a26603d2ab7839df77e))
* improve Anthropic API proxy handling and update default models ([#8105](https://github.com/windmill-labs/windmill/issues/8105)) ([a9968d0](https://github.com/windmill-labs/windmill/commit/a9968d0aed446a090b158c3269ffeb6907330933))
* optimize slow list_assets query for recents loading ([#8103](https://github.com/windmill-labs/windmill/issues/8103)) ([0c204b6](https://github.com/windmill-labs/windmill/commit/0c204b69bdd319af2706c1add552622678cd343f))
* remove duplicate num_columns in test_parse_relation test ([cff9e2c](https://github.com/windmill-labs/windmill/commit/cff9e2c5c22b3c1a0b5891839fe59e4058ded888))
* resolve Vite dependency pre-bundling errors ([#8102](https://github.com/windmill-labs/windmill/issues/8102)) ([07ddcd2](https://github.com/windmill-labs/windmill/commit/07ddcd2a08c103246b2b60f9df1ffb477ff97006))
* use @-prefixed LIKE pattern for email domain matching ([#8101](https://github.com/windmill-labs/windmill/issues/8101)) ([02d5447](https://github.com/windmill-labs/windmill/commit/02d5447e1d567a18b0d6eb24f3423bd675f6cbe8))
* use main runtime handle in QuickJS eval to prevent connection pool poisoning ([#8106](https://github.com/windmill-labs/windmill/issues/8106)) ([af2aca5](https://github.com/windmill-labs/windmill/commit/af2aca56b04c7a3fd25f096f2471292489923431))
## [1.644.0](https://github.com/windmill-labs/windmill/compare/v1.643.0...v1.644.0) (2026-02-24)
### Features
* **cli:** detect missing folders on sync push and add 'wmill folder add-missing' ([#8011](https://github.com/windmill-labs/windmill/issues/8011)) ([835db5d](https://github.com/windmill-labs/windmill/commit/835db5d290a151f38f4e879ed7ffbda5d1c4b24f))
### Bug Fixes
* prevent concurrent index migrations from re-running on every startup ([#8069](https://github.com/windmill-labs/windmill/issues/8069)) ([8ff2340](https://github.com/windmill-labs/windmill/commit/8ff2340c0c08ce49a809c8958a9862ffb1681642))
## [1.643.0](https://github.com/windmill-labs/windmill/compare/v1.642.0...v1.643.0) (2026-02-24)
### Features
* add fileset resource type support ([32c4b47](https://github.com/windmill-labs/windmill/commit/32c4b474f92f3dbbd2077fab70bdf9e407581626))
* add fileset resource type support ([#8063](https://github.com/windmill-labs/windmill/issues/8063)) ([c15b9ab](https://github.com/windmill-labs/windmill/commit/c15b9abe5eb2a1566a7ce4b18784c961d178a669))
* add light mode for navigation sidebar ([#8057](https://github.com/windmill-labs/windmill/issues/8057)) ([0935bf9](https://github.com/windmill-labs/windmill/commit/0935bf9fc460c03c6d8469b93036e43714517ef2))
* **aiagent:** handle ai agent as tool ([#8031](https://github.com/windmill-labs/windmill/issues/8031)) ([de6fd16](https://github.com/windmill-labs/windmill/commit/de6fd160d56c1037adbbe785f195483c25982e1c))
* Unified filters and new runs page ([#8027](https://github.com/windmill-labs/windmill/issues/8027)) ([9b28c85](https://github.com/windmill-labs/windmill/commit/9b28c85469d6b2a8590810b313b030d9f00ee9e3))
### Bug Fixes
* address code review findings for fileset feature ([1b4489a](https://github.com/windmill-labs/windmill/commit/1b4489acac3b050f0a783548bacfc9bdf33ee593))
* address second round of review findings ([753c05a](https://github.com/windmill-labs/windmill/commit/753c05a03089b95b4ade68d3bf61c8818de422ce))
* **backend:** decimal between 0 and -1 in mssql ([#8051](https://github.com/windmill-labs/windmill/issues/8051)) ([9686608](https://github.com/windmill-labs/windmill/commit/9686608355615a50c8395f6e2fd51dcc25498226))
* **backend:** use filename instead of content_type to detect file fields in multipart form data ([#8054](https://github.com/windmill-labs/windmill/issues/8054)) ([0aa885d](https://github.com/windmill-labs/windmill/commit/0aa885db67d77202205fc1609e841b8ffd9a8121))
* exclude app_theme resources from workspace tab ([9c513b2](https://github.com/windmill-labs/windmill/commit/9c513b2c62acc369179fb9e404e1f4007cd854c6))
* fileset editor takes full height with matching header ([9ac0789](https://github.com/windmill-labs/windmill/commit/9ac07897cf99f3af27801e435c7376a46ef760c9))
* prevent iframe from overriding file selection after file creation ([7f3ddd7](https://github.com/windmill-labs/windmill/commit/7f3ddd7edd3ea993642aadd55cdba0ac2ea1eb9f))
* resolve svelte warnings and type error in fileset components ([4c06d74](https://github.com/windmill-labs/windmill/commit/4c06d74bd01ca2dda848be421d70dd5268520992))
* restore full-width file tree items in raw app sidebar ([5bac8b0](https://github.com/windmill-labs/windmill/commit/5bac8b093dbe913a563b02573959c64dd405ff61))
* suppress iframe setActiveDocument during file population ([1abfeea](https://github.com/windmill-labs/windmill/commit/1abfeea81a645c59934d62257ad869ed7b475634))
* update git sync init script to hub version 28158 ([#8061](https://github.com/windmill-labs/windmill/issues/8061)) ([705e186](https://github.com/windmill-labs/windmill/commit/705e186f3d4c7d8f8a88fc84b379ed9fe800a6b2))
* use correct column name completed_at instead of ended_at in count_completed_jobs_detail ([#8066](https://github.com/windmill-labs/windmill/issues/8066)) ([3aba0ed](https://github.com/windmill-labs/windmill/commit/3aba0ed2508debdc78a6631e49b074a97635f21d))
## [1.642.0](https://github.com/windmill-labs/windmill/compare/v1.641.0...v1.642.0) (2026-02-22)

View File

@@ -1,33 +1,68 @@
# Windmill
# Windmill Development Guide
Open-source platform for internal tools, workflows, API integrations, background jobs, and UIs. Rust backend + Svelte 5 frontend.
## Overview
## Workflow
Windmill is an open-source developer platform for building internal tools, workflows, API integrations, background jobs, workflows, and user interfaces. See @windmill-overview.mdc for full platform details.
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
3. **Execute**: Follow coding patterns from skills (`rust-backend`, `svelte-frontend`)
4. **Validate**: After every change, run the appropriate checks per `docs/validation.md`
## New Feature Implementation Guidelines
## Documentation
When implementing new features in Windmill, follow these best practices:
- **Validation**: `docs/validation.md` — what checks to run based on what you changed
- **Enterprise**: `docs/enterprise.md` — EE file conventions and PR workflow
- **Backend patterns**: use the `rust-backend` skill when writing Rust code
- **Frontend patterns**: use the `svelte-frontend` skill when writing Svelte code
- **Domain guides**: `.claude/skills/native-trigger/` and `frontend/tutorial-system-guide.mdc`
- **Brand/UI guidelines**: `frontend/brand-guidelines.md`
- **Clean Code First**: Write clean, readable, and maintainable code. Prioritize clarity over cleverness.
- **Avoid Duplication at All Costs**: Before writing new code, thoroughly search for existing implementations that can be reused or extended.
- **Adapt Existing Code**: Refactor and generalize existing code when necessary to avoid logic duplication. Extract common patterns into reusable utilities.
- **Follow Established Patterns**: Study existing code patterns in the codebase and maintain consistency with established conventions.
- **Single Responsibility**: Each function, component, and module should have a single, well-defined responsibility.
- **Incremental Implementation**: Break large features into smaller, reviewable chunks that can be implemented and tested incrementally.
## Language-Specific Guides
- Backend (Rust): see `backend/CLAUDE.md` and the `rust-backend` skill: `.claude/skills/rust-backend/SKILL.md`
- Frontend (Svelte 5): see `frontend/CLAUDE.md` and the `svelte-frontend` skill: `.claude/skills/svelte-frontend/SKILL.md`
## Dev Environment
- **Backend**: `cargo run` from `backend/` (API at http://localhost:8000)
- **Frontend**: `REMOTE=http://localhost:8000 npm run dev` from `frontend/` (port 3000+)
- **DB**: `psql postgres://postgres:changeme@localhost:5432/windmill`
- **Login**: `admin@windmill.dev` / `changeme`
- **Instance settings**: navigate to `/#superadmin-settings`
- **Frontend**: `REMOTE=http://localhost:8000 npm run dev` from `frontend/`
- The `REMOTE` env var configures the Vite proxy target. Without it, API calls proxy to `https://app.windmill.dev` instead of the local backend.
- The dev server starts on port 3000 (or 3001+ if 3000 is in use).
- **Default login**: `admin@windmill.dev` / `changeme`
- **Instance settings**: navigate to `/#superadmin-settings` (opens the drawer overlay)
## Core Principles
## UI Testing with Playwright MCP
- Search for existing code to reuse before writing new code
- Follow established patterns in the codebase
- Keep changes focused — don't refactor beyond what's asked
When testing the frontend with the Playwright MCP tools:
1. **Start servers**: Launch backend (`cargo run`) and frontend (`REMOTE=http://localhost:8000 npm run dev`) as background tasks
2. **Wait for readiness**: Backend takes ~60s to compile; check output for `health check completed`. Frontend starts in ~5s.
3. **Login flow**: Navigate to `/user/login`, click "Log in without third-party", fill email/password, submit
4. **Instance settings drawer**: Navigate to `/#superadmin-settings` to open the drawer directly
5. **Toggle components**: The YAML toggle uses a custom `<Toggle>` component where the checkbox is visually hidden (`sr-only`). Click the wrapper `<label>` element (the parent container with `cursor=pointer`), not the checkbox ref directly.
6. **Console errors to ignore**: `critical_alerts` 404s are expected on CE builds (EE-only endpoint). VSCode worker 404s are dev-mode artifacts.
## Code Validation (MUST DO)
After making code changes, you MUST run the appropriate checks and fix all errors before considering the work done:
- **Backend**: Run `cargo check` from the `backend/` directory. Only enable the feature flags needed for the code you changed — check `backend/Cargo.toml` `[features]` section to identify which flags gate the crates/modules you modified. For example: `cargo check --features enterprise,parquet` if you only touched enterprise and parquet code.
- **Frontend**: Run `npm run check` from the `frontend/` directory.
## Querying the Database
`backend/summarized_schema.txt` provides a compact overview of all tables, columns, types, ENUMs, and foreign keys. Use it to quickly understand the data model and relationships. Note: this file is a simplified summary — it omits indexes, constraints details, and other metadata.
For exact table definitions (indexes, constraints, column defaults, etc.), query the database directly:
```bash
psql postgres://postgres:changeme@localhost:5432/windmill
```
Useful psql commands:
- `\d <table_name>` — full table definition with indexes and constraints
- `\di <table_name>*` — list indexes for a table
- `\d+ <table_name>` — extended table info including storage and descriptions
This is also helpful for:
- Inspecting database state during development
- Testing queries before implementing them in Rust
- Debugging data-related issues

View File

@@ -58,7 +58,7 @@ FROM node:24-alpine as frontend
# install dependencies
WORKDIR /frontend
COPY ./frontend/package.json ./frontend/package-lock.json ./frontend/.npmrc ./
COPY ./frontend/package.json ./frontend/package-lock.json ./
COPY ./frontend/scripts/ ./scripts/
RUN npm ci
@@ -126,7 +126,7 @@ ARG POWERSHELL_DEB_VERSION=7.5.0-1
ARG KUBECTL_VERSION=1.28.7
ARG HELM_VERSION=3.14.3
# NOTE: If changing, also change go version in workspace dependencies template at WorkspaceDependenciesEditor.svelte
ARG GO_VERSION=1.26.0
ARG GO_VERSION=1.25.0
ARG APP=/usr/src/app
ARG WITH_POWERSHELL=true
ARG WITH_KUBECTL=true
@@ -256,7 +256,7 @@ COPY --from=windmill_duckdb_ffi_internal_builder /windmill-duckdb-ffi-internal/t
COPY --from=denoland/deno:2.2.1 --chmod=755 /usr/bin/deno /usr/bin/deno
COPY --from=oven/bun:1.3.10 /usr/local/bin/bun /usr/bin/bun
COPY --from=oven/bun:1.3.8 /usr/local/bin/bun /usr/bin/bun
# Install windmill CLI
RUN bun install -g windmill-cli \

234
Dockerfile.sandbox Normal file
View File

@@ -0,0 +1,234 @@
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
ca-certificates \
git \
iptables \
gosu \
sudo \
unzip \
# Rust native build deps (for cargo check)
pkg-config \
cmake \
clang \
mold \
libtool \
libssl-dev \
libxml2-dev \
libxmlsec1-dev \
libxslt1-dev \
libffi-dev \
zlib1g-dev \
libcurl4-openssl-dev \
libclang-dev \
libkrb5-dev \
libsasl2-dev \
# PostgreSQL (for local DB during development)
postgresql \
postgresql-client \
# Node.js 22 (for npm run check / frontend dev)
&& curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
&& apt-get install -y --no-install-recommends nodejs \
&& rm -rf /var/lib/apt/lists/* \
# Container runs as arbitrary UIDs (--user uid:gid). These three lines make
# sudo work for any UID:
# 1) NOPASSWD rule so sudo never prompts for a password
# 2) Writable passwd/group so the entrypoint can register the dynamic UID
# 3) Writable shadow so unix_chkpwd can validate the account (without this,
# sudo fails with "account validation failure, is your account locked?")
&& echo "ALL ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/sandbox \
&& chmod 0440 /etc/sudoers.d/sandbox \
&& chmod 666 /etc/passwd /etc/group /etc/shadow
# ── GitHub CLI (for PR creation) ──────────────────────────────────────────────
RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
-o /usr/share/keyrings/githubcli-archive-keyring.gpg \
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
> /etc/apt/sources.list.d/github-cli.list \
&& apt-get update && apt-get install -y --no-install-recommends gh \
&& rm -rf /var/lib/apt/lists/*
# ── Rust toolchain ────────────────────────────────────────────────────────────
# Install under /usr/local/lib/ so bins are world-readable with default umask.
# CARGO_HOME is overridden to /tmp/.cargo at the end for mutable runtime state.
ENV RUSTUP_HOME=/usr/local/lib/rustup CARGO_HOME=/usr/local/lib/cargo
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | \
sh -s -- -y --default-toolchain stable --profile minimal && \
ln -s /usr/local/lib/cargo/bin/* /usr/local/bin/
RUN cargo install sqlx-cli --no-default-features --features native-tls,postgres && \
cargo install cargo-watch && \
ln -sf /usr/local/lib/cargo/bin/sqlx /usr/local/bin/sqlx && \
ln -sf /usr/local/lib/cargo/bin/cargo-watch /usr/local/bin/cargo-watch
# ── Register dynamic runtime users ───────────────────────────────────────────
RUN cat <<'SCRIPT' > /usr/local/bin/register-dynamic-user.sh
#!/bin/sh
set -eu
uid="${1:-}"
gid="${2:-}"
if [ -z "$uid" ] || [ -z "$gid" ]; then
echo "register-dynamic-user: usage: register-dynamic-user <uid> <gid>" >&2
exit 1
fi
if ! getent group "$gid" >/dev/null 2>&1; then
echo "sandbox:x:${gid}:" >> /etc/group
fi
if ! getent passwd "$uid" >/dev/null 2>&1; then
echo "sandbox:x:${uid}:${gid}:sandbox:/tmp:/bin/sh" >> /etc/passwd
fi
# Add a shadow entry ("*" = no password) so unix_chkpwd doesn't reject sudo.
if ! grep -q "^sandbox:" /etc/shadow 2>/dev/null; then
echo "sandbox:*:19000:0:99999:7:::" >> /etc/shadow
fi
SCRIPT
RUN chmod +x /usr/local/bin/register-dynamic-user.sh
# ── Network init script (iptables firewall + privilege drop) ──────────────────
RUN cat <<'SCRIPT' > /usr/local/bin/network-init.sh
#!/bin/bash
set -euo pipefail
if [ -n "${WM_PROXY_HOST:-}" ] && [ -n "${WM_PROXY_PORT:-}" ]; then
# Resolve hostnames to ALL IPs (multi-A records, round-robin DNS)
PROXY_IPS=$(getent ahostsv4 "$WM_PROXY_HOST" | awk '{print $1}' | sort -u)
RPC_HOST="${WM_RPC_HOST:-$WM_PROXY_HOST}"
RPC_IPS=$(getent ahostsv4 "$RPC_HOST" | awk '{print $1}' | sort -u)
if [ -z "$PROXY_IPS" ] || [ -z "$RPC_IPS" ]; then
echo "network-init: failed to resolve proxy/RPC host" >&2
exit 1
fi
# IPv4: default deny outbound
iptables -P OUTPUT DROP
iptables -A OUTPUT -o lo -j ACCEPT
iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
# Allow DNS (UDP/TCP 53) to configured nameservers.
if [ -f /etc/resolv.conf ]; then
grep '^nameserver' /etc/resolv.conf | awk '{print $2}' | while read -r ns; do
iptables -A OUTPUT -d "$ns" -p udp --dport 53 -j ACCEPT
iptables -A OUTPUT -d "$ns" -p tcp --dport 53 -j ACCEPT
done
fi
# Allow ALL resolved proxy IPs (handles multi-A DNS)
for ip in $PROXY_IPS; do
iptables -A OUTPUT -d "$ip" -p tcp --dport "$WM_PROXY_PORT" -j ACCEPT
done
# Allow ALL resolved RPC IPs
if [ -n "${WM_RPC_PORT:-}" ]; then
for ip in $RPC_IPS; do
iptables -A OUTPUT -d "$ip" -p tcp --dport "$WM_RPC_PORT" -j ACCEPT
done
fi
# Reject (not drop) everything else to fail fast instead of hanging
iptables -A OUTPUT -j REJECT
# IPv6: block entirely to prevent leaks (fail closed)
if ip6tables -L -n >/dev/null 2>&1; then
ip6tables -P OUTPUT DROP
ip6tables -A OUTPUT -o lo -j ACCEPT
ip6tables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
ip6tables -A OUTPUT -j REJECT
else
if ! sysctl -w net.ipv6.conf.all.disable_ipv6=1 2>/dev/null; then
echo "network-init: failed to block IPv6 (neither ip6tables nor sysctl available)" >&2
exit 1
fi
fi
fi
# Add sandbox user/group so sudo works after dropping privileges.
if [ -z "${WM_TARGET_UID:-}" ] || [ -z "${WM_TARGET_GID:-}" ]; then
echo "network-init: WM_TARGET_UID and WM_TARGET_GID are required" >&2
exit 1
fi
/usr/local/bin/register-dynamic-user.sh "${WM_TARGET_UID}" "${WM_TARGET_GID}"
# Fix PTY ownership so the unprivileged user can read/write the terminal.
if [ -t 0 ]; then
chown "${WM_TARGET_UID}:${WM_TARGET_GID}" "$(tty)"
fi
# Drop privileges and exec the user command.
exec gosu "${WM_TARGET_UID}:${WM_TARGET_GID}" env HOME=/tmp "$@"
SCRIPT
RUN chmod +x /usr/local/bin/network-init.sh
# ── workmux (sandbox RPC) ────────────────────────────────────────────────────
RUN curl -fsSL https://raw.githubusercontent.com/raine/workmux/main/scripts/install.sh | bash
# ── Claude Code ───────────────────────────────────────────────────────────────
RUN curl -fsSL https://claude.ai/install.sh | bash && \
target="$(readlink -f /root/.local/bin/claude)" && \
mv /root/.local/share/claude /usr/local/lib/claude && \
ln -s "/usr/local/lib/claude/versions/$(basename "$target")" /usr/local/bin/claude && \
mkdir -p /tmp/.local/bin && \
ln -s /usr/local/bin/claude /tmp/.local/bin/claude
# ── Codex ─────────────────────────────────────────────────────────────────────
RUN npm i -g @openai/codex
# ── Bun ───────────────────────────────────────────────────────────────────────
ENV BUN_INSTALL=/usr/local/lib/bun
RUN curl -fsSL https://bun.sh/install | bash && \
ln -s /usr/local/lib/bun/bin/bun /usr/local/bin/bun && \
ln -s /usr/local/lib/bun/bin/bunx /usr/local/bin/bunx
# ── Playwright + Chromium (for screenshots) ──────────────────────────────────
ENV PLAYWRIGHT_BROWSERS_PATH=/usr/local/lib/playwright-browsers
RUN bun add -g @playwright/test \
&& bunx playwright install chromium --with-deps \
&& chmod -R a+rwX /usr/local/lib/playwright-browsers \
&& rm -rf /var/lib/apt/lists/* /tmp/bunx-*
# ── AWS CLI (for S3-compatible uploads to R2) ─────────────────────────────────
RUN curl -fsSL "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o /tmp/awscliv2.zip \
&& unzip -q /tmp/awscliv2.zip -d /tmp \
&& /tmp/aws/install \
&& rm -rf /tmp/aws /tmp/awscliv2.zip
ENV AWS_DEFAULT_REGION=auto
# ── Runtime env for arbitrary UID ─────────────────────────────────────────────
# Mutable state goes to /tmp (writable by any UID). Toolchains stay read-only.
ENV CARGO_HOME=/tmp/.cargo BUN_TMPDIR=/tmp
# ── Entrypoint ────────────────────────────────────────────────────────────────
RUN cat <<'ENTRY' > /usr/local/bin/entrypoint.sh
#!/bin/sh
/usr/local/bin/register-dynamic-user.sh "$(id -u)" "$(id -g)"
# Start PostgreSQL (unix socket in /tmp, owned by postgres user)
mkdir -p /tmp/pgdata && sudo chown postgres:postgres /tmp/pgdata
if [ ! -f /tmp/pgdata/PG_VERSION ]; then
sudo -u postgres /usr/lib/postgresql/15/bin/initdb -D /tmp/pgdata --auth=trust
fi
sudo -u postgres /usr/lib/postgresql/15/bin/pg_ctl -D /tmp/pgdata -l /tmp/pg.log start -o "-k /tmp"
sudo -u postgres psql -h /tmp -c "CREATE ROLE sandbox SUPERUSER LOGIN" 2>/dev/null || true
sudo -u postgres createdb -h /tmp windmill 2>/dev/null || true
# Run database migrations so sqlx compile-time checks work
if [ -d "$PWD/backend/migrations" ]; then
DATABASE_URL="postgres://sandbox@localhost/windmill?host=/tmp" \
sqlx migrate run --source "$PWD/backend/migrations" 2>/dev/null || true
fi
# Install frontend dependencies and generate backend client
if [ -d "$PWD/frontend" ]; then
(cd "$PWD/frontend" && npm install && npm run generate-backend-client) 2>/dev/null || true
fi
exec "$@"
ENTRY
RUN chmod +x /usr/local/bin/entrypoint.sh
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]

View File

@@ -65,7 +65,7 @@ Setting up zsh autocomplete is also recommended — see the [workmux docs](https
Each worktree is assigned a **slot** that determines its ports:
| Slot | Backend | Frontend |
| ---- | ------- | -------- |
|------|---------|----------|
| 0 | 8000 | 3000 |
| 1 | 8010 | 3010 |
| 2 | 8020 | 3020 |
@@ -170,8 +170,7 @@ The setup is defined in `.workmux.yaml` at the repo root. Key sections:
- **`post_create`**: Runs `scripts/worktree-env` to generate `.env.local` with port assignments
- **`panes`**: Defines the tmux layout (agent, backend, frontend)
- **`files.copy`**: Copies `backend/.env` and `scripts/` into each worktree
The `post_create` hook also copies `frontend/node_modules` using `cp -a` (preserves `.bin/` symlinks that `cp -r` would dereference).
- **`files.symlink`**: Symlinks `node_modules` and `.svelte-kit` to avoid reinstalling per worktree
## Enterprise (EE) Code Access
@@ -192,98 +191,6 @@ 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.
## 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
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.
**With `wm` (workmux):**
Set `CARGO_FEATURES` as an environment variable before creating the worktree:
```bash
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.
**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
Default credentials: `admin@windmill.dev` / `changeme`

View File

@@ -1 +0,0 @@
# Test PR for shell-quoting bug verification

View File

@@ -20,8 +20,7 @@
"resource",
"variable",
"ducklake",
"datatable",
"volume"
"datatable"
]
}
}

View File

@@ -37,11 +37,6 @@
"ordinal": 6,
"name": "format_extension",
"type_info": "Varchar"
},
{
"ordinal": 7,
"name": "is_fileset",
"type_info": "Bool"
}
],
"parameters": {
@@ -57,8 +52,7 @@
true,
true,
true,
true,
false
true
]
},
"hash": "03d63d2e64b012f624d2731b5bcb8849c74a9474777be61edf0ed43ddda07ef3"

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO flow_version (workspace_id, path, value, schema, created_by)\n VALUES ($1, $2, $3, $4::text::json, $5)\n RETURNING id",
"query": "INSERT INTO flow_version (workspace_id, path, value, schema, created_by) \n VALUES ($1, $2, $3, $4::text::json, $5)\n RETURNING id",
"describe": {
"columns": [
{
@@ -22,5 +22,5 @@
false
]
},
"hash": "a9c805423e700b0acceb7c3dc43d1d3f9d4f56da25f588d281638e449d99a0d9"
"hash": "07f5290e90533eac50b890a0d7f4a5e73ac111c838f687fe8647636827aae8b5"
}

View File

@@ -0,0 +1,202 @@
{
"db_name": "PostgreSQL",
"query": "SELECT * FROM workspace_settings WHERE teams_team_id = $1 AND teams_command_script IS NOT NULL",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "workspace_id",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "slack_team_id",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "slack_name",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "slack_command_script",
"type_info": "Varchar"
},
{
"ordinal": 4,
"name": "slack_email",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "customer_id",
"type_info": "Varchar"
},
{
"ordinal": 6,
"name": "plan",
"type_info": "Varchar"
},
{
"ordinal": 7,
"name": "webhook",
"type_info": "Text"
},
{
"ordinal": 8,
"name": "deploy_to",
"type_info": "Varchar"
},
{
"ordinal": 9,
"name": "ai_config",
"type_info": "Jsonb"
},
{
"ordinal": 10,
"name": "large_file_storage",
"type_info": "Jsonb"
},
{
"ordinal": 11,
"name": "git_sync",
"type_info": "Jsonb"
},
{
"ordinal": 12,
"name": "default_app",
"type_info": "Varchar"
},
{
"ordinal": 13,
"name": "default_scripts",
"type_info": "Jsonb"
},
{
"ordinal": 14,
"name": "deploy_ui",
"type_info": "Jsonb"
},
{
"ordinal": 15,
"name": "mute_critical_alerts",
"type_info": "Bool"
},
{
"ordinal": 16,
"name": "color",
"type_info": "Varchar"
},
{
"ordinal": 17,
"name": "operator_settings",
"type_info": "Jsonb"
},
{
"ordinal": 18,
"name": "teams_command_script",
"type_info": "Text"
},
{
"ordinal": 19,
"name": "teams_team_id",
"type_info": "Text"
},
{
"ordinal": 20,
"name": "teams_team_name",
"type_info": "Text"
},
{
"ordinal": 21,
"name": "git_app_installations",
"type_info": "Jsonb"
},
{
"ordinal": 22,
"name": "ducklake",
"type_info": "Jsonb"
},
{
"ordinal": 23,
"name": "slack_oauth_client_id",
"type_info": "Varchar"
},
{
"ordinal": 24,
"name": "slack_oauth_client_secret",
"type_info": "Varchar"
},
{
"ordinal": 25,
"name": "datatable",
"type_info": "Jsonb"
},
{
"ordinal": 26,
"name": "teams_team_guid",
"type_info": "Text"
},
{
"ordinal": 27,
"name": "auto_invite",
"type_info": "Jsonb"
},
{
"ordinal": 28,
"name": "error_handler",
"type_info": "Jsonb"
},
{
"ordinal": 29,
"name": "success_handler",
"type_info": "Jsonb"
},
{
"ordinal": 30,
"name": "public_app_execution_limit_per_minute",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
true,
true,
true,
false,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
false,
true,
true,
true,
true,
true,
true,
true,
true,
true
]
},
"hash": "08f288d2781d823e109a9e5b8848234ca7d1efeee9661f3901f298da375e73f7"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT email FROM password WHERE ($2::text = '*' OR email LIKE CONCAT('%@', $2::text)) AND NOT EXISTS (\n SELECT 1 FROM usr WHERE workspace_id = $1::text AND email = password.email\n )",
"query": "SELECT email FROM password WHERE ($2::text = '*' OR email LIKE CONCAT('%', $2::text)) AND NOT EXISTS (\n SELECT 1 FROM usr WHERE workspace_id = $1::text AND email = password.email\n )",
"describe": {
"columns": [
{
@@ -19,5 +19,5 @@
false
]
},
"hash": "886a921adc115f0a9c6f3a68381bd8f5a16866135120175d9073b9b2c41bbd51"
"hash": "0ef37117c369f03236e18f9dbb1f3d52776c8cb73f2507199c6ca16d4d2405ba"
}

View File

@@ -43,7 +43,8 @@
"aiagent",
"unassigned_script",
"unassigned_flow",
"unassigned_singlestepflow"
"unassigned_singlestepflow",
"snapshotbuild"
]
}
}

View File

@@ -1,29 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT email, edited_by FROM schedule WHERE path = $1 AND workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "email",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "edited_by",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false,
false
]
},
"hash": "17aafb72843659df9594d6d2466d2afaf26e666ffe52e0ea85792ea31b63410c"
}

View File

@@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO resource_type (workspace_id, name, schema, description, edited_at, created_by, format_extension, is_fileset)\n SELECT $2, name, schema, description, edited_at, created_by, format_extension, is_fileset\n FROM resource_type\n WHERE workspace_id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Varchar"
]
},
"nullable": []
},
"hash": "1c2157ce14e90f0751d7f0a9f2dbb3c5a5789a32423e75260098a5300a4af986"
}

View File

@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM token WHERE workspace_id = $1 AND label IS DISTINCT FROM 'session' RETURNING token",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "token",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false
]
},
"hash": "2d6607b3c38fe72b5663c32de58dacbabed4c5ae28101e3ae2694f96fd055a91"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO workspace_invite\n (workspace_id, email, is_admin, operator)\n SELECT $1::text, email, false, $3 FROM password WHERE ($2::text = '*' OR email LIKE CONCAT('%@', $2::text)) AND NOT EXISTS (\n SELECT 1 FROM usr WHERE workspace_id = $1::text AND email = password.email\n )\n ON CONFLICT DO NOTHING",
"query": "INSERT INTO workspace_invite\n (workspace_id, email, is_admin, operator)\n SELECT $1::text, email, false, $3 FROM password WHERE ($2::text = '*' OR email LIKE CONCAT('%', $2::text)) AND NOT EXISTS (\n SELECT 1 FROM usr WHERE workspace_id = $1::text AND email = password.email\n )\n ON CONFLICT DO NOTHING",
"describe": {
"columns": [],
"parameters": {
@@ -12,5 +12,5 @@
},
"nullable": []
},
"hash": "c0fad64e5d707ffa29d236f558e23b608168dc3a1b3857d2ad33ec20627acbff"
"hash": "2e1d1c59bfc53d58962251822c85cf9a26e3b2888702e5e9d5fc1b082901df09"
}

View File

@@ -1,28 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT workspace_id, teams_command_script FROM workspace_settings WHERE teams_team_id = $1 AND teams_command_script IS NOT NULL",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "workspace_id",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "teams_command_script",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
true
]
},
"hash": "34721bce20aa8b2a2c6b9bd5455735f1a2270f23d73de95101e6350f6df40acc"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE schedule SET\n schedule = $1,\n timezone = $2,\n args = $3,\n on_failure = $4,\n on_failure_times = $5,\n on_failure_exact = $6,\n on_failure_extra_args = $7,\n on_recovery = $8,\n on_recovery_times = $9,\n on_recovery_extra_args = $10,\n on_success = $11,\n on_success_extra_args = $12,\n ws_error_handler_muted = $13,\n retry = $14,\n summary = $15,\n no_flow_overlap = $16,\n tag = $17,\n paused_until = $18,\n path = $19,\n workspace_id = $20,\n cron_version = COALESCE($21, cron_version),\n description = $22,\n dynamic_skip = $23,\n email = COALESCE($24, email),\n edited_by = $25\n WHERE path = $19 AND workspace_id = $20\n RETURNING\n workspace_id,\n path,\n edited_by,\n edited_at,\n schedule,\n timezone,\n enabled,\n script_path,\n is_flow,\n args AS \"args: _\",\n extra_perms,\n email,\n error,\n on_failure,\n on_failure_times,\n on_failure_exact,\n on_failure_extra_args AS \"on_failure_extra_args: _\",\n on_recovery,\n on_recovery_times,\n on_recovery_extra_args AS \"on_recovery_extra_args: _\",\n on_success,\n on_success_extra_args AS \"on_success_extra_args: _\",\n ws_error_handler_muted,\n retry,\n no_flow_overlap,\n summary,\n description,\n tag,\n paused_until,\n cron_version,\n dynamic_skip\n ",
"query": "\n UPDATE schedule SET\n schedule = $1,\n timezone = $2,\n args = $3,\n on_failure = $4,\n on_failure_times = $5,\n on_failure_exact = $6,\n on_failure_extra_args = $7,\n on_recovery = $8,\n on_recovery_times = $9,\n on_recovery_extra_args = $10,\n on_success = $11,\n on_success_extra_args = $12,\n ws_error_handler_muted = $13,\n retry = $14,\n summary = $15,\n no_flow_overlap = $16,\n tag = $17,\n paused_until = $18,\n path = $19,\n workspace_id = $20,\n cron_version = COALESCE($21, cron_version),\n description = $22,\n dynamic_skip = $23\n WHERE path = $19 AND workspace_id = $20\n RETURNING\n workspace_id,\n path,\n edited_by,\n edited_at,\n schedule,\n timezone,\n enabled,\n script_path,\n is_flow,\n args AS \"args: _\",\n extra_perms,\n email,\n error,\n on_failure,\n on_failure_times,\n on_failure_exact,\n on_failure_extra_args AS \"on_failure_extra_args: _\",\n on_recovery,\n on_recovery_times,\n on_recovery_extra_args AS \"on_recovery_extra_args: _\",\n on_success,\n on_success_extra_args AS \"on_success_extra_args: _\",\n ws_error_handler_muted,\n retry,\n no_flow_overlap,\n summary,\n description,\n tag,\n paused_until,\n cron_version,\n dynamic_skip\n ",
"describe": {
"columns": [
{
@@ -183,8 +183,6 @@
"Text",
"Text",
"Text",
"Varchar",
"Varchar",
"Varchar"
]
},
@@ -222,5 +220,5 @@
true
]
},
"hash": "987d79f7c6d7bc148cc8aab67e47161cfca045966e995e28c7a7ad090cffeda0"
"hash": "4144c87c25a939aafb2f57da189d94d038bcad7a36fbf87e0403c89a979c5b3f"
}

View File

@@ -42,7 +42,8 @@
"aiagent",
"unassigned_script",
"unassigned_flow",
"unassigned_singlestepflow"
"unassigned_singlestepflow",
"snapshotbuild"
]
}
}

View File

@@ -38,7 +38,8 @@
"aiagent",
"unassigned_script",
"unassigned_flow",
"unassigned_singlestepflow"
"unassigned_singlestepflow",
"snapshotbuild"
]
}
}

View File

@@ -77,7 +77,8 @@
"aiagent",
"unassigned_script",
"unassigned_flow",
"unassigned_singlestepflow"
"unassigned_singlestepflow",
"snapshotbuild"
]
}
}

View File

@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "\n WITH prev_sd AS (\n DELETE FROM debounce_stale_data WHERE job_id = $1 RETURNING to_relock\n ) INSERT INTO debounce_stale_data (job_id, to_relock)\n VALUES ($2, array_cat((SELECT to_relock FROM prev_sd), $3))\n ON CONFLICT (job_id) DO UPDATE SET to_relock = EXCLUDED.to_relock\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Uuid",
"TextArray"
]
},
"nullable": []
},
"hash": "61b37cb4db6e60c2d35f7d23db5afbe04e040a8dcd1d93afaaaa320665c8779a"
}

View File

@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO resource_type (workspace_id, name, schema, description, edited_at, created_by, format_extension)\n SELECT $2, name, schema, description, edited_at, created_by, format_extension\n FROM resource_type\n WHERE workspace_id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Varchar"
]
},
"nullable": []
},
"hash": "7abd579d3ec97853ac36cc8dad29013eb133a28cd848bf8fdf9571b2ee402a3e"
}

View File

@@ -37,11 +37,6 @@
"ordinal": 6,
"name": "format_extension",
"type_info": "Varchar"
},
{
"ordinal": 7,
"name": "is_fileset",
"type_info": "Bool"
}
],
"parameters": {
@@ -56,8 +51,7 @@
true,
true,
true,
true,
false
true
]
},
"hash": "7b1239ad6460e8f5fb41bfe12f662a779528784ec8cf3f6dcce5545ab90bf234"

View File

@@ -44,7 +44,8 @@
"aiagent",
"unassigned_script",
"unassigned_flow",
"unassigned_singlestepflow"
"unassigned_singlestepflow",
"snapshotbuild"
]
}
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT schema, description, format_extension, is_fileset\n FROM resource_type\n WHERE workspace_id = $1 AND name = $2",
"query": "SELECT schema, description, format_extension\n FROM resource_type\n WHERE workspace_id = $1 AND name = $2",
"describe": {
"columns": [
{
@@ -17,11 +17,6 @@
"ordinal": 2,
"name": "format_extension",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "is_fileset",
"type_info": "Bool"
}
],
"parameters": {
@@ -33,9 +28,8 @@
"nullable": [
true,
true,
true,
false
true
]
},
"hash": "2768622b76ad92c05f4f44d997aff285707e1a43ce85e5bb8e87849d78a0637f"
"hash": "7bc9fc05dbd162866bef1fdd3e7faeb50429881ed1bc962903f06e4b3d5f8d44"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO token (token, email, label, expiration, scopes, workspace_id)\n SELECT $1::varchar, $2::varchar, $3::varchar, now() + ($4 || ' seconds')::interval, $5::text[], $6::varchar\n WHERE NOT EXISTS(SELECT 1 FROM workspace WHERE id = $6 AND deleted = true)",
"query": "INSERT INTO token (token, email, label, expiration, scopes, workspace_id)\n VALUES ($1, $2, $3, now() + ($4 || ' seconds')::interval, $5, $6)",
"describe": {
"columns": [],
"parameters": {
@@ -15,5 +15,5 @@
},
"nullable": []
},
"hash": "d32448f6b329cf98dad42b218a630c0cf40a99edb4ae9fe3e9be485ab1077b3a"
"hash": "7e4aa6b19b110bca423b3a3f428826d92b9808c64ef989fef2142bc8e02d6630"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT runnable_id as \"runnable_id: ScriptHash\", raw_flow as \"raw_flow: _\", kind as \"kind: _\", parent_job, flow_step_id FROM v2_job WHERE id = $1",
"query": "SELECT runnable_id as \"runnable_id: ScriptHash\", raw_flow as \"raw_flow: _\", kind as \"kind: _\" FROM v2_job WHERE id = $1",
"describe": {
"columns": [
{
@@ -42,21 +42,12 @@
"aiagent",
"unassigned_script",
"unassigned_flow",
"unassigned_singlestepflow"
"unassigned_singlestepflow",
"snapshotbuild"
]
}
}
}
},
{
"ordinal": 3,
"name": "parent_job",
"type_info": "Uuid"
},
{
"ordinal": 4,
"name": "flow_step_id",
"type_info": "Varchar"
}
],
"parameters": {
@@ -67,10 +58,8 @@
"nullable": [
true,
true,
false,
true,
true
false
]
},
"hash": "7aaa5b0bd873c2029e2201d287ea0aaae04678ac105374bbe387e534a6cb6333"
"hash": "805d633de90fee335f1726284eda0dbc200d45960fb8dea867492c8c7dd096d5"
}

View File

@@ -1,29 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT email, edited_by FROM http_trigger WHERE path = $1 AND workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "email",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "edited_by",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false,
false
]
},
"hash": "8311a553c44221751ffdbbe6a997d6feba8d43292daf6c5433b66bd8450e8854"
}

View File

@@ -1,34 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT name, format_extension, is_fileset FROM resource_type WHERE (format_extension IS NOT NULL OR is_fileset = true) AND (workspace_id = $1 OR workspace_id = 'admins')",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "name",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "format_extension",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "is_fileset",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
true,
false
]
},
"hash": "842775bcf91d747abb11ffe9c98fa1208595e012590606ef6667ea3a78105883"
}

View File

@@ -1,23 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT on_behalf_of_email FROM flow WHERE path = $1 AND workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "on_behalf_of_email",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
true
]
},
"hash": "85a6a85fd126a8bfedd65d6b38d22c65911ab9cf0414c33a3321a1d43af49795"
}

View File

@@ -102,7 +102,8 @@
"aiagent",
"unassigned_script",
"unassigned_flow",
"unassigned_singlestepflow"
"unassigned_singlestepflow",
"snapshotbuild"
]
}
}

View File

@@ -32,7 +32,8 @@
"aiagent",
"unassigned_script",
"unassigned_flow",
"unassigned_singlestepflow"
"unassigned_singlestepflow",
"snapshotbuild"
]
}
}

View File

@@ -72,7 +72,8 @@
"aiagent",
"unassigned_script",
"unassigned_flow",
"unassigned_singlestepflow"
"unassigned_singlestepflow",
"snapshotbuild"
]
}
}

View File

@@ -77,7 +77,8 @@
"aiagent",
"unassigned_script",
"unassigned_flow",
"unassigned_singlestepflow"
"unassigned_singlestepflow",
"snapshotbuild"
]
}
}

View File

@@ -16,8 +16,7 @@
"resource",
"variable",
"ducklake",
"datatable",
"volume"
"datatable"
]
}
}

View File

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

View File

@@ -102,7 +102,8 @@
"aiagent",
"unassigned_script",
"unassigned_flow",
"unassigned_singlestepflow"
"unassigned_singlestepflow",
"snapshotbuild"
]
}
}

View File

@@ -72,7 +72,8 @@
"aiagent",
"unassigned_script",
"unassigned_flow",
"unassigned_singlestepflow"
"unassigned_singlestepflow",
"snapshotbuild"
]
}
}

View File

@@ -37,11 +37,6 @@
"ordinal": 6,
"name": "format_extension",
"type_info": "Varchar"
},
{
"ordinal": 7,
"name": "is_fileset",
"type_info": "Bool"
}
],
"parameters": {
@@ -56,8 +51,7 @@
true,
true,
true,
true,
false
true
]
},
"hash": "b8d392ccfcccafe0c19511b3567bc11779b1052b0948c410468a8aeba1d26d33"

View File

@@ -41,7 +41,8 @@
"aiagent",
"unassigned_script",
"unassigned_flow",
"unassigned_singlestepflow"
"unassigned_singlestepflow",
"snapshotbuild"
]
}
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO token\n (token, email, label, expiration, super_admin, scopes, workspace_id)\n SELECT $1, $2, $3, $4, $5, $6, $7\n WHERE $7::varchar IS NULL OR NOT EXISTS(\n SELECT 1 FROM workspace WHERE id = $7 AND deleted = true\n )",
"query": "INSERT INTO token\n (token, email, label, expiration, super_admin, scopes, workspace_id)\n VALUES ($1, $2, $3, $4, $5, $6, $7)",
"describe": {
"columns": [],
"parameters": {
@@ -16,5 +16,5 @@
},
"nullable": []
},
"hash": "e33be0991702ae3a295db7defc6d19d914307a95d72bb0fb447e5b367d52f6a0"
"hash": "c624f15f3e321b1eecf123da9bf0b18e8c1d16ef25ffb9d04e5447d0d583d55c"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM _sqlx_migrations WHERE\n version=20250131115248 OR version=20250902085503 OR version=20250201145630 OR\n version=20250201145631 OR version=20250201145632 OR version=20251006143821",
"query": "DELETE FROM _sqlx_migrations WHERE\n version=20250131115248 OR version=20250902085503 OR version=20250201145630 OR\n version=20250201145631 OR version=20250201145632 OR version=20251006143821 OR\n version=20260207000001 OR version=20260207000002 OR version=20260207000003 OR version=20260207000004",
"describe": {
"columns": [],
"parameters": {
@@ -8,5 +8,5 @@
},
"nullable": []
},
"hash": "8d4ad4ee75fb149c36a9f6a0c4cf5fd981473f45d1b71b4b8236e021f7c8682d"
"hash": "c6bcf0d9e211bc03e3338682295f4995e1d622917367c478742addd073245ad5"
}

View File

@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO workspace_invite (workspace_id, email, is_admin, operator)\n SELECT $1, email, is_admin, operator\n FROM usr\n WHERE workspace_id = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Text"
]
},
"nullable": []
},
"hash": "cd399a3a797d1733fb9071ebca3f5928a3c7eba2983431844581fd2393312a2e"
}

View File

@@ -1,29 +1,28 @@
{
"db_name": "PostgreSQL",
"query": "SELECT email, edited_by FROM websocket_trigger WHERE path = $1 AND workspace_id = $2",
"query": "\n SELECT name, format_extension FROM resource_type WHERE format_extension IS NOT NULL AND (workspace_id = $1 OR workspace_id = 'admins')",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "email",
"name": "name",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "edited_by",
"name": "format_extension",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false,
false
true
]
},
"hash": "075d4749299af2cb81162bf396bec6aa89de43ec201c911196763e03e644ca7a"
"hash": "cf1cef7e0fe2e7e3db96b0ec005360361b9eec023a6fc2a4a7a917f59d86af4d"
}

View File

@@ -41,7 +41,8 @@
"aiagent",
"unassigned_script",
"unassigned_flow",
"unassigned_singlestepflow"
"unassigned_singlestepflow",
"snapshotbuild"
]
}
}

View File

@@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO group_\n VALUES ($1, 'wm_deployers', 'Members can preserve the original author when deploying to this workspace')",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar"
]
},
"nullable": []
},
"hash": "dda45bcc53e94659838e98b6b9e7a55be0e31aee3008d5190f09c1f15e5b47dd"
}

View File

@@ -31,7 +31,8 @@
"aiagent",
"unassigned_script",
"unassigned_flow",
"unassigned_singlestepflow"
"unassigned_singlestepflow",
"snapshotbuild"
]
}
}

View File

@@ -1,23 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT on_behalf_of_email FROM script WHERE path = $1 AND workspace_id = $2 ORDER BY created_at DESC LIMIT 1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "on_behalf_of_email",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
true
]
},
"hash": "e1f43cb65201b4f0965a4e18f0c918ae51fee667472d0cc2796ffdba4138d2ee"
}

View File

@@ -37,7 +37,8 @@
"aiagent",
"unassigned_script",
"unassigned_flow",
"unassigned_singlestepflow"
"unassigned_singlestepflow",
"snapshotbuild"
]
}
}

View File

@@ -77,7 +77,8 @@
"aiagent",
"unassigned_script",
"unassigned_flow",
"unassigned_singlestepflow"
"unassigned_singlestepflow",
"snapshotbuild"
]
}
}

View File

@@ -1,23 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT on_behalf_of_email FROM script WHERE path = $1 AND workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "on_behalf_of_email",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
true
]
},
"hash": "e8d948274840699c5f7485ee4bc00b72c11bd226f99eade7e9a0da4605539283"
}

View File

@@ -37,11 +37,6 @@
"ordinal": 6,
"name": "format_extension",
"type_info": "Varchar"
},
{
"ordinal": 7,
"name": "is_fileset",
"type_info": "Bool"
}
],
"parameters": {
@@ -54,8 +49,7 @@
true,
true,
true,
true,
false
true
]
},
"hash": "eb1f7f01461f5a7540c273b37e5d578c31cf151ab3ef813f7aada76533761e12"

View File

@@ -32,7 +32,8 @@
"aiagent",
"unassigned_script",
"unassigned_flow",
"unassigned_singlestepflow"
"unassigned_singlestepflow",
"snapshotbuild"
]
}
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO resource_type\n (workspace_id, name, schema, description, created_by, format_extension, is_fileset, edited_at)\n VALUES ($1, $2, $3, $4, $5, $6, $7, now())",
"query": "INSERT INTO resource_type\n (workspace_id, name, schema, description, created_by, format_extension, edited_at)\n VALUES ($1, $2, $3, $4, $5, $6, now())",
"describe": {
"columns": [],
"parameters": {
@@ -10,11 +10,10 @@
"Jsonb",
"Text",
"Varchar",
"Varchar",
"Bool"
"Varchar"
]
},
"nullable": []
},
"hash": "5899c7614f195fdd23e38389e52b004f957aafa2201b80638b5f87a625373f00"
"hash": "ffedbb3a2676a6d7b71f81f89109a02a8dba90d40144e942527f8a3fc36dfbc1"
}

View File

@@ -1,8 +1,98 @@
# Backend (Rust)
# Backend Development (Rust)
- **Coding patterns**: MUST use the `rust-backend` skill when writing Rust code
- **Validation**: `docs/validation.md` — which `cargo check` flags to use
- **Enterprise**: `docs/enterprise.md` — EE file conventions and PR workflow
- **DB schema**: `backend/summarized_schema.txt`
- **API routes entry point**: `windmill-api/src/lib.rs`
- **OpenAPI spec**: `windmill-api/openapi.yaml`
## Project Structure
Windmill uses a workspace-based architecture with multiple crates:
- **windmill-api**: API server functionality
- **windmill-worker**: Job execution
- **windmill-common**: Shared code used by all crates
- **windmill-queue**: Job & flow queuing
- **windmill-audit**: Audit logging
- Other specialized crates (git-sync, autoscaling, etc.)
## Key References (MUST FOLLOW THESE)
- You MUST follow best-practices by using the `rust-backend` skill, everytime you write RUST code.
- When working with the database: read `summarized_schema.txt` before starting
- When working with the API routes: you can read `windmill-api/src/lib.rs` to get started
## Adding New Code
### Module Organization
- Place new code in the appropriate crate based on functionality
- For API endpoints, create or modify files in `windmill-api/src/` organized by domain
- For shared functionality, use `windmill-common/src/`
- Follow existing patterns for file structure and organization
### API Endpoints
- Follow existing patterns in the `windmill-api` crate
- Use axum's routing system and extractors
- Update `backend/windmill-api/openapi.yaml` after modifying API endpoints
### Database Changes
- Update database schema with migration if necessary
- Use `sqlx` for database operations with prepared statements
- Use transactions for multi-step operations
- To apply pending migrations: `sqlx migrate run` (never manually run .sql files)
- **Never use `SQLX_OFFLINE=true`** — a live database is always available for compilation
- After all code changes are done, run `./update-sqlx` to regenerate the offline query cache
## Enterprise Features
- Enterprise files use the `*_ee.rs` suffix
- Enterprise source is in `windmill-ee-private` folder (sibling directory at `../../windmill-ee-private` or `~/windmill-ee-private`), symlinked into each crate's `src/`
- The `_ee.rs` files are gitignored in the main repo — they are tracked only in the `windmill-ee-private` repo
- You can and should modify `windmill-ee-private` directly when needed (e.g., when creating new crates that need EE code, mirror the package structure there)
- Use feature flags: `#[cfg(feature = "enterprise")]`
- Isolate enterprise code in separate modules
### EE PR Workflow (MUST DO when modifying `*_ee.rs` files)
When you modify any `*_ee.rs` file and create a PR on the windmill repo, you **MUST** also:
1. **Create a matching branch** in the `windmill-ee-private` repo (use the same branch name). If using worktrees, the EE worktree is at `~/windmill-ee-private__worktrees/<branch-name>/`
2. **Commit and push** the `_ee.rs` changes in that branch
3. **Create a PR** on `windmill-ee-private` with a link to the companion windmill PR
4. **Update `ee-repo-ref.txt`**: Run `bash write_latest_ee_ref.sh` from `backend/` to write the latest EE commit hash. **Important**: the script may fall back to `~/windmill-ee-private` (main branch) instead of the worktree — verify it wrote the correct commit hash from your branch, not from main. If wrong, manually write the correct hash.
5. **Commit `ee-repo-ref.txt`** in the windmill repo so CI picks up the correct EE ref
## Code Validation (MUST DO)
After making backend changes, you MUST run `cargo check` and fix all errors and warnings before considering the work done.
Only enable the feature flags relevant to your changes — do NOT use `all_sqlx_features` as it compiles the entire codebase and is very slow. Check the `[features]` section in `Cargo.toml` to identify which flags gate the crates/modules you modified.
Examples:
```bash
# Changed core code (no feature-gated modules)
cargo check
# Changed code behind the enterprise feature
cargo check --features enterprise
# Changed kafka trigger code
cargo check --features kafka
```
## Git Workflow
- **Never push directly to main** — always create a branch and open a pull request
## Testing
- Write unit tests for core functionality
- Use the `#[cfg(test)]` module for test code
- For database tests, use the existing test utilities
## Common Crates
- **tokio**: Async runtime
- **axum**: Web server and routing
- **sqlx**: Database operations
- **serde**: Serialization/deserialization
- **tracing**: Logging and diagnostics
- **reqwest**: HTTP client

250
backend/Cargo.lock generated
View File

@@ -490,7 +490,7 @@ dependencies = [
"memchr",
"num",
"regex",
"regex-syntax 0.8.10",
"regex-syntax 0.8.9",
]
[[package]]
@@ -2259,9 +2259,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "chrono"
version = "0.4.44"
version = "0.4.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118"
dependencies = [
"iana-time-zone",
"js-sys",
@@ -3507,7 +3507,7 @@ dependencies = [
"log",
"recursive",
"regex",
"regex-syntax 0.8.10",
"regex-syntax 0.8.9",
]
[[package]]
@@ -5526,7 +5526,7 @@ checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298"
dependencies = [
"bit-set 0.8.0",
"regex-automata",
"regex-syntax 0.8.10",
"regex-syntax 0.8.9",
]
[[package]]
@@ -5537,7 +5537,7 @@ checksum = "72cf461f865c862bb7dc573f643dd6a2b6842f7c30b07882b56bd148cc2761b8"
dependencies = [
"bit-set 0.8.0",
"regex-automata",
"regex-syntax 0.8.10",
"regex-syntax 0.8.9",
]
[[package]]
@@ -5588,7 +5588,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78"
dependencies = [
"cfg-if",
"rustix 1.1.4",
"rustix 1.1.3",
"windows-sys 0.59.0",
]
@@ -5804,7 +5804,7 @@ version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8640e34b88f7652208ce9e88b1a37a2ae95227d84abec377ccd3c5cfeb141ed4"
dependencies = [
"rustix 1.1.4",
"rustix 1.1.3",
"windows-sys 0.59.0",
]
@@ -6254,7 +6254,7 @@ dependencies = [
"bstr",
"log",
"regex-automata",
"regex-syntax 0.8.10",
"regex-syntax 0.8.9",
]
[[package]]
@@ -8049,14 +8049,13 @@ dependencies = [
[[package]]
name = "libredox"
version = "0.1.14"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a"
checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616"
dependencies = [
"bitflags 2.9.4",
"libc",
"plain",
"redox_syscall 0.7.3",
"redox_syscall 0.7.1",
]
[[package]]
@@ -8093,9 +8092,9 @@ dependencies = [
[[package]]
name = "libz-sys"
version = "1.1.24"
version = "1.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4735e9cbde5aac84a5ce588f6b23a90b9b0b528f6c5a8db8a4aff300463a0839"
checksum = "15d118bbf3771060e7311cc7bb0545b01d08a8b4a7de949198dec1fa0ca1c0f7"
dependencies = [
"cc",
"libc",
@@ -8117,9 +8116,9 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
[[package]]
name = "linux-raw-sys"
version = "0.12.1"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
[[package]]
name = "litemap"
@@ -8600,9 +8599,9 @@ dependencies = [
[[package]]
name = "moka"
version = "0.12.14"
version = "0.12.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85f8024e1c8e71c778968af91d43700ce1d11b219d127d79fb2934153b82b42b"
checksum = "b4ac832c50ced444ef6be0767a008b02c106a909ba79d1d830501e94b96f6b7e"
dependencies = [
"async-lock",
"crossbeam-channel",
@@ -9730,9 +9729,9 @@ dependencies = [
[[package]]
name = "owo-colors"
version = "4.3.0"
version = "4.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d"
checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52"
[[package]]
name = "p224"
@@ -10086,18 +10085,18 @@ dependencies = [
[[package]]
name = "pin-project"
version = "1.1.11"
version = "1.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517"
checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
version = "1.1.11"
version = "1.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6"
checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
dependencies = [
"proc-macro2",
"quote",
@@ -10106,9 +10105,9 @@ dependencies = [
[[package]]
name = "pin-project-lite"
version = "0.2.17"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
[[package]]
name = "pin-utils"
@@ -10160,12 +10159,6 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
name = "plain"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
[[package]]
name = "png"
version = "0.17.16"
@@ -10862,9 +10855,9 @@ dependencies = [
[[package]]
name = "range-alloc"
version = "0.1.5"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca45419789ae5a7899559e9512e58ca889e41f04f1f2445e9f4b290ceccd1d08"
checksum = "c3d6831663a5098ea164f89cff59c6284e95f4e3c76ce9848d4529f5ccca9bde"
[[package]]
name = "raw-cpuid"
@@ -10996,9 +10989,9 @@ dependencies = [
[[package]]
name = "redox_syscall"
version = "0.7.3"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16"
checksum = "35985aa610addc02e24fc232012c86fd11f14111180f902b67e2d5331f8ebf2b"
dependencies = [
"bitflags 2.9.4",
]
@@ -11054,7 +11047,7 @@ dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax 0.8.10",
"regex-syntax 0.8.9",
]
[[package]]
@@ -11065,7 +11058,7 @@ checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax 0.8.10",
"regex-syntax 0.8.9",
]
[[package]]
@@ -11082,9 +11075,9 @@ checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
[[package]]
name = "regex-syntax"
version = "0.8.10"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c"
[[package]]
name = "relative-path"
@@ -11605,14 +11598,14 @@ dependencies = [
[[package]]
name = "rustix"
version = "1.1.4"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34"
dependencies = [
"bitflags 2.9.4",
"errno",
"libc",
"linux-raw-sys 0.12.1",
"linux-raw-sys 0.11.0",
"windows-sys 0.61.2",
]
@@ -12593,9 +12586,9 @@ checksum = "1b6709c7b6754dca1311b3c73e79fcce40dd414c782c66d88e8823030093b02b"
[[package]]
name = "sketches-ddsketch"
version = "0.3.1"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c6f73aeb92d671e0cc4dca167e59b2deb6387c375391bc99ee743f326994a2b"
checksum = "c1e9a774a6c28142ac54bb25d25562e6bcf957493a184f15ad4eebccb23e410a"
dependencies = [
"serde",
]
@@ -13781,7 +13774,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d60769b80ad7953d8a7b2c70cdfe722bbcdcac6bccc8ac934c40c034d866fc18"
dependencies = [
"byteorder",
"regex-syntax 0.8.10",
"regex-syntax 0.8.9",
"utf8-ranges",
]
@@ -13845,14 +13838,14 @@ dependencies = [
[[package]]
name = "tempfile"
version = "3.26.0"
version = "3.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0"
checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1"
dependencies = [
"fastrand",
"getrandom 0.4.1",
"once_cell",
"rustix 1.1.4",
"rustix 1.1.3",
"windows-sys 0.61.2",
]
@@ -13871,7 +13864,7 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0"
dependencies = [
"rustix 1.1.4",
"rustix 1.1.3",
"windows-sys 0.60.2",
]
@@ -14715,7 +14708,7 @@ checksum = "0203df02a3b6dd63575cc1d6e609edc2181c9a11867a271b25cfd2abff3ec5ca"
dependencies = [
"cc",
"regex",
"regex-syntax 0.8.10",
"regex-syntax 0.8.9",
"tree-sitter-language",
]
@@ -15732,7 +15725,7 @@ dependencies = [
[[package]]
name = "windmill"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"anyhow",
"async-nats",
@@ -15796,7 +15789,7 @@ dependencies = [
[[package]]
name = "windmill-alerting"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -15809,7 +15802,7 @@ dependencies = [
[[package]]
name = "windmill-api"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"anyhow",
"argon2",
@@ -15947,7 +15940,7 @@ dependencies = [
[[package]]
name = "windmill-api-agent-workers"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -15970,7 +15963,7 @@ dependencies = [
[[package]]
name = "windmill-api-assets"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -15983,7 +15976,7 @@ dependencies = [
[[package]]
name = "windmill-api-auth"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16009,7 +16002,7 @@ dependencies = [
[[package]]
name = "windmill-api-client"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"reqwest 0.12.28",
"serde",
@@ -16019,7 +16012,7 @@ dependencies = [
[[package]]
name = "windmill-api-configs"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16036,7 +16029,7 @@ dependencies = [
[[package]]
name = "windmill-api-debug"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"axum 0.7.9",
"base64 0.22.1",
@@ -16059,7 +16052,7 @@ dependencies = [
[[package]]
name = "windmill-api-embeddings"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16082,7 +16075,7 @@ dependencies = [
[[package]]
name = "windmill-api-flow-conversations"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16098,7 +16091,7 @@ dependencies = [
[[package]]
name = "windmill-api-flows"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16118,7 +16111,7 @@ dependencies = [
[[package]]
name = "windmill-api-groups"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16138,7 +16131,7 @@ dependencies = [
[[package]]
name = "windmill-api-inputs"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16152,7 +16145,7 @@ dependencies = [
[[package]]
name = "windmill-api-integration-tests"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"anyhow",
"async-nats",
@@ -16174,12 +16167,11 @@ dependencies = [
"windmill-common",
"windmill-native-triggers",
"windmill-test-utils",
"windmill-worker",
]
[[package]]
name = "windmill-api-jobs"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16204,7 +16196,7 @@ dependencies = [
[[package]]
name = "windmill-api-npm-proxy"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"axum 0.7.9",
"flate2",
@@ -16215,14 +16207,13 @@ dependencies = [
"tar",
"tower-http",
"tracing",
"url",
"windmill-api-auth",
"windmill-common",
]
[[package]]
name = "windmill-api-openapi"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16243,7 +16234,7 @@ dependencies = [
[[package]]
name = "windmill-api-schedule"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16263,7 +16254,7 @@ dependencies = [
[[package]]
name = "windmill-api-scripts"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16293,7 +16284,7 @@ dependencies = [
[[package]]
name = "windmill-api-settings"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16320,7 +16311,7 @@ dependencies = [
[[package]]
name = "windmill-api-sse"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"lazy_static",
"serde",
@@ -16332,7 +16323,7 @@ dependencies = [
[[package]]
name = "windmill-api-users"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"argon2",
"axum 0.7.9",
@@ -16355,7 +16346,7 @@ dependencies = [
[[package]]
name = "windmill-api-workers"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16369,7 +16360,7 @@ dependencies = [
[[package]]
name = "windmill-api-workspaces"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16399,7 +16390,7 @@ dependencies = [
[[package]]
name = "windmill-audit"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"chrono",
"lazy_static",
@@ -16413,7 +16404,7 @@ dependencies = [
[[package]]
name = "windmill-autoscaling"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16432,7 +16423,7 @@ dependencies = [
[[package]]
name = "windmill-common"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"aes-gcm",
"anyhow",
@@ -16531,7 +16522,7 @@ dependencies = [
[[package]]
name = "windmill-dep-map"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"chrono",
"itertools 0.14.0",
@@ -16550,7 +16541,7 @@ dependencies = [
[[package]]
name = "windmill-git-sync"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"regex",
"serde",
@@ -16565,7 +16556,7 @@ dependencies = [
[[package]]
name = "windmill-indexer"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"anyhow",
"astral-tokio-tar",
@@ -16589,7 +16580,7 @@ dependencies = [
[[package]]
name = "windmill-jseval"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"anyhow",
"futures",
@@ -16606,7 +16597,7 @@ dependencies = [
[[package]]
name = "windmill-macros"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"itertools 0.14.0",
"lazy_static",
@@ -16622,7 +16613,7 @@ dependencies = [
[[package]]
name = "windmill-mcp"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"anyhow",
"async-trait",
@@ -16643,7 +16634,7 @@ dependencies = [
[[package]]
name = "windmill-native-triggers"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"anyhow",
"async-trait",
@@ -16674,7 +16665,7 @@ dependencies = [
[[package]]
name = "windmill-oauth"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"anyhow",
"async-oauth2",
@@ -16698,7 +16689,7 @@ dependencies = [
[[package]]
name = "windmill-object-store"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"anyhow",
"async-stream",
@@ -16732,7 +16723,7 @@ dependencies = [
[[package]]
name = "windmill-operator"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"anyhow",
"futures",
@@ -16750,7 +16741,7 @@ dependencies = [
[[package]]
name = "windmill-parser"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"convert_case 0.6.0",
"serde",
@@ -16759,7 +16750,7 @@ dependencies = [
[[package]]
name = "windmill-parser-bash"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -16771,7 +16762,7 @@ dependencies = [
[[package]]
name = "windmill-parser-csharp"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"anyhow",
"serde_json",
@@ -16783,7 +16774,7 @@ dependencies = [
[[package]]
name = "windmill-parser-go"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"anyhow",
"gosyn",
@@ -16795,7 +16786,7 @@ dependencies = [
[[package]]
name = "windmill-parser-graphql"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -16807,7 +16798,7 @@ dependencies = [
[[package]]
name = "windmill-parser-java"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"anyhow",
"serde_json",
@@ -16819,7 +16810,7 @@ dependencies = [
[[package]]
name = "windmill-parser-nu"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"anyhow",
"nu-parser",
@@ -16830,7 +16821,7 @@ dependencies = [
[[package]]
name = "windmill-parser-php"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -16841,7 +16832,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -16854,7 +16845,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py-imports"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -16878,7 +16869,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ruby"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -16892,7 +16883,7 @@ dependencies = [
[[package]]
name = "windmill-parser-rust"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"anyhow",
"convert_case 0.6.0",
@@ -16909,7 +16900,7 @@ dependencies = [
[[package]]
name = "windmill-parser-sql"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -16924,7 +16915,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -16943,7 +16934,7 @@ dependencies = [
[[package]]
name = "windmill-parser-yaml"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"anyhow",
"serde",
@@ -16954,7 +16945,7 @@ dependencies = [
[[package]]
name = "windmill-queue"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -16991,7 +16982,7 @@ dependencies = [
[[package]]
name = "windmill-runtime-nativets"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"anyhow",
"const_format",
@@ -17029,9 +17020,8 @@ dependencies = [
[[package]]
name = "windmill-sql-datatype-parser-wasm"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"getrandom 0.3.4",
"wasm-bindgen",
"wasm-bindgen-test",
"windmill-parser",
@@ -17040,7 +17030,7 @@ dependencies = [
[[package]]
name = "windmill-store"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -17069,7 +17059,7 @@ dependencies = [
[[package]]
name = "windmill-test-utils"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -17092,7 +17082,7 @@ dependencies = [
[[package]]
name = "windmill-trigger"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17125,7 +17115,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-email"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17145,7 +17135,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-gcp"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17179,7 +17169,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-http"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17214,7 +17204,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-kafka"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17237,7 +17227,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-mqtt"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17261,7 +17251,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-nats"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"anyhow",
"async-nats",
@@ -17285,7 +17275,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-postgres"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17320,7 +17310,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-sqs"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17348,7 +17338,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-websocket"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17371,7 +17361,7 @@ dependencies = [
[[package]]
name = "windmill-types"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"anyhow",
"bitflags 2.9.4",
@@ -17389,7 +17379,7 @@ dependencies = [
[[package]]
name = "windmill-worker"
version = "1.647.2"
version = "1.642.0"
dependencies = [
"anyhow",
"async-once-cell",
@@ -18262,7 +18252,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
dependencies = [
"libc",
"rustix 1.1.4",
"rustix 1.1.3",
]
[[package]]
@@ -18359,18 +18349,18 @@ dependencies = [
[[package]]
name = "zerocopy"
version = "0.8.40"
version = "0.8.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5"
checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.40"
version = "0.8.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953"
checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517"
dependencies = [
"proc-macro2",
"quote",

View File

@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.647.2"
version = "1.642.0"
authors.workspace = true
edition.workspace = true
@@ -76,7 +76,7 @@ members = [
exclude = ["./windmill-duckdb-ffi-internal"]
[workspace.package]
version = "1.647.2"
version = "1.642.0"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"
@@ -159,12 +159,12 @@ all_languages = ["python", "deno_core", "rust", "mysql", "oracledb", "duckdb", "
# For windows we have another set of languages enabled
all_languages_windows = ["python", "deno_core", "rust", "mysql", "oracledb", "duckdb", "mssql-winauth", "bigquery", "csharp", "nu", "php", "java"]
# Edition meta-features: shared groups
run_inline = ["windmill-api/run_inline"]
inline_preview = ["windmill-api/inline_preview"]
oss_core = [
"embedding", "parquet", "openidconnect", "license",
"http_trigger", "zip", "oauth2", "postgres_trigger",
"mqtt_trigger", "websocket", "smtp", "native_trigger",
"static_frontend", "mcp", "bedrock", "run_inline",
"static_frontend", "mcp", "bedrock", "inline_preview",
"quickjs"
]
ce_core = ["oss_core", "private", "operator"]
@@ -351,7 +351,7 @@ tower-cookies = "^0.10"
serde = "=1.0.219"
serde_json = { version = "^1", features = ["preserve_order", "raw_value"] }
serde_yml = "0.0.12"
uuid = { version = "^1", features = ["serde", "v4", "js"] }
uuid = { version = "^1", features = ["serde", "v4"] }
thiserror = "^2"
anyhow = "^1"
chrono = { version = "^0.4", features = ["serde"] }

View File

@@ -1 +1 @@
8ffae1f43b31dc8136714fa612d22b6301773e27
0fede4b1086bc1456be9cc55b203228c979c5c5e

View File

@@ -1 +0,0 @@
ALTER TABLE resource_type DROP COLUMN is_fileset;

View File

@@ -1 +0,0 @@
ALTER TABLE resource_type ADD COLUMN is_fileset BOOLEAN NOT NULL DEFAULT FALSE;

View File

@@ -1,5 +0,0 @@
INSERT INTO group_ (workspace_id, name, summary, extra_perms)
SELECT id, 'wm_deployers', 'Members can preserve the original author when deploying to this workspace', '{}'::jsonb
FROM workspace
WHERE NOT deleted
ON CONFLICT (workspace_id, name) DO UPDATE SET summary = EXCLUDED.summary;

View File

@@ -1,10 +0,0 @@
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'custom_instance_user') THEN
ALTER ROLE custom_instance_user NOREPLICATION;
END IF;
EXCEPTION
WHEN others THEN
RAISE NOTICE 'Error revoking REPLICATION from custom_instance_user: %', SQLERRM;
END
$$;

View File

@@ -1,10 +0,0 @@
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'custom_instance_user') THEN
ALTER ROLE custom_instance_user REPLICATION;
END IF;
EXCEPTION
WHEN others THEN
RAISE NOTICE 'Error granting REPLICATION to custom_instance_user: %', SQLERRM;
END
$$;

View File

@@ -1,5 +0,0 @@
DROP INDEX IF EXISTS idx_asset_ws_path_kind_recent;
-- Restore the dropped indexes
CREATE INDEX IF NOT EXISTS idx_asset_workspace_created_id ON asset (workspace_id, created_at DESC, id DESC);
CREATE INDEX IF NOT EXISTS idx_asset_kind_path ON asset (workspace_id, kind, path);

View File

@@ -1,11 +0,0 @@
-- Covering index for the list_assets CTE: GROUP BY (path, kind) + MAX(created_at, id) + ORDER BY
-- Includes usage_kind and usage_path to allow full index-only scan (avoiding heap lookups for filter conditions)
CREATE INDEX IF NOT EXISTS idx_asset_ws_path_kind_recent
ON asset (workspace_id, path, kind, created_at DESC, id DESC)
INCLUDE (usage_kind, usage_path);
-- Drop indexes now subsumed by idx_asset_ws_path_kind_recent:
-- idx_asset_workspace_created_id (workspace_id, created_at DESC, id DESC) - only used by list_assets CTE
-- idx_asset_kind_path (workspace_id, kind, path) - only used by list_assets CTE/outer join, covered by new index + PK
DROP INDEX IF EXISTS idx_asset_workspace_created_id;
DROP INDEX IF EXISTS idx_asset_kind_path;

View File

@@ -1 +0,0 @@
DROP INDEX IF EXISTS ix_v2_job_completed_failure_workspace;

View File

@@ -1,7 +0,0 @@
-- Partial index for fast failure/canceled filtering on the runs page.
-- When failures are sparse (<1%) this avoids scanning millions of successful jobs.
-- The query orders by completed_at DESC (switched from created_at when success=false),
-- so this index provides both filtering and ordering in a single scan.
CREATE INDEX IF NOT EXISTS ix_v2_job_completed_failure_workspace
ON v2_job_completed (workspace_id, completed_at DESC)
WHERE status IN ('failure', 'canceled');

View File

@@ -1,4 +1,4 @@
use std::collections::{BTreeMap, HashSet};
use std::collections::BTreeMap;
use sqlparser::{
ast::{
@@ -45,10 +45,6 @@ struct AssetCollector {
var_identifiers: BTreeMap<String, (AssetKind, String)>,
// e.g USE dl;
currently_used_asset: Option<(AssetKind, String)>,
// CTE names in scope (stack for nested queries)
cte_name_stack: Vec<HashSet<String>>,
// Locally created tables (not attached to an asset)
local_table_names: HashSet<String>,
}
impl AssetCollector {
@@ -58,30 +54,9 @@ impl AssetCollector {
current_access_type_stack: Vec::with_capacity(8),
var_identifiers: BTreeMap::new(),
currently_used_asset: None,
cte_name_stack: Vec::new(),
local_table_names: HashSet::new(),
}
}
/// If the name resolves to an attached asset, record it. Otherwise, register it as a local
/// table/view so that subsequent references are not mistakenly attributed to the active asset.
fn track_table_definition(&mut self, name: &ObjectName) {
if let Some(asset) = self.get_associated_asset_from_obj_name(name, Some(W)) {
self.assets.push(asset);
} else if let Some(simple_name) = get_trivial_obj_name(name) {
self.local_table_names.insert(simple_name.to_lowercase());
}
}
fn is_locally_defined(&self, name: &str) -> bool {
let name_lower = name.to_lowercase();
self.local_table_names.contains(&name_lower)
|| self
.cte_name_stack
.iter()
.any(|set| set.contains(&name_lower))
}
// Detect when we do 'a.b' and 'a' is associated with an asset in var_identifiers
// Or when we access 'b' and we did USE a;
fn get_associated_asset_from_obj_name(
@@ -97,14 +72,6 @@ impl AssetCollector {
return None;
}
if name.0.len() == 1 {
if let Some(ident) = name.0.first().and_then(|id| id.as_ident()) {
if self.is_locally_defined(&ident.value) {
return None;
}
}
}
if name.0.len() == 1 || name.0.len() == 2 {
if name
.0
@@ -485,7 +452,6 @@ impl Visitor for AssetCollector {
) -> std::ops::ControlFlow<Self::Break> {
match statement {
sqlparser::ast::Statement::Query(q) => {
self.cte_name_stack.push(collect_cte_names(q));
if let Some(select) = q.body.as_select() {
// First, handle table references (adds table-level assets)
for t in &select.from {
@@ -646,11 +612,17 @@ impl Visitor for AssetCollector {
}
sqlparser::ast::Statement::CreateTable(create_table) => {
self.track_table_definition(&create_table.name);
if let Some(asset) =
self.get_associated_asset_from_obj_name(&create_table.name, Some(W))
{
self.assets.push(asset);
}
}
sqlparser::ast::Statement::CreateView { name, .. } => {
self.track_table_definition(name);
if let Some(asset) = self.get_associated_asset_from_obj_name(name, Some(W)) {
self.assets.push(asset);
}
}
sqlparser::ast::Statement::Copy { target: CopyTarget::File { filename }, .. } => {
@@ -700,20 +672,16 @@ impl Visitor for AssetCollector {
fn post_visit_statement(
&mut self,
statement: &sqlparser::ast::Statement,
_statement: &sqlparser::ast::Statement,
) -> std::ops::ControlFlow<Self::Break> {
if matches!(statement, sqlparser::ast::Statement::Query(_)) {
self.cte_name_stack.pop();
}
std::ops::ControlFlow::Continue(())
}
fn pre_visit_query(
&mut self,
query: &sqlparser::ast::Query,
_query: &sqlparser::ast::Query,
) -> std::ops::ControlFlow<Self::Break> {
self.current_access_type_stack.push(R);
self.cte_name_stack.push(collect_cte_names(query));
std::ops::ControlFlow::Continue(())
}
@@ -722,22 +690,12 @@ impl Visitor for AssetCollector {
_query: &sqlparser::ast::Query,
) -> std::ops::ControlFlow<Self::Break> {
self.current_access_type_stack.pop();
self.cte_name_stack.pop();
std::ops::ControlFlow::Continue(())
}
// We do not use pre_visit_relation because we cannot know if an ObjectName is a table or a function
}
fn collect_cte_names(query: &sqlparser::ast::Query) -> HashSet<String> {
query.with.as_ref().map_or_else(HashSet::new, |with| {
with.cte_tables
.iter()
.map(|cte| cte.alias.name.value.to_lowercase())
.collect()
})
}
fn is_read_fn(fname: &str) -> bool {
fname.eq_ignore_ascii_case("read_parquet")
|| fname.eq_ignore_ascii_case("read_csv")
@@ -1551,235 +1509,6 @@ mod tests {
assert!(result[0].columns.is_none());
}
#[test]
fn test_sql_asset_parser_cte_not_treated_as_asset() {
let input = r#"
ATTACH 'ducklake://my_dl' AS dl;
USE dl;
WITH tmp AS (SELECT 1 AS x)
SELECT * FROM tmp;
SELECT * FROM real_table;
"#;
let s = parse_assets(input).map(|s| s.assets);
assert_eq!(
s.map_err(|e| e.to_string()),
Ok(vec![ParseAssetsResult {
kind: AssetKind::Ducklake,
path: "my_dl/real_table".to_string(),
access_type: Some(R),
columns: None
},])
);
}
#[test]
fn test_sql_asset_parser_cte_scope_does_not_leak() {
let input = r#"
ATTACH 'ducklake://my_dl' AS dl;
USE dl;
WITH tmp AS (SELECT 1) SELECT * FROM tmp;
SELECT * FROM tmp;
"#;
let s = parse_assets(input).map(|s| s.assets);
assert_eq!(
s.map_err(|e| e.to_string()),
Ok(vec![ParseAssetsResult {
kind: AssetKind::Ducklake,
path: "my_dl/tmp".to_string(),
access_type: Some(R),
columns: None
},])
);
}
#[test]
fn test_sql_asset_parser_multiple_ctes() {
let input = r#"
ATTACH 'ducklake://my_dl' AS dl;
USE dl;
WITH cte1 AS (SELECT 1), cte2 AS (SELECT 2)
SELECT * FROM cte1 JOIN cte2 ON true;
SELECT * FROM real_table;
"#;
let s = parse_assets(input).map(|s| s.assets);
assert_eq!(
s.map_err(|e| e.to_string()),
Ok(vec![ParseAssetsResult {
kind: AssetKind::Ducklake,
path: "my_dl/real_table".to_string(),
access_type: Some(R),
columns: None
},])
);
}
#[test]
fn test_sql_asset_parser_local_create_table_overrides_asset() {
let input = r#"
CREATE TABLE local_tbl (id INT);
ATTACH 'ducklake://my_dl' AS dl;
USE dl;
SELECT * FROM local_tbl;
SELECT * FROM asset_table;
"#;
let s = parse_assets(input).map(|s| s.assets);
assert_eq!(
s.map_err(|e| e.to_string()),
Ok(vec![ParseAssetsResult {
kind: AssetKind::Ducklake,
path: "my_dl/asset_table".to_string(),
access_type: Some(R),
columns: None
},])
);
}
#[test]
fn test_sql_asset_parser_create_table_with_use_is_still_asset() {
let input = r#"
ATTACH 'ducklake' AS dl; USE dl;
CREATE TABLE friends (
name text,
age int
);
INSERT INTO friends VALUES ($name, $age);
SELECT * FROM friends;
"#;
let s = parse_assets(input).map(|s| s.assets);
assert_eq!(
s.map_err(|e| e.to_string()),
Ok(vec![ParseAssetsResult {
kind: AssetKind::Ducklake,
path: "main/friends".to_string(),
access_type: Some(RW),
columns: None
},])
);
}
#[test]
fn test_sql_asset_parser_local_create_view_overrides_asset() {
let input = r#"
CREATE VIEW my_view AS SELECT 1;
ATTACH 'ducklake://my_dl' AS dl;
USE dl;
SELECT * FROM my_view;
SELECT * FROM asset_table;
"#;
let s = parse_assets(input).map(|s| s.assets);
assert_eq!(
s.map_err(|e| e.to_string()),
Ok(vec![ParseAssetsResult {
kind: AssetKind::Ducklake,
path: "my_dl/asset_table".to_string(),
access_type: Some(R),
columns: None
},])
);
}
#[test]
fn test_sql_asset_parser_create_view_with_use_is_still_asset() {
let input = r#"
ATTACH 'ducklake://my_dl' AS dl;
USE dl;
CREATE VIEW my_view AS SELECT 1;
SELECT * FROM my_view;
"#;
let s = parse_assets(input).map(|s| s.assets);
assert_eq!(
s.map_err(|e| e.to_string()),
Ok(vec![ParseAssetsResult {
kind: AssetKind::Ducklake,
path: "my_dl/my_view".to_string(),
access_type: Some(RW),
columns: None
},])
);
}
#[test]
fn test_sql_asset_parser_cte_mixed_with_asset_tables() {
let input = r#"
ATTACH 'ducklake://my_dl' AS dl;
USE dl;
WITH tmp AS (SELECT 1 AS x)
SELECT * FROM tmp JOIN real_table ON true;
"#;
let s = parse_assets(input).map(|s| s.assets);
assert_eq!(
s.map_err(|e| e.to_string()),
Ok(vec![ParseAssetsResult {
kind: AssetKind::Ducklake,
path: "my_dl/real_table".to_string(),
access_type: Some(R),
columns: None
},])
);
}
#[test]
fn test_sql_asset_parser_local_table_insert_and_select() {
let input = r#"
CREATE TABLE staging (id INT, val TEXT);
ATTACH 'ducklake://my_dl' AS dl;
USE dl;
INSERT INTO staging VALUES (1, 'a');
SELECT * FROM staging;
INSERT INTO real_table VALUES (2, 'b');
"#;
let s = parse_assets(input).map(|s| s.assets);
assert_eq!(
s.map_err(|e| e.to_string()),
Ok(vec![ParseAssetsResult {
kind: AssetKind::Ducklake,
path: "my_dl/real_table".to_string(),
access_type: Some(W),
columns: None
},])
);
}
#[test]
fn test_sql_asset_parser_qualified_ref_bypasses_local() {
// Even if 'tbl' is local, 'dl.tbl' is an explicit asset reference
let input = r#"
CREATE TABLE tbl (id INT);
ATTACH 'ducklake://my_dl' AS dl;
SELECT * FROM dl.tbl;
"#;
let s = parse_assets(input).map(|s| s.assets);
assert_eq!(
s.map_err(|e| e.to_string()),
Ok(vec![ParseAssetsResult {
kind: AssetKind::Ducklake,
path: "my_dl/tbl".to_string(),
access_type: Some(R),
columns: None
},])
);
}
#[test]
fn test_sql_asset_parser_cte_case_insensitive() {
let input = r#"
ATTACH 'ducklake://my_dl' AS dl;
USE dl;
WITH MyTable AS (SELECT 1)
SELECT * FROM mytable;
"#;
let s = parse_assets(input).map(|s| s.assets);
assert_eq!(
s.map_err(|e| e.to_string()),
Ok(vec![ParseAssetsResult {
kind: AssetKind::Ducklake,
path: "my_dl".to_string(),
access_type: None,
columns: None
},])
);
}
#[test]
fn test_sql_asset_parser_s3_read_csv_columns() {
let input = r#"

View File

@@ -58,23 +58,16 @@ pub fn parse_oracledb_sig(code: &str) -> anyhow::Result<MainArgSignature> {
}
pub fn parse_pgsql_sig(code: &str) -> anyhow::Result<MainArgSignature> {
let (sig, _) = parse_pgsql_sig_with_typed_schema(code)?;
Ok(sig)
}
pub fn parse_pgsql_sig_with_typed_schema(code: &str) -> anyhow::Result<(MainArgSignature, bool)> {
let parsed = parse_pg_file(&code)?;
if let Some((args, typed_schema)) = parsed {
Ok((
MainArgSignature {
star_args: false,
star_kwargs: false,
args,
no_main_func: None,
has_preprocessor: None,
},
typed_schema,
))
if let Some(x) = parsed {
let args = x;
Ok(MainArgSignature {
star_args: false,
star_kwargs: false,
args,
no_main_func: None,
has_preprocessor: None,
})
} else {
Err(anyhow!("Error parsing sql".to_string()))
}
@@ -223,7 +216,7 @@ lazy_static::lazy_static! {
static ref RE_ARG_MYSQL: Regex = Regex::new(r#"(?m)^-- \? (\w+) \((\w+)\)(?: ?\= ?(.+))? *(?:\r|\n|$)"#).unwrap();
pub static ref RE_ARG_MYSQL_NAMED: Regex = Regex::new(r#"(?m)^-- :([a-z_][a-z0-9_]*) \((\w+(?:\([\w, ]+\))?)\)(?: ?\= ?(.+))? *(?:\r|\n|$)"#).unwrap();
static ref RE_ARG_PGSQL: Regex = Regex::new(r#"(?m)^-- \$(\d+) (\w+)(?: \(([A-Za-z0-9_\[\]]+)\))?(?: ?\= ?(.+))? *(?:\r|\n|$)"#).unwrap();
static ref RE_ARG_PGSQL: Regex = Regex::new(r#"(?m)^-- \$(\d+) (\w+)(?: ?\= ?(.+))? *(?:\r|\n|$)"#).unwrap();
// -- @name (type) = default
static ref RE_ARG_BIGQUERY: Regex = Regex::new(r#"(?m)^-- @(\w+) \((\w+(?:\[\])?)\)(?: ?\= ?(.+))? *(?:\r|\n|$)"#).unwrap();
@@ -485,62 +478,21 @@ pub fn parse_pg_statement_arg_indices(code: &str) -> HashSet<i32> {
arg_indices
}
fn parse_pg_file(code: &str) -> anyhow::Result<Option<(Vec<Arg>, bool)>> {
fn parse_pg_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
let mut args = vec![];
// Track which args have explicit types in declaration comments
let mut explicitly_typed_args: HashSet<i32> = HashSet::new();
// First pass: collect args from declaration comments (-- $1 argName (type))
for cap in RE_ARG_PGSQL.captures_iter(code) {
let idx = cap
.get(1)
.and_then(|x| x.as_str().parse::<i32>().ok())
.ok_or_else(|| anyhow!("Impossible to parse arg digit"))?;
let name = cap.get(2).map(|x| x.as_str().to_string()).unwrap();
let explicit_type = cap.get(3).map(|x| x.as_str().to_string().to_lowercase());
let default = cap.get(4).map(|x| x.as_str().to_string());
let has_default = default.is_some();
if let Some(typ) = explicit_type {
// If explicitly typed, use that type and don't infer from usage
explicitly_typed_args.insert(idx);
let parsed_typ = parse_pg_typ(typ.as_str());
let parsed_default = default.and_then(|x| parsed_default(&parsed_typ, x));
args.push(Arg {
name,
typ: parsed_typ,
default: parsed_default,
otyp: Some(typ),
has_default,
oidx: Some(idx),
});
}
}
// Second pass: infer types from usage for non-explicitly-typed args
let mut hm: HashMap<i32, String> = HashMap::new();
for cap in RE_CODE_PGSQL.captures_iter(code) {
let idx = cap
.get(1)
.and_then(|x| x.as_str().parse::<i32>().ok())
.ok_or_else(|| anyhow!("Impossible to parse arg digit"))?;
// Skip if this arg was explicitly typed in declaration
if explicitly_typed_args.contains(&idx) {
continue;
}
let typ = cap
.get(2)
.map(|cap| transform_types_with_spaces(&cap, &code))
.unwrap_or("text");
hm.insert(idx, typ.to_string());
hm.insert(
cap.get(1)
.and_then(|x| x.as_str().parse::<i32>().ok())
.ok_or_else(|| anyhow!("Impossible to parse arg digit"))?,
typ.to_string(),
);
}
// Add inferred args
for (i, v) in hm.iter() {
let typ = v.to_lowercase();
args.push(Arg {
@@ -552,28 +504,19 @@ fn parse_pg_file(code: &str) -> anyhow::Result<Option<(Vec<Arg>, bool)>> {
oidx: Some(*i),
});
}
// Sort by index
args.sort_by(|a, b| a.oidx.unwrap().cmp(&b.oidx.unwrap()));
// Third pass: update names and defaults for inferred args
for cap in RE_ARG_PGSQL.captures_iter(code) {
let i = cap
.get(1)
.and_then(|x| x.as_str().parse::<i32>().ok())
.map(|x| x);
// Skip explicitly typed args (already handled)
if i.is_some_and(|idx| explicitly_typed_args.contains(&idx)) {
continue;
}
if let Some(arg_pos) = args
.iter()
.position(|x| i.is_some_and(|i| x.oidx.unwrap() == i))
{
let name = cap.get(2).map(|x| x.as_str().to_string()).unwrap();
let default = cap.get(4).map(|x| x.as_str().to_string());
let default = cap.get(3).map(|x| x.as_str().to_string());
let has_default = default.is_some();
let oarg = args[arg_pos].clone();
let parsed_default = default.and_then(|x| parsed_default(&oarg.typ, x));
@@ -589,10 +532,8 @@ fn parse_pg_file(code: &str) -> anyhow::Result<Option<(Vec<Arg>, bool)>> {
}
}
let typed_schema = !explicitly_typed_args.is_empty();
args.append(&mut parse_sql_sanitized_interpolation(code));
Ok(Some((args, typed_schema)))
Ok(Some(args))
}
// The regex doesn't parse types with space such as "character varying"
@@ -1365,186 +1306,4 @@ SELECT * FROM table_name WHERE thing = :name4;
Ok(())
}
#[test]
fn test_parse_pgsql_explicit_type_at_declaration() -> anyhow::Result<()> {
let code = r#"
-- $1 user_id (bigint)
-- $2 email
SELECT * FROM users WHERE id = $1 AND email = $2::text;
"#;
assert_eq!(
parse_pgsql_sig(code)?,
MainArgSignature {
star_args: false,
star_kwargs: false,
args: vec![
Arg {
otyp: Some("bigint".to_string()),
name: "user_id".to_string(),
typ: Typ::Int,
default: None,
has_default: false,
oidx: Some(1),
},
Arg {
otyp: Some("text".to_string()),
name: "email".to_string(),
typ: Typ::Str(None),
default: None,
has_default: false,
oidx: Some(2),
},
],
no_main_func: None,
has_preprocessor: None
}
);
Ok(())
}
#[test]
fn test_parse_pgsql_explicit_type_with_default() -> anyhow::Result<()> {
let code = r#"
-- $1 limit (integer) = 10
-- $2 offset (bigint) = 0
SELECT * FROM users LIMIT $1 OFFSET $2;
"#;
assert_eq!(
parse_pgsql_sig(code)?,
MainArgSignature {
star_args: false,
star_kwargs: false,
args: vec![
Arg {
otyp: Some("integer".to_string()),
name: "limit".to_string(),
typ: Typ::Int,
default: Some(json!(10)),
has_default: true,
oidx: Some(1),
},
Arg {
otyp: Some("bigint".to_string()),
name: "offset".to_string(),
typ: Typ::Int,
default: Some(json!(0)),
has_default: true,
oidx: Some(2),
},
],
no_main_func: None,
has_preprocessor: None
}
);
Ok(())
}
#[test]
fn test_parse_pgsql_mixed_explicit_and_inferred() -> anyhow::Result<()> {
let code = r#"
-- $1 user_id (bigint)
-- $2 status
-- $3 created_at (timestamptz)
SELECT * FROM users
WHERE id = $1
AND status = $2::text
AND created_at > $3;
"#;
assert_eq!(
parse_pgsql_sig(code)?,
MainArgSignature {
star_args: false,
star_kwargs: false,
args: vec![
Arg {
otyp: Some("bigint".to_string()),
name: "user_id".to_string(),
typ: Typ::Int,
default: None,
has_default: false,
oidx: Some(1),
},
Arg {
otyp: Some("text".to_string()),
name: "status".to_string(),
typ: Typ::Str(None),
default: None,
has_default: false,
oidx: Some(2),
},
Arg {
otyp: Some("timestamptz".to_string()),
name: "created_at".to_string(),
typ: Typ::Datetime,
default: None,
has_default: false,
oidx: Some(3),
},
],
no_main_func: None,
has_preprocessor: None
}
);
Ok(())
}
#[test]
fn test_parse_pgsql_explicit_type_array() -> anyhow::Result<()> {
let code = r#"
-- $1 ids (bigint[])
SELECT * FROM users WHERE id = ANY($1);
"#;
assert_eq!(
parse_pgsql_sig(code)?,
MainArgSignature {
star_args: false,
star_kwargs: false,
args: vec![Arg {
otyp: Some("bigint[]".to_string()),
name: "ids".to_string(),
typ: Typ::List(Box::new(Typ::Int)),
default: None,
has_default: false,
oidx: Some(1),
},],
no_main_func: None,
has_preprocessor: None
}
);
Ok(())
}
#[test]
fn test_parse_pgsql_explicit_type_does_not_infer_from_usage() -> anyhow::Result<()> {
// Even though $1 is used as ::integer in the query,
// the explicit type (text) should take precedence
let code = r#"
-- $1 value (text)
SELECT $1::integer;
"#;
assert_eq!(
parse_pgsql_sig(code)?,
MainArgSignature {
star_args: false,
star_kwargs: false,
args: vec![Arg {
otyp: Some("text".to_string()),
name: "value".to_string(),
typ: Typ::Str(None),
default: None,
has_default: false,
oidx: Some(1),
},],
no_main_func: None,
has_preprocessor: None
}
);
Ok(())
}
}

View File

@@ -16,7 +16,4 @@ wasm-bindgen-test.workspace = true
[dependencies]
windmill-parser.workspace = true
windmill-parser-sql.workspace = true
wasm-bindgen.workspace = true
# getrandom 0.3 is pulled in transitively by rand 0.9 (via windmill-types).
# It requires the "wasm_js" feature to work on wasm32-unknown-unknown.
getrandom3 = { package = "getrandom", version = "0.3", features = ["wasm_js"] }
wasm-bindgen.workspace = true

View File

@@ -137,7 +137,7 @@ raw_app: path(char), version(int), workspace_id(char), summary(char), edited_at(
FK: (workspace_id) -> workspace(id)
resource: workspace_id(char), path(char), value(jsonb), description(text), resource_type(char), extra_perms(jsonb), edited_at(ts), created_by(char)
FK: (workspace_id) -> workspace(id)
resource_type: workspace_id(char), name(char), schema(jsonb), description(text), edited_at(ts), created_by(char), format_extension(char), is_fileset(bool)
resource_type: workspace_id(char), name(char), schema(jsonb), description(text), edited_at(ts), created_by(char), format_extension(char)
FK: (workspace_id) -> workspace(id)
resume_job: id(uuid), job(uuid), flow(uuid), created_at(ts), value(jsonb), approver(char), resume_id(int), approved(bool)
FK: (flow) -> v2_job_queue(id)

View File

@@ -447,7 +447,6 @@ def main():
deployment_message: None,
visible_to_runner_only: None,
on_behalf_of_email: None,
preserve_on_behalf_of: None,
ws_error_handler_muted: None,
})
.send()
@@ -509,7 +508,6 @@ def main():
policy: None,
deployment_message: None,
custom_path: None,
preserve_on_behalf_of: None,
})
.send()
.await

View File

@@ -1,186 +0,0 @@
-- Fixture for preserve_on_behalf_of integration tests
-- Extends base.sql with a deployer user in the wm_deployers group
-- Include all base setup (workspace, admin user, etc.)
INSERT INTO workspace
(id, name, owner)
VALUES ('test-workspace', 'test-workspace', 'test-user')
ON CONFLICT DO NOTHING;
INSERT INTO usr(workspace_id, email, username, is_admin, role) VALUES
('test-workspace', 'test@windmill.dev', 'test-user', true, 'Admin')
ON CONFLICT DO NOTHING;
INSERT INTO workspace_key(workspace_id, kind, key) VALUES
('test-workspace', 'cloud', 'test-key')
ON CONFLICT DO NOTHING;
INSERT INTO workspace_settings (workspace_id) VALUES
('test-workspace')
ON CONFLICT DO NOTHING;
INSERT INTO group_ (workspace_id, name, summary, extra_perms) VALUES
('test-workspace', 'all', 'All users', '{}')
ON CONFLICT DO NOTHING;
-- Create the wm_deployers group
INSERT INTO group_ (workspace_id, name, summary, extra_perms) VALUES
('test-workspace', 'wm_deployers', 'Users allowed to deploy and preserve on_behalf_of', '{}')
ON CONFLICT DO NOTHING;
INSERT INTO password(email, password_hash, login_type, super_admin, verified, name, username)
VALUES ('test@windmill.dev', 'not-a-real-hash', 'password', true, true, 'Test User', 'test-user')
ON CONFLICT DO NOTHING;
INSERT INTO password(email, password_hash, login_type, super_admin, verified, name)
VALUES ('test2@windmill.dev', 'not-a-real-hash', 'password', false, true, 'Test User 2')
ON CONFLICT DO NOTHING;
-- Deployer user (non-admin but in wm_deployers group)
INSERT INTO password(email, password_hash, login_type, super_admin, verified, name)
VALUES ('deployer@windmill.dev', 'not-a-real-hash', 'password', false, true, 'Deployer User')
ON CONFLICT DO NOTHING;
-- Original user whose on_behalf_of should be preserved
INSERT INTO password(email, password_hash, login_type, super_admin, verified, name)
VALUES ('original@windmill.dev', 'not-a-real-hash', 'password', false, true, 'Original User')
ON CONFLICT DO NOTHING;
INSERT INTO usr(workspace_id, email, username, is_admin, role) VALUES
('test-workspace', 'test2@windmill.dev', 'test-user-2', false, 'User')
ON CONFLICT DO NOTHING;
-- Deployer user in workspace
INSERT INTO usr(workspace_id, email, username, is_admin, role) VALUES
('test-workspace', 'deployer@windmill.dev', 'deployer-user', false, 'User')
ON CONFLICT DO NOTHING;
-- Original user in workspace (whose on_behalf_of should be preserved)
INSERT INTO usr(workspace_id, email, username, is_admin, role) VALUES
('test-workspace', 'original@windmill.dev', 'original-user', false, 'User')
ON CONFLICT DO NOTHING;
-- Add deployer user to wm_deployers group
INSERT INTO usr_to_group(workspace_id, group_, usr) VALUES
('test-workspace', 'wm_deployers', 'deployer-user')
ON CONFLICT DO NOTHING;
-- Tokens for all users
INSERT INTO token(token, email, label, super_admin) VALUES ('SECRET_TOKEN', 'test@windmill.dev', 'test token', true)
ON CONFLICT DO NOTHING;
INSERT INTO token(token, email, label, super_admin) VALUES ('SECRET_TOKEN_2', 'test2@windmill.dev', 'test token 2', false)
ON CONFLICT DO NOTHING;
INSERT INTO token(token, email, label, super_admin) VALUES ('DEPLOYER_TOKEN', 'deployer@windmill.dev', 'deployer token', false)
ON CONFLICT DO NOTHING;
INSERT INTO token(token, email, label, super_admin) VALUES ('ORIGINAL_TOKEN', 'original@windmill.dev', 'original token', false)
ON CONFLICT DO NOTHING;
GRANT ALL PRIVILEGES ON TABLE workspace_key TO windmill_admin;
GRANT ALL PRIVILEGES ON TABLE workspace_key TO windmill_user;
CREATE OR REPLACE FUNCTION "notify_insert_on_completed_job" ()
RETURNS TRIGGER AS $$
BEGIN
PERFORM pg_notify('completed', NEW.id::text);
RETURN NEW;
END;
$$ LANGUAGE PLPGSQL;
DROP TRIGGER IF EXISTS "notify_insert_on_completed_job" ON "v2_job_completed";
CREATE TRIGGER "notify_insert_on_completed_job"
AFTER INSERT ON "v2_job_completed"
FOR EACH ROW
EXECUTE FUNCTION "notify_insert_on_completed_job" ();
CREATE OR REPLACE FUNCTION "notify_queue" ()
RETURNS TRIGGER AS $$
BEGIN
PERFORM pg_notify('queued', NEW.id::text);
RETURN NEW;
END;
$$ LANGUAGE PLPGSQL;
DROP TRIGGER IF EXISTS "notify_queue_after_insert" ON "v2_job_queue";
CREATE TRIGGER "notify_queue_after_insert"
AFTER INSERT ON "v2_job_queue"
FOR EACH ROW
EXECUTE FUNCTION "notify_queue" ();
DROP TRIGGER IF EXISTS "notify_queue_after_flow_status_update" ON "v2_job_status";
CREATE TRIGGER "notify_queue_after_flow_status_update"
AFTER UPDATE ON "v2_job_status"
FOR EACH ROW
WHEN (NEW.flow_status IS DISTINCT FROM OLD.flow_status)
EXECUTE FUNCTION "notify_queue" ();
-- Apply phase 4:
DROP FUNCTION IF EXISTS v2_job_after_update CASCADE;
DROP FUNCTION IF EXISTS v2_job_completed_before_insert CASCADE;
DROP FUNCTION IF EXISTS v2_job_completed_before_update CASCADE;
DROP FUNCTION IF EXISTS v2_job_queue_after_insert CASCADE;
DROP FUNCTION IF EXISTS v2_job_queue_before_insert CASCADE;
DROP FUNCTION IF EXISTS v2_job_queue_before_update CASCADE;
DROP FUNCTION IF EXISTS v2_job_runtime_before_insert CASCADE;
DROP FUNCTION IF EXISTS v2_job_runtime_before_update CASCADE;
DROP FUNCTION IF EXISTS v2_job_status_before_insert CASCADE;
DROP FUNCTION IF EXISTS v2_job_status_before_update CASCADE;
DROP VIEW IF EXISTS completed_job, completed_job_view, job, queue, queue_view CASCADE;
ALTER TABLE v2_job_queue
DROP COLUMN IF EXISTS __parent_job CASCADE,
DROP COLUMN IF EXISTS __created_by CASCADE,
DROP COLUMN IF EXISTS __script_hash CASCADE,
DROP COLUMN IF EXISTS __script_path CASCADE,
DROP COLUMN IF EXISTS __args CASCADE,
DROP COLUMN IF EXISTS __logs CASCADE,
DROP COLUMN IF EXISTS __raw_code CASCADE,
DROP COLUMN IF EXISTS __canceled CASCADE,
DROP COLUMN IF EXISTS __last_ping CASCADE,
DROP COLUMN IF EXISTS __job_kind CASCADE,
DROP COLUMN IF EXISTS __env_id CASCADE,
DROP COLUMN IF EXISTS __schedule_path CASCADE,
DROP COLUMN IF EXISTS __permissioned_as CASCADE,
DROP COLUMN IF EXISTS __flow_status CASCADE,
DROP COLUMN IF EXISTS __raw_flow CASCADE,
DROP COLUMN IF EXISTS __is_flow_step CASCADE,
DROP COLUMN IF EXISTS __language CASCADE,
DROP COLUMN IF EXISTS __same_worker CASCADE,
DROP COLUMN IF EXISTS __raw_lock CASCADE,
DROP COLUMN IF EXISTS __pre_run_error CASCADE,
DROP COLUMN IF EXISTS __email CASCADE,
DROP COLUMN IF EXISTS __visible_to_owner CASCADE,
DROP COLUMN IF EXISTS __mem_peak CASCADE,
DROP COLUMN IF EXISTS __root_job CASCADE,
DROP COLUMN IF EXISTS __leaf_jobs CASCADE,
DROP COLUMN IF EXISTS __concurrent_limit CASCADE,
DROP COLUMN IF EXISTS __concurrency_time_window_s CASCADE,
DROP COLUMN IF EXISTS __timeout CASCADE,
DROP COLUMN IF EXISTS __flow_step_id CASCADE,
DROP COLUMN IF EXISTS __cache_ttl CASCADE;
LOCK TABLE v2_job_queue IN ACCESS EXCLUSIVE MODE;
ALTER TABLE v2_job_completed
DROP COLUMN IF EXISTS __parent_job CASCADE,
DROP COLUMN IF EXISTS __created_by CASCADE,
DROP COLUMN IF EXISTS __created_at CASCADE,
DROP COLUMN IF EXISTS __success CASCADE,
DROP COLUMN IF EXISTS __script_hash CASCADE,
DROP COLUMN IF EXISTS __script_path CASCADE,
DROP COLUMN IF EXISTS __args CASCADE,
DROP COLUMN IF EXISTS __logs CASCADE,
DROP COLUMN IF EXISTS __raw_code CASCADE,
DROP COLUMN IF EXISTS __canceled CASCADE,
DROP COLUMN IF EXISTS __job_kind CASCADE,
DROP COLUMN IF EXISTS __env_id CASCADE,
DROP COLUMN IF EXISTS __schedule_path CASCADE,
DROP COLUMN IF EXISTS __permissioned_as CASCADE,
DROP COLUMN IF EXISTS __raw_flow CASCADE,
DROP COLUMN IF EXISTS __is_flow_step CASCADE,
DROP COLUMN IF EXISTS __language CASCADE,
DROP COLUMN IF EXISTS __is_skipped CASCADE,
DROP COLUMN IF EXISTS __raw_lock CASCADE,
DROP COLUMN IF EXISTS __email CASCADE,
DROP COLUMN IF EXISTS __visible_to_owner CASCADE,
DROP COLUMN IF EXISTS __tag CASCADE,
DROP COLUMN IF EXISTS __priority CASCADE;

File diff suppressed because it is too large Load Diff

View File

@@ -1,306 +0,0 @@
//! Integration tests for workspace protection rulesets.
//!
//! Tests verify that DisableDirectDeployment protection rules correctly
//! block/allow operations based on user permissions.
use serde_json::json;
use sqlx::{Pool, Postgres};
use windmill_common::workspaces::invalidate_protection_rules_cache;
use windmill_test_utils::*;
fn client() -> reqwest::Client {
reqwest::Client::new()
}
fn authed(builder: reqwest::RequestBuilder, token: &str) -> reqwest::RequestBuilder {
builder.header("Authorization", format!("Bearer {}", token))
}
fn new_script(path: &str, summary: &str) -> serde_json::Value {
json!({
"path": path,
"summary": summary,
"description": "",
"content": "export async function main() { return 42; }",
"language": "deno",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {},
"required": []
}
})
}
fn new_flow(path: &str, summary: &str) -> serde_json::Value {
json!({
"path": path,
"summary": summary,
"description": "",
"value": { "modules": [] },
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {},
"required": []
}
})
}
/// Comprehensive test for protection rules functionality.
/// Tests all essential cases in a single test to avoid cache interference.
#[sqlx::test(fixtures("base"))]
async fn test_protection_rules(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
invalidate_protection_rules_cache("test-workspace");
let server = ApiServer::start(db.clone()).await?;
let port = server.addr.port();
let base = format!("http://localhost:{port}/api/w/test-workspace");
// ========================================
// 1. Without protection rule, non-admin can create scripts and flows
// ========================================
let resp = authed(
client().post(format!("{base}/scripts/create")),
"SECRET_TOKEN_2",
)
.json(&new_script("u/test-user-2/script_no_rule", "No rule"))
.send()
.await?;
assert_eq!(
resp.status(),
201,
"Should create script without rule: {}",
resp.text().await?
);
let resp = authed(
client().post(format!("{base}/flows/create")),
"SECRET_TOKEN_2",
)
.json(&new_flow("u/test-user-2/flow_no_rule", "No rule"))
.send()
.await?;
assert_eq!(
resp.status(),
201,
"Should create flow without rule: {}",
resp.text().await?
);
// ========================================
// 2. Non-admin cannot create protection rules
// ========================================
let resp = authed(
client().post(format!("{base}/workspaces/protection_rules")),
"SECRET_TOKEN_2",
)
.json(&json!({
"name": "unauthorized-rule",
"rules": ["DisableDirectDeployment"],
"bypass_users": [],
"bypass_groups": []
}))
.send()
.await?;
assert!(
!resp.status().is_success(),
"Non-admin should not create rules: {}",
resp.status()
);
// ========================================
// 3. Admin creates protection rule
// ========================================
let resp = authed(
client().post(format!("{base}/workspaces/protection_rules")),
"SECRET_TOKEN",
)
.json(&json!({
"name": "test-rule",
"rules": ["DisableDirectDeployment"],
"bypass_users": [],
"bypass_groups": []
}))
.send()
.await?;
assert_eq!(
resp.status(),
200,
"Admin should create rule: {}",
resp.text().await?
);
// ========================================
// 4. With rule, non-admin is blocked from creating scripts/flows
// ========================================
let resp = authed(
client().post(format!("{base}/scripts/create")),
"SECRET_TOKEN_2",
)
.json(&new_script("u/test-user-2/blocked_script", "Blocked"))
.send()
.await?;
assert!(
!resp.status().is_success(),
"Non-admin should be blocked from scripts: {}",
resp.status()
);
let body = resp.text().await?;
assert!(
body.contains("blocked") || body.contains("Blocked"),
"Error should mention blocking: {}",
body
);
let resp = authed(
client().post(format!("{base}/flows/create")),
"SECRET_TOKEN_2",
)
.json(&new_flow("u/test-user-2/blocked_flow", "Blocked"))
.send()
.await?;
assert!(
!resp.status().is_success(),
"Non-admin should be blocked from flows: {}",
resp.status()
);
// ========================================
// 5. Admin bypasses protection rule
// ========================================
let resp = authed(
client().post(format!("{base}/scripts/create")),
"SECRET_TOKEN",
)
.json(&new_script("u/test-user/admin_script", "Admin"))
.send()
.await?;
assert_eq!(
resp.status(),
201,
"Admin should bypass rule: {}",
resp.text().await?
);
// ========================================
// 6. Update rule to bypass test-user-2
// ========================================
let resp = authed(
client().post(format!("{base}/workspaces/protection_rules/test-rule")),
"SECRET_TOKEN",
)
.json(&json!({
"rules": ["DisableDirectDeployment"],
"bypass_users": ["test-user-2"],
"bypass_groups": []
}))
.send()
.await?;
assert_eq!(
resp.status(),
200,
"Should update rule: {}",
resp.text().await?
);
// Invalidate cache to pick up the update
invalidate_protection_rules_cache("test-workspace");
// ========================================
// 7. Bypassed user can now create
// ========================================
let resp = authed(
client().post(format!("{base}/scripts/create")),
"SECRET_TOKEN_2",
)
.json(&new_script("u/test-user-2/bypassed_script", "Bypassed"))
.send()
.await?;
assert_eq!(
resp.status(),
201,
"Bypassed user should create: {}",
resp.text().await?
);
// ========================================
// 8. Non-bypassed user (test-user-3) is still blocked
// ========================================
let resp = authed(
client().post(format!("{base}/scripts/create")),
"SECRET_TOKEN_3",
)
.json(&new_script("u/test-user-3/still_blocked", "Blocked"))
.send()
.await?;
assert!(
!resp.status().is_success(),
"Non-bypassed user should be blocked: {}",
resp.status()
);
// ========================================
// 9. Delete rule
// ========================================
let resp = authed(
client().delete(format!("{base}/workspaces/protection_rules/test-rule")),
"SECRET_TOKEN",
)
.send()
.await?;
assert_eq!(
resp.status(),
200,
"Should delete rule: {}",
resp.text().await?
);
// Invalidate cache to pick up the deletion
invalidate_protection_rules_cache("test-workspace");
// ========================================
// 10. After deletion, non-admin can create again
// ========================================
let resp = authed(
client().post(format!("{base}/scripts/create")),
"SECRET_TOKEN_3",
)
.json(&new_script("u/test-user-3/after_delete", "After delete"))
.send()
.await?;
assert_eq!(
resp.status(),
201,
"Should create after rule deletion: {}",
resp.text().await?
);
// ========================================
// 11. Verify rule list is empty
// ========================================
let resp = authed(
client().get(format!("{base}/workspaces/protection_rules")),
"SECRET_TOKEN",
)
.send()
.await?;
assert_eq!(resp.status(), 200);
let rules: Vec<serde_json::Value> = resp.json().await?;
assert!(rules.is_empty(), "Should have no rules after deletion");
Ok(())
}

View File

@@ -10,7 +10,6 @@ use windmill_common::{
assets::{AssetKind, AssetUsageKind},
db::UserDB,
error::JsonResult,
utils::escape_ilike_pattern,
};
use windmill_api_auth::ApiAuthed;
@@ -28,14 +27,9 @@ struct ListAssetsQuery {
per_page: i64,
cursor_created_at: Option<chrono::DateTime<chrono::Utc>>,
cursor_id: Option<i64>,
pub asset_path: Option<String>,
pub usage_path: Option<String>,
pub asset_kinds: Option<String>,
// Exact path match filter
pub path: Option<String>,
// Filter by matching a subset of the columns using base64 encoded json subset
pub columns: Option<String>,
pub broad_filter: Option<String>,
asset_path: Option<String>,
usage_path: Option<String>,
asset_kinds: Option<String>,
}
fn default_per_page() -> i64 {
@@ -81,24 +75,12 @@ async fn list_assets(
let mut param_count = 2; // $1 = workspace_id, $2 = limit
// Asset path filter (ILIKE pattern match)
// Asset path filter
if query.asset_path.is_some() {
param_count += 1;
asset_summary_filters.push(format!("asset.path ILIKE ${}", param_count));
}
// Exact path filter
if query.path.is_some() {
param_count += 1;
asset_summary_filters.push(format!("asset.path = ${}", param_count));
}
// Columns filter (check if JSONB has all specified keys)
if query.columns.is_some() {
param_count += 1;
asset_summary_filters.push(format!("asset.columns ?& ${}", param_count));
}
// Usage path filter - for jobs, also check runnable_path
let needs_job_join_in_cte = query.usage_path.is_some();
if query.usage_path.is_some() {
@@ -130,14 +112,6 @@ async fn list_assets(
asset_summary_filters.push(format!("asset.kind = ANY(${})", param_count));
}
if query.broad_filter.is_some() {
param_count += 1;
asset_summary_filters.push(format!(
"(asset.path ILIKE ${p} OR asset.kind::text ILIKE ${p})",
p = param_count
));
}
let asset_summary_where = asset_summary_filters.join(" AND ");
// Build cursor condition
@@ -154,7 +128,7 @@ async fn list_assets(
format!(
r#"FROM asset
LEFT JOIN v2_job job_cte ON asset.usage_kind = 'job'
AND job_cte.id = CASE WHEN asset.usage_kind = 'job' THEN asset.usage_path::uuid END
AND asset.usage_path = job_cte.id::text
AND job_cte.workspace_id = $1"#
)
} else {
@@ -219,7 +193,7 @@ async fn list_assets(
) = resource.path
AND resource.workspace_id = $1
LEFT JOIN v2_job job ON asset.usage_kind = 'job'
AND job.id = CASE WHEN asset.usage_kind = 'job' THEN asset.usage_path::uuid END
AND asset.usage_path = job.id::text
AND job.workspace_id = $1
WHERE asset.workspace_id = $1
AND (asset.kind <> 'resource' OR resource.path IS NOT NULL)
@@ -234,25 +208,11 @@ async fn list_assets(
let mut query_builder = sqlx::query(&sql).bind(&w_id).bind(limit);
if let Some(ref asset_path) = query.asset_path {
query_builder = query_builder.bind(format!("%{}%", escape_ilike_pattern(asset_path)));
}
if let Some(ref path) = query.path {
query_builder = query_builder.bind(path);
}
if let Some(ref columns) = query.columns {
// Columns is a comma-separated string, split into array for ?& operator
let columns_array: Vec<String> = columns
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
query_builder = query_builder.bind(columns_array);
query_builder = query_builder.bind(format!("%{}%", asset_path));
}
if let Some(ref usage_path) = query.usage_path {
query_builder = query_builder.bind(format!("%{}%", escape_ilike_pattern(usage_path)));
query_builder = query_builder.bind(format!("%{}%", usage_path));
}
if let Some(ref asset_kinds) = asset_kinds {
@@ -261,10 +221,6 @@ async fn list_assets(
}
}
if let Some(ref broad_filter) = query.broad_filter {
query_builder = query_builder.bind(format!("%{}%", escape_ilike_pattern(broad_filter)));
}
if let (Some(cursor_created_at), Some(cursor_id)) = (query.cursor_created_at, query.cursor_id) {
query_builder = query_builder.bind(cursor_created_at).bind(cursor_id);
}

View File

@@ -534,13 +534,10 @@ pub async fn create_token_internal(
));
}
}
let rows = sqlx::query!(
sqlx::query!(
"INSERT INTO token
(token, email, label, expiration, super_admin, scopes, workspace_id)
SELECT $1, $2, $3, $4, $5, $6, $7
WHERE $7::varchar IS NULL OR NOT EXISTS(
SELECT 1 FROM workspace WHERE id = $7 AND deleted = true
)",
VALUES ($1, $2, $3, $4, $5, $6, $7)",
token,
authed.email,
token_config.label,
@@ -551,11 +548,6 @@ pub async fn create_token_internal(
)
.execute(&mut *tx)
.await?;
if rows.rows_affected() == 0 {
return Err(Error::BadRequest(
"Cannot create a token for an archived workspace".to_string(),
));
}
audit_log(
&mut *tx,

View File

@@ -13,7 +13,7 @@ default = []
enterprise = ["dep:windmill-autoscaling"]
private = []
python = []
run_inline = ["dep:windmill-worker", "dep:itertools"]
inline_preview = ["dep:windmill-worker", "dep:itertools"]
[dependencies]
windmill-api-auth.workspace = true

View File

@@ -283,14 +283,14 @@ async fn native_kubernetes_autoscaling_healthcheck() -> Result<(), error::Error>
}
async fn list_available_python_versions() -> error::JsonResult<Vec<String>> {
#[cfg(not(all(feature = "python", feature = "run_inline")))]
#[cfg(not(all(feature = "python", feature = "inline_preview")))]
return Err(error::Error::BadRequest(
"Python listing available only with 'python' feature enabled".to_string(),
));
#[cfg(all(feature = "python", feature = "run_inline"))]
#[cfg(all(feature = "python", feature = "inline_preview"))]
use itertools::Itertools;
#[cfg(all(feature = "python", feature = "run_inline"))]
#[cfg(all(feature = "python", feature = "inline_preview"))]
return Ok(Json(
windmill_worker::PyV::list_available_python_versions()
.await

View File

@@ -503,11 +503,7 @@ async fn create_flow(
nf.tag,
nf.dedicated_worker,
nf.visible_to_runner_only.unwrap_or(false),
windmill_common::resolve_on_behalf_of_email(
nf.on_behalf_of_email.as_deref(),
nf.preserve_on_behalf_of.unwrap_or(false),
&authed,
),
nf.on_behalf_of_email.and(Some(&authed.email)),
nf.ws_error_handler_muted.unwrap_or(false),
sqlx::types::Json(&nf.value) as _,
schema_str,
@@ -517,7 +513,7 @@ async fn create_flow(
.await?;
let version = sqlx::query_scalar!(
"INSERT INTO flow_version (workspace_id, path, value, schema, created_by)
"INSERT INTO flow_version (workspace_id, path, value, schema, created_by)
VALUES ($1, $2, $3, $4::text::json, $5)
RETURNING id",
w_id,
@@ -559,29 +555,6 @@ async fn create_flow(
),
)
.await?;
if let Some(on_behalf_of) = windmill_common::check_on_behalf_of_preservation(
nf.on_behalf_of_email.as_deref(),
nf.preserve_on_behalf_of.unwrap_or(false),
&authed,
&authed.email,
) {
audit_log(
&mut *tx,
&authed,
"flows.on_behalf_of",
ActionKind::Create,
&w_id,
Some(&nf.path),
Some(
[
("on_behalf_of", on_behalf_of.as_str()),
("action", "create"),
]
.into(),
),
)
.await?;
}
let mut args: HashMap<String, Box<serde_json::value::RawValue>> = HashMap::new();
if let Some(dm) = nf.deployment_message {
@@ -967,11 +940,7 @@ async fn update_flow(
nf.tag,
nf.dedicated_worker,
nf.visible_to_runner_only.unwrap_or(false),
windmill_common::resolve_on_behalf_of_email(
nf.on_behalf_of_email.as_deref(),
nf.preserve_on_behalf_of.unwrap_or(false),
&authed,
),
nf.on_behalf_of_email.and(Some(&authed.email)),
nf.ws_error_handler_muted.unwrap_or(false),
sqlx::types::Json(&nf.value) as _,
schema_str,
@@ -1134,29 +1103,6 @@ async fn update_flow(
),
)
.await?;
if let Some(on_behalf_of) = windmill_common::check_on_behalf_of_preservation(
nf.on_behalf_of_email.as_deref(),
nf.preserve_on_behalf_of.unwrap_or(false),
&authed,
&authed.email,
) {
audit_log(
&mut *tx,
&authed,
"flows.on_behalf_of",
ActionKind::Update,
&w_id,
Some(&nf.path),
Some(
[
("on_behalf_of", on_behalf_of.as_str()),
("action", "update"),
]
.into(),
),
)
.await?;
}
webhook.send_message(
w_id.clone(),

View File

@@ -14,7 +14,6 @@ private = ["windmill-test-utils/private", "dep:aws-config", "dep:aws-credential-
enterprise = ["windmill-test-utils/enterprise", "dep:base64"]
deno_core = ["windmill-test-utils/deno_core"]
mcp = []
run_inline = ["dep:windmill-worker", "windmill-test-utils/run_inline", "windmill-test-utils/duckdb"]
[dependencies]
windmill-test-utils.workspace = true
@@ -22,7 +21,6 @@ windmill-api-client.workspace = true
windmill-common = { workspace = true, default-features = false }
windmill-native-triggers = { workspace = true, features = ["native_trigger"] }
windmill-api-auth.workspace = true
windmill-worker = { workspace = true, optional = true }
sqlx.workspace = true
serde_json.workspace = true
serde.workspace = true

View File

@@ -54,21 +54,6 @@ INSERT INTO resource (workspace_id, path, value, description, resource_type, ext
VALUES ('test-workspace', 'u/test-user/scalar_var_resource', '"$var:u/test-user/db_password"',
'Scalar var ref', 'string', '{}', 'test-user');
-- === fileset resource type test data ===
INSERT INTO resource_type (workspace_id, name, schema, description, created_by, is_fileset)
VALUES ('test-workspace', 'test_fileset', '{}',
'Test fileset type', 'test-user', true);
INSERT INTO resource_type (workspace_id, name, schema, description, created_by, format_extension)
VALUES ('test-workspace', 'test_file', '{"type": "object", "properties": {"content": {"type": "string"}}}',
'Test file type', 'test-user', 'txt');
INSERT INTO resource (workspace_id, path, value, description, resource_type, extra_perms, created_by)
VALUES ('test-workspace', 'u/test-user/fileset_resource',
'{"config.yaml": "key: value", "data/input.json": "{\"items\": []}"}',
'A fileset resource', 'test_fileset', '{}', 'test-user');
-- === mcp_tools test data ===
INSERT INTO resource (workspace_id, path, value, description, resource_type, extra_perms, created_by)

View File

@@ -69,12 +69,8 @@ async fn test_resource_endpoints(db: Pool<Postgres>) -> anyhow::Result<()> {
assert_eq!(resp.status(), 404);
// --- get_value_interpolated ---
let resp = authed_get(
port,
"get_value_interpolated",
"u/test-user/simple_resource",
)
.await;
let resp =
authed_get(port, "get_value_interpolated", "u/test-user/simple_resource").await;
assert_eq!(resp.status(), 200);
assert_eq!(
resp.json::<serde_json::Value>().await?,
@@ -82,12 +78,8 @@ async fn test_resource_endpoints(db: Pool<Postgres>) -> anyhow::Result<()> {
);
// $var: interpolation
let resp = authed_get(
port,
"get_value_interpolated",
"u/test-user/resource_with_var",
)
.await;
let resp =
authed_get(port, "get_value_interpolated", "u/test-user/resource_with_var").await;
assert_eq!(resp.status(), 200);
assert_eq!(
resp.json::<serde_json::Value>().await?,
@@ -95,12 +87,8 @@ async fn test_resource_endpoints(db: Pool<Postgres>) -> anyhow::Result<()> {
);
// $res: interpolation
let resp = authed_get(
port,
"get_value_interpolated",
"u/test-user/resource_with_res",
)
.await;
let resp =
authed_get(port, "get_value_interpolated", "u/test-user/resource_with_res").await;
assert_eq!(resp.status(), 200);
assert_eq!(
resp.json::<serde_json::Value>().await?,
@@ -108,7 +96,8 @@ async fn test_resource_endpoints(db: Pool<Postgres>) -> anyhow::Result<()> {
);
// mixed $var: and $res: refs
let resp = authed_get(port, "get_value_interpolated", "u/test-user/resource_mixed").await;
let resp =
authed_get(port, "get_value_interpolated", "u/test-user/resource_mixed").await;
assert_eq!(resp.status(), 200);
assert_eq!(
resp.json::<serde_json::Value>().await?,
@@ -116,12 +105,8 @@ async fn test_resource_endpoints(db: Pool<Postgres>) -> anyhow::Result<()> {
);
// chained $res: -> $var:
let resp = authed_get(
port,
"get_value_interpolated",
"u/test-user/chained_resource",
)
.await;
let resp =
authed_get(port, "get_value_interpolated", "u/test-user/chained_resource").await;
assert_eq!(resp.status(), 200);
assert_eq!(
resp.json::<serde_json::Value>().await?,
@@ -129,7 +114,8 @@ async fn test_resource_endpoints(db: Pool<Postgres>) -> anyhow::Result<()> {
);
// null value
let resp = authed_get(port, "get_value_interpolated", "u/test-user/null_resource").await;
let resp =
authed_get(port, "get_value_interpolated", "u/test-user/null_resource").await;
assert_eq!(resp.status(), 200);
assert_eq!(
resp.json::<serde_json::Value>().await?,
@@ -137,7 +123,8 @@ async fn test_resource_endpoints(db: Pool<Postgres>) -> anyhow::Result<()> {
);
// not found
let resp = authed_get(port, "get_value_interpolated", "u/test-user/nonexistent").await;
let resp =
authed_get(port, "get_value_interpolated", "u/test-user/nonexistent").await;
assert_eq!(resp.status(), 404);
// array passthrough
@@ -175,9 +162,7 @@ async fn test_resource_endpoints(db: Pool<Postgres>) -> anyhow::Result<()> {
"expected at least 10 resources from fixture, got {}",
list.len()
);
assert!(list
.iter()
.any(|r| r["path"] == "u/test-user/simple_resource"));
assert!(list.iter().any(|r| r["path"] == "u/test-user/simple_resource"));
// list with resource_type filter
let resp = authed(client().get(format!("{base}/list?resource_type=mcp_server")))
@@ -274,11 +259,9 @@ async fn test_resource_endpoints(db: Pool<Postgres>) -> anyhow::Result<()> {
assert_eq!(body["description"], "Updated description");
// --- update_value ---
let resp = authed(client().post(resource_url(
port,
"update_value",
"u/test-user/new_resource",
)))
let resp = authed(
client().post(resource_url(port, "update_value", "u/test-user/new_resource")),
)
.json(&json!({"value": {"url": "https://final.com"}}))
.send()
.await
@@ -292,44 +275,35 @@ async fn test_resource_endpoints(db: Pool<Postgres>) -> anyhow::Result<()> {
);
// --- delete ---
let resp = authed(client().delete(resource_url(port, "delete", "u/test-user/new_resource")))
.send()
.await
.unwrap();
let resp = authed(
client().delete(resource_url(port, "delete", "u/test-user/new_resource")),
)
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
let resp = authed_get(port, "exists", "u/test-user/new_resource").await;
assert_eq!(resp.json::<bool>().await?, false);
// delete nonexistent -> 404
let resp = authed(client().delete(resource_url(port, "delete", "u/test-user/new_resource")))
.send()
.await
.unwrap();
let resp = authed(
client().delete(resource_url(port, "delete", "u/test-user/new_resource")),
)
.send()
.await
.unwrap();
assert_eq!(resp.status(), 404);
// --- file_resource_type_to_file_ext_map ---
let resp = authed(client().get(format!("{base}/file_resource_type_to_file_ext_map")))
.send()
.await
.unwrap();
let resp = authed(client().get(format!(
"{base}/file_resource_type_to_file_ext_map"
)))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
let ext_map = resp.json::<serde_json::Value>().await?;
// Verify the map includes fileset type info with is_fileset flag (no format_extension)
let fileset_info = &ext_map["test_fileset"];
assert_eq!(fileset_info["format_extension"], serde_json::Value::Null);
assert_eq!(fileset_info["is_fileset"], true);
// Verify non-fileset file type
let file_info = &ext_map["test_file"];
assert_eq!(file_info["format_extension"], "txt");
assert_eq!(file_info["is_fileset"], false);
// --- fileset resource value ---
let resp = authed_get(port, "get_value", "u/test-user/fileset_resource").await;
assert_eq!(resp.status(), 200);
let fileset_val = resp.json::<serde_json::Value>().await?;
assert_eq!(fileset_val["config.yaml"], "key: value");
assert_eq!(fileset_val["data/input.json"], "{\"items\": []}");
resp.json::<serde_json::Value>().await?;
// --- resource types ---
@@ -410,68 +384,17 @@ async fn test_resource_endpoints(db: Pool<Postgres>) -> anyhow::Result<()> {
assert_eq!(body["description"], "Updated type desc");
// type/delete
let resp = authed(client().delete(resource_url(port, "type/delete", "new_test_type")))
.send()
.await
.unwrap();
let resp = authed(
client().delete(resource_url(port, "type/delete", "new_test_type")),
)
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
let resp = authed_get(port, "type/exists", "new_test_type").await;
assert_eq!(resp.json::<bool>().await?, false);
// --- fileset resource type CRUD ---
// type/get for fileset type - verify is_fileset is returned
let resp = authed_get(port, "type/get", "test_fileset").await;
assert_eq!(resp.status(), 200);
let body = resp.json::<serde_json::Value>().await?;
assert_eq!(body["name"], "test_fileset");
assert_eq!(body["is_fileset"], true);
assert_eq!(body["format_extension"], serde_json::Value::Null);
// type/get for non-fileset type - verify is_fileset is false
let resp = authed_get(port, "type/get", "test_db").await;
assert_eq!(resp.status(), 200);
let body = resp.json::<serde_json::Value>().await?;
assert_eq!(body["is_fileset"], false);
// type/create fileset type (no format_extension needed)
let resp = authed(client().post(format!("{base}/type/create")))
.json(&json!({
"name": "new_fileset_type",
"description": "A fileset type",
"schema": {},
"is_fileset": true
}))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 201);
let resp = authed_get(port, "type/get", "new_fileset_type").await;
let body = resp.json::<serde_json::Value>().await?;
assert_eq!(body["is_fileset"], true);
assert_eq!(body["format_extension"], serde_json::Value::Null);
// type/update - set is_fileset on existing type
let resp = authed(client().post(resource_url(port, "type/update", "new_fileset_type")))
.json(&json!({"is_fileset": false}))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
let resp = authed_get(port, "type/get", "new_fileset_type").await;
let body = resp.json::<serde_json::Value>().await?;
assert_eq!(body["is_fileset"], false);
// cleanup
let resp = authed(client().delete(resource_url(port, "type/delete", "new_fileset_type")))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
Ok(())
}

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