Compare commits
20 Commits
frontdev
...
glm/select
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa22ec69d8 | ||
|
|
da460fa6b4 | ||
|
|
f03bd04cac | ||
|
|
e11a333998 | ||
|
|
2ff72e34dd | ||
|
|
732aff1d5c | ||
|
|
13ff7f418e | ||
|
|
d89d36728b | ||
|
|
47c027716b | ||
|
|
9347a04ed5 | ||
|
|
360b43caba | ||
|
|
1dd2b0a1b2 | ||
|
|
19f30ee6d5 | ||
|
|
90139c7754 | ||
|
|
4aa1f01210 | ||
|
|
bfb65fc8b4 | ||
|
|
b54bea7192 | ||
|
|
793bb7b405 | ||
|
|
fb0f0031d9 | ||
|
|
077eb91c0f |
30
.claude/hooks/resolve-symlinks.sh
Executable file
30
.claude/hooks/resolve-symlinks.sh
Executable 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
|
||||
36
.claude/hooks/restore-symlinks.sh
Executable file
36
.claude/hooks/restore-symlinks.sh
Executable 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
|
||||
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
4
.github/DockerfileBackendTests
vendored
4
.github/DockerfileBackendTests
vendored
@@ -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
|
||||
|
||||
35
.github/workflows/backend-test.yml
vendored
35
.github/workflows/backend-test.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/build-publish-rh-image.yml
vendored
2
.github/workflows/build-publish-rh-image.yml
vendored
@@ -9,7 +9,7 @@ permissions: write-all
|
||||
|
||||
jobs:
|
||||
build_ee:
|
||||
runs-on: ubicloud-standard-4
|
||||
runs-on: ubicloud
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
|
||||
@@ -9,7 +9,7 @@ permissions: write-all
|
||||
|
||||
jobs:
|
||||
build_ee:
|
||||
runs-on: ubicloud-standard-4
|
||||
runs-on: ubicloud
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
|
||||
43
.github/workflows/cli-tests.yml
vendored
43
.github/workflows/cli-tests.yml
vendored
@@ -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:
|
||||
|
||||
41
.github/workflows/discord-notification.yml
vendored
41
.github/workflows/discord-notification.yml
vendored
@@ -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 }}
|
||||
|
||||
4
.github/workflows/npm_on_release.yml
vendored
4
.github/workflows/npm_on_release.yml
vendored
@@ -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 }}
|
||||
|
||||
@@ -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
7
.gitignore
vendored
@@ -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/
|
||||
|
||||
10
.mcp.json
10
.mcp.json
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
121
CHANGELOG.md
121
CHANGELOG.md
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"]
|
||||
@@ -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 |
|
||||
|
||||
@@ -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`
|
||||
@@ -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"
|
||||
}
|
||||
@@ -46,11 +46,11 @@
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
|
||||
@@ -43,8 +43,7 @@
|
||||
"aiagent",
|
||||
"unassigned_script",
|
||||
"unassigned_flow",
|
||||
"unassigned_singlestepflow",
|
||||
"snapshotbuild"
|
||||
"unassigned_singlestepflow"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
65
backend/.sqlx/query-1ba2e23d4ba816048ec1e88af9e342867fc0443cabea16d111afa2b91d3fe03b.json
generated
Normal file
65
backend/.sqlx/query-1ba2e23d4ba816048ec1e88af9e342867fc0443cabea16d111afa2b91d3fe03b.json
generated
Normal 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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
35
backend/.sqlx/query-454ace9ce391725ef4f4c129cd66e4c12a5c40f512b70551958178c8b4d6c183.json
generated
Normal file
35
backend/.sqlx/query-454ace9ce391725ef4f4c129cd66e4c12a5c40f512b70551958178c8b4d6c183.json
generated
Normal 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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -42,8 +42,7 @@
|
||||
"aiagent",
|
||||
"unassigned_script",
|
||||
"unassigned_flow",
|
||||
"unassigned_singlestepflow",
|
||||
"snapshotbuild"
|
||||
"unassigned_singlestepflow"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,8 +38,7 @@
|
||||
"aiagent",
|
||||
"unassigned_script",
|
||||
"unassigned_flow",
|
||||
"unassigned_singlestepflow",
|
||||
"snapshotbuild"
|
||||
"unassigned_singlestepflow"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
18
backend/.sqlx/query-5b8c1803f0ccead11517fbc8a9bdc0227dc3922217fa18f0b71ff0484d65838c.json
generated
Normal file
18
backend/.sqlx/query-5b8c1803f0ccead11517fbc8a9bdc0227dc3922217fa18f0b71ff0484d65838c.json
generated
Normal 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"
|
||||
}
|
||||
@@ -77,8 +77,7 @@
|
||||
"aiagent",
|
||||
"unassigned_script",
|
||||
"unassigned_flow",
|
||||
"unassigned_singlestepflow",
|
||||
"snapshotbuild"
|
||||
"unassigned_singlestepflow"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -44,8 +44,7 @@
|
||||
"aiagent",
|
||||
"unassigned_script",
|
||||
"unassigned_flow",
|
||||
"unassigned_singlestepflow",
|
||||
"snapshotbuild"
|
||||
"unassigned_singlestepflow"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -42,8 +42,7 @@
|
||||
"aiagent",
|
||||
"unassigned_script",
|
||||
"unassigned_flow",
|
||||
"unassigned_singlestepflow",
|
||||
"snapshotbuild"
|
||||
"unassigned_singlestepflow"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
15
backend/.sqlx/query-8360ab72d60f07dde6ecae599e6531b5b86862029ab51fdbdd44ec16239108e2.json
generated
Normal file
15
backend/.sqlx/query-8360ab72d60f07dde6ecae599e6531b5b86862029ab51fdbdd44ec16239108e2.json
generated
Normal 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"
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO workspace_settings (workspace_id) VALUES ('ws2')",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "8cf5af21cde4e4de45f995efa2a9b56ce20c26869ca78d7e17b3504b92ae85b1"
|
||||
}
|
||||
@@ -102,8 +102,7 @@
|
||||
"aiagent",
|
||||
"unassigned_script",
|
||||
"unassigned_flow",
|
||||
"unassigned_singlestepflow",
|
||||
"snapshotbuild"
|
||||
"unassigned_singlestepflow"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -32,8 +32,7 @@
|
||||
"aiagent",
|
||||
"unassigned_script",
|
||||
"unassigned_flow",
|
||||
"unassigned_singlestepflow",
|
||||
"snapshotbuild"
|
||||
"unassigned_singlestepflow"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -72,8 +72,7 @@
|
||||
"aiagent",
|
||||
"unassigned_script",
|
||||
"unassigned_flow",
|
||||
"unassigned_singlestepflow",
|
||||
"snapshotbuild"
|
||||
"unassigned_singlestepflow"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,8 +77,7 @@
|
||||
"aiagent",
|
||||
"unassigned_script",
|
||||
"unassigned_flow",
|
||||
"unassigned_singlestepflow",
|
||||
"snapshotbuild"
|
||||
"unassigned_singlestepflow"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO v2_job_runtime (id) VALUES ($1)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "a68754521bf751450602f04dd4243199a18885e1739a5a0e7f6100eab6f3c803"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -102,8 +102,7 @@
|
||||
"aiagent",
|
||||
"unassigned_script",
|
||||
"unassigned_flow",
|
||||
"unassigned_singlestepflow",
|
||||
"snapshotbuild"
|
||||
"unassigned_singlestepflow"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,8 +72,7 @@
|
||||
"aiagent",
|
||||
"unassigned_script",
|
||||
"unassigned_flow",
|
||||
"unassigned_singlestepflow",
|
||||
"snapshotbuild"
|
||||
"unassigned_singlestepflow"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -41,8 +41,7 @@
|
||||
"aiagent",
|
||||
"unassigned_script",
|
||||
"unassigned_flow",
|
||||
"unassigned_singlestepflow",
|
||||
"snapshotbuild"
|
||||
"unassigned_singlestepflow"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -41,8 +41,7 @@
|
||||
"aiagent",
|
||||
"unassigned_script",
|
||||
"unassigned_flow",
|
||||
"unassigned_singlestepflow",
|
||||
"snapshotbuild"
|
||||
"unassigned_singlestepflow"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -31,8 +31,7 @@
|
||||
"aiagent",
|
||||
"unassigned_script",
|
||||
"unassigned_flow",
|
||||
"unassigned_singlestepflow",
|
||||
"snapshotbuild"
|
||||
"unassigned_singlestepflow"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,8 +37,7 @@
|
||||
"aiagent",
|
||||
"unassigned_script",
|
||||
"unassigned_flow",
|
||||
"unassigned_singlestepflow",
|
||||
"snapshotbuild"
|
||||
"unassigned_singlestepflow"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,8 +77,7 @@
|
||||
"aiagent",
|
||||
"unassigned_script",
|
||||
"unassigned_flow",
|
||||
"unassigned_singlestepflow",
|
||||
"snapshotbuild"
|
||||
"unassigned_singlestepflow"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
20
backend/.sqlx/query-f11d3a43f804a33e412a69be54824d7666d4dac0b824095c4c4fefb1f802c6f7.json
generated
Normal file
20
backend/.sqlx/query-f11d3a43f804a33e412a69be54824d7666d4dac0b824095c4c4fefb1f802c6f7.json
generated
Normal 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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -32,8 +32,7 @@
|
||||
"aiagent",
|
||||
"unassigned_script",
|
||||
"unassigned_flow",
|
||||
"unassigned_singlestepflow",
|
||||
"snapshotbuild"
|
||||
"unassigned_singlestepflow"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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
459
backend/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -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" }
|
||||
|
||||
@@ -1 +1 @@
|
||||
0fede4b1086bc1456be9cc55b203228c979c5c5e
|
||||
9f6e1e533df7711600ec2b8d5f0c958448db1a20
|
||||
@@ -1 +0,0 @@
|
||||
ALTER TABLE worker_ping DROP COLUMN IF EXISTS native_mode;
|
||||
@@ -1 +0,0 @@
|
||||
ALTER TABLE worker_ping ADD COLUMN IF NOT EXISTS native_mode BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
@@ -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",
|
||||
|
||||
10
backend/tests/fixtures/wmill_cli_test.sql
vendored
10
backend/tests/fixtures/wmill_cli_test.sql
vendored
@@ -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');
|
||||
@@ -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
Reference in New Issue
Block a user