Compare commits

..

20 Commits

Author SHA1 Message Date
Guilhem
fa22ec69d8 fix: only show email validation error after dropdown closes
Add onClose callback to AutocompleteSelect and use it in AddUser
to defer validation until the user finishes interacting with the
dropdown, avoiding visual noise between the dropdown and error.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:33:22 +00:00
Guilhem
da460fa6b4 nit 2026-02-17 09:27:57 +00:00
Guilhem
f03bd04cac fix: use AutocompleteSelect loading prop instead of separate branch
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 08:59:01 +00:00
Guilhem
e11a333998 fix: address PR review comments
- Add LIMIT 1000 to list_instance_emails SQL query
- Add loading state to prevent UI flash in AddUser
- Show email validation error only after blur
- Guard loadInstanceEmails with workspaceStore check

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 08:20:14 +00:00
Guilhem
2ff72e34dd Merge remote-tracking branch 'origin/main' into glm/select-users 2026-02-16 18:38:22 +00:00
Guilhem
732aff1d5c Merge branch 'main' into claude/issue-7902-20260211-1113 2026-02-16 16:32:49 +00:00
Guilhem
13ff7f418e Merge branch 'main' into claude/issue-7902-20260211-1113 2026-02-16 15:50:33 +00:00
windmill-internal-app[bot]
d89d36728b chore: update ee-repo-ref to c927593b0b49867284fc64d50c59daba6c70da84
This commit updates the EE repository reference after PR #415 was merged in windmill-ee-private.

Previous ee-repo-ref: 8c214ec5039be5353f5fea920e27b0c6af61e1fd

New ee-repo-ref: c927593b0b49867284fc64d50c59daba6c70da84

Automated by sync-ee-ref workflow.
2026-02-16 15:48:08 +00:00
Guilhem
47c027716b chore: remove test_dev select page
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 15:39:18 +00:00
Guilhem
9347a04ed5 style: format test_dev select page
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 15:38:41 +00:00
Guilhem
360b43caba nit 2026-02-16 15:38:22 +00:00
Guilhem
1dd2b0a1b2 refactor: extract AutocompleteSelect from Select component
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 15:31:22 +00:00
Guilhem
19f30ee6d5 Merge remote-tracking branch 'origin/main' into glm/select-users 2026-02-16 15:01:41 +00:00
Guilhem
90139c7754 Merge remote-tracking branch 'origin/main' into glm/select-users 2026-02-16 14:51:51 +00:00
Guilhem
4aa1f01210 refactor: replace Select placeholder hack with autocomplete model
Replace the bind:value getter/setter and placeholder-as-display hack with
a standard controlled input pattern (value={inputValue} + oninput handler).

- Remove showPlaceholderOnOpen prop (replaced by default behavior)
- Add allowUserInput prop: typed text syncs to value in real-time
- Separate rawLabel (for filtering) from displayText (for transformed display)
- Hide dropdown when no items match in allowUserInput mode
- Simplify AddUser to use allowUserInput instead of createText/onCreateItem

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 14:51:06 +00:00
Guilhem
bfb65fc8b4 fix: allow editing custom email in Select component
When a custom value (not in the items list) is selected, reopening
the dropdown now pre-fills the filter with the current value so users
can directly edit it. Also hide the "Use custom email" create option
when the filter matches the current value, and show the regular
placeholder instead of the value when editing a custom entry.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 11:52:54 +00:00
Guilhem
b54bea7192 feat: add email validation and use TextInput in AddUser
Add client-side email format validation with error message display
and disable the Add button when email is invalid. Replace plain
<input> elements with TextInput component for consistent styling.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 11:32:31 +00:00
Guilhem
793bb7b405 fix: restrict list_instance_emails to workspace admins
Move the endpoint from global to workspace-scoped route
(/w/{workspace}/users/list_instance_emails) and add require_admin
check so only workspace admins can list instance emails.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 10:54:06 +00:00
Guilhem
fb0f0031d9 fix: filter out existing workspace members from instance emails select
Also fixes "Add new userss" typo.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 10:25:15 +00:00
claude[bot]
077eb91c0f feat: add list instance emails endpoint and use Select in AddUser
Add a new GET /users/list_instance_emails endpoint that returns all
instance user emails (from the password table). This endpoint is only
available on non-cloud instances.

In the frontend AddUser component, when the instance emails are
available (non-cloud), replace the email text input with a Select
component that allows both selecting from existing instance users and
typing custom emails.

Closes #7902

Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2026-02-11 11:24:13 +00:00
463 changed files with 10669 additions and 27966 deletions

View File

@@ -0,0 +1,30 @@
#!/usr/bin/env bash
# Resolve _ee.rs symlinks to actual files so Claude can read them
# This script runs before each user prompt is processed
set -e
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-/home/farhad/windmill}"
MANIFEST_FILE="$PROJECT_DIR/.claude/hooks/.symlink-manifest"
# Find all _ee.rs symlinks and store their targets
find "$PROJECT_DIR" -name "*_ee.rs" -type l 2>/dev/null | while read -r symlink; do
target=$(readlink -f "$symlink" 2>/dev/null) || continue
# Only process if target file exists
if [[ -f "$target" ]]; then
# Store symlink path and target in manifest
echo "$symlink|$target" >> "$MANIFEST_FILE.tmp"
# Replace symlink with actual file content
rm "$symlink"
cp "$target" "$symlink"
fi
done
# Atomically replace manifest
if [[ -f "$MANIFEST_FILE.tmp" ]]; then
mv "$MANIFEST_FILE.tmp" "$MANIFEST_FILE"
fi
exit 0

View File

@@ -0,0 +1,36 @@
#!/usr/bin/env bash
# Restore _ee.rs symlinks after Claude finishes processing
# This script runs when Claude stops
# IMPORTANT: Copies any modifications back to the target before restoring symlinks
set -e
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-/home/farhad/windmill}"
MANIFEST_FILE="$PROJECT_DIR/.claude/hooks/.symlink-manifest"
# Check if manifest exists
if [[ ! -f "$MANIFEST_FILE" ]]; then
exit 0
fi
# Read manifest and restore symlinks
while IFS='|' read -r symlink target; do
if [[ -n "$symlink" && -n "$target" ]]; then
# If the file exists (not a symlink) and target exists, copy changes back
if [[ -f "$symlink" && ! -L "$symlink" && -e "$target" ]]; then
# Copy the potentially modified file back to the target
cp "$symlink" "$target"
fi
# Remove the regular file (which was a copy)
rm -f "$symlink" 2>/dev/null || true
# Recreate the symlink
ln -s "$target" "$symlink" 2>/dev/null || true
fi
done < "$MANIFEST_FILE"
# Clean up manifest
rm -f "$MANIFEST_FILE"
exit 0

View File

@@ -1,8 +1,5 @@
{
"permissions": {
"additionalDirectories": [
"../windmill-ee-private"
],
"allow": [
"Bash(ls:*)",
"Bash(grep:*)",
@@ -66,6 +63,39 @@
},
"enableAllProjectMcpServers": true,
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/resolve-symlinks.sh",
"timeout": 30
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/restore-symlinks.sh",
"timeout": 30
}
]
}
],
"SessionEnd": [
{
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/restore-symlinks.sh",
"timeout": 30
}
]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write",

View File

@@ -581,28 +581,7 @@ In `frontend/src/lib/components/graph/renderers/triggers/TriggersBadge.svelte`:
2. Add to `baseConfig` with `countKey` (the dynamic `availableNativeServices` loop does NOT set `countKey`)
3. Add to the `allTypes` array
### Step 14: Update TriggersWrapper.svelte
In `frontend/src/lib/components/triggers/TriggersWrapper.svelte`:
Add a `{:else if selectedTrigger.type === 'yourservice'}` case that renders `<NativeTriggersPanel service="yourservice" ...>` with the same props pattern as the existing native trigger cases (e.g., `nextcloud`).
### Step 15: Update AddTriggersButton.svelte
In `frontend/src/lib/components/triggers/AddTriggersButton.svelte`:
1. Add `yourserviceAvailable` state variable
2. Add `setYourserviceState()` async function using `isServiceAvailable('yourservice', $workspaceStore!)`
3. Call it at module level
4. Add a dropdown entry to `addTriggerItems` with `hidden: !yourserviceAvailable`
### Step 16: Update TriggersEditor.svelte Delete Handling
In `frontend/src/lib/components/triggers/TriggersEditor.svelte`:
Add your service to the `nativeTriggerServices` map in `deleteDeployedTrigger()`. Native triggers use `NativeTriggerService.deleteNativeTrigger({ workspace, serviceName, externalId })` instead of the standard `path`-based delete.
### Step 17: Update OpenAPI Spec and Regenerate Types
### Step 14: Update OpenAPI Spec and Regenerate Types
Add to `JobTriggerKind` enum in `backend/windmill-api/openapi.yaml`, then:

View File

@@ -44,10 +44,6 @@ RUN /usr/local/bin/python3 -m pip install pip-tools
# Bun
COPY --from=oven/bun:1.3.8 /usr/local/bin/bun /usr/bin/bun
# Install windmill CLI
RUN bun install -g windmill-cli \
&& ln -s $(bun pm bin -g)/wmill /usr/bin/wmill
ARG TARGETPLATFORM
# Deno

View File

@@ -19,7 +19,7 @@ defaults:
jobs:
cargo_test:
runs-on: blacksmith-16vcpu-ubuntu-2404
runs-on: ubicloud-standard-16
services:
postgres:
image: postgres
@@ -70,16 +70,6 @@ jobs:
with:
ruby-version: "3.3"
bundler-cache: false
- name: Install windmill CLI from source
run: |
cd $GITHUB_WORKSPACE/cli
bash gen_wm_client.sh
bun install
mkdir -p "$HOME/.local/bin"
printf '#!/bin/sh\nexec bun run "%s/cli/src/main.ts" "$@"\n' "$GITHUB_WORKSPACE" > "$HOME/.local/bin/wmill"
chmod +x "$HOME/.local/bin/wmill"
echo "$HOME/.local/bin" >> $GITHUB_PATH
working-directory: /
- name: Install PowerShell, mold and clang
run: |
sudo apt-get update && sudo apt-get install -y powershell mold clang libcurl4-openssl-dev
@@ -88,20 +78,6 @@ jobs:
with:
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"
@@ -189,12 +165,6 @@ jobs:
fi
echo "NPM_TOKEN=${NPM_TOKEN}" >> $GITHUB_ENV
{
echo "TEST_NPMRC<<NPMRC_EOF"
echo "@windmill-test:registry=http://localhost:4873/"
echo "//localhost:4873/:_authToken=${NPM_TOKEN}"
echo "NPMRC_EOF"
} >> $GITHUB_ENV
echo "Got NPM token successfully: ${NPM_TOKEN:0:10}..."
# Configure npm globally with the auth token
@@ -229,7 +199,7 @@ jobs:
fi
echo "Verified: Package requires authentication for @windmill-test/private-pkg"
- name: Cache DuckDB FFI module build
uses: useblacksmith/cache@v1
uses: actions/cache@v3
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') }}
@@ -245,7 +215,6 @@ 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

View File

@@ -9,7 +9,7 @@ permissions: write-all
jobs:
build_ee:
runs-on: ubicloud-standard-4
runs-on: ubicloud
steps:
- uses: actions/checkout@v4
with:

View File

@@ -9,7 +9,7 @@ permissions: write-all
jobs:
build_ee:
runs-on: ubicloud-standard-4
runs-on: ubicloud
steps:
- uses: actions/checkout@v4
with:

View File

@@ -23,16 +23,16 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Deno
uses: denoland/setup-deno@v2
with:
deno-version: v2.x
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Generate Windmill client
working-directory: cli
run: ./gen_wm_client.sh
@@ -69,6 +69,11 @@ jobs:
cache: true
cache-workspaces: backend
- name: Setup Deno
uses: denoland/setup-deno@v2
with:
deno-version: v2.x
- name: Setup Node.js
uses: actions/setup-node@v4
with:
@@ -85,10 +90,6 @@ jobs:
- name: Symlink Node to /usr/bin/node
run: sudo ln -sf $(which node) /usr/bin/node
- name: Install dependencies
working-directory: cli
run: bun install
- name: Generate Windmill clients
working-directory: cli
run: |
@@ -100,10 +101,12 @@ jobs:
env:
DATABASE_URL: postgres://postgres:changeme@localhost:5432
CI_MINIMAL_FEATURES: "true"
run: bun test --timeout 120000 test/
run: |
deno test --no-check --allow-all test/ \
--ignore=test/cargo_backend_example.test.ts
test-windows:
runs-on: blacksmith-16vcpu-windows-2025
runs-on: windows-latest
steps:
- name: Checkout code
@@ -123,6 +126,11 @@ jobs:
cache: true
cache-workspaces: backend
- name: Setup Deno
uses: denoland/setup-deno@v2
with:
deno-version: v2.x
- name: Setup Node.js
uses: actions/setup-node@v4
with:
@@ -142,10 +150,6 @@ jobs:
echo "BUN_PATH=$bunPath" >> $env:GITHUB_OUTPUT
echo "NODE_BIN_PATH=$nodePath" >> $env:GITHUB_OUTPUT
- name: Install dependencies
working-directory: cli
run: bun install
- name: Generate Windmill clients
working-directory: cli
shell: bash
@@ -161,12 +165,9 @@ jobs:
CI_MINIMAL_FEATURES: "true"
BUN_PATH: ${{ steps.runtime-paths.outputs.BUN_PATH }}
NODE_BIN_PATH: ${{ steps.runtime-paths.outputs.NODE_BIN_PATH }}
run: bun test --timeout 120000 test/
- name: Keep runner alive for SSH debug
if: failure()
shell: pwsh
run: Start-Sleep -Seconds 3600
run: |
deno test --no-check --allow-all test/ `
--ignore=test/cargo_backend_example.test.ts
# Combined summary job for branch protection
test-summary:

View File

@@ -6,12 +6,6 @@ on:
- opened
- ready_for_review
- closed
issue_comment:
types:
- created
pull_request_review_comment:
types:
- created
jobs:
notify_discord_when_pr_opened:
@@ -39,38 +33,3 @@ jobs:
PR_NUMBER: ${{ github.event.pull_request.number }}
secrets:
DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_AI_BOT_TOKEN }}
notify_discord_on_comment:
if: >
github.event_name == 'issue_comment'
&& github.event.issue.pull_request
&& 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.issue.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:
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:
DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_AI_BOT_TOKEN }}

View File

@@ -25,9 +25,9 @@ jobs:
with:
node-version: "20.x"
registry-url: "https://registry.npmjs.org"
- uses: oven-sh/setup-bun@v2
- uses: denoland/setup-deno@v2
with:
bun-version: latest
deno-version: v2.x
- run: cd cli && ./build.sh && cd npm && npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -24,22 +24,9 @@ on:
DISCORD_GUILD_ID:
description: "The Discord guild ID"
type: string
COMMENT_BODY:
description: "The comment body"
type: string
default: ""
COMMENT_AUTHOR:
description: "The comment author"
type: string
default: ""
COMMENT_URL:
description: "The comment URL"
type: string
default: ""
secrets:
DISCORD_WEBHOOK_URL:
description: "Discord Webhook URL"
required: false
DISCORD_BOT_TOKEN:
description: "Discord Bot Token"
@@ -130,54 +117,3 @@ jobs:
curl -X PUT \
-H "Authorization: Bot $BOT_TOKEN" \
"https://discord.com/api/v10/channels/$thread_id/messages/$message_id/reactions/%E2%9C%85/@me"
post_comment:
runs-on: ubuntu-latest
if: ${{ inputs.PR_STATUS == 'comment' }}
steps:
- name: Post comment to Discord thread
env:
BOT_TOKEN: ${{ secrets.DISCORD_BOT_TOKEN }}
CHANNEL_ID: ${{ inputs.DISCORD_CHANNEL_ID }}
GUILD_ID: ${{ inputs.DISCORD_GUILD_ID }}
PR_NUMBER: ${{ inputs.PR_NUMBER }}
COMMENT_BODY: ${{ inputs.COMMENT_BODY }}
COMMENT_AUTHOR: ${{ inputs.COMMENT_AUTHOR }}
COMMENT_URL: ${{ inputs.COMMENT_URL }}
run: |
# 1) Find the thread by PR number
threads=$(curl -s -H "Authorization: Bot $BOT_TOKEN" \
"https://discord.com/api/v10/guilds/${GUILD_ID}/threads/active")
thread_id=$(echo "$threads" | jq -r \
--arg cid "$CHANNEL_ID" \
--arg pref "#${PR_NUMBER}:" \
'.threads[] | select(.parent_id == $cid and (.name | startswith($pref))) | .id')
if [ -z "$thread_id" ]; then
echo "Thread not found for PR #${PR_NUMBER}, skipping"
exit 0
fi
# 2) Truncate comment body to fit Discord's 2000 char limit
# Reserve space for the author line + link (~100 chars)
max_body=1800
if [ ${#COMMENT_BODY} -gt $max_body ]; then
# For bot comments, show the tail (conclusions/code tend to be at the end)
if [[ "$COMMENT_AUTHOR" == *"[bot]"* ]] || [[ "$COMMENT_AUTHOR" == *"-bot"* ]]; then
truncated_body="...${COMMENT_BODY: -$max_body}"
else
truncated_body="${COMMENT_BODY:0:$max_body}..."
fi
else
truncated_body="$COMMENT_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: []}}')
curl -s -X POST \
-H "Authorization: Bot $BOT_TOKEN" \
-H "Content-Type: application/json" \
-d "$payload" \
"https://discord.com/api/v10/channels/${thread_id}/messages"

7
.gitignore vendored
View File

@@ -14,16 +14,9 @@ backend/.minio-data
!.aiderignore
rust-client/Cargo.toml
# Worktree-generated port isolation
.env.local
# Worktree-specific Claude Code settings (generated by scripts/worktree-env)
.claude/settings.local.json
# Symlinked cache directories (for git worktrees)
backend/target
frontend/node_modules
typescript-client/node_modules
frontend/.svelte-kit
backend/chrome_profiler.json
.fast-check/

View File

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

View File

@@ -1,75 +0,0 @@
main_branch: main
merge_strategy: rebase
# worktree_dir: .worktrees
worktree_naming: basename
worktree_prefix: ""
# Default: "wm-"
window_prefix: "wm-"
auto_name:
model: "claude-sonnet-4.6"
system_prompt: |
Generate a concise git branch name based on the task description.
Rules:
- Use kebab-case (lowercase with hyphens)
- Keep it short: 1-3 words, max 4 if necessary
- Focus on the core task/feature, not implementation details
- No prefixes like feat/, fix/, chore/
Examples of good branch names:
- "Add dark mode toggle" → dark-mode
- "Fix the search results not showing" → fix-search
- "Refactor the authentication module" → auth-refactor
- "Add CSV export to reports" → export-csv
- "Shell completion is broken" → shell-completion
Output ONLY the branch name, nothing else.
background: true
# Commands to run in new worktree before tmux window opens.
# These block window creation - use for short tasks only.
# Use "<global>" to inherit from global config.
# Set to empty list to disable: `post_create: []`
# post_create:
# - "<global>"
# - mise use
post_create:
- ./scripts/worktree-env
pre_remove:
- ./scripts/worktree-cleanup
panes:
- command: >-
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
- Pane 1: backend (cargo watch -x run)\n
- 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."
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'
split: horizontal
- 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:
copy:
- backend/.env
- scripts/
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,126 +1,5 @@
# Changelog
## [1.642.0](https://github.com/windmill-labs/windmill/compare/v1.641.0...v1.642.0) (2026-02-22)
### Features
* **cli:** add consistent get/list/new subcommands for all item types ([#8047](https://github.com/windmill-labs/windmill/issues/8047)) ([4fedfdf](https://github.com/windmill-labs/windmill/commit/4fedfdfd11aa8ca7fff6f7aed5ae2b313888f878))
### Bug Fixes
* make WM_FLOW_PATH available in flow step previews ([#8042](https://github.com/windmill-labs/windmill/issues/8042)) ([a91c532](https://github.com/windmill-labs/windmill/commit/a91c532ecadce63cea965c497351fa1a6f39697a))
* preserve debouncing settings for flows with preprocessors ([#8043](https://github.com/windmill-labs/windmill/issues/8043)) ([a00927b](https://github.com/windmill-labs/windmill/commit/a00927b3008a2d953fde1d461723a3c92f375eb4))
## [1.641.0](https://github.com/windmill-labs/windmill/compare/v1.640.0...v1.641.0) (2026-02-21)
### Features
* add .npmrc support for private npm registries ([#8039](https://github.com/windmill-labs/windmill/issues/8039)) ([9eb1531](https://github.com/windmill-labs/windmill/commit/9eb15312f663aa6d700e8ac562d7b5c75c2221f7))
### Bug Fixes
* add created_by ownership check to update/delete saved inputs ([#8038](https://github.com/windmill-labs/windmill/issues/8038)) ([e8a13ed](https://github.com/windmill-labs/windmill/commit/e8a13edde7c0ba2ef80344ab7c7288e7bb2eb6b5))
* run substitute_ee_code.sh after creating EE worktree ([b330f38](https://github.com/windmill-labs/windmill/commit/b330f388894ecd9cc6b64297420ac6f032d32f72))
* tag bunnative dependency jobs as bun instead of nativets ([#8045](https://github.com/windmill-labs/windmill/issues/8045)) ([fd5ebc2](https://github.com/windmill-labs/windmill/commit/fd5ebc2fda589c022074c3bb4dcdb447c7f86cf0))
## [1.640.0](https://github.com/windmill-labs/windmill/compare/v1.639.0...v1.640.0) (2026-02-20)
### Features
* add windmill-ee-private worktree support to workmux ([#8034](https://github.com/windmill-labs/windmill/issues/8034)) ([9f3dd0b](https://github.com/windmill-labs/windmill/commit/9f3dd0bf2b2ba7c622093c54b7b6b5e7ebb26b74))
* **cli:** add --locks-required flag to wmill lint and sync push ([#8026](https://github.com/windmill-labs/windmill/issues/8026)) ([4abe589](https://github.com/windmill-labs/windmill/commit/4abe58939787f375ccfef5b2dbcfbd7e86cff076))
* dedicated nativets ([#8021](https://github.com/windmill-labs/windmill/issues/8021)) ([37c9acb](https://github.com/windmill-labs/windmill/commit/37c9acb232c64c98ecfb64754f5b69b31047c625))
* Support column detection on S3 objects in DuckDB ([#8018](https://github.com/windmill-labs/windmill/issues/8018)) ([87f3de9](https://github.com/windmill-labs/windmill/commit/87f3de9ae5975c88b6748e297f84a539aec4c0ca))
### Bug Fixes
* Fix DuckDB incorrect pg password encoding ([#8028](https://github.com/windmill-labs/windmill/issues/8028)) ([90b1a7a](https://github.com/windmill-labs/windmill/commit/90b1a7a531bce5621ea4de4792a8c9d3d3beec3d))
* **frontend:** use completed_at instead of created_at for job history ([#8022](https://github.com/windmill-labs/windmill/issues/8022)) ([24d7921](https://github.com/windmill-labs/windmill/commit/24d7921bcf23543759719ffd2463959c627b61b8))
### Performance Improvements
* lazy-load JSZip in RawAppEditorHeader ([#8012](https://github.com/windmill-labs/windmill/issues/8012)) ([a1ba10a](https://github.com/windmill-labs/windmill/commit/a1ba10a29e12ab5f553bd9aad74067cc5b3ead9e))
## [1.639.0](https://github.com/windmill-labs/windmill/compare/v1.638.4...v1.639.0) (2026-02-18)
### Features
* improve FolderPicker with edit icon pattern ([#7995](https://github.com/windmill-labs/windmill/issues/7995)) ([db8aa8a](https://github.com/windmill-labs/windmill/commit/db8aa8a0839b5729f0bb847e7a71766c7883ff36))
### Bug Fixes
* default automate_username_creation to true when setting is missing ([#8006](https://github.com/windmill-labs/windmill/issues/8006)) ([d2d08f8](https://github.com/windmill-labs/windmill/commit/d2d08f8817e6e7818eb4b6f092e66ae039f0c756))
* handle raw app folder deletion in sync push without yaml parse error ([#7994](https://github.com/windmill-labs/windmill/issues/7994)) ([f6d99dd](https://github.com/windmill-labs/windmill/commit/f6d99dd18c06a7f5aea93122276dd68c45772b43))
### Performance Improvements
* **cli:** skip relock more accurate ([#7993](https://github.com/windmill-labs/windmill/issues/7993)) ([cd4151a](https://github.com/windmill-labs/windmill/commit/cd4151a84b2c1e0f2e616079091d0429bf469f4e))
## [1.638.4](https://github.com/windmill-labs/windmill/compare/v1.638.3...v1.638.4) (2026-02-17)
### Bug Fixes
* **frontend:** add folder picker validation, error handling, and loading state ([#7987](https://github.com/windmill-labs/windmill/issues/7987)) ([4ea1692](https://github.com/windmill-labs/windmill/commit/4ea1692ee27adbba583d8ead753fa8a19099183f))
* **frontend:** improve folder picker with sticky create button and drawer flow ([#7985](https://github.com/windmill-labs/windmill/issues/7985)) ([a46924a](https://github.com/windmill-labs/windmill/commit/a46924a0f21314826c00fa4ac61885bdf3700421))
## [1.638.3](https://github.com/windmill-labs/windmill/compare/v1.638.2...v1.638.3) (2026-02-17)
### Bug Fixes
* always create guidance files during wmill init ([#7974](https://github.com/windmill-labs/windmill/issues/7974)) ([f387daa](https://github.com/windmill-labs/windmill/commit/f387daa2a6c7eb260981a19c58374062f652fca6))
* **frontend:** incorrect job result on the runs page ([#7982](https://github.com/windmill-labs/windmill/issues/7982)) ([2d53939](https://github.com/windmill-labs/windmill/commit/2d5393941cf17d45d1d4ff840766f07bd482f70b))
* **frontend:** preserve user config when trimming oneOf non-selected keys ([b094649](https://github.com/windmill-labs/windmill/commit/b0946495863e206d12922536d2cae24cb78b55fc))
## [1.638.2](https://github.com/windmill-labs/windmill/compare/v1.638.1...v1.638.2) (2026-02-17)
### Bug Fixes
* **backend:** gcp private key parsing ([#7979](https://github.com/windmill-labs/windmill/issues/7979)) ([5b7bb2f](https://github.com/windmill-labs/windmill/commit/5b7bb2fb84a12433c48f1cdfc022edff0cbc88ea))
* yaml settings UI mask rsa_keys and jwt_secret ([71608bf](https://github.com/windmill-labs/windmill/commit/71608bf669658241b4ce4e1da3a83f1045dea1f6))
## [1.638.1](https://github.com/windmill-labs/windmill/compare/v1.638.0...v1.638.1) (2026-02-17)
### Bug Fixes
* **operator:** improve configmap handling of older license keys ([b7bec1a](https://github.com/windmill-labs/windmill/commit/b7bec1a83d97a823ff6fc7d7fa549b975f848066))
## [1.638.0](https://github.com/windmill-labs/windmill/compare/v1.637.0...v1.638.0) (2026-02-17)
### Features
* add native_mode as typed field on WorkerGroupConfig ([3e313cc](https://github.com/windmill-labs/windmill/commit/3e313cc4e864108d7dee866e784dff428883cadf))
* show all settings in YAML UI and protect from empty overwrites ([#7976](https://github.com/windmill-labs/windmill/issues/7976)) ([b3eeee4](https://github.com/windmill-labs/windmill/commit/b3eeee413114cb54b5932542b14d8904a3c6c93c))
### Bug Fixes
* add missing google native triggers to triggers panel ([#7966](https://github.com/windmill-labs/windmill/issues/7966)) ([bb03c62](https://github.com/windmill-labs/windmill/commit/bb03c62c2819d40acd676d10cc586958f4117b5d))
* download audit logs ([#7965](https://github.com/windmill-labs/windmill/issues/7965)) ([bba319b](https://github.com/windmill-labs/windmill/commit/bba319b2826f4d264ecebef3258d3c3f16237cc5))
* improve operator ConfigMap settings handling ([#7975](https://github.com/windmill-labs/windmill/issues/7975)) ([2019aec](https://github.com/windmill-labs/windmill/commit/2019aecf4253edcf7b33e30862f642b303948440))
## [1.637.0](https://github.com/windmill-labs/windmill/compare/v1.636.0...v1.637.0) (2026-02-17)
### Features
* **frontend:** inline edit summary & path from header ([#7968](https://github.com/windmill-labs/windmill/issues/7968)) ([eb5a8da](https://github.com/windmill-labs/windmill/commit/eb5a8dab74822eb3e43557cf1c85bf14d6e1910f))
* native mode ([#7939](https://github.com/windmill-labs/windmill/issues/7939)) ([535e108](https://github.com/windmill-labs/windmill/commit/535e108cbf5070a6a23183389007db63fb07a58f))
## [1.636.0](https://github.com/windmill-labs/windmill/compare/v1.635.1...v1.636.0) (2026-02-16)

View File

@@ -258,10 +258,6 @@ COPY --from=denoland/deno:2.2.1 --chmod=755 /usr/bin/deno /usr/bin/deno
COPY --from=oven/bun:1.3.8 /usr/local/bin/bun /usr/bin/bun
# Install windmill CLI
RUN bun install -g windmill-cli \
&& ln -s $(bun pm bin -g)/wmill /usr/bin/wmill
COPY --from=php:8.3.7-cli /usr/local/bin/php /usr/bin/php
COPY --from=composer:2.7.6 /usr/bin/composer /usr/bin/composer

View File

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

@@ -257,7 +257,6 @@ On self-hosted instances, you might want to import all the approved resource typ
| BASE_URL | http://localhost:8000 | The base url that is exposed publicly to access your instance. Is overriden by the instance settings if any. | Server |
| ZOMBIE_JOB_TIMEOUT | 30 | The timeout after which a job is considered to be zombie if the worker did not send pings about processing the job (every server check for zombie jobs every 30s) | Server |
| RESTART_ZOMBIE_JOBS | true | If true then a zombie job is restarted (in-place with the same uuid and some logs), if false the zombie job is failed | Server |
| NATIVE_MODE | false | Enable native mode: sets NUM_WORKERS=8, rejects non-native jobs (nativets, postgresql, mysql, etc.) | Worker |
| SLEEP_QUEUE | 50 | The number of ms to sleep in between the last check for new jobs in the DB. It is multiplied by NUM_WORKERS such that in average, for one worker instance, there is one pull every SLEEP_QUEUE ms. | Worker |
| KEEP_JOB_DIR | false | Keep the job directory after the job is done. Useful for debugging. | Worker |
| LICENSE_KEY (EE only) | None | License key checked at startup for the Enterprise Edition of Windmill | Worker |

View File

@@ -1,196 +0,0 @@
# Windmill Development with workmux
This guide covers the workmux-based development setup for Windmill. Each worktree gets its own tmux window with a Claude Code agent, a backend server (with auto-reload), and a frontend dev server — all on isolated ports.
## Prerequisites
- tmux
- Rust toolchain (rustup)
- Node.js + npm
- PostgreSQL running locally (see `backend/.env`)
## Installation
### 1. Install workmux
```bash
cargo install workmux
```
### 2. Install the Claude Code plugin
```bash
workmux claude install
```
This lets workmux manage Claude Code agents in worktree panes.
### 3. Install cargo-watch
Used for auto-recompiling the backend on file changes:
```bash
cargo install cargo-watch
```
### 4. Install llm CLI (required for auto branch naming)
workmux uses the `llm` CLI to automatically generate branch names from prompts. Install it with:
```bash
uv tool install llm
llm install llm-anthropic
```
Then set your Anthropic API key:
```bash
llm keys set anthropic
# paste your API key when prompted
```
### 5. Recommended: shell alias and autocomplete
Set up a `wm` alias for convenience:
```bash
# Add to your ~/.zshrc
alias wm="workmux"
```
Setting up zsh autocomplete is also recommended — see the [workmux docs](https://github.com/rubenfiszel/workmux) for instructions.
## Port Slot System
Each worktree is assigned a **slot** that determines its ports:
| Slot | Backend | Frontend |
|------|---------|----------|
| 0 | 8000 | 3000 |
| 1 | 8010 | 3010 |
| 2 | 8020 | 3020 |
| 3 | 8030 | 3030 |
| ... | ... | ... |
- **Slot 0** is reserved for the main worktree (default `cargo run` / `npm run dev`).
- Without `WM_SLOT`, the script auto-assigns the first available slot (starting from 1) and prints it.
- With `WM_SLOT=N`, it uses that slot and errors if the ports are taken.
## SSH Port Forwarding
If you develop over SSH, add this to `~/.ssh/config` on your **local machine** to pre-configure tunnels for each slot:
```
Host windmill-dev
HostName <remote-ip>
User <username>
# Slot 0 (main worktree)
LocalForward 8000 localhost:8000
LocalForward 3000 localhost:3000
# Slot 1
LocalForward 8010 localhost:8010
LocalForward 3010 localhost:3010
# Slot 2
LocalForward 8020 localhost:8020
LocalForward 3020 localhost:3020
# Slot 3
LocalForward 8030 localhost:8030
LocalForward 3030 localhost:3030
```
Then connect once and all tunnels are active:
```bash
ssh windmill-dev
```
Access the frontend at `http://localhost:<frontend-port>` in your local browser.
## Quickstart
```bash
# Create a new worktree (auto-assigns slot, prints ports)
workmux add my-feature
# Or with an explicit slot
WM_SLOT=2 workmux add my-feature
# Create a worktree and immediately send a prompt to the agent
workmux add -A -p "fix the login bug in auth.rs"
```
The `add` command creates the worktree but does **not** open it. To open the tmux window and start working:
```bash
workmux open my-feature
```
This will open a tmux window with three panes:
- **Claude Code agent** (focused)
- **Backend**: `cargo watch -x run` on the assigned port (auto-reloads on save)
- **Frontend**: `npm run dev` proxying to the backend
When using `-A` with `add`, the worktree is created and opened automatically, and the prompt is sent to the agent right away.
Check which ports were assigned:
```bash
cat <worktree-path>/.env.local
```
### Sending work to the agent
```bash
# Send a prompt to the agent in a worktree
workmux send my-feature "fix the login bug in auth.rs"
# Check agent status
workmux status
```
### Merging and cleaning up
We never merge worktrees directly — always create a PR on GitHub and let it be merged there. Once the PR is merged, clean up the worktree:
```bash
# Close the tmux window but keep the worktree
workmux close my-feature
# After your PR is merged, remove the worktree, branch, and tmux window
workmux rm my-feature
```
> **Note**: Do not use `workmux merge`. Always go through a PR to get your changes into main. You can ask the Claude Code agent in the worktree to create the PR for you.
## Configuration
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
- **`files.symlink`**: Symlinks `node_modules` and `.svelte-kit` to avoid reinstalling per worktree
## Enterprise (EE) Code Access
The enterprise source code lives in the `windmill-ee-private` repository (sibling to this repo). When you create a worktree, `scripts/worktree-env` automatically creates a matching EE worktree on the same branch and configures Claude Code's `additionalDirectories` to grant access.
### Sandbox setup
When using sandbox mode, the container needs explicit mounts to access the EE repo. Add the following to your global workmux config (`~/.config/workmux/config.yaml`):
```yaml
sandbox:
extra_mounts:
- host_path: ~/windmill-ee-private
writable: true
- host_path: ~/windmill-ee-private__worktrees
writable: true
```
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.
## Login
Default credentials: `admin@windmill.dev` / `changeme`

View File

@@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO v2_job_queue (id, workspace_id, scheduled_for, tag)\n SELECT unnest($1::uuid[]), 'test-workspace', now(), 'flow'",
"describe": {
"columns": [],
"parameters": {
"Left": [
"UuidArray"
]
},
"nullable": []
},
"hash": "0681b850c033619e1b9498376263681f875a5aba22170ca50ec8b578f7fa478b"
}

View File

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

View File

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

View File

@@ -1,26 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n WITH completed AS (\n INSERT INTO v2_job_completed\n (workspace_id, id, started_at, duration_ms, result,\n flow_status, workflow_as_code_status, status, worker)\n SELECT\n q.workspace_id, q.id, q.started_at,\n (EXTRACT('epoch' FROM now()) - EXTRACT('epoch' FROM COALESCE(q.started_at, now()))) * 1000,\n CASE WHEN q.running\n THEN $3::text::jsonb\n ELSE $4::text::jsonb\n END,\n s.flow_status,\n s.workflow_as_code_status,\n 'skipped'::job_status,\n q.worker\n FROM v2_job_queue q\n LEFT JOIN v2_job_status s ON s.id = q.id\n WHERE q.id = $1\n ON CONFLICT (id) DO UPDATE SET status = EXCLUDED.status, result = EXCLUDED.result\n RETURNING 1 AS x\n ), _deleted AS (\n DELETE FROM v2_job_queue WHERE id = $1\n ), _logged AS (\n INSERT INTO job_logs (logs, job_id, workspace_id)\n VALUES ($5, $1, $2)\n ON CONFLICT (job_id) DO UPDATE SET logs = concat(job_logs.logs, EXCLUDED.logs)\n )\n SELECT x FROM completed\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "x",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Uuid",
"Varchar",
"Text",
"Text",
"Text"
]
},
"nullable": [
null
]
},
"hash": "1437b432d2c23e30eb05443e83069cdb049f65ec299b0778ce14677728cf6346"
}

View File

@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT result::text FROM v2_job_completed WHERE id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "result",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
null
]
},
"hash": "18b6262a60400f2b58ab26615466c23b4c1a7805c66b70b0fcfb7d33b122a7bf"
}

View File

@@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO v2_job_queue (id, workspace_id, scheduled_for, tag)\n VALUES ($1, $2, now(), 'flow')",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Varchar"
]
},
"nullable": []
},
"hash": "1af6885dbc5055281acb82b3e57f7dba2e4b04d9535058fab695660a14bf8890"
}

View File

@@ -0,0 +1,65 @@
{
"db_name": "PostgreSQL",
"query": "SELECT client, refresh_token, grant_type, cc_client_id, cc_client_secret, cc_token_url, mcp_server_url, is_workspace_integration FROM account WHERE workspace_id = $1 AND id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "client",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "refresh_token",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "grant_type",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "cc_client_id",
"type_info": "Varchar"
},
{
"ordinal": 4,
"name": "cc_client_secret",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "cc_token_url",
"type_info": "Varchar"
},
{
"ordinal": 6,
"name": "mcp_server_url",
"type_info": "Text"
},
{
"ordinal": 7,
"name": "is_workspace_integration",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Int4"
]
},
"nullable": [
false,
false,
false,
true,
true,
true,
true,
false
]
},
"hash": "1ba2e23d4ba816048ec1e88af9e342867fc0443cabea16d111afa2b91d3fe03b"
}

View File

@@ -1,16 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO v2_job (id, kind, tag, created_by, permissioned_as, permissioned_as_email, workspace_id, runnable_path)\n VALUES ($1, 'flow', 'flow', 'test-user', 'u/test-user', 'test@windmill.dev', $2, $3)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Varchar",
"Varchar"
]
},
"nullable": []
},
"hash": "27f70ebe788cca2e88732d8bf978883037bebca4cf75ba459858e4fb197f940b"
}

View File

@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT debounce_batch FROM v2_job_debounce_batch WHERE id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "debounce_batch",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false
]
},
"hash": "2a95f18e80c55a7e8178a4bd2b781d41fa47efd4da5bb9bc2d72b9aa1e33617f"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT worker, worker_instance, worker_group, vcpus, memory, ping_at, started_at, custom_tags, occupancy_rate_15s, occupancy_rate_5m, occupancy_rate_30m, native_mode FROM worker_ping WHERE ping_at > now() - interval '30 days' ORDER BY started_at",
"query": "SELECT worker, worker_instance, worker_group, vcpus, memory, ping_at, started_at, custom_tags, occupancy_rate_15s, occupancy_rate_5m, occupancy_rate_30m FROM worker_ping WHERE ping_at > now() - interval '30 days' ORDER BY started_at",
"describe": {
"columns": [
{
@@ -57,11 +57,6 @@
"ordinal": 10,
"name": "occupancy_rate_30m",
"type_info": "Float4"
},
{
"ordinal": 11,
"name": "native_mode",
"type_info": "Bool"
}
],
"parameters": {
@@ -78,9 +73,8 @@
true,
true,
true,
true,
false
true
]
},
"hash": "dcc6928bc273fcbe52bcef43f9d06d8bb8c68a1b04b3c2cce7491dde5d727446"
"hash": "2b3b634b15eb58b95ce26b5a591258b54fb7bf21ae85e7a390ad73489c2247ac"
}

View File

@@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO v2_job (id, kind, tag, created_by, permissioned_as, permissioned_as_email, workspace_id, runnable_path)\n VALUES ($1, 'flow', 'flow', 'test-user', 'u/test-user', 'test@windmill.dev', 'ws2', 'f/test/flow')",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": []
},
"hash": "4010328a9f1611064f497726b69c08625a55a4dab25c3d9b5ece07e44d14915b"
}

View File

@@ -0,0 +1,35 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO debounce_key (job_id, key)\n VALUES ($1, $2)\n ON CONFLICT (key)\n DO UPDATE SET\n previous_job_id = debounce_key.job_id,\n job_id = EXCLUDED.job_id, -- replace current job with new one \n debounced_times = debounce_key.debounced_times + 1 -- evaluated only if conflict,\n -- conflict means there is already existing value,\n -- which means overriding it will also imply adding new entry to v2_job_debounce_batch and thus debouncing the job\n -- so the counter should be incremented\n RETURNING\n debounced_times,\n first_started_at,\n previous_job_id AS job_id_to_debounce\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "debounced_times",
"type_info": "Int4"
},
{
"ordinal": 1,
"name": "first_started_at",
"type_info": "Timestamptz"
},
{
"ordinal": 2,
"name": "job_id_to_debounce",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Uuid",
"Varchar"
]
},
"nullable": [
false,
false,
true
]
},
"hash": "454ace9ce391725ef4f4c129cd66e4c12a5c40f512b70551958178c8b4d6c183"
}

View File

@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "WITH ids AS (\n SELECT id as job_id FROM v2_job_debounce_batch WHERE debounce_batch = (\n SELECT debounce_batch FROM v2_job_debounce_batch WHERE id = $1\n )\n ) SELECT args->>'items' FROM ids LEFT JOIN v2_job ON v2_job.id = ids.job_id",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "?column?",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
null
]
},
"hash": "48536968f4173715d4ef8293683c2a3eb4bd22fbe18c34890a3dc4e96e4e6133"
}

View File

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

View File

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

View File

@@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO v2_job_queue (id, workspace_id, scheduled_for, tag)\n SELECT unnest($1::uuid[]), 'test-workspace', now(), 'deno'",
"describe": {
"columns": [],
"parameters": {
"Left": [
"UuidArray"
]
},
"nullable": []
},
"hash": "539d661500254e2e346490710f5772cb88a1ab6bbddd97a77e06644ac0f61762"
}

View File

@@ -0,0 +1,18 @@
{
"db_name": "PostgreSQL",
"query": "WITH job_result AS (\n SELECT result\n FROM v2_job_completed\n WHERE id = $1\n ),\n updated_queue AS (\n UPDATE v2_job_queue\n SET running = false,\n tag = COALESCE($3, tag)\n WHERE id = $2\n )\n UPDATE v2_job\n SET\n tag = COALESCE($3, tag),\n concurrent_limit = COALESCE($4, concurrent_limit),\n concurrency_time_window_s = COALESCE($5, concurrency_time_window_s),\n args = COALESCE(\n CASE\n WHEN job_result.result IS NULL THEN NULL\n WHEN jsonb_typeof(job_result.result) = 'object'\n THEN job_result.result\n WHEN jsonb_typeof(job_result.result) = 'null'\n THEN NULL\n ELSE jsonb_build_object('value', job_result.result)\n END,\n '{}'::jsonb\n ),\n preprocessed = TRUE\n FROM job_result\n WHERE v2_job.id = $2;\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Uuid",
"Varchar",
"Int4",
"Int4"
]
},
"nullable": []
},
"hash": "5b8c1803f0ccead11517fbc8a9bdc0227dc3922217fa18f0b71ff0484d65838c"
}

View File

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

View File

@@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE debounce_key SET first_started_at = now() - interval '20 seconds' WHERE key = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text"
]
},
"nullable": []
},
"hash": "66342c32f7ae0238803cb1896d9f23a74b64573f77dd32189a25b6e8369f147b"
}

View File

@@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO v2_job (id, kind, tag, created_by, permissioned_as, permissioned_as_email, workspace_id, runnable_path)\n SELECT unnest($1::uuid[]), 'flow', 'flow', 'test-user', 'u/test-user', 'test@windmill.dev', 'test-workspace', 'f/test/flow'",
"describe": {
"columns": [],
"parameters": {
"Left": [
"UuidArray"
]
},
"nullable": []
},
"hash": "66faba2137791e0cb1353545c06f9f7c23a1559e7a761db7c2195736b8b30709"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT worker, worker_instance, EXTRACT(EPOCH FROM (now() - ping_at))::integer as last_ping, started_at, ip, jobs_executed,\n CASE WHEN $4 IS TRUE THEN current_job_id ELSE NULL END as last_job_id, CASE WHEN $4 IS TRUE THEN current_job_workspace_id ELSE NULL END as last_job_workspace_id,\n custom_tags, worker_group, wm_version, occupancy_rate, occupancy_rate_15s, occupancy_rate_5m, occupancy_rate_30m, memory, vcpus, memory_usage, wm_memory_usage, job_isolation, native_mode\n FROM worker_ping\n WHERE ($1::integer IS NULL AND ping_at > now() - interval '5 minute') OR (ping_at > now() - ($1 || ' seconds')::interval)\n ORDER BY ping_at desc LIMIT $2 OFFSET $3",
"query": "SELECT worker, worker_instance, EXTRACT(EPOCH FROM (now() - ping_at))::integer as last_ping, started_at, ip, jobs_executed,\n CASE WHEN $4 IS TRUE THEN current_job_id ELSE NULL END as last_job_id, CASE WHEN $4 IS TRUE THEN current_job_workspace_id ELSE NULL END as last_job_workspace_id,\n custom_tags, worker_group, wm_version, occupancy_rate, occupancy_rate_15s, occupancy_rate_5m, occupancy_rate_30m, memory, vcpus, memory_usage, wm_memory_usage, job_isolation\n FROM worker_ping\n WHERE ($1::integer IS NULL AND ping_at > now() - interval '5 minute') OR (ping_at > now() - ($1 || ' seconds')::interval)\n ORDER BY ping_at desc LIMIT $2 OFFSET $3",
"describe": {
"columns": [
{
@@ -102,11 +102,6 @@
"ordinal": 19,
"name": "job_isolation",
"type_info": "Text"
},
{
"ordinal": 20,
"name": "native_mode",
"type_info": "Bool"
}
],
"parameters": {
@@ -137,9 +132,8 @@
true,
true,
true,
true,
false
true
]
},
"hash": "68bca8f839e47705b11d312ee874eceaa3d1d24d9053ad4aea94b9f8465585ca"
"hash": "771a858a4b7ca41b6787e61f5a4a5c9c4d48fd213852e2f997cd4b2420580d30"
}

View File

@@ -1,19 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "WITH job_result AS (\n SELECT result\n FROM v2_job_completed\n WHERE id = $1\n ),\n updated_queue AS (\n UPDATE v2_job_queue\n SET running = false,\n tag = COALESCE($3, tag),\n scheduled_for = COALESCE($6, scheduled_for)\n WHERE id = $2\n )\n UPDATE v2_job\n SET\n tag = COALESCE($3, tag),\n concurrent_limit = COALESCE($4, concurrent_limit),\n concurrency_time_window_s = COALESCE($5, concurrency_time_window_s),\n args = COALESCE(\n CASE\n WHEN job_result.result IS NULL THEN NULL\n WHEN jsonb_typeof(job_result.result) = 'object'\n THEN job_result.result\n WHEN jsonb_typeof(job_result.result) = 'null'\n THEN NULL\n ELSE jsonb_build_object('value', job_result.result)\n END,\n '{}'::jsonb\n ),\n preprocessed = TRUE\n FROM job_result\n WHERE v2_job.id = $2;\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Uuid",
"Varchar",
"Int4",
"Int4",
"Timestamptz"
]
},
"nullable": []
},
"hash": "79b437ad31ddab94310989b8fb6a1c130b9be1ab4b6a100fffffd687677b9c92"
}

View File

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

View File

@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT logs as \"logs!\" FROM job_logs WHERE job_id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "logs!",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
true
]
},
"hash": "7ca599330c9913c7e66b27e2ffcfa18d53cbdd16e179749f0aea7980a901b23c"
}

View File

@@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO v2_job_queue (id, workspace_id, scheduled_for, tag)\n VALUES ($1, $2, now(), 'deno')",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Varchar"
]
},
"nullable": []
},
"hash": "7ca7dabfe360845a5b57552b0d02267d5dbbc488bc7ab990c0bda1594bf5ef3a"
}

View File

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

View File

@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO v2_job_debounce_batch (id, debounce_batch)\n -- if it the first one, nextval will be evaluated, otherwise take from the job we will debounce\n SELECT\n $2,\n COALESCE(\n (\n SELECT debounce_batch\n FROM v2_job_debounce_batch\n WHERE id = $1\n LIMIT 1\n ), -- maybe use current batch\n nextval('debounce_batch_seq')\n )\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Uuid"
]
},
"nullable": []
},
"hash": "8360ab72d60f07dde6ecae599e6531b5b86862029ab51fdbdd44ec16239108e2"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO workspace_settings (workspace_id) VALUES ('ws2')",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "8cf5af21cde4e4de45f995efa2a9b56ce20c26869ca78d7e17b3504b92ae85b1"
}

View File

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

View File

@@ -1,34 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT job_id, previous_job_id, debounced_times FROM debounce_key WHERE key = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "job_id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "previous_job_id",
"type_info": "Uuid"
},
{
"ordinal": 2,
"name": "debounced_times",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
true,
false
]
},
"hash": "8f442110817244aa9533b014aa3d74a6582937dfe4759932b63b8e531984008e"
}

View File

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

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO worker_ping (worker_instance, worker, ip, custom_tags, worker_group, dedicated_worker, dedicated_workers, wm_version, vcpus, memory, job_isolation, native_mode) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) ON CONFLICT (worker)\n DO UPDATE set ip = EXCLUDED.ip, custom_tags = EXCLUDED.custom_tags, worker_group = EXCLUDED.worker_group, dedicated_workers = EXCLUDED.dedicated_workers, native_mode = EXCLUDED.native_mode",
"query": "INSERT INTO worker_ping (worker_instance, worker, ip, custom_tags, worker_group, dedicated_worker, dedicated_workers, wm_version, vcpus, memory, job_isolation) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) ON CONFLICT (worker)\n DO UPDATE set ip = EXCLUDED.ip, custom_tags = EXCLUDED.custom_tags, worker_group = EXCLUDED.worker_group, dedicated_workers = EXCLUDED.dedicated_workers",
"describe": {
"columns": [],
"parameters": {
@@ -15,11 +15,10 @@
"Varchar",
"Int8",
"Int8",
"Text",
"Bool"
"Text"
]
},
"nullable": []
},
"hash": "298fa4f8eb05b4c3f33b608b0cdb6ed918af2df012de33acb3befd3fcccbc257"
"hash": "97c61b6a9a5112ea484565236959a544511d5d501fb737da8110a8725b883465"
}

View File

@@ -1,35 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n WITH dk AS (\n INSERT INTO debounce_key (job_id, key)\n VALUES ($1, $2)\n ON CONFLICT (key)\n DO UPDATE SET\n previous_job_id = debounce_key.job_id,\n job_id = EXCLUDED.job_id,\n debounced_times = debounce_key.debounced_times + 1\n RETURNING\n debounced_times,\n first_started_at,\n previous_job_id AS job_id_to_debounce\n ), _batch AS (\n INSERT INTO v2_job_debounce_batch (id, debounce_batch)\n SELECT\n $1,\n COALESCE(\n (SELECT debounce_batch FROM v2_job_debounce_batch WHERE id = dk.job_id_to_debounce LIMIT 1),\n nextval('debounce_batch_seq')\n )\n FROM dk\n )\n SELECT debounced_times, first_started_at, job_id_to_debounce FROM dk\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "debounced_times",
"type_info": "Int4"
},
{
"ordinal": 1,
"name": "first_started_at",
"type_info": "Timestamptz"
},
{
"ordinal": 2,
"name": "job_id_to_debounce",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Uuid",
"Varchar"
]
},
"nullable": [
false,
false,
true
]
},
"hash": "98033aae3182bde22d5b2ff08ef6e8a4f8f3a9bf04238b33e9caf46836df73d9"
}

View File

@@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO v2_job (id, kind, tag, created_by, permissioned_as, permissioned_as_email, workspace_id)\n VALUES ($1, 'noop', 'deno', 'test-user', 'u/test-user', 'test@windmill.dev', $2)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Varchar"
]
},
"nullable": []
},
"hash": "9f50ec7681a1fcd11cb452c7aba7e8897e49a4b6affa4e9976680336b6bd3115"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO workspace (id, name, owner) VALUES ('ws2', 'Workspace 2', 'test-user')",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "a057ff9f5998a162ae6de05f6127b7eefc826af7bf1bb89fd758f7c03c881033"
}

View File

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

View File

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

View File

@@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO v2_job_runtime (id) VALUES ($1)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": []
},
"hash": "a68754521bf751450602f04dd4243199a18885e1739a5a0e7f6100eab6f3c803"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE worker_ping SET ping_at = now(), jobs_executed = $1, custom_tags = $2,\n occupancy_rate = $3, memory_usage = $4, wm_memory_usage = $5, vcpus = COALESCE($7, vcpus),\n memory = COALESCE($8, memory), occupancy_rate_15s = $9, occupancy_rate_5m = $10, occupancy_rate_30m = $11, native_mode = $12 WHERE worker = $6",
"query": "UPDATE worker_ping SET ping_at = now(), jobs_executed = $1, custom_tags = $2,\n occupancy_rate = $3, memory_usage = $4, wm_memory_usage = $5, vcpus = COALESCE($7, vcpus),\n memory = COALESCE($8, memory), occupancy_rate_15s = $9, occupancy_rate_5m = $10, occupancy_rate_30m = $11 WHERE worker = $6",
"describe": {
"columns": [],
"parameters": {
@@ -15,11 +15,10 @@
"Int8",
"Float4",
"Float4",
"Float4",
"Bool"
"Float4"
]
},
"nullable": []
},
"hash": "a41c4cbaffdb714e4a963557de5a4011744d684eb24e03cb4beae6a512613159"
"hash": "aa523c363186575b4bd2537b8e2430e6938e7cc35f8c9e2d1c5459a85443cbdd"
}

View File

@@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE v2_job_queue SET runnable_settings_handle = $1 WHERE id = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Uuid"
]
},
"nullable": []
},
"hash": "abb56f78aa39c6b6ae8b0ccb7b724c1f80d717e7c9d76b287da63cb5ee8e8b25"
}

View File

@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT 1 as x FROM v2_job_completed WHERE id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "x",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
null
]
},
"hash": "adb98040c8039e5cc27fe0579941f723b38d1784bd2f1b16d8724f1f1612dcbf"
}

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT v2_job_queue.id, v2_job.tag, v2_job_queue.scheduled_for, v2_job_queue.workspace_id FROM v2_job_queue LEFT JOIN v2_job ON v2_job_queue.id = v2_job.id WHERE running = false AND scheduled_for < now() - ($1 || ' minutes')::interval AND v2_job.trigger_kind IS DISTINCT FROM 'schedule'::job_trigger_kind",
"query": "SELECT v2_job_queue.id, v2_job.tag, v2_job_queue.scheduled_for, v2_job_queue.workspace_id FROM v2_job_queue LEFT JOIN v2_job ON v2_job_queue.id = v2_job.id WHERE running = false AND scheduled_for < now() - ($1 || ' minutes')::interval",
"describe": {
"columns": [
{
@@ -36,5 +36,5 @@
false
]
},
"hash": "53648c069749df45c0459d733b3e429af20c69c841fb0c3bceafe3ea6c3f5329"
"hash": "b45e17ad532a23b394226c9a5d7ab5a21e20202dbbf9c67831cc62eb067cd2ba"
}

View File

@@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO v2_job_runtime (id) SELECT unnest($1::uuid[])",
"describe": {
"columns": [],
"parameters": {
"Left": [
"UuidArray"
]
},
"nullable": []
},
"hash": "b4a9abcb38997587b28655b0f4a212a5bd4039b57fab20b163617e33a4c9dd46"
}

View File

@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT 1 as x FROM v2_job_queue WHERE id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "x",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
null
]
},
"hash": "b795dc228f93c8b9bedb4a3e7467d941819a7202214e0634580bdc2ec30f0b70"
}

View File

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

View File

@@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO v2_job_queue (id, workspace_id, scheduled_for, tag) VALUES ($1, 'ws2', now(), 'flow')",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": []
},
"hash": "c1a1ae759ebb84fde3e6d2727991b2a1fcc73e520fee79653bb960dc00c3e2db"
}

View File

@@ -1,35 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n WITH dk AS (\n INSERT INTO debounce_key (job_id, key)\n VALUES ($1, $2)\n ON CONFLICT (key)\n DO UPDATE SET\n previous_job_id = debounce_key.job_id,\n job_id = EXCLUDED.job_id,\n debounced_times = debounce_key.debounced_times + 1\n RETURNING\n debounced_times,\n first_started_at,\n previous_job_id AS job_id_to_debounce\n ), _batch AS (\n INSERT INTO v2_job_debounce_batch (id, debounce_batch)\n SELECT\n $1,\n COALESCE(\n (SELECT debounce_batch FROM v2_job_debounce_batch WHERE id = dk.job_id_to_debounce LIMIT 1),\n nextval('debounce_batch_seq')\n )\n FROM dk\n )\n SELECT debounced_times, first_started_at, job_id_to_debounce FROM dk\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "debounced_times",
"type_info": "Int4"
},
{
"ordinal": 1,
"name": "first_started_at",
"type_info": "Timestamptz"
},
{
"ordinal": 2,
"name": "job_id_to_debounce",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Uuid",
"Varchar"
]
},
"nullable": [
false,
false,
true
]
},
"hash": "c2347460b73ae9d3167031c263032e97ebefb46be9e58bd3da9067748075311b"
}

View File

@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*) as \"count!\" FROM v2_job_queue WHERE id = ANY($1)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count!",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"UuidArray"
]
},
"nullable": [
null
]
},
"hash": "c63a1949247f1618f6b6acee9bf6b4d3081dfed2e6ff533dbff0bdfd52687cbb"
}

View File

@@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO v2_job (id, kind, tag, created_by, permissioned_as, permissioned_as_email, workspace_id)\n SELECT unnest($1::uuid[]), 'noop', 'deno', 'test-user', 'u/test-user', 'test@windmill.dev', 'test-workspace'",
"describe": {
"columns": [],
"parameters": {
"Left": [
"UuidArray"
]
},
"nullable": []
},
"hash": "c6d963e5cefeea728414892df9f28a89f435fc0fb7e55f243b021200f33d2151"
}

View File

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

View File

@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*) as \"count!\" FROM v2_job_completed WHERE id = ANY($1)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count!",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"UuidArray"
]
},
"nullable": [
null
]
},
"hash": "cc309de42a3b630bb83d1b2437633ef0b98ce5e5fba1f5c1dda3ddd874ff3a39"
}

View File

@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT debounce_batch FROM v2_job_debounce_batch WHERE id = ANY($1)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "debounce_batch",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"UuidArray"
]
},
"nullable": [
false
]
},
"hash": "ccfed494a8d89eb2c88d72738c341a4dd87701b0636eb9fa001cd1d9cbcd663b"
}

View File

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

View File

@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT debounce_batch FROM v2_job_debounce_batch WHERE id = ANY($1) ORDER BY debounce_batch",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "debounce_batch",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"UuidArray"
]
},
"nullable": [
false
]
},
"hash": "d9400849888dd021b0504b93004ab5e76296ed437c210942363ba06845f9f963"
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,20 @@
{
"db_name": "PostgreSQL",
"query": "SELECT email FROM password ORDER BY email LIMIT 1000",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "email",
"type_info": "Varchar"
}
],
"parameters": {
"Left": []
},
"nullable": [
false
]
},
"hash": "f11d3a43f804a33e412a69be54824d7666d4dac0b824095c4c4fefb1f802c6f7"
}

View File

@@ -1,17 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO v2_job (id, kind, tag, created_by, permissioned_as, permissioned_as_email, workspace_id, runnable_path, args)\n VALUES ($1, 'flow', 'flow', 'test-user', 'u/test-user', 'test@windmill.dev', $2, $3, $4)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Varchar",
"Varchar",
"Jsonb"
]
},
"nullable": []
},
"hash": "f8cec94b94098e752f7c71cbe5e9996410a07bacebc37ed21e0bedf1a33a8fdc"
}

View File

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

View File

@@ -1,8 +0,0 @@
panes:
# Pane 1: Install dependencies, then start dev server
- command: cargo run
# Pane 2: AI agent
- command: <agent>
split: horizontal
focus: true

View File

@@ -44,22 +44,11 @@ Windmill uses a workspace-based architecture with multiple crates:
## 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
- Enterprise source is in `windmill-ee-private` folder (sibling directory at `../../windmill-ee-private`), symlinked into each crate's `src/`
- 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.

459
backend/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,12 @@
[package]
name = "windmill"
version = "1.642.0"
version = "1.636.0"
authors.workspace = true
edition.workspace = true
[workspace]
resolver = "2"
members = [
"./windmill-object-store",
"./windmill-api",
"./windmill-api-scripts",
"./windmill-api-flows",
@@ -76,7 +75,7 @@ members = [
exclude = ["./windmill-duckdb-ffi-internal"]
[workspace.package]
version = "1.642.0"
version = "1.636.0"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"
@@ -97,19 +96,19 @@ lto = "thin"
[features]
default = []
private = ["windmill-api/private", "windmill-api-agent-workers?/private", "windmill-autoscaling/private", "windmill-common/private", "windmill-object-store/private", "windmill-git-sync/private", "windmill-indexer/private", "windmill-operator?/private", "windmill-queue/private", "windmill-worker/private", "windmill-test-utils/private"]
private = ["windmill-api/private", "windmill-api-agent-workers?/private", "windmill-autoscaling/private", "windmill-common/private", "windmill-git-sync/private", "windmill-indexer/private", "windmill-operator?/private", "windmill-queue/private", "windmill-worker/private", "windmill-test-utils/private"]
agent_worker_server = ["windmill-api/agent_worker_server", "dep:windmill-api-agent-workers", "windmill-test-utils/agent_worker_server"]
enterprise = ["windmill-worker/enterprise", "windmill-queue/enterprise", "windmill-api/enterprise", "windmill-api-agent-workers?/enterprise", "dep:windmill-autoscaling", "windmill-autoscaling/enterprise", "windmill-git-sync/enterprise", "windmill-common/prometheus", "windmill-common/enterprise", "windmill-object-store/enterprise"]
enterprise = ["windmill-worker/enterprise", "windmill-queue/enterprise", "windmill-api/enterprise", "windmill-api-agent-workers?/enterprise", "dep:windmill-autoscaling", "windmill-autoscaling/enterprise", "windmill-git-sync/enterprise", "windmill-common/prometheus", "windmill-common/enterprise"]
local_reports = ["windmill-common/local_reports"]
enterprise_saml = ["windmill-api/enterprise_saml", "oauth2"]
stripe = ["windmill-api/stripe"]
benchmark = ["windmill-api/benchmark", "windmill-worker/benchmark", "windmill-queue/benchmark", "windmill-common/benchmark", "windmill-api-agent-workers?/benchmark"]
benchmark = ["windmill-api/benchmark", "windmill-worker/benchmark", "windmill-queue/benchmark", "windmill-common/benchmark"]
embedding = ["windmill-api/embedding"]
parquet = ["windmill-api/parquet", "windmill-common/parquet", "windmill-object-store/parquet", "windmill-worker/parquet"]
parquet = ["windmill-api/parquet", "windmill-common/parquet", "windmill-worker/parquet", "dep:object_store"]
prometheus = ["windmill-common/prometheus", "windmill-api/prometheus", "windmill-worker/prometheus", "windmill-queue/prometheus", "dep:prometheus"]
flow_testing = ["windmill-worker/flow_testing"]
quickjs = ["windmill-worker/quickjs", "windmill-api/quickjs"]
openidconnect = ["windmill-api/openidconnect", "windmill-common/openidconnect", "windmill-object-store/openidconnect"]
openidconnect = ["windmill-api/openidconnect", "windmill-common/openidconnect"]
cloud = ["windmill-queue/cloud", "windmill-worker/cloud", "windmill-common/cloud", "windmill-api/cloud"]
jemalloc = ["windmill-common/jemalloc", "dep:tikv-jemallocator", "dep:tikv-jemalloc-sys", "dep:tikv-jemalloc-ctl"]
tantivy = ["dep:windmill-indexer", "windmill-api/tantivy", "windmill-indexer/enterprise", "windmill-indexer/parquet", "windmill-common/tantivy", "enterprise", "parquet"]
@@ -200,7 +199,6 @@ tokio-stream.workspace = true
dotenv.workspace = true
windmill-queue.workspace = true
windmill-common = { workspace = true, default-features = false }
windmill-object-store.workspace = true
windmill-git-sync.workspace = true
windmill-api = { workspace = true, default-features = false }
windmill-api-agent-workers = { workspace = true, optional = true }
@@ -230,6 +228,7 @@ serde_derive.workspace = true
serde_yml.workspace = true
serde.workspace = true
windmill-runtime-nativets = { workspace = true, optional = true }
object_store = { workspace = true, optional = true }
sha1 = { workspace = true, optional = true }
constant_time_eq = { workspace = true, optional = true }
rustls.workspace = true
@@ -254,7 +253,6 @@ axum.workspace = true
serde.workspace = true
windmill-api-client.workspace = true
tempfile.workspace = true
windmill-parser-ts.workspace = true
rumqttc.workspace = true
rdkafka.workspace = true
async-nats.workspace = true
@@ -270,7 +268,6 @@ windmill-worker = { path = "./windmill-worker" }
windmill-dep-map = { path = "./windmill-dep-map" }
windmill-types = { path = "./windmill-types" }
windmill-common = { path = "./windmill-common", default-features = false }
windmill-object-store = { path = "./windmill-object-store" }
windmill-audit = { path = "./windmill-audit" }
windmill-git-sync = { path = "./windmill-git-sync" }
windmill-autoscaling = { path = "./windmill-autoscaling" }
@@ -480,7 +477,7 @@ bit-vec = "=0.6.3"
mappable-rc = "^0"
mysql_async = { version = "*", default-features = false, features = ["minimal", "default", "native-tls-tls", "rust_decimal"]}
postgres-native-tls = "^0"
native-tls = ">=0.2, <0.2.17"
native-tls = "^0"
# samael will break compilation on MacOS. Use this fork instead to make it work
# samael = { git="https://github.com/njaremko/samael", rev="464d015e3ae393e4b5dd00b4d6baa1b617de0dd6", features = ["xmlsec"] }
libxml = { version = "=0.3.3" }

View File

@@ -1 +1 @@
0fede4b1086bc1456be9cc55b203228c979c5c5e
9f6e1e533df7711600ec2b8d5f0c958448db1a20

View File

@@ -1 +0,0 @@
ALTER TABLE worker_ping DROP COLUMN IF EXISTS native_mode;

View File

@@ -1 +0,0 @@
ALTER TABLE worker_ping ADD COLUMN IF NOT EXISTS native_mode BOOLEAN NOT NULL DEFAULT false;

View File

@@ -16,7 +16,6 @@ regex.workspace = true
[dependencies]
windmill-parser.workspace = true
windmill-types.workspace = true
anyhow.workspace = true
lazy_static.workspace = true
serde_json.workspace = true

View File

@@ -2,8 +2,8 @@ use std::collections::BTreeMap;
use sqlparser::{
ast::{
CopyTarget, Expr, FunctionArg, FunctionArgExpr, ObjectName, ObjectNamePart, SelectItem,
TableFactor, TableObject, Value, ValueWithSpan, Visit, Visitor,
CopyTarget, Expr, ObjectName, ObjectNamePart, SelectItem, TableFactor, TableObject, Value,
ValueWithSpan, Visit, Visitor,
},
dialect::DuckDbDialect,
parser::Parser,
@@ -125,72 +125,6 @@ impl AssetCollector {
Some(ParseAssetsResult { kind: *kind, access_type, path, columns: None })
}
/// If `table_factor` is a string literal used directly as a table name (e.g. FROM 's3:///file.parquet'),
/// return a `ParseAssetsResult` for it.
fn get_s3_asset_from_str_literal_table(
&self,
table_factor: &TableFactor,
) -> Option<ParseAssetsResult> {
let name = match table_factor {
TableFactor::Table { name, args: None, .. } => name,
_ => return None,
};
let s3_str = get_str_lit_from_obj_name(name)?;
let (kind, path) = parse_asset_syntax(s3_str, false)?;
if kind != AssetKind::S3Object {
return None;
}
Some(ParseAssetsResult {
kind,
path: path.to_string(),
access_type: Some(R),
columns: None,
})
}
/// If `table_factor` is a read function (read_parquet/read_csv/read_json) whose first
/// positional argument is an S3 string literal, return a `ParseAssetsResult` for it.
fn get_s3_asset_from_table_function(
&self,
table_factor: &TableFactor,
) -> Option<ParseAssetsResult> {
let (name, args) = match table_factor {
TableFactor::Table { name, args: Some(args), .. } => (name, args),
_ => return None,
};
let fname = get_trivial_obj_name(name)?;
if !is_read_fn(fname) {
return None;
}
let s3_str = args.args.first().and_then(|arg| match arg {
FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(ValueWithSpan {
value: Value::SingleQuotedString(s),
..
})))
| FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(ValueWithSpan {
value: Value::DoubleQuotedString(s),
..
}))) => Some(s.as_str()),
_ => None,
})?;
let (kind, path) = parse_asset_syntax(s3_str, false)?;
if kind != AssetKind::S3Object {
return None;
}
Some(ParseAssetsResult {
kind,
path: path.to_string(),
access_type: Some(R),
columns: None,
})
}
fn handle_string_literal(&mut self, s: &str) {
// Check if the string matches our asset syntax patterns
if let Some((kind, path)) = parse_asset_syntax(s, false) {
@@ -256,19 +190,13 @@ impl AssetCollector {
projection: &[SelectItem],
from_tables: &[sqlparser::ast::TableWithJoins],
) {
// Check if this is a single-table SELECT (to avoid ambiguity).
// For S3 table functions (read_parquet/read_csv/read_json), detect the asset even
// though args are present, since we know the file path from the string literal arg.
// Check if this is a single-table SELECT (to avoid ambiguity)
let single_table = if from_tables.len() == 1 {
let relation = &from_tables[0].relation;
if let TableFactor::Table { name, args, .. } = relation {
let has_args = args.as_ref().map_or(false, |a| !a.args.is_empty());
if has_args {
self.get_s3_asset_from_table_function(relation)
} else {
self.get_associated_asset_from_obj_name(name, Some(R))
.or_else(|| self.get_s3_asset_from_str_literal_table(relation))
if let TableFactor::Table { name, args, .. } = &from_tables[0].relation {
if args.is_some() && args.as_ref().map_or(0, |a| a.args.len()) > 0 {
return; // Skip table functions
}
self.get_associated_asset_from_obj_name(name, Some(R))
} else {
None
}
@@ -276,48 +204,26 @@ impl AssetCollector {
None
};
// Build a map of table aliases/names to assets for multi-table queries.
// For S3 table functions, only aliased references are unambiguous
// (e.g. SELECT t.col1 FROM read_parquet('s3://...') AS t).
// Build a map of table aliases/names to assets for multi-table queries
let mut table_to_asset: BTreeMap<String, ParseAssetsResult> = BTreeMap::new();
for table_with_joins in from_tables {
if let TableFactor::Table { name, alias, args, .. } = &table_with_joins.relation {
let has_args = args.as_ref().map_or(false, |a| !a.args.is_empty());
if has_args {
// For table functions, only add to the alias map when an alias is present
if let Some(alias) = alias {
if let Some(asset) =
self.get_s3_asset_from_table_function(&table_with_joins.relation)
{
table_to_asset.insert(alias.name.value.clone(), asset);
}
}
} else if let Some(asset) = self
.get_associated_asset_from_obj_name(name, Some(R))
.or_else(|| {
self.get_s3_asset_from_str_literal_table(&table_with_joins.relation)
})
{
// For string literal S3 tables (e.g. FROM 's3:///file.parquet'), only add to
// the alias map when an alias is present (to avoid false positives).
// For regular named tables, use alias or table name as key.
let is_str_literal = get_str_lit_from_obj_name(name).is_some();
if is_str_literal {
if let Some(alias) = alias {
table_to_asset.insert(alias.name.value.clone(), asset);
}
if args.is_some() && args.as_ref().map_or(0, |a| a.args.len()) > 0 {
continue; // Skip table functions
}
if let Some(asset) = self.get_associated_asset_from_obj_name(name, Some(R)) {
// Use alias if present, otherwise use the table name
let table_key = if let Some(alias) = alias {
alias.name.value.clone()
} else {
let table_key = if let Some(alias) = alias {
alias.name.value.clone()
} else {
name.0
.last()
.and_then(|id| id.as_ident())
.map(|id| id.value.clone())
.unwrap_or_default()
};
table_to_asset.insert(table_key, asset);
}
// For qualified names like "dl.table1", use just the last part
name.0
.last()
.and_then(|id| id.as_ident())
.map(|id| id.value.clone())
.unwrap_or_default()
};
table_to_asset.insert(table_key, asset);
}
}
}
@@ -1365,163 +1271,4 @@ mod tests {
assert_eq!(columns.get("age"), Some(&W)); // Only written
assert_eq!(columns.get("id"), Some(&R)); // Only read
}
#[test]
fn test_sql_asset_parser_s3_single_table_column_detection() {
let input = r#"
SELECT col1, col2 FROM read_parquet('s3:///example_file.parquet');
"#;
let result = parse_assets(input).unwrap().assets;
assert_eq!(result.len(), 1);
assert_eq!(result[0].kind, AssetKind::S3Object);
assert_eq!(result[0].path, "/example_file.parquet");
assert_eq!(result[0].access_type, Some(R));
let columns = result[0].columns.as_ref().expect("Should have columns");
assert_eq!(columns.len(), 2);
assert_eq!(columns.get("col1"), Some(&R));
assert_eq!(columns.get("col2"), Some(&R));
}
#[test]
fn test_sql_asset_parser_s3_single_table_column_with_alias() {
let input = r#"
SELECT col1 AS c1, col2 AS c2 FROM read_parquet('s3:///example_file.parquet');
"#;
let result = parse_assets(input).unwrap().assets;
assert_eq!(result.len(), 1);
let columns = result[0].columns.as_ref().expect("Should have columns");
assert_eq!(columns.get("col1"), Some(&R));
assert_eq!(columns.get("col2"), Some(&R));
}
#[test]
fn test_sql_asset_parser_s3_wildcard_no_columns() {
let input = r#"
SELECT * FROM read_parquet('s3:///example_file.parquet');
"#;
let result = parse_assets(input).unwrap().assets;
assert_eq!(result.len(), 1);
assert_eq!(result[0].kind, AssetKind::S3Object);
assert_eq!(result[0].path, "/example_file.parquet");
assert!(result[0].columns.is_none());
}
#[test]
fn test_sql_asset_parser_s3_table_alias_qualified_columns() {
let input = r#"
SELECT t.col1, t.col2 FROM read_parquet('s3:///example_file.parquet') AS t;
"#;
let result = parse_assets(input).unwrap().assets;
assert_eq!(result.len(), 1);
assert_eq!(result[0].kind, AssetKind::S3Object);
assert_eq!(result[0].path, "/example_file.parquet");
let columns = result[0].columns.as_ref().expect("Should have columns");
assert_eq!(columns.get("col1"), Some(&R));
assert_eq!(columns.get("col2"), Some(&R));
}
#[test]
fn test_sql_asset_parser_s3_multi_table_aliased_columns() {
let input = r#"
SELECT t1.col1, t2.col2
FROM read_parquet('s3:///file1.parquet') AS t1,
read_csv('s3://bucket/file2.csv') AS t2;
"#;
let result = parse_assets(input).unwrap().assets;
assert_eq!(result.len(), 2);
assert!(result.iter().any(|a| {
a.path == "/file1.parquet"
&& a.columns.as_ref().map_or(false, |c| c.contains_key("col1"))
}));
assert!(result.iter().any(|a| {
a.path == "bucket/file2.csv"
&& a.columns.as_ref().map_or(false, |c| c.contains_key("col2"))
}));
}
#[test]
fn test_sql_asset_parser_s3_multi_table_no_alias_no_columns() {
// Without aliases, unqualified columns in a multi-table query are ambiguous
let input = r#"
SELECT col1, col2
FROM read_parquet('s3:///file1.parquet'),
read_parquet('s3:///file2.parquet');
"#;
let result = parse_assets(input).unwrap().assets;
// Table-level assets should still be detected, but no columns
assert_eq!(result.iter().filter(|a| a.columns.is_some()).count(), 0);
}
#[test]
fn test_sql_asset_parser_s3_str_literal_table_column_detection() {
// FROM 's3:///file.parquet' (string literal as table, no read_parquet wrapper)
let input = r#"
SELECT a,b,c FROM 's3:///test.parquet';
"#;
let result = parse_assets(input).unwrap().assets;
assert_eq!(result.len(), 1);
assert_eq!(result[0].kind, AssetKind::S3Object);
assert_eq!(result[0].path, "/test.parquet");
assert_eq!(result[0].access_type, Some(R));
let columns = result[0].columns.as_ref().expect("Should have columns");
assert_eq!(columns.len(), 3);
assert_eq!(columns.get("a"), Some(&R));
assert_eq!(columns.get("b"), Some(&R));
assert_eq!(columns.get("c"), Some(&R));
}
#[test]
fn test_sql_asset_parser_s3_str_literal_table_with_alias_columns() {
let input = r#"
SELECT t.col1, t.col2 FROM 's3://bucket/file.parquet' AS t;
"#;
let result = parse_assets(input).unwrap().assets;
assert_eq!(result.len(), 1);
assert_eq!(result[0].kind, AssetKind::S3Object);
assert_eq!(result[0].path, "bucket/file.parquet");
let columns = result[0].columns.as_ref().expect("Should have columns");
assert_eq!(columns.get("col1"), Some(&R));
assert_eq!(columns.get("col2"), Some(&R));
}
#[test]
fn test_sql_asset_parser_s3_str_literal_wildcard_no_columns() {
let input = r#"
SELECT * FROM 's3:///test.parquet';
"#;
let result = parse_assets(input).unwrap().assets;
assert_eq!(result.len(), 1);
assert_eq!(result[0].kind, AssetKind::S3Object);
assert!(result[0].columns.is_none());
}
#[test]
fn test_sql_asset_parser_s3_read_csv_columns() {
let input = r#"
SELECT name, age FROM read_csv('s3://my-bucket/data.csv');
"#;
let result = parse_assets(input).unwrap().assets;
assert_eq!(result.len(), 1);
assert_eq!(result[0].kind, AssetKind::S3Object);
assert_eq!(result[0].path, "my-bucket/data.csv");
let columns = result[0].columns.as_ref().expect("Should have columns");
assert_eq!(columns.get("name"), Some(&R));
assert_eq!(columns.get("age"), Some(&R));
}
}

View File

@@ -141,7 +141,19 @@ pub fn parse_db_resource(code: &str) -> Option<String> {
cap.map(|x| x.get(1).map(|x| x.as_str().to_string()).unwrap())
}
pub use windmill_types::s3::{s3_mode_extension, S3ModeFormat};
#[derive(Clone, Copy, Debug)]
pub enum S3ModeFormat {
Json,
Csv,
Parquet,
}
pub fn s3_mode_extension(format: S3ModeFormat) -> &'static str {
match format {
S3ModeFormat::Json => "json",
S3ModeFormat::Csv => "csv",
S3ModeFormat::Parquet => "parquet",
}
}
pub struct S3ModeArgs {
pub prefix: Option<String>,
pub storage: Option<String>,

View File

@@ -59,6 +59,3 @@ wasm-bindgen.workspace = true
serde_json.workspace = true
getrandom = { workspace = true, features = ["js"] }
# 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"] }

View File

@@ -47,8 +47,8 @@ use windmill_common::{
JWT_SECRET_SETTING, KEEP_JOB_DIR_SETTING, LICENSE_KEY_SETTING, MAVEN_REPOS_SETTING,
MAVEN_SETTINGS_XML_SETTING, MONITOR_LOGS_ON_OBJECT_STORE_SETTING, NO_DEFAULT_MAVEN_SETTING,
NPM_CONFIG_REGISTRY_SETTING, NUGET_CONFIG_SETTING, OAUTH_SETTING, OTEL_SETTING,
OTEL_TRACING_PROXY_SETTING, PIP_INDEX_URL_SETTING, POWERSHELL_REPO_PAT_SETTING,
POWERSHELL_REPO_URL_SETTING, REQUEST_SIZE_LIMIT_SETTING,
OTEL_TRACING_PROXY_SETTING, PIP_INDEX_URL_SETTING,
POWERSHELL_REPO_PAT_SETTING, POWERSHELL_REPO_URL_SETTING, REQUEST_SIZE_LIMIT_SETTING,
REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING, RETENTION_PERIOD_SECS_SETTING,
RUBY_REPOS_SETTING, SAML_METADATA_SETTING, SCIM_TOKEN_SETTING, SMTP_SETTING, TEAMS_SETTING,
TIMEOUT_WAIT_RESULT_SETTING, UV_INDEX_STRATEGY_SETTING,
@@ -61,8 +61,8 @@ use windmill_common::{
MODE_AND_ADDONS,
},
worker::{
is_native_mode_from_env, reload_custom_tags_setting, Connection, HUB_CACHE_DIR,
HUB_RT_CACHE_DIR, NATIVE_MODE_RESOLVED, TMP_DIR, TMP_LOGS_DIR, WORKER_GROUP,
reload_custom_tags_setting, Connection, HUB_CACHE_DIR, HUB_RT_CACHE_DIR, TMP_DIR,
TMP_LOGS_DIR, WORKER_GROUP,
},
KillpillSender, DEFAULT_HUB_BASE_URL, METRICS_ENABLED,
};
@@ -108,7 +108,7 @@ use crate::monitor::{
};
#[cfg(feature = "parquet")]
use windmill_object_store::reload_object_store_setting;
use windmill_common::s3_helpers::reload_object_store_setting;
const DEFAULT_NUM_WORKERS: usize = 1;
const DEFAULT_PORT: u16 = 8000;
@@ -485,7 +485,8 @@ fn print_help() {
println!(" cache [hubPaths.json] Pre-cache hub scripts (default: ./hubPaths.json)");
println!(" cache-rt Pre-cache hub resource types");
println!(" sync-config <file> Sync instance config from a YAML file to the database");
println!(" operator Run the Kubernetes operator (watches a ConfigMap)");
println!(" operator Run the Kubernetes operator (watches WindmillInstance CRDs)");
println!(" operator crd Print the WindmillInstance CRD YAML to stdout");
println!();
println!("Environment variables (name = default):");
println!(" DATABASE_URL = <required> The Postgres database url.");
@@ -632,11 +633,17 @@ async fn windmill_main() -> anyhow::Result<()> {
}
#[cfg(feature = "operator")]
"operator" => {
let sub_arg = std::env::args().nth(2).unwrap_or_default();
if sub_arg == "crd" {
windmill_operator::print_crd_yaml();
return Ok(());
}
tracing_subscriber::fmt::init();
tracing::info!("Starting Windmill Kubernetes operator...");
tracing::info!("Connecting to database...");
let db = crate::db_connect::initial_connection().await?;
tracing::info!("Database connected. Starting ConfigMap watcher...");
tracing::info!("Database connected. Starting controller...");
windmill_operator::run(db).await?;
return Ok(());
}
@@ -646,9 +653,6 @@ async fn windmill_main() -> anyhow::Result<()> {
#[allow(unused_mut)]
let mut num_workers = if mode == Mode::Server || mode == Mode::Indexer || mode == Mode::MCP {
0
} else if is_native_mode_from_env() {
println!("Native mode enabled: forcing NUM_WORKERS=8");
8
} else {
std::env::var("NUM_WORKERS")
.ok()
@@ -656,21 +660,11 @@ async fn windmill_main() -> anyhow::Result<()> {
.unwrap_or(DEFAULT_NUM_WORKERS as i32)
};
if num_workers > 1 && !is_native_mode_from_env() {
if std::env::var("I_ACK_NUM_WORKERS_IS_UNSAFE").is_ok_and(|x| x == "1" || x == "true") {
println!(
"WARNING: Running with NUM_WORKERS={} without native mode. \
This is not recommended. Use at your own risk.",
num_workers
);
} else {
eprintln!(
"WARNING: NUM_WORKERS={} > 1 is only safe for native workers. \
Falling back to NUM_WORKERS=1. Set NATIVE_MODE=true for native-only workers.",
num_workers
);
num_workers = 1;
}
// TODO: maybe gate behind debug_assertions?
if num_workers > 1 && !std::env::var("WORKER_GROUP").is_ok_and(|x| x == "native") {
println!(
"We STRONGLY recommend using at most 1 worker per container, use at your own risks"
);
}
let server_mode = !std::env::var("DISABLE_SERVER")
@@ -930,16 +924,6 @@ Windmill Community Edition {GIT_VERSION}
)
.await;
// native_mode may also be set via DB worker group config (not just env).
// NATIVE_MODE_RESOLVED is updated by load_worker_config during initial_load.
if worker_mode
&& !is_native_mode_from_env()
&& NATIVE_MODE_RESOLVED.load(std::sync::atomic::Ordering::Relaxed)
{
num_workers = 8;
tracing::info!("Native mode detected from worker config: forcing NUM_WORKERS=8");
}
monitor_db(
&conn,
&base_internal_url,

View File

@@ -39,6 +39,8 @@ use windmill_common::ee_oss::{jobs_waiting_alerts, worker_groups_alerts};
#[cfg(feature = "oauth2")]
use windmill_common::global_settings::OAUTH_SETTING;
#[cfg(feature = "parquet")]
use windmill_common::s3_helpers::reload_object_store_setting;
use windmill_common::{
agent_workers::DECODED_AGENT_TOKEN,
apps::APP_WORKSPACED_ROUTE,
@@ -54,7 +56,7 @@ use windmill_common::{
HUB_API_SECRET_SETTING, HUB_BASE_URL_SETTING, INSTANCE_PYTHON_VERSION_SETTING,
JOB_DEFAULT_TIMEOUT_SECS_SETTING, JOB_ISOLATION_SETTING, JWT_SECRET_SETTING,
KEEP_JOB_DIR_SETTING, LICENSE_KEY_SETTING, MONITOR_LOGS_ON_OBJECT_STORE_SETTING,
NPMRC_SETTING, NPM_CONFIG_REGISTRY_SETTING, NUGET_CONFIG_SETTING, OTEL_SETTING,
NPM_CONFIG_REGISTRY_SETTING, NUGET_CONFIG_SETTING, OTEL_SETTING,
OTEL_TRACING_PROXY_SETTING, PIP_INDEX_URL_SETTING, POWERSHELL_REPO_PAT_SETTING,
POWERSHELL_REPO_URL_SETTING, REQUEST_SIZE_LIMIT_SETTING,
REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING, RETENTION_PERIOD_SECS_SETTING,
@@ -82,20 +84,18 @@ use windmill_common::{
OTEL_METRICS_ENABLED, OTEL_TRACING_ENABLED, SERVICE_LOG_RETENTION_SECS,
};
use windmill_common::{client::AuthedClient, global_settings::APP_WORKSPACED_ROUTE_SETTING};
#[cfg(feature = "parquet")]
use windmill_object_store::reload_object_store_setting;
use windmill_queue::{cancel_job, get_queued_job_v2, SameWorkerPayload};
use windmill_worker::{
result_processor::handle_job_error, JobCompletedSender, JobIsolationLevel,
OtelTracingProxySettings, SameWorkerSender, BUNFIG_INSTALL_SCOPES, CARGO_REGISTRIES,
INSTANCE_PYTHON_VERSION, JAVA_HOME_DIR, JOB_DEFAULT_TIMEOUT, JOB_ISOLATION, KEEP_JOB_DIR,
MAVEN_REPOS, MAVEN_SETTINGS_XML, NO_DEFAULT_MAVEN, NPMRC, NPM_CONFIG_REGISTRY,
NSJAIL_AVAILABLE, NUGET_CONFIG, OTEL_TRACING_PROXY_SETTINGS, PIP_EXTRA_INDEX_URL,
PIP_INDEX_URL, POWERSHELL_REPO_PAT, POWERSHELL_REPO_URL, UV_INDEX_STRATEGY,
MAVEN_REPOS, MAVEN_SETTINGS_XML, NO_DEFAULT_MAVEN, NPM_CONFIG_REGISTRY, NSJAIL_AVAILABLE,
NUGET_CONFIG, OTEL_TRACING_PROXY_SETTINGS, PIP_EXTRA_INDEX_URL, PIP_INDEX_URL,
POWERSHELL_REPO_PAT, POWERSHELL_REPO_URL, UV_INDEX_STRATEGY,
};
#[cfg(feature = "parquet")]
use windmill_object_store::ObjectStoreReload;
use windmill_common::s3_helpers::ObjectStoreReload;
#[cfg(feature = "enterprise")]
use crate::ee_oss::verify_license_key;
@@ -239,16 +239,11 @@ pub async fn initial_load(
Connection::Http(_) => {
// TODO: reload worker config from http
let mut config = WORKER_CONFIG.write().await;
let worker_tags = DECODED_AGENT_TOKEN
.as_ref()
.map(|x| x.tags.clone())
.unwrap_or_default();
// we only check from env as native_mode is not stored in the token
let native_mode = windmill_common::worker::is_native_mode_from_env();
windmill_common::worker::NATIVE_MODE_RESOLVED
.store(native_mode, std::sync::atomic::Ordering::Relaxed);
*config = WorkerConfig {
worker_tags,
worker_tags: DECODED_AGENT_TOKEN
.as_ref()
.map(|x| x.tags.clone())
.unwrap_or_default(),
env_vars: load_env_vars(
load_whitelist_env_vars_from_env(),
&std::collections::HashMap::new(),
@@ -262,7 +257,6 @@ pub async fn initial_load(
cache_clear: None,
additional_python_paths: None,
pip_local_dependencies: None,
native_mode,
};
}
}
@@ -330,7 +324,6 @@ pub async fn initial_load(
reload_uv_index_strategy_setting(&conn).await;
reload_npm_config_registry_setting(&conn).await;
reload_bunfig_install_scopes_setting(&conn).await;
reload_npmrc_setting(&conn).await;
reload_instance_python_version_setting(&conn).await;
reload_nuget_config_setting(&conn).await;
reload_powershell_repo_url_setting(&conn).await;
@@ -713,7 +706,7 @@ async fn send_log_file_to_object_store(
}
#[cfg(feature = "parquet")]
let s3_client = windmill_object_store::get_object_store().await;
let s3_client = windmill_common::s3_helpers::get_object_store().await;
#[cfg(feature = "parquet")]
if let Some(s3_client) = s3_client {
let path = std::path::Path::new(TMP_WINDMILL_LOGS_SERVICE)
@@ -726,7 +719,7 @@ async fn send_log_file_to_object_store(
tracing::error!("Error reading log file: {:?}", e);
return;
}
let path = windmill_object_store::object_store_reexports::Path::from_url_path(format!(
let path = object_store::path::Path::from_url_path(format!(
"{}{hostname}/{highest_file}",
windmill_common::tracing_init::LOGS_SERVICE
));
@@ -1175,7 +1168,7 @@ async fn delete_log_files_from_disk_and_store(
_s3_prefix: &str,
) {
#[cfg(feature = "parquet")]
let os = windmill_object_store::get_object_store().await;
let os = windmill_common::s3_helpers::get_object_store().await;
#[cfg(not(feature = "parquet"))]
let os: Option<()> = None;
@@ -1205,10 +1198,7 @@ async fn delete_log_files_from_disk_and_store(
#[cfg(feature = "parquet")]
if _should_del_from_store {
if let Some(os) = _os2 {
let p = windmill_object_store::object_store_reexports::Path::from(format!(
"{}{}",
_s3_prefix, path
));
let p = object_store::path::Path::from(format!("{}{}", _s3_prefix, path));
if let Err(e) = os.delete(&p).await {
tracing::error!("Failed to delete from object store {}: {e}", p.to_string())
} else {
@@ -1307,10 +1297,6 @@ pub async fn reload_bunfig_install_scopes_setting(conn: &Connection) {
.await;
}
pub async fn reload_npmrc_setting(conn: &Connection) {
reload_option_setting_with_tracing(conn, NPMRC_SETTING, "NPMRC", NPMRC.clone()).await;
}
pub async fn reload_nuget_config_setting(conn: &Connection) {
reload_option_setting_with_tracing(
conn,
@@ -2256,11 +2242,6 @@ pub async fn reload_worker_config(db: &DB, tx: KillpillSender, kill_if_change: b
tracing::info!("Periodic script interval config changed, sending killpill. Expecting to be restarted by supervisor.");
let _ = tx.send();
}
if (*wc).native_mode != config.native_mode {
tracing::info!("Native mode config changed, sending killpill. Expecting to be restarted by supervisor.");
let _ = tx.send();
}
}
drop(wc);
@@ -2349,7 +2330,7 @@ pub async fn reload_base_url_setting(conn: &Connection) -> error::Result<()> {
async fn stale_job_cancellation(db: &Pool<Postgres>) {
if let Some(threshold) = *STALE_JOB_THRESHOLD_MINUTES {
let stale_jobs = sqlx::query!(
"SELECT v2_job_queue.id, v2_job.tag, v2_job_queue.scheduled_for, v2_job_queue.workspace_id FROM v2_job_queue LEFT JOIN v2_job ON v2_job_queue.id = v2_job.id WHERE running = false AND scheduled_for < now() - ($1 || ' minutes')::interval AND v2_job.trigger_kind IS DISTINCT FROM 'schedule'::job_trigger_kind",
"SELECT v2_job_queue.id, v2_job.tag, v2_job_queue.scheduled_for, v2_job_queue.workspace_id FROM v2_job_queue LEFT JOIN v2_job ON v2_job_queue.id = v2_job.id WHERE running = false AND scheduled_for < now() - ($1 || ' minutes')::interval",
threshold.to_string()
)
.fetch_all(db)

View File

@@ -7,7 +7,6 @@ REVERT="NO"
COPY="NO"
MOVE_NEW_FILES="NO"
EE_CODE_DIR="../windmill-ee-private/"
DIR_EXPLICIT="NO"
while [[ $# -gt 0 ]]; do
case $1 in
@@ -35,7 +34,6 @@ while [[ $# -gt 0 ]]; do
# Path to the local directory of the windmill-ee-private repository. By defaults, it
# assumes it is cloned next to the Windmill OSS repo.
EE_CODE_DIR="$2"
DIR_EXPLICIT="YES"
shift # past argument
shift # past value
;;
@@ -55,26 +53,9 @@ if [[ $EE_CODE_DIR == /* ]]; then
else
EE_CODE_DIR="${root_dirpath}/${EE_CODE_DIR}"
fi
# Fallback to ~/windmill-ee-private if the default location doesn't exist
if [ ! -d "${EE_CODE_DIR}" ]; then
EE_CODE_DIR="${HOME}/windmill-ee-private"
fi
# Unless --dir was explicitly set, try to find an EE worktree on the same branch
if [ "$DIR_EXPLICIT" == "NO" ] && [ -d "${HOME}/windmill-ee-private" ]; then
current_branch=$(git -C "${root_dirpath}" branch --show-current 2>/dev/null || true)
if [ -n "$current_branch" ]; then
ee_worktree=$(git -C "${HOME}/windmill-ee-private" worktree list 2>/dev/null \
| awk -v branch="[${current_branch}]" '$NF == branch {print $1; exit}')
if [ -n "$ee_worktree" ] && [ -d "$ee_worktree" ]; then
EE_CODE_DIR="$ee_worktree"
fi
fi
fi
echo "EE code directory = ${EE_CODE_DIR} | Revert = ${REVERT}"
if [ ! -d "${EE_CODE_DIR}" ]; then
echo "Windmill EE repo not found, please clone it next to this repository (or use the --dir option) and try again"
echo "> git clone git@github.com:windmill-labs/windmill-ee-private.git"

View File

@@ -1,474 +0,0 @@
#!/usr/bin/env bash
# End-to-end debounce tests against the running backend API
# Usage: BACKEND_PORT=8030 ./test_debounce_e2e.sh
set -uo pipefail
BASE="http://localhost:${BACKEND_PORT:-8030}/api"
W="admins"
EMAIL="admin@windmill.dev"
PASSWORD="changeme"
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
pass=0
fail=0
log_pass() { echo -e "${GREEN}PASS${NC}: $1"; ((pass++)) || true; }
log_fail() { echo -e "${RED}FAIL${NC}: $1$2"; ((fail++)) || true; }
log_info() { echo -e "${YELLOW}INFO${NC}: $1"; }
# Unique suffix for idempotent re-runs
TS=$(date +%s)
# --- Auth ---
log_info "Logging in..."
TOKEN=$(curl -s "$BASE/auth/login" \
-H 'Content-Type: application/json' \
-d "{\"email\":\"$EMAIL\",\"password\":\"$PASSWORD\"}")
if [ -z "$TOKEN" ]; then
echo "Failed to login"; exit 1
fi
AUTH="Authorization: Bearer $TOKEN"
log_info "Logged in"
# --- Helpers ---
api() {
# Usage: api METHOD path [data]
local method="$1" path="$2" data="${3:-}"
if [ -n "$data" ]; then
curl -s "$BASE/w/$W/$path" -X "$method" -H "$AUTH" -H 'Content-Type: application/json' -d "$data"
else
curl -s "$BASE/w/$W/$path" -X "$method" -H "$AUTH"
fi
}
wait_job() {
local job_id="$1" max_wait="${2:-30}"
for _ in $(seq 1 "$max_wait"); do
local r
r=$(api GET "jobs/completed/get_result_maybe/$job_id")
if echo "$r" | jq -e '.completed == true' > /dev/null 2>&1; then
echo "$r"; return 0
fi
sleep 1
done
echo '{"completed":false,"error":"timeout"}'; return 1
}
BUN_EMPTY_LOCK=$'{"dependencies": {}}\n//bun.lock\n'
create_script() {
# Usage: create_script path language content [extra_json_fields]
# Note: lock must be non-empty; empty string ("") is treated as None by the backend
# (scripts.rs:798-800), which triggers dependency resolution instead of direct deployment.
# For bun scripts, the lock must contain "//bun.lock" as a split pattern.
local path="$1" lang="$2" content="$3" extra="${4:-}"
local json
json=$(jq -n \
--arg path "$path" \
--arg lang "$lang" \
--arg content "$content" \
--arg summary "test" \
--arg desc "test" \
--arg lock "$BUN_EMPTY_LOCK" \
'{path: $path, language: $lang, content: $content, summary: $summary, description: $desc, lock: $lock}')
if [ -n "$extra" ]; then
json=$(echo "$json" | jq ". + $extra")
fi
local hash
hash=$(api POST "scripts/create" "$json")
# Small delay for DB visibility after tx commit
sleep 0.2
echo "$hash"
}
run_script() {
# Usage: run_script path args_json
api POST "jobs/run/p/$1" "$2"
}
###############################################################################
# TEST 1: Deploy a script and run it 5 times in close succession
###############################################################################
echo ""
log_info "=== TEST 1: Deploy & run script 5 times rapidly ==="
P1="u/admin/e2e_simple_$TS"
H1=$(create_script "$P1" "bun" 'export function main(x: number = 0) { return { result: x * 2 }; }')
if echo "$H1" | grep -qE '^[0-9a-f]{16}$'; then
log_pass "Script created: $H1"
else
log_fail "Script creation" "$H1"
fi
log_info "Running 5 times rapidly..."
JOB_IDS=()
for i in $(seq 1 5); do
JID=$(run_script "$P1" "{\"x\": $i}")
JOB_IDS+=("$JID")
done
log_info "Jobs: ${JOB_IDS[*]}"
log_info "Waiting for completion..."
all_ok=true
for i in "${!JOB_IDS[@]}"; do
JID="${JOB_IDS[$i]}"
R=$(wait_job "$JID" 30)
success=$(echo "$R" | jq -r '.success // false')
value=$(echo "$R" | jq -r '.result.result // "null"')
expected=$(( (i + 1) * 2 ))
if [ "$success" = "true" ] && [ "$value" = "$expected" ]; then
log_pass "Job $((i+1)): x=$((i+1))$value (correct)"
else
log_fail "Job $((i+1))" "success=$success value=$value expected=$expected"
all_ok=false
fi
done
if [ "$all_ok" = "true" ]; then
log_pass "All 5 runs completed correctly (no debounce — different args)"
fi
###############################################################################
# TEST 2: Redeploy script WITHOUT lock in close succession
###############################################################################
echo ""
log_info "=== TEST 2: Redeploy without lock in rapid succession ==="
P2="u/admin/e2e_nolock_$TS"
# Deploy 5 versions of the same script without lock → triggers dependency jobs
DEPLOY_HASHES=()
for i in $(seq 1 5); do
content="export function main(x: number = 0) { return { result: x * $i, version: $i }; }"
parent_extra=""
if [ "${#DEPLOY_HASHES[@]}" -gt 0 ]; then
last_hash="${DEPLOY_HASHES[-1]}"
parent_extra="{\"parent_hash\": \"$last_hash\"}"
fi
# Deploy without lock (omit lock field entirely)
json=$(jq -n \
--arg path "$P2" \
--arg content "$content" \
--arg summary "v$i" \
--arg desc "test" \
'{path: $path, language: "bun", content: $content, summary: $summary, description: $desc}')
if [ -n "$parent_extra" ]; then
json=$(echo "$json" | jq ". + $parent_extra")
fi
hash=$(api POST "scripts/create" "$json")
if echo "$hash" | grep -qE '^[0-9a-f]{16}$'; then
DEPLOY_HASHES+=("$hash")
log_info "Deploy $i: $hash"
else
log_fail "Deploy $i" "$hash"
# If path conflict, the script already exists from a previous version
break
fi
sleep 0.1
done
# Wait for dependency resolution
log_info "Waiting 15s for dependency jobs..."
sleep 15
# Check the latest script — should have lock resolved
SCRIPT_INFO=$(api GET "scripts/get/p/$P2")
LOCK=$(echo "$SCRIPT_INFO" | jq -r '.lock // "null"')
if [ "$LOCK" != "null" ] && [ -n "$LOCK" ]; then
log_pass "Latest version has lock resolved"
else
log_info "Lock not yet resolved: $LOCK"
fi
# Run the latest version to verify it works
sleep 0.5
JID2=$(run_script "$P2" '{"x": 10}')
if echo "$JID2" | grep -qE '^[0-9a-f-]{36}$'; then
R2=$(wait_job "$JID2" 30)
success=$(echo "$R2" | jq -r '.success // false')
if [ "$success" = "true" ]; then
version=$(echo "$R2" | jq -r '.result.version // "?"')
log_pass "Latest version runs: version=$version"
else
err=$(echo "$R2" | jq -r '.result.error.message // "unknown"' 2>/dev/null)
log_fail "Run latest version" "success=false err=$err"
fi
else
log_fail "Run latest version" "bad job id: $JID2"
fi
###############################################################################
# TEST 3: Script with debounce_delay_s — rapid runs with SAME args
###############################################################################
echo ""
log_info "=== TEST 3: Debounce with same args (should debounce) ==="
P3="u/admin/e2e_debounce_$TS"
H3=$(create_script "$P3" "bun" \
'export function main(x: number = 0) { return { result: x }; }' \
'{"debounce_delay_s": 3}')
if echo "$H3" | grep -qE '^[0-9a-f]{16}$'; then
log_pass "Debounce script created: $H3"
else
log_fail "Debounce script creation" "$H3"
fi
log_info "Running 5 times with same args {x: 42}..."
DEB_IDS=()
for i in $(seq 1 5); do
JID=$(run_script "$P3" '{"x": 42}')
DEB_IDS+=("$JID")
log_info " Run $i: $JID"
done
log_info "Waiting 10s for debounce delay (3s) + execution..."
sleep 10
executed=0
skipped=0
for JID in "${DEB_IDS[@]}"; do
if ! echo "$JID" | grep -qE '^[0-9a-f-]{36}$'; then
log_info " Invalid job id: $JID"
continue
fi
R=$(wait_job "$JID" 5 2>/dev/null || echo '{"completed":false}')
completed=$(echo "$R" | jq -r '.completed // false')
success=$(echo "$R" | jq -r '.success // false')
if [ "$completed" = "true" ] && [ "$success" = "true" ]; then
((executed++)) || true
elif [ "$completed" = "true" ]; then
((skipped++)) || true
fi
done
log_info "Results: $executed executed, $skipped skipped out of ${#DEB_IDS[@]}"
if [ "$executed" -eq 1 ] && [ "$skipped" -ge 3 ]; then
log_pass "Debouncing perfect: 1 executed, $skipped skipped"
elif [ "$executed" -le 2 ] && [ "$skipped" -ge 2 ]; then
log_pass "Debouncing working: $executed executed, $skipped skipped"
else
log_fail "Debounce same args" "executed=$executed skipped=$skipped (want ~1 exec, ~4 skip)"
fi
###############################################################################
# TEST 3b: Different args should NOT debounce against each other
###############################################################################
echo ""
log_info "=== TEST 3b: Debounce with different args (should NOT debounce) ==="
DIFF_IDS=()
for i in $(seq 1 3); do
JID=$(run_script "$P3" "{\"x\": $((i * 100))}")
DIFF_IDS+=("$JID")
done
log_info "Waiting 8s..."
sleep 8
diff_exec=0
for JID in "${DIFF_IDS[@]}"; do
if ! echo "$JID" | grep -qE '^[0-9a-f-]{36}$'; then continue; fi
R=$(wait_job "$JID" 5 2>/dev/null || echo '{"completed":false}')
success=$(echo "$R" | jq -r '.success // false')
if [ "$success" = "true" ]; then ((diff_exec++)) || true; fi
done
if [ "$diff_exec" -eq 3 ]; then
log_pass "Different args: all 3 executed independently"
else
log_fail "Different args" "only $diff_exec/3 executed"
fi
###############################################################################
# TEST 4: Custom debounce_key with $args interpolation
###############################################################################
echo ""
log_info "=== TEST 4: Custom debounce key ==="
P4="u/admin/e2e_custom_key_$TS"
H4=$(create_script "$P4" "bun" \
'export function main(event_id: string = "", data: string = "") { return { event_id, data }; }' \
'{"debounce_delay_s": 3, "debounce_key": "event#$args.event_id"}')
if echo "$H4" | grep -qE '^[0-9a-f]{16}$'; then
log_pass "Custom key script created: $H4"
else
log_fail "Custom key script creation" "$H4"
fi
# Same event_id → should debounce
log_info "3 runs with same event_id..."
SAME_IDS=()
for i in $(seq 1 3); do
JID=$(run_script "$P4" "{\"event_id\": \"evt_001\", \"data\": \"payload_$i\"}")
SAME_IDS+=("$JID")
done
# Different event_id → should NOT debounce
JID_DIFF=$(run_script "$P4" '{"event_id": "evt_002", "data": "different"}')
log_info "Waiting 8s..."
sleep 8
same_exec=0
same_skip=0
for JID in "${SAME_IDS[@]}"; do
if ! echo "$JID" | grep -qE '^[0-9a-f-]{36}$'; then continue; fi
R=$(wait_job "$JID" 5 2>/dev/null || echo '{"completed":false}')
completed=$(echo "$R" | jq -r '.completed // false')
success=$(echo "$R" | jq -r '.success // false')
if [ "$completed" = "true" ] && [ "$success" = "true" ]; then
data=$(echo "$R" | jq -r '.result.data // "?"')
((same_exec++)) || true
log_info " Executed: data=$data"
elif [ "$completed" = "true" ]; then
((same_skip++)) || true
fi
done
log_info "Same event_id: $same_exec executed, $same_skip skipped"
if [ "$same_exec" -eq 1 ] && [ "$same_skip" -ge 1 ]; then
log_pass "Custom key debounce: same event_id debounced correctly"
elif [ "$same_exec" -le 2 ]; then
log_pass "Custom key debounce working: $same_exec executed, $same_skip skipped"
else
log_fail "Custom key debounce" "exec=$same_exec skip=$same_skip"
fi
# Check different event_id ran independently
if echo "$JID_DIFF" | grep -qE '^[0-9a-f-]{36}$'; then
R_DIFF=$(wait_job "$JID_DIFF" 10 2>/dev/null || echo '{"completed":false}')
diff_success=$(echo "$R_DIFF" | jq -r '.success // false')
if [ "$diff_success" = "true" ]; then
log_pass "Different event_id: executed independently"
else
log_info "Different event_id: success=$diff_success"
fi
fi
###############################################################################
# TEST 5: Git sync with bad target — debounced deployment callbacks
###############################################################################
echo ""
log_info "=== TEST 5: Git sync debounce + aggregation ==="
# Create git repo resource
api POST "resources/create?update_if_exists=true" '{
"path": "u/admin/e2e_bad_git_repo",
"description": "Bad git repo for testing",
"resource_type": "git_repository",
"value": {"url": "https://github.com/nonexistent/nope.git", "branch": "main", "token": "bad"}
}' > /dev/null 2>&1
log_info "Created git repo resource"
# Create a sync script at a folder path where the 2nd segment is a number >= 28103.
# is_script_meets_min_version parses split("/").skip(1).next() as the version number.
# This enables debounce_delay_s=5 and debounce_args_to_accumulate=["items"].
api POST "folders/create" '{"name": "28103"}' > /dev/null 2>&1
P5="f/28103/e2e_sync_$TS"
H5=$(create_script "$P5" "bun" \
'export function main(repo_url_resource_path: string = "", workspace_id: string = "", items: any[] = [], use_individual_branch: boolean = false, group_by_folder: boolean = false, parent_workspace_id: string = "") { return { synced: items.length, items }; }')
if echo "$H5" | grep -qE '^[0-9a-f]{16}$'; then
log_pass "Sync script created: $H5"
else
log_fail "Sync script creation" "$H5"
fi
# Configure git sync with include_path to match deployed scripts.
# Without include_path, path_matches_filters returns false and no DeploymentCallback is created.
api POST "workspaces/edit_git_sync_config" "{
\"git_sync_settings\": {
\"include_type\": [\"script\"],
\"include_path\": [\"**\"],
\"repositories\": [{
\"script_path\": \"$P5\",
\"git_repo_resource_path\": \"\$res:u/admin/e2e_bad_git_repo\",
\"use_individual_branch\": false,
\"group_by_folder\": false
}]
}
}" > /dev/null 2>&1
log_pass "Git sync configured with include_path and versioned folder script path"
# Deploy 5 scripts rapidly to trigger git sync.
# Scripts are created with lock="" (via create_script), so handle_deployment_metadata
# fires immediately after tx commit (not after dependency resolution).
log_info "Deploying 5 scripts to trigger git sync..."
for i in $(seq 1 5); do
dp="u/admin/e2e_gitsync_${TS}_$i"
create_script "$dp" "bun" "export function main() { return { v: $i }; }" > /dev/null
log_info " Deployed $dp"
done
# Wait for debounce delay (5s) + execution
log_info "Waiting 15s for debounce (5s) + execution..."
sleep 15
# Check deployment callback jobs for our sync script.
# Debounced jobs have is_skipped=true (but success=true), so we use is_skipped to distinguish.
SYNC_JOBS=$(api GET "jobs/completed/list?script_path_exact=$P5&job_kinds=deploymentcallback")
SYNC_TOTAL=$(echo "$SYNC_JOBS" | jq 'length')
SYNC_EXECUTED=$(echo "$SYNC_JOBS" | jq '[.[] | select(.is_skipped != true)] | length')
SYNC_SKIPPED=$(echo "$SYNC_JOBS" | jq '[.[] | select(.is_skipped == true)] | length')
log_info "Sync jobs: total=$SYNC_TOTAL executed=$SYNC_EXECUTED skipped=$SYNC_SKIPPED"
if [ "$SYNC_TOTAL" -gt 0 ]; then
# With debouncing (5s delay), rapid deploys should be consolidated.
# All 5 jobs are created but most should be skipped (debounced).
if [ "$SYNC_SKIPPED" -gt 0 ]; then
log_pass "Git sync debouncing: $SYNC_EXECUTED executed, $SYNC_SKIPPED debounced out of $SYNC_TOTAL"
else
log_fail "Git sync debouncing" "No jobs were debounced ($SYNC_TOTAL all executed independently)"
fi
# Check if items were aggregated in the executed (non-skipped) job(s)
for idx in $(seq 0 $((SYNC_TOTAL - 1))); do
is_skipped=$(echo "$SYNC_JOBS" | jq -r ".[$idx].is_skipped")
[ "$is_skipped" = "true" ] && continue
jid=$(echo "$SYNC_JOBS" | jq -r ".[$idx].id")
r=$(api GET "jobs/completed/get_result/$jid")
items_count=$(echo "$r" | jq '.items | length // 0')
log_info " Executed sync job $jid: items=$items_count"
if [ "$items_count" -gt 1 ]; then
log_pass "Items aggregated: $items_count items in single sync job"
fi
done
else
# Check queued — jobs may still be pending debounce delay
Q=$(api GET "jobs/queue/list?script_path_exact=$P5&job_kinds=deploymentcallback")
QC=$(echo "$Q" | jq 'length')
log_info "No completed sync jobs. $QC queued."
if [ "$QC" -gt 0 ] && [ "$QC" -lt 5 ]; then
log_pass "Git sync debouncing (queued): $QC jobs for 5 deploys"
elif [ "$QC" -eq 0 ]; then
log_fail "Git sync" "No deployment callback jobs found (completed or queued)"
fi
fi
# Cleanup git sync
api POST "workspaces/edit_git_sync_config" '{"git_sync_settings": null}' > /dev/null 2>&1
log_info "Git sync config cleared"
###############################################################################
# Summary
###############################################################################
echo ""
echo "========================================="
echo -e "Results: ${GREEN}$pass passed${NC}, ${RED}$fail failed${NC}"
echo "========================================="
if [ "$fail" -gt 0 ]; then
exit 1
fi

View File

@@ -1,8 +1,8 @@
use windmill_test_utils::*;
use sqlx::postgres::Postgres;
use sqlx::Pool;
use windmill_common::jobs::{JobPayload, RawCode};
use windmill_common::scripts::ScriptLang;
use windmill_test_utils::*;
// ============================================================================
// Basic Execution Tests
@@ -27,8 +27,8 @@ export function main() {
path: None,
language: ScriptLang::Bun,
lock: None,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default().into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
@@ -63,8 +63,8 @@ export function main(name: string, count: number) {
path: None,
language: ScriptLang::Bun,
lock: None,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default().into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
@@ -104,9 +104,8 @@ export function main() {
path: None,
language: ScriptLang::Bun,
lock: None,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default(
)
.into(),
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default().into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
@@ -136,9 +135,8 @@ export function main() {
path: None,
language: ScriptLang::Bun,
lock: None,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default(
)
.into(),
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default().into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
@@ -169,9 +167,8 @@ export function main() {
path: None,
language: ScriptLang::Bun,
lock: None,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default(
)
.into(),
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default().into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
@@ -210,8 +207,8 @@ export async function main() {
path: None,
language: ScriptLang::Bun,
lock: None,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default().into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
@@ -248,9 +245,8 @@ export function main() {
path: None,
language: ScriptLang::Bun,
lock: None,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default(
)
.into(),
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default().into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
@@ -280,9 +276,8 @@ export function main() {
path: None,
language: ScriptLang::Bun,
lock: None,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default(
)
.into(),
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default().into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
@@ -323,8 +318,8 @@ export function main() {
path: None,
language: ScriptLang::Bun,
lock: None,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default().into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
@@ -363,8 +358,8 @@ export function notMain() {
path: None,
language: ScriptLang::Bun,
lock: None,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default().into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
@@ -403,8 +398,8 @@ export function main() {
path: None,
language: ScriptLang::Bun,
lock: None,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default().into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
@@ -442,8 +437,8 @@ export function main() {
path: None,
language: ScriptLang::Bun,
lock: None,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default().into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
@@ -479,8 +474,8 @@ export function main() {
path: None,
language: ScriptLang::Bun,
lock: None,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default().into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
@@ -521,8 +516,8 @@ export function main() {
path: None,
language: ScriptLang::Bun,
lock: None,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default().into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
@@ -618,9 +613,8 @@ export function main() {
path: Some("f/nested/test_deep".to_string()),
language: ScriptLang::Bun,
lock: None,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default(
)
.into(),
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default().into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
@@ -653,9 +647,8 @@ export function main() {
path: Some("f/nested/test_deep_relative".to_string()),
language: ScriptLang::Bun,
lock: None,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default(
)
.into(),
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default().into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
@@ -700,8 +693,8 @@ export function main() {
path: Some("f/circular/test_both".to_string()),
language: ScriptLang::Bun,
lock: None,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default().into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
@@ -748,8 +741,8 @@ export function main(x: number) {
path: None,
language: ScriptLang::Bun,
lock: None,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default().into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
@@ -798,8 +791,8 @@ export function main() {
path: None,
language: ScriptLang::Bun,
lock: None,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default().into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
@@ -843,8 +836,8 @@ export function main() {
path: None,
language: ScriptLang::Bun,
lock: None,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default().into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
@@ -866,11 +859,11 @@ export function main() {
// ============================================================================
mod dedicated_worker_protocol {
use windmill_test_utils::{parse_dedicated_worker_line, DedicatedWorkerResult};
use std::io::{BufRead, BufReader, Write};
use std::process::{Command, Stdio};
use windmill_test_utils::{parse_dedicated_worker_line, DedicatedWorkerResult};
use windmill_worker::{
build_loader, generate_dedicated_worker_wrapper, LoaderMode, BUN_DEDICATED_WORKER_ARGS,
build_loader, generate_dedicated_worker_wrapper, BUN_DEDICATED_WORKER_ARGS, LoaderMode,
BUN_PATH, NODE_BIN_PATH,
};
@@ -941,8 +934,12 @@ mod dedicated_worker_protocol {
let temp_dir = tempfile::tempdir().unwrap();
// Create files and get the wrapper path (bundled for node, raw for bun)
let wrapper_path =
create_test_worker_files(temp_dir.path(), script, arg_names, runtime == "node");
let wrapper_path = create_test_worker_files(
temp_dir.path(),
script,
arg_names,
runtime == "node",
);
let wrapper_str = wrapper_path.to_str().unwrap();
// Build args matching production behavior
@@ -995,10 +992,7 @@ mod dedicated_worker_protocol {
match parse_dedicated_worker_line(response.trim()) {
DedicatedWorkerResult::Success(value) => results.push(Ok(value)),
DedicatedWorkerResult::Error(err) => {
let msg = err["message"]
.as_str()
.unwrap_or("Unknown error")
.to_string();
let msg = err["message"].as_str().unwrap_or("Unknown error").to_string();
results.push(Err(msg));
}
other => panic!("Unexpected response: {:?}", other),
@@ -1168,8 +1162,8 @@ export function main(name: string) {
path: None,
language: ScriptLang::Bun,
lock: None,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default().into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
@@ -1196,68 +1190,6 @@ export function main(name: string) {
Ok(())
}
/// Test that full .npmrc content works for bun jobs with private registries.
/// Requires:
/// - `TEST_NPMRC` environment variable set to the full .npmrc content
#[cfg(feature = "private_registry_test")]
#[sqlx::test(fixtures("base"))]
async fn test_bun_job_private_npmrc(db: Pool<Postgres>) -> anyhow::Result<()> {
use windmill_worker::NPMRC;
let npmrc_content = std::env::var("TEST_NPMRC")
.expect("TEST_NPMRC must be set when running private_registry_test");
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
let port = server.addr.port();
{
let mut npmrc = NPMRC.write().await;
*npmrc = Some(npmrc_content.clone());
}
let content = r#"
import { greet } from "@windmill-test/private-pkg";
export function main(name: string) {
return greet(name);
}
"#
.to_owned();
let job = JobPayload::Code(RawCode {
hash: None,
content,
path: None,
language: ScriptLang::Bun,
lock: None,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
});
let result = RunJob::from(job)
.arg("name", serde_json::json!("World"))
.run_until_complete(&db, false, port)
.await
.json_result()
.unwrap();
{
let mut npmrc = NPMRC.write().await;
*npmrc = None;
}
assert_eq!(
result,
serde_json::json!("Hello from private package, World!")
);
Ok(())
}
/// Tests for RELATIVE_BUN_BUILDER (loader_builder.bun.js)
/// These tests verify Bun's behavior for import scanning and package.json generation.
/// Purpose: Catch regressions when upgrading Bun versions.
@@ -1309,8 +1241,8 @@ mod bun_builder_tests {
}
// Read generated package.json
let package_json =
std::fs::read_to_string(dir.join("package.json")).expect("package.json not generated");
let package_json = std::fs::read_to_string(dir.join("package.json"))
.expect("package.json not generated");
serde_json::from_str(&package_json).expect("Invalid JSON in package.json")
}
@@ -1325,10 +1257,7 @@ export function main() { return lodash; }
let pkg = run_builder(main_ts);
let deps = pkg["dependencies"].as_object().unwrap();
assert!(
deps.contains_key("lodash"),
"lodash should be in dependencies"
);
assert!(deps.contains_key("lodash"), "lodash should be in dependencies");
assert_eq!(deps["lodash"], "latest");
}
@@ -1342,10 +1271,7 @@ export function main() { return _; }
let pkg = run_builder(main_ts);
let deps = pkg["dependencies"].as_object().unwrap();
assert!(
deps.contains_key("lodash"),
"lodash should be in dependencies"
);
assert!(deps.contains_key("lodash"), "lodash should be in dependencies");
assert_eq!(deps["lodash"], "4.17.21");
}
@@ -1378,18 +1304,9 @@ export function main() { return { lodash, axios, dayjs }; }
let pkg = run_builder(main_ts);
let deps = pkg["dependencies"].as_object().unwrap();
assert!(
deps.contains_key("lodash"),
"lodash should be in dependencies"
);
assert!(
deps.contains_key("axios"),
"axios should be in dependencies"
);
assert!(
deps.contains_key("dayjs"),
"dayjs should be in dependencies"
);
assert!(deps.contains_key("lodash"), "lodash should be in dependencies");
assert!(deps.contains_key("axios"), "axios should be in dependencies");
assert!(deps.contains_key("dayjs"), "dayjs should be in dependencies");
assert_eq!(deps.len(), 3, "Should have exactly 3 dependencies");
}
@@ -1413,15 +1330,8 @@ export function main() { return { fs, path, lodash }; }
!deps.contains_key("path"),
"path (builtin) should NOT be in dependencies"
);
assert!(
deps.contains_key("lodash"),
"lodash should be in dependencies"
);
assert_eq!(
deps.len(),
1,
"Should have exactly 1 dependency (lodash only)"
);
assert!(deps.contains_key("lodash"), "lodash should be in dependencies");
assert_eq!(deps.len(), 1, "Should have exactly 1 dependency (lodash only)");
}
/// Test: semver.order() resolves version conflicts (picks lowest version)
@@ -1437,10 +1347,7 @@ export function main() { return { a, b }; }
let pkg = run_builder(main_ts);
let deps = pkg["dependencies"].as_object().unwrap();
assert!(
deps.contains_key("lodash"),
"lodash should be in dependencies"
);
assert!(deps.contains_key("lodash"), "lodash should be in dependencies");
// The builder sorts by semver and picks the first (lowest) version
assert_eq!(
deps["lodash"], "4.17.10",

View File

@@ -1,10 +0,0 @@
-- Fixture for testing wmill CLI variable/resource get from bash scripts
INSERT INTO variable (workspace_id, path, value, is_secret, description, extra_perms)
VALUES ('test-workspace', 'u/test-user/test_var', 'hello from variable', false, 'A test variable', '{"u/test-user": true}');
INSERT INTO resource_type (workspace_id, name, schema, description, created_by)
VALUES ('test-workspace', 'test_object', '{}', 'Test object type', 'test-user');
INSERT INTO resource (workspace_id, path, value, description, resource_type, extra_perms, created_by)
VALUES ('test-workspace', 'u/test-user/test_res', '{"host": "localhost", "port": 5432}', 'A test resource', 'test_object', '{"u/test-user": true}', 'test-user');

View File

@@ -240,7 +240,7 @@ async fn test_from_db_worker_config_prefix_stripping(db: Pool<Postgres>) {
config.worker_configs.contains_key("my_group_name"),
"worker__ prefix should be stripped"
);
assert_eq!(config.worker_configs["my_group_name"].extra["cache_clear"], serde_json::json!(5));
assert_eq!(config.worker_configs["my_group_name"].cache_clear, Some(5));
}
#[sqlx::test(fixtures("base"))]
@@ -280,7 +280,6 @@ async fn test_apply_settings_diff_upserts_only(db: Pool<Postgres>) {
m
},
deletes: vec![],
..Default::default()
};
apply_settings_diff(&db, &diff).await.unwrap();
@@ -304,7 +303,6 @@ async fn test_apply_settings_diff_deletes_only(db: Pool<Postgres>) {
let diff = SettingsDiff {
upserts: BTreeMap::new(),
deletes: vec!["to_delete_1".to_string(), "to_delete_2".to_string()],
..Default::default()
};
apply_settings_diff(&db, &diff).await.unwrap();
@@ -325,7 +323,6 @@ async fn test_apply_settings_diff_upserts_and_deletes(db: Pool<Postgres>) {
m
},
deletes: vec!["old_key".to_string()],
..Default::default()
};
apply_settings_diff(&db, &diff).await.unwrap();
@@ -361,7 +358,6 @@ async fn test_apply_settings_diff_upsert_overwrites(db: Pool<Postgres>) {
m
},
deletes: vec![],
..Default::default()
};
apply_settings_diff(&db, &diff).await.unwrap();
@@ -389,7 +385,6 @@ async fn test_apply_settings_diff_complex_json(db: Pool<Postgres>) {
m
},
deletes: vec![],
..Default::default()
};
apply_settings_diff(&db, &diff).await.unwrap();
@@ -401,11 +396,7 @@ async fn test_apply_settings_diff_complex_json(db: Pool<Postgres>) {
#[sqlx::test(fixtures("base"))]
async fn test_apply_settings_diff_delete_nonexistent_is_noop(db: Pool<Postgres>) {
let diff =
SettingsDiff {
upserts: BTreeMap::new(),
deletes: vec!["does_not_exist".to_string()],
..Default::default()
};
SettingsDiff { upserts: BTreeMap::new(), deletes: vec!["does_not_exist".to_string()] };
// Should not error
apply_settings_diff(&db, &diff).await.unwrap();
@@ -657,11 +648,7 @@ async fn test_roundtrip_to_settings_map_from_db_consistency(db: Pool<Postgres>)
};
let map = original.to_settings_map();
let diff = SettingsDiff {
upserts: map.into_iter().collect(),
deletes: vec![],
..Default::default()
};
let diff = SettingsDiff { upserts: map.into_iter().collect(), deletes: vec![] };
apply_settings_diff(&db, &diff).await.unwrap();
@@ -707,7 +694,6 @@ async fn test_idempotent_apply(db: Pool<Postgres>) {
m
},
deletes: vec![],
..Default::default()
};
// Apply twice
@@ -866,7 +852,7 @@ async fn test_full_config_roundtrip(db: Pool<Postgres>) {
assert_eq!(otel.tracing_enabled, Some(true));
assert_eq!(config.worker_configs.len(), 2);
assert_eq!(config.worker_configs["default"].extra["cache_clear"], serde_json::json!(7));
assert_eq!(config.worker_configs["default"].cache_clear, Some(7));
let gpu_auto = config.worker_configs["gpu"].autoscaling.as_ref().unwrap();
assert!(gpu_auto.enabled);
assert_eq!(gpu_auto.min_workers, Some(0));

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