Compare commits
166 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36aadbebec | ||
|
|
0aa885db67 | ||
|
|
9686608355 | ||
|
|
f0b7c96d04 | ||
|
|
b60f309a0c | ||
|
|
bedba3b75e | ||
|
|
771d67c849 | ||
|
|
46d486960a | ||
|
|
ca73267cbb | ||
|
|
6eabb8db63 | ||
|
|
5cc8f20cd2 | ||
|
|
ea5312e940 | ||
|
|
c62ba73ce4 | ||
|
|
a00927b300 | ||
|
|
98ac164ac8 | ||
|
|
ed6aaeeea3 | ||
|
|
5e415c0a12 | ||
|
|
35dded1347 | ||
|
|
aedf012c84 | ||
|
|
62fea97547 | ||
|
|
96c2d88d91 | ||
|
|
5eb308ad35 | ||
|
|
cfa04e1188 | ||
|
|
0d520f730b | ||
|
|
3c89c28e71 | ||
|
|
4fedfdfd11 | ||
|
|
8cdc7d6e9e | ||
|
|
51077245c7 | ||
|
|
9e387c3559 | ||
|
|
7aea965803 | ||
|
|
8446e3b551 | ||
|
|
a6d6136d57 | ||
|
|
c93b2e287c | ||
|
|
a91c532eca | ||
|
|
18b3c1ae5c | ||
|
|
93f927c1c1 | ||
|
|
83f21510f3 | ||
|
|
5c1f69ddcd | ||
|
|
a2cefdf0a2 | ||
|
|
3f2bd424c7 | ||
|
|
0d5f42e89e | ||
|
|
dfad07881d | ||
|
|
c4de11a406 | ||
|
|
fd5ebc2fda | ||
|
|
7b6ba7093a | ||
|
|
8042e33c38 | ||
|
|
57d23c92c5 | ||
|
|
f21140f7cd | ||
|
|
2800226bd4 | ||
|
|
2ae82796fc | ||
|
|
d1290ba777 | ||
|
|
9de0060884 | ||
|
|
3df8964fc6 | ||
|
|
fb0b2234ba | ||
|
|
4064ec3a3d | ||
|
|
0db6cbd10c | ||
|
|
ab91f78017 | ||
|
|
1e0245ca9a | ||
|
|
bead746bb8 | ||
|
|
268b5ee2a8 | ||
|
|
24f2571b37 | ||
|
|
ed1a655317 | ||
|
|
a4b440a20d | ||
|
|
b550be8711 | ||
|
|
071129f03b | ||
|
|
e9ac1ce9eb | ||
|
|
587142ddac | ||
|
|
d9ed0c318f | ||
|
|
4959a0553a | ||
|
|
c4323e40c1 | ||
|
|
696b8de1ed | ||
|
|
5f6dda9060 | ||
|
|
8490f4435d | ||
|
|
fa2f65e512 | ||
|
|
b9dec43d2a | ||
|
|
5227b76c2f | ||
|
|
1643d654a8 | ||
|
|
0d3f956e74 | ||
|
|
b330f38889 | ||
|
|
9eb15312f6 | ||
|
|
e8a13edde7 | ||
|
|
65a8789dfd | ||
|
|
6299e7a36a | ||
|
|
bcbfe4659d | ||
|
|
b2fac069df | ||
|
|
4ab08cb6a1 | ||
|
|
d6d4d85d8f | ||
|
|
81d386d365 | ||
|
|
a1b878842f | ||
|
|
00c3e9baf0 | ||
|
|
118dcb59af | ||
|
|
bd583be239 | ||
|
|
c1f7cb5d42 | ||
|
|
d502ef5029 | ||
|
|
bc7ca9982b | ||
|
|
d772083573 | ||
|
|
ea38419353 | ||
|
|
87f3de9ae5 | ||
|
|
e3460aba89 | ||
|
|
37c9acb232 | ||
|
|
9f3dd0bf2b | ||
|
|
ba9960d8db | ||
|
|
f05b00aa8a | ||
|
|
ff6c49b43e | ||
|
|
90b1a7a531 | ||
|
|
795abccc19 | ||
|
|
3e4cad5f70 | ||
|
|
4abe589397 | ||
|
|
adfd8b4df0 | ||
|
|
24d7921bcf | ||
|
|
ed87e1b08d | ||
|
|
f3697f99d9 | ||
|
|
7a59e2b466 | ||
|
|
ad2f81a1bd | ||
|
|
e099a9e697 | ||
|
|
7f8e7cb5f9 | ||
|
|
7052a36026 | ||
|
|
9ea9f36e03 | ||
|
|
99018eca0d | ||
|
|
a1ba10a29e | ||
|
|
dbec70aedd | ||
|
|
3bb58ebfd9 | ||
|
|
0e23077b34 | ||
|
|
43e74da292 | ||
|
|
57ca7dbca0 | ||
|
|
25701a0639 | ||
|
|
ea4fb64262 | ||
|
|
a9f816a3bf | ||
|
|
ba724250cf | ||
|
|
4d1d17580b | ||
|
|
17f9536a76 | ||
|
|
02e50c915e | ||
|
|
d2d08f8817 | ||
|
|
ede29d0914 | ||
|
|
f6d99dd18c | ||
|
|
858a037435 | ||
|
|
6bf544f507 | ||
|
|
cd4151a84b | ||
|
|
db8aa8a083 | ||
|
|
e9f82e9058 | ||
|
|
6691cde402 | ||
|
|
4ea1692ee2 | ||
|
|
90fa5b3ced | ||
|
|
45b959711e | ||
|
|
a46924a0f2 | ||
|
|
907ed41093 | ||
|
|
f387daa2a6 | ||
|
|
b094649586 | ||
|
|
3ed86816fb | ||
|
|
2d5393941c | ||
|
|
6d1d1f162b | ||
|
|
5b7bb2fb84 | ||
|
|
71608bf669 | ||
|
|
47c7fe83f4 | ||
|
|
4b8bb72857 | ||
|
|
b7bec1a83d | ||
|
|
8971dd660c | ||
|
|
b3eeee4131 | ||
|
|
bba319b282 | ||
|
|
bb03c62c28 | ||
|
|
2019aecf42 | ||
|
|
3e313cc4e8 | ||
|
|
c3a76c2cc5 | ||
|
|
eb5a8dab74 | ||
|
|
f02ef6d03c | ||
|
|
535e108cbf |
@@ -1,30 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,36 +0,0 @@
|
||||
#!/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,5 +1,8 @@
|
||||
{
|
||||
"permissions": {
|
||||
"additionalDirectories": [
|
||||
"../windmill-ee-private"
|
||||
],
|
||||
"allow": [
|
||||
"Bash(ls:*)",
|
||||
"Bash(grep:*)",
|
||||
@@ -63,39 +66,6 @@
|
||||
},
|
||||
"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,7 +581,28 @@ 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 OpenAPI Spec and Regenerate Types
|
||||
### 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
|
||||
|
||||
Add to `JobTriggerKind` enum in `backend/windmill-api/openapi.yaml`, then:
|
||||
|
||||
|
||||
4
.github/DockerfileBackendTests
vendored
4
.github/DockerfileBackendTests
vendored
@@ -44,6 +44,10 @@ 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: ubicloud-standard-16
|
||||
runs-on: blacksmith-16vcpu-ubuntu-2404
|
||||
services:
|
||||
postgres:
|
||||
image: postgres
|
||||
@@ -70,6 +70,16 @@ 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
|
||||
@@ -78,6 +88,20 @@ 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"
|
||||
@@ -165,6 +189,12 @@ 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
|
||||
@@ -199,7 +229,7 @@ jobs:
|
||||
fi
|
||||
echo "Verified: Package requires authentication for @windmill-test/private-pkg"
|
||||
- name: Cache DuckDB FFI module build
|
||||
uses: actions/cache@v3
|
||||
uses: useblacksmith/cache@v1
|
||||
with:
|
||||
path: ./backend/windmill-duckdb-ffi-internal/target
|
||||
key: ${{ runner.os }}-duckdb-ffi-${{ hashFiles('./backend/windmill-duckdb-ffi-internal/src/**/*.rs', './backend/windmill-duckdb-ffi-internal/Cargo.toml', './backend/windmill-duckdb-ffi-internal/Cargo.lock') }}
|
||||
@@ -215,6 +245,7 @@ jobs:
|
||||
RUST_LOG_STYLE: never
|
||||
CARGO_NET_GIT_FETCH_WITH_CLI: true
|
||||
CARGO_BUILD_JOBS: 12
|
||||
CARGO_INCREMENTAL: 1
|
||||
WMDEBUG_FORCE_V0_WORKSPACE_DEPENDENCIES: 1
|
||||
WMDEBUG_FORCE_RUNNABLE_SETTINGS_V0: 1
|
||||
WMDEBUG_FORCE_NO_LEGACY_DEBOUNCING_COMPAT: 1
|
||||
|
||||
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
|
||||
runs-on: ubicloud-standard-4
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
|
||||
@@ -9,7 +9,7 @@ permissions: write-all
|
||||
|
||||
jobs:
|
||||
build_ee:
|
||||
runs-on: ubicloud
|
||||
runs-on: ubicloud-standard-4
|
||||
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,11 +69,6 @@ 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:
|
||||
@@ -90,6 +85,10 @@ 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: |
|
||||
@@ -101,12 +100,10 @@ jobs:
|
||||
env:
|
||||
DATABASE_URL: postgres://postgres:changeme@localhost:5432
|
||||
CI_MINIMAL_FEATURES: "true"
|
||||
run: |
|
||||
deno test --no-check --allow-all test/ \
|
||||
--ignore=test/cargo_backend_example.test.ts
|
||||
run: bun test --timeout 120000 test/
|
||||
|
||||
test-windows:
|
||||
runs-on: windows-latest
|
||||
runs-on: blacksmith-16vcpu-windows-2025
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
@@ -126,11 +123,6 @@ 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:
|
||||
@@ -150,6 +142,10 @@ 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
|
||||
@@ -165,9 +161,12 @@ jobs:
|
||||
CI_MINIMAL_FEATURES: "true"
|
||||
BUN_PATH: ${{ steps.runtime-paths.outputs.BUN_PATH }}
|
||||
NODE_BIN_PATH: ${{ steps.runtime-paths.outputs.NODE_BIN_PATH }}
|
||||
run: |
|
||||
deno test --no-check --allow-all test/ `
|
||||
--ignore=test/cargo_backend_example.test.ts
|
||||
run: bun test --timeout 120000 test/
|
||||
|
||||
- name: Keep runner alive for SSH debug
|
||||
if: failure()
|
||||
shell: pwsh
|
||||
run: Start-Sleep -Seconds 3600
|
||||
|
||||
# Combined summary job for branch protection
|
||||
test-summary:
|
||||
|
||||
41
.github/workflows/discord-notification.yml
vendored
41
.github/workflows/discord-notification.yml
vendored
@@ -6,6 +6,12 @@ on:
|
||||
- opened
|
||||
- ready_for_review
|
||||
- closed
|
||||
issue_comment:
|
||||
types:
|
||||
- created
|
||||
pull_request_review_comment:
|
||||
types:
|
||||
- created
|
||||
|
||||
jobs:
|
||||
notify_discord_when_pr_opened:
|
||||
@@ -33,3 +39,38 @@ 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: denoland/setup-deno@v2
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
deno-version: v2.x
|
||||
bun-version: latest
|
||||
- run: cd cli && ./build.sh && cd npm && npm publish
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
@@ -24,9 +24,22 @@ 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"
|
||||
|
||||
@@ -117,3 +130,54 @@ 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,9 +14,16 @@ 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,10 +3,12 @@
|
||||
"svelte": {
|
||||
"type": "http",
|
||||
"url": "https://mcp.svelte.dev/mcp"
|
||||
},
|
||||
"playwright": {
|
||||
"command": "npx",
|
||||
"args": ["@playwright/mcp@latest"]
|
||||
}
|
||||
},
|
||||
"playwright": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"@playwright/mcp@latest"
|
||||
]
|
||||
}
|
||||
}
|
||||
75
.workmux.yaml
Normal file
75
.workmux.yaml
Normal file
@@ -0,0 +1,75 @@
|
||||
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,5 +1,126 @@
|
||||
# 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,6 +258,10 @@ 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
|
||||
|
||||
|
||||
234
Dockerfile.sandbox
Normal file
234
Dockerfile.sandbox
Normal file
@@ -0,0 +1,234 @@
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
curl \
|
||||
ca-certificates \
|
||||
git \
|
||||
iptables \
|
||||
gosu \
|
||||
sudo \
|
||||
unzip \
|
||||
# Rust native build deps (for cargo check)
|
||||
pkg-config \
|
||||
cmake \
|
||||
clang \
|
||||
mold \
|
||||
libtool \
|
||||
libssl-dev \
|
||||
libxml2-dev \
|
||||
libxmlsec1-dev \
|
||||
libxslt1-dev \
|
||||
libffi-dev \
|
||||
zlib1g-dev \
|
||||
libcurl4-openssl-dev \
|
||||
libclang-dev \
|
||||
libkrb5-dev \
|
||||
libsasl2-dev \
|
||||
# PostgreSQL (for local DB during development)
|
||||
postgresql \
|
||||
postgresql-client \
|
||||
# Node.js 22 (for npm run check / frontend dev)
|
||||
&& curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
|
||||
&& apt-get install -y --no-install-recommends nodejs \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
# Container runs as arbitrary UIDs (--user uid:gid). These three lines make
|
||||
# sudo work for any UID:
|
||||
# 1) NOPASSWD rule so sudo never prompts for a password
|
||||
# 2) Writable passwd/group so the entrypoint can register the dynamic UID
|
||||
# 3) Writable shadow so unix_chkpwd can validate the account (without this,
|
||||
# sudo fails with "account validation failure, is your account locked?")
|
||||
&& echo "ALL ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/sandbox \
|
||||
&& chmod 0440 /etc/sudoers.d/sandbox \
|
||||
&& chmod 666 /etc/passwd /etc/group /etc/shadow
|
||||
|
||||
# ── GitHub CLI (for PR creation) ──────────────────────────────────────────────
|
||||
RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
|
||||
-o /usr/share/keyrings/githubcli-archive-keyring.gpg \
|
||||
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
|
||||
> /etc/apt/sources.list.d/github-cli.list \
|
||||
&& apt-get update && apt-get install -y --no-install-recommends gh \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# ── Rust toolchain ────────────────────────────────────────────────────────────
|
||||
# Install under /usr/local/lib/ so bins are world-readable with default umask.
|
||||
# CARGO_HOME is overridden to /tmp/.cargo at the end for mutable runtime state.
|
||||
ENV RUSTUP_HOME=/usr/local/lib/rustup CARGO_HOME=/usr/local/lib/cargo
|
||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | \
|
||||
sh -s -- -y --default-toolchain stable --profile minimal && \
|
||||
ln -s /usr/local/lib/cargo/bin/* /usr/local/bin/
|
||||
RUN cargo install sqlx-cli --no-default-features --features native-tls,postgres && \
|
||||
cargo install cargo-watch && \
|
||||
ln -sf /usr/local/lib/cargo/bin/sqlx /usr/local/bin/sqlx && \
|
||||
ln -sf /usr/local/lib/cargo/bin/cargo-watch /usr/local/bin/cargo-watch
|
||||
|
||||
# ── Register dynamic runtime users ───────────────────────────────────────────
|
||||
RUN cat <<'SCRIPT' > /usr/local/bin/register-dynamic-user.sh
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
uid="${1:-}"
|
||||
gid="${2:-}"
|
||||
|
||||
if [ -z "$uid" ] || [ -z "$gid" ]; then
|
||||
echo "register-dynamic-user: usage: register-dynamic-user <uid> <gid>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! getent group "$gid" >/dev/null 2>&1; then
|
||||
echo "sandbox:x:${gid}:" >> /etc/group
|
||||
fi
|
||||
|
||||
if ! getent passwd "$uid" >/dev/null 2>&1; then
|
||||
echo "sandbox:x:${uid}:${gid}:sandbox:/tmp:/bin/sh" >> /etc/passwd
|
||||
fi
|
||||
|
||||
# Add a shadow entry ("*" = no password) so unix_chkpwd doesn't reject sudo.
|
||||
if ! grep -q "^sandbox:" /etc/shadow 2>/dev/null; then
|
||||
echo "sandbox:*:19000:0:99999:7:::" >> /etc/shadow
|
||||
fi
|
||||
SCRIPT
|
||||
RUN chmod +x /usr/local/bin/register-dynamic-user.sh
|
||||
|
||||
# ── Network init script (iptables firewall + privilege drop) ──────────────────
|
||||
RUN cat <<'SCRIPT' > /usr/local/bin/network-init.sh
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
if [ -n "${WM_PROXY_HOST:-}" ] && [ -n "${WM_PROXY_PORT:-}" ]; then
|
||||
# Resolve hostnames to ALL IPs (multi-A records, round-robin DNS)
|
||||
PROXY_IPS=$(getent ahostsv4 "$WM_PROXY_HOST" | awk '{print $1}' | sort -u)
|
||||
RPC_HOST="${WM_RPC_HOST:-$WM_PROXY_HOST}"
|
||||
RPC_IPS=$(getent ahostsv4 "$RPC_HOST" | awk '{print $1}' | sort -u)
|
||||
|
||||
if [ -z "$PROXY_IPS" ] || [ -z "$RPC_IPS" ]; then
|
||||
echo "network-init: failed to resolve proxy/RPC host" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# IPv4: default deny outbound
|
||||
iptables -P OUTPUT DROP
|
||||
iptables -A OUTPUT -o lo -j ACCEPT
|
||||
iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
|
||||
|
||||
# Allow DNS (UDP/TCP 53) to configured nameservers.
|
||||
if [ -f /etc/resolv.conf ]; then
|
||||
grep '^nameserver' /etc/resolv.conf | awk '{print $2}' | while read -r ns; do
|
||||
iptables -A OUTPUT -d "$ns" -p udp --dport 53 -j ACCEPT
|
||||
iptables -A OUTPUT -d "$ns" -p tcp --dport 53 -j ACCEPT
|
||||
done
|
||||
fi
|
||||
|
||||
# Allow ALL resolved proxy IPs (handles multi-A DNS)
|
||||
for ip in $PROXY_IPS; do
|
||||
iptables -A OUTPUT -d "$ip" -p tcp --dport "$WM_PROXY_PORT" -j ACCEPT
|
||||
done
|
||||
|
||||
# Allow ALL resolved RPC IPs
|
||||
if [ -n "${WM_RPC_PORT:-}" ]; then
|
||||
for ip in $RPC_IPS; do
|
||||
iptables -A OUTPUT -d "$ip" -p tcp --dport "$WM_RPC_PORT" -j ACCEPT
|
||||
done
|
||||
fi
|
||||
|
||||
# Reject (not drop) everything else to fail fast instead of hanging
|
||||
iptables -A OUTPUT -j REJECT
|
||||
|
||||
# IPv6: block entirely to prevent leaks (fail closed)
|
||||
if ip6tables -L -n >/dev/null 2>&1; then
|
||||
ip6tables -P OUTPUT DROP
|
||||
ip6tables -A OUTPUT -o lo -j ACCEPT
|
||||
ip6tables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
|
||||
ip6tables -A OUTPUT -j REJECT
|
||||
else
|
||||
if ! sysctl -w net.ipv6.conf.all.disable_ipv6=1 2>/dev/null; then
|
||||
echo "network-init: failed to block IPv6 (neither ip6tables nor sysctl available)" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Add sandbox user/group so sudo works after dropping privileges.
|
||||
if [ -z "${WM_TARGET_UID:-}" ] || [ -z "${WM_TARGET_GID:-}" ]; then
|
||||
echo "network-init: WM_TARGET_UID and WM_TARGET_GID are required" >&2
|
||||
exit 1
|
||||
fi
|
||||
/usr/local/bin/register-dynamic-user.sh "${WM_TARGET_UID}" "${WM_TARGET_GID}"
|
||||
|
||||
# Fix PTY ownership so the unprivileged user can read/write the terminal.
|
||||
if [ -t 0 ]; then
|
||||
chown "${WM_TARGET_UID}:${WM_TARGET_GID}" "$(tty)"
|
||||
fi
|
||||
|
||||
# Drop privileges and exec the user command.
|
||||
exec gosu "${WM_TARGET_UID}:${WM_TARGET_GID}" env HOME=/tmp "$@"
|
||||
SCRIPT
|
||||
RUN chmod +x /usr/local/bin/network-init.sh
|
||||
|
||||
# ── workmux (sandbox RPC) ────────────────────────────────────────────────────
|
||||
RUN curl -fsSL https://raw.githubusercontent.com/raine/workmux/main/scripts/install.sh | bash
|
||||
|
||||
# ── Claude Code ───────────────────────────────────────────────────────────────
|
||||
RUN curl -fsSL https://claude.ai/install.sh | bash && \
|
||||
target="$(readlink -f /root/.local/bin/claude)" && \
|
||||
mv /root/.local/share/claude /usr/local/lib/claude && \
|
||||
ln -s "/usr/local/lib/claude/versions/$(basename "$target")" /usr/local/bin/claude && \
|
||||
mkdir -p /tmp/.local/bin && \
|
||||
ln -s /usr/local/bin/claude /tmp/.local/bin/claude
|
||||
|
||||
# ── Codex ─────────────────────────────────────────────────────────────────────
|
||||
RUN npm i -g @openai/codex
|
||||
|
||||
# ── Bun ───────────────────────────────────────────────────────────────────────
|
||||
ENV BUN_INSTALL=/usr/local/lib/bun
|
||||
RUN curl -fsSL https://bun.sh/install | bash && \
|
||||
ln -s /usr/local/lib/bun/bin/bun /usr/local/bin/bun && \
|
||||
ln -s /usr/local/lib/bun/bin/bunx /usr/local/bin/bunx
|
||||
|
||||
# ── Playwright + Chromium (for screenshots) ──────────────────────────────────
|
||||
ENV PLAYWRIGHT_BROWSERS_PATH=/usr/local/lib/playwright-browsers
|
||||
RUN bun add -g @playwright/test \
|
||||
&& bunx playwright install chromium --with-deps \
|
||||
&& chmod -R a+rwX /usr/local/lib/playwright-browsers \
|
||||
&& rm -rf /var/lib/apt/lists/* /tmp/bunx-*
|
||||
|
||||
# ── AWS CLI (for S3-compatible uploads to R2) ─────────────────────────────────
|
||||
RUN curl -fsSL "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o /tmp/awscliv2.zip \
|
||||
&& unzip -q /tmp/awscliv2.zip -d /tmp \
|
||||
&& /tmp/aws/install \
|
||||
&& rm -rf /tmp/aws /tmp/awscliv2.zip
|
||||
|
||||
ENV AWS_DEFAULT_REGION=auto
|
||||
|
||||
# ── Runtime env for arbitrary UID ─────────────────────────────────────────────
|
||||
# Mutable state goes to /tmp (writable by any UID). Toolchains stay read-only.
|
||||
ENV CARGO_HOME=/tmp/.cargo BUN_TMPDIR=/tmp
|
||||
|
||||
# ── Entrypoint ────────────────────────────────────────────────────────────────
|
||||
RUN cat <<'ENTRY' > /usr/local/bin/entrypoint.sh
|
||||
#!/bin/sh
|
||||
/usr/local/bin/register-dynamic-user.sh "$(id -u)" "$(id -g)"
|
||||
|
||||
# Start PostgreSQL (unix socket in /tmp, owned by postgres user)
|
||||
mkdir -p /tmp/pgdata && sudo chown postgres:postgres /tmp/pgdata
|
||||
if [ ! -f /tmp/pgdata/PG_VERSION ]; then
|
||||
sudo -u postgres /usr/lib/postgresql/15/bin/initdb -D /tmp/pgdata --auth=trust
|
||||
fi
|
||||
sudo -u postgres /usr/lib/postgresql/15/bin/pg_ctl -D /tmp/pgdata -l /tmp/pg.log start -o "-k /tmp"
|
||||
sudo -u postgres psql -h /tmp -c "CREATE ROLE sandbox SUPERUSER LOGIN" 2>/dev/null || true
|
||||
sudo -u postgres createdb -h /tmp windmill 2>/dev/null || true
|
||||
|
||||
# Run database migrations so sqlx compile-time checks work
|
||||
if [ -d "$PWD/backend/migrations" ]; then
|
||||
DATABASE_URL="postgres://sandbox@localhost/windmill?host=/tmp" \
|
||||
sqlx migrate run --source "$PWD/backend/migrations" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Install frontend dependencies and generate backend client
|
||||
if [ -d "$PWD/frontend" ]; then
|
||||
(cd "$PWD/frontend" && npm install && npm run generate-backend-client) 2>/dev/null || true
|
||||
fi
|
||||
|
||||
exec "$@"
|
||||
ENTRY
|
||||
RUN chmod +x /usr/local/bin/entrypoint.sh
|
||||
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
|
||||
@@ -257,6 +257,7 @@ 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 |
|
||||
|
||||
196
README_WORKMUX_DEV.md
Normal file
196
README_WORKMUX_DEV.md
Normal file
@@ -0,0 +1,196 @@
|
||||
# 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`
|
||||
14
backend/.sqlx/query-0681b850c033619e1b9498376263681f875a5aba22170ca50ec8b578f7fa478b.json
generated
Normal file
14
backend/.sqlx/query-0681b850c033619e1b9498376263681f875a5aba22170ca50ec8b578f7fa478b.json
generated
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"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": [
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
|
||||
@@ -43,7 +43,8 @@
|
||||
"aiagent",
|
||||
"unassigned_script",
|
||||
"unassigned_flow",
|
||||
"unassigned_singlestepflow"
|
||||
"unassigned_singlestepflow",
|
||||
"snapshotbuild"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
26
backend/.sqlx/query-1437b432d2c23e30eb05443e83069cdb049f65ec299b0778ce14677728cf6346.json
generated
Normal file
26
backend/.sqlx/query-1437b432d2c23e30eb05443e83069cdb049f65ec299b0778ce14677728cf6346.json
generated
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
22
backend/.sqlx/query-18b6262a60400f2b58ab26615466c23b4c1a7805c66b70b0fcfb7d33b122a7bf.json
generated
Normal file
22
backend/.sqlx/query-18b6262a60400f2b58ab26615466c23b4c1a7805c66b70b0fcfb7d33b122a7bf.json
generated
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
15
backend/.sqlx/query-1af6885dbc5055281acb82b3e57f7dba2e4b04d9535058fab695660a14bf8890.json
generated
Normal file
15
backend/.sqlx/query-1af6885dbc5055281acb82b3e57f7dba2e4b04d9535058fab695660a14bf8890.json
generated
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
16
backend/.sqlx/query-27f70ebe788cca2e88732d8bf978883037bebca4cf75ba459858e4fb197f940b.json
generated
Normal file
16
backend/.sqlx/query-27f70ebe788cca2e88732d8bf978883037bebca4cf75ba459858e4fb197f940b.json
generated
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"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,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) 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",
|
||||
"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",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -15,10 +15,11 @@
|
||||
"Varchar",
|
||||
"Int8",
|
||||
"Int8",
|
||||
"Text"
|
||||
"Text",
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "97c61b6a9a5112ea484565236959a544511d5d501fb737da8110a8725b883465"
|
||||
"hash": "298fa4f8eb05b4c3f33b608b0cdb6ed918af2df012de33acb3befd3fcccbc257"
|
||||
}
|
||||
22
backend/.sqlx/query-2a95f18e80c55a7e8178a4bd2b781d41fa47efd4da5bb9bc2d72b9aa1e33617f.json
generated
Normal file
22
backend/.sqlx/query-2a95f18e80c55a7e8178a4bd2b781d41fa47efd4da5bb9bc2d72b9aa1e33617f.json
generated
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
14
backend/.sqlx/query-4010328a9f1611064f497726b69c08625a55a4dab25c3d9b5ece07e44d14915b.json
generated
Normal file
14
backend/.sqlx/query-4010328a9f1611064f497726b69c08625a55a4dab25c3d9b5ece07e44d14915b.json
generated
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
22
backend/.sqlx/query-48536968f4173715d4ef8293683c2a3eb4bd22fbe18c34890a3dc4e96e4e6133.json
generated
Normal file
22
backend/.sqlx/query-48536968f4173715d4ef8293683c2a3eb4bd22fbe18c34890a3dc4e96e4e6133.json
generated
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"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,7 +42,8 @@
|
||||
"aiagent",
|
||||
"unassigned_script",
|
||||
"unassigned_flow",
|
||||
"unassigned_singlestepflow"
|
||||
"unassigned_singlestepflow",
|
||||
"snapshotbuild"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,8 @@
|
||||
"aiagent",
|
||||
"unassigned_script",
|
||||
"unassigned_flow",
|
||||
"unassigned_singlestepflow"
|
||||
"unassigned_singlestepflow",
|
||||
"snapshotbuild"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
"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",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -36,5 +36,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "b45e17ad532a23b394226c9a5d7ab5a21e20202dbbf9c67831cc62eb067cd2ba"
|
||||
"hash": "53648c069749df45c0459d733b3e429af20c69c841fb0c3bceafe3ea6c3f5329"
|
||||
}
|
||||
14
backend/.sqlx/query-539d661500254e2e346490710f5772cb88a1ab6bbddd97a77e06644ac0f61762.json
generated
Normal file
14
backend/.sqlx/query-539d661500254e2e346490710f5772cb88a1ab6bbddd97a77e06644ac0f61762.json
generated
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
@@ -1,18 +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 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,7 +77,8 @@
|
||||
"aiagent",
|
||||
"unassigned_script",
|
||||
"unassigned_flow",
|
||||
"unassigned_singlestepflow"
|
||||
"unassigned_singlestepflow",
|
||||
"snapshotbuild"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
14
backend/.sqlx/query-66342c32f7ae0238803cb1896d9f23a74b64573f77dd32189a25b6e8369f147b.json
generated
Normal file
14
backend/.sqlx/query-66342c32f7ae0238803cb1896d9f23a74b64573f77dd32189a25b6e8369f147b.json
generated
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
14
backend/.sqlx/query-66faba2137791e0cb1353545c06f9f7c23a1559e7a761db7c2195736b8b30709.json
generated
Normal file
14
backend/.sqlx/query-66faba2137791e0cb1353545c06f9f7c23a1559e7a761db7c2195736b8b30709.json
generated
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"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\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, 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",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -102,6 +102,11 @@
|
||||
"ordinal": 19,
|
||||
"name": "job_isolation",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 20,
|
||||
"name": "native_mode",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -132,8 +137,9 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "771a858a4b7ca41b6787e61f5a4a5c9c4d48fd213852e2f997cd4b2420580d30"
|
||||
"hash": "68bca8f839e47705b11d312ee874eceaa3d1d24d9053ad4aea94b9f8465585ca"
|
||||
}
|
||||
19
backend/.sqlx/query-79b437ad31ddab94310989b8fb6a1c130b9be1ab4b6a100fffffd687677b9c92.json
generated
Normal file
19
backend/.sqlx/query-79b437ad31ddab94310989b8fb6a1c130b9be1ab4b6a100fffffd687677b9c92.json
generated
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"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,7 +44,8 @@
|
||||
"aiagent",
|
||||
"unassigned_script",
|
||||
"unassigned_flow",
|
||||
"unassigned_singlestepflow"
|
||||
"unassigned_singlestepflow",
|
||||
"snapshotbuild"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
22
backend/.sqlx/query-7ca599330c9913c7e66b27e2ffcfa18d53cbdd16e179749f0aea7980a901b23c.json
generated
Normal file
22
backend/.sqlx/query-7ca599330c9913c7e66b27e2ffcfa18d53cbdd16e179749f0aea7980a901b23c.json
generated
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
15
backend/.sqlx/query-7ca7dabfe360845a5b57552b0d02267d5dbbc488bc7ab990c0bda1594bf5ef3a.json
generated
Normal file
15
backend/.sqlx/query-7ca7dabfe360845a5b57552b0d02267d5dbbc488bc7ab990c0bda1594bf5ef3a.json
generated
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"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,7 +42,8 @@
|
||||
"aiagent",
|
||||
"unassigned_script",
|
||||
"unassigned_flow",
|
||||
"unassigned_singlestepflow"
|
||||
"unassigned_singlestepflow",
|
||||
"snapshotbuild"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
12
backend/.sqlx/query-8cf5af21cde4e4de45f995efa2a9b56ce20c26869ca78d7e17b3504b92ae85b1.json
generated
Normal file
12
backend/.sqlx/query-8cf5af21cde4e4de45f995efa2a9b56ce20c26869ca78d7e17b3504b92ae85b1.json
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO workspace_settings (workspace_id) VALUES ('ws2')",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "8cf5af21cde4e4de45f995efa2a9b56ce20c26869ca78d7e17b3504b92ae85b1"
|
||||
}
|
||||
@@ -102,7 +102,8 @@
|
||||
"aiagent",
|
||||
"unassigned_script",
|
||||
"unassigned_flow",
|
||||
"unassigned_singlestepflow"
|
||||
"unassigned_singlestepflow",
|
||||
"snapshotbuild"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
34
backend/.sqlx/query-8f442110817244aa9533b014aa3d74a6582937dfe4759932b63b8e531984008e.json
generated
Normal file
34
backend/.sqlx/query-8f442110817244aa9533b014aa3d74a6582937dfe4759932b63b8e531984008e.json
generated
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"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,7 +32,8 @@
|
||||
"aiagent",
|
||||
"unassigned_script",
|
||||
"unassigned_flow",
|
||||
"unassigned_singlestepflow"
|
||||
"unassigned_singlestepflow",
|
||||
"snapshotbuild"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
35
backend/.sqlx/query-98033aae3182bde22d5b2ff08ef6e8a4f8f3a9bf04238b33e9caf46836df73d9.json
generated
Normal file
35
backend/.sqlx/query-98033aae3182bde22d5b2ff08ef6e8a4f8f3a9bf04238b33e9caf46836df73d9.json
generated
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
15
backend/.sqlx/query-9f50ec7681a1fcd11cb452c7aba7e8897e49a4b6affa4e9976680336b6bd3115.json
generated
Normal file
15
backend/.sqlx/query-9f50ec7681a1fcd11cb452c7aba7e8897e49a4b6affa4e9976680336b6bd3115.json
generated
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
12
backend/.sqlx/query-a057ff9f5998a162ae6de05f6127b7eefc826af7bf1bb89fd758f7c03c881033.json
generated
Normal file
12
backend/.sqlx/query-a057ff9f5998a162ae6de05f6127b7eefc826af7bf1bb89fd758f7c03c881033.json
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO workspace (id, name, owner) VALUES ('ws2', 'Workspace 2', 'test-user')",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "a057ff9f5998a162ae6de05f6127b7eefc826af7bf1bb89fd758f7c03c881033"
|
||||
}
|
||||
@@ -72,7 +72,8 @@
|
||||
"aiagent",
|
||||
"unassigned_script",
|
||||
"unassigned_flow",
|
||||
"unassigned_singlestepflow"
|
||||
"unassigned_singlestepflow",
|
||||
"snapshotbuild"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 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, native_mode = $12 WHERE worker = $6",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -15,10 +15,11 @@
|
||||
"Int8",
|
||||
"Float4",
|
||||
"Float4",
|
||||
"Float4"
|
||||
"Float4",
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "aa523c363186575b4bd2537b8e2430e6938e7cc35f8c9e2d1c5459a85443cbdd"
|
||||
"hash": "a41c4cbaffdb714e4a963557de5a4011744d684eb24e03cb4beae6a512613159"
|
||||
}
|
||||
@@ -77,7 +77,8 @@
|
||||
"aiagent",
|
||||
"unassigned_script",
|
||||
"unassigned_flow",
|
||||
"unassigned_singlestepflow"
|
||||
"unassigned_singlestepflow",
|
||||
"snapshotbuild"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
14
backend/.sqlx/query-a68754521bf751450602f04dd4243199a18885e1739a5a0e7f6100eab6f3c803.json
generated
Normal file
14
backend/.sqlx/query-a68754521bf751450602f04dd4243199a18885e1739a5a0e7f6100eab6f3c803.json
generated
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO v2_job_runtime (id) VALUES ($1)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "a68754521bf751450602f04dd4243199a18885e1739a5a0e7f6100eab6f3c803"
|
||||
}
|
||||
15
backend/.sqlx/query-abb56f78aa39c6b6ae8b0ccb7b724c1f80d717e7c9d76b287da63cb5ee8e8b25.json
generated
Normal file
15
backend/.sqlx/query-abb56f78aa39c6b6ae8b0ccb7b724c1f80d717e7c9d76b287da63cb5ee8e8b25.json
generated
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
22
backend/.sqlx/query-adb98040c8039e5cc27fe0579941f723b38d1784bd2f1b16d8724f1f1612dcbf.json
generated
Normal file
22
backend/.sqlx/query-adb98040c8039e5cc27fe0579941f723b38d1784bd2f1b16d8724f1f1612dcbf.json
generated
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"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,7 +102,8 @@
|
||||
"aiagent",
|
||||
"unassigned_script",
|
||||
"unassigned_flow",
|
||||
"unassigned_singlestepflow"
|
||||
"unassigned_singlestepflow",
|
||||
"snapshotbuild"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +72,8 @@
|
||||
"aiagent",
|
||||
"unassigned_script",
|
||||
"unassigned_flow",
|
||||
"unassigned_singlestepflow"
|
||||
"unassigned_singlestepflow",
|
||||
"snapshotbuild"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
14
backend/.sqlx/query-b4a9abcb38997587b28655b0f4a212a5bd4039b57fab20b163617e33a4c9dd46.json
generated
Normal file
14
backend/.sqlx/query-b4a9abcb38997587b28655b0f4a212a5bd4039b57fab20b163617e33a4c9dd46.json
generated
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO v2_job_runtime (id) SELECT unnest($1::uuid[])",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"UuidArray"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "b4a9abcb38997587b28655b0f4a212a5bd4039b57fab20b163617e33a4c9dd46"
|
||||
}
|
||||
22
backend/.sqlx/query-b795dc228f93c8b9bedb4a3e7467d941819a7202214e0634580bdc2ec30f0b70.json
generated
Normal file
22
backend/.sqlx/query-b795dc228f93c8b9bedb4a3e7467d941819a7202214e0634580bdc2ec30f0b70.json
generated
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"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,7 +41,8 @@
|
||||
"aiagent",
|
||||
"unassigned_script",
|
||||
"unassigned_flow",
|
||||
"unassigned_singlestepflow"
|
||||
"unassigned_singlestepflow",
|
||||
"snapshotbuild"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
14
backend/.sqlx/query-c1a1ae759ebb84fde3e6d2727991b2a1fcc73e520fee79653bb960dc00c3e2db.json
generated
Normal file
14
backend/.sqlx/query-c1a1ae759ebb84fde3e6d2727991b2a1fcc73e520fee79653bb960dc00c3e2db.json
generated
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
35
backend/.sqlx/query-c2347460b73ae9d3167031c263032e97ebefb46be9e58bd3da9067748075311b.json
generated
Normal file
35
backend/.sqlx/query-c2347460b73ae9d3167031c263032e97ebefb46be9e58bd3da9067748075311b.json
generated
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
22
backend/.sqlx/query-c63a1949247f1618f6b6acee9bf6b4d3081dfed2e6ff533dbff0bdfd52687cbb.json
generated
Normal file
22
backend/.sqlx/query-c63a1949247f1618f6b6acee9bf6b4d3081dfed2e6ff533dbff0bdfd52687cbb.json
generated
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
14
backend/.sqlx/query-c6d963e5cefeea728414892df9f28a89f435fc0fb7e55f243b021200f33d2151.json
generated
Normal file
14
backend/.sqlx/query-c6d963e5cefeea728414892df9f28a89f435fc0fb7e55f243b021200f33d2151.json
generated
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
14
backend/.sqlx/query-c9530931f670eab1208c4a284a55afdc3fcbb0eb5f98fd63e2ec89442becbfaa.json
generated
Normal file
14
backend/.sqlx/query-c9530931f670eab1208c4a284a55afdc3fcbb0eb5f98fd63e2ec89442becbfaa.json
generated
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n WITH _ AS (\n UPDATE debounce_key\n SET debounced_times = 0,\n first_started_at = now(),\n previous_job_id = NULL\n WHERE job_id = $1\n )\n UPDATE v2_job_debounce_batch\n SET debounce_batch = nextval('debounce_batch_seq')\n WHERE id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "c9530931f670eab1208c4a284a55afdc3fcbb0eb5f98fd63e2ec89442becbfaa"
|
||||
}
|
||||
22
backend/.sqlx/query-cc309de42a3b630bb83d1b2437633ef0b98ce5e5fba1f5c1dda3ddd874ff3a39.json
generated
Normal file
22
backend/.sqlx/query-cc309de42a3b630bb83d1b2437633ef0b98ce5e5fba1f5c1dda3ddd874ff3a39.json
generated
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
22
backend/.sqlx/query-ccfed494a8d89eb2c88d72738c341a4dd87701b0636eb9fa001cd1d9cbcd663b.json
generated
Normal file
22
backend/.sqlx/query-ccfed494a8d89eb2c88d72738c341a4dd87701b0636eb9fa001cd1d9cbcd663b.json
generated
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"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,7 +41,8 @@
|
||||
"aiagent",
|
||||
"unassigned_script",
|
||||
"unassigned_flow",
|
||||
"unassigned_singlestepflow"
|
||||
"unassigned_singlestepflow",
|
||||
"snapshotbuild"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
22
backend/.sqlx/query-d9400849888dd021b0504b93004ab5e76296ed437c210942363ba06845f9f963.json
generated
Normal file
22
backend/.sqlx/query-d9400849888dd021b0504b93004ab5e76296ed437c210942363ba06845f9f963.json
generated
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
@@ -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 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, native_mode FROM worker_ping WHERE ping_at > now() - interval '30 days' ORDER BY started_at",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -57,6 +57,11 @@
|
||||
"ordinal": 10,
|
||||
"name": "occupancy_rate_30m",
|
||||
"type_info": "Float4"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "native_mode",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -73,8 +78,9 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "2b3b634b15eb58b95ce26b5a591258b54fb7bf21ae85e7a390ad73489c2247ac"
|
||||
"hash": "dcc6928bc273fcbe52bcef43f9d06d8bb8c68a1b04b3c2cce7491dde5d727446"
|
||||
}
|
||||
@@ -31,7 +31,8 @@
|
||||
"aiagent",
|
||||
"unassigned_script",
|
||||
"unassigned_flow",
|
||||
"unassigned_singlestepflow"
|
||||
"unassigned_singlestepflow",
|
||||
"snapshotbuild"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,8 @@
|
||||
"aiagent",
|
||||
"unassigned_script",
|
||||
"unassigned_flow",
|
||||
"unassigned_singlestepflow"
|
||||
"unassigned_singlestepflow",
|
||||
"snapshotbuild"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,7 +77,8 @@
|
||||
"aiagent",
|
||||
"unassigned_script",
|
||||
"unassigned_flow",
|
||||
"unassigned_singlestepflow"
|
||||
"unassigned_singlestepflow",
|
||||
"snapshotbuild"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
17
backend/.sqlx/query-f8cec94b94098e752f7c71cbe5e9996410a07bacebc37ed21e0bedf1a33a8fdc.json
generated
Normal file
17
backend/.sqlx/query-f8cec94b94098e752f7c71cbe5e9996410a07bacebc37ed21e0bedf1a33a8fdc.json
generated
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"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,7 +32,8 @@
|
||||
"aiagent",
|
||||
"unassigned_script",
|
||||
"unassigned_flow",
|
||||
"unassigned_singlestepflow"
|
||||
"unassigned_singlestepflow",
|
||||
"snapshotbuild"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
8
backend/.workmux.yaml
Normal file
8
backend/.workmux.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
panes:
|
||||
# Pane 1: Install dependencies, then start dev server
|
||||
- command: cargo run
|
||||
|
||||
# Pane 2: AI agent
|
||||
- command: <agent>
|
||||
split: horizontal
|
||||
focus: true
|
||||
@@ -44,11 +44,22 @@ 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`), symlinked into each crate's `src/`
|
||||
- Enterprise source is in `windmill-ee-private` folder (sibling directory at `../../windmill-ee-private` or `~/windmill-ee-private`), symlinked into each crate's `src/`
|
||||
- The `_ee.rs` files are gitignored in the main repo — they are tracked only in the `windmill-ee-private` repo
|
||||
- You can and should modify `windmill-ee-private` directly when needed (e.g., when creating new crates that need EE code, mirror the package structure there)
|
||||
- Use feature flags: `#[cfg(feature = "enterprise")]`
|
||||
- Isolate enterprise code in separate modules
|
||||
|
||||
### EE PR Workflow (MUST DO when modifying `*_ee.rs` files)
|
||||
|
||||
When you modify any `*_ee.rs` file and create a PR on the windmill repo, you **MUST** also:
|
||||
|
||||
1. **Create a matching branch** in the `windmill-ee-private` repo (use the same branch name). If using worktrees, the EE worktree is at `~/windmill-ee-private__worktrees/<branch-name>/`
|
||||
2. **Commit and push** the `_ee.rs` changes in that branch
|
||||
3. **Create a PR** on `windmill-ee-private` with a link to the companion windmill PR
|
||||
4. **Update `ee-repo-ref.txt`**: Run `bash write_latest_ee_ref.sh` from `backend/` to write the latest EE commit hash. **Important**: the script may fall back to `~/windmill-ee-private` (main branch) instead of the worktree — verify it wrote the correct commit hash from your branch, not from main. If wrong, manually write the correct hash.
|
||||
5. **Commit `ee-repo-ref.txt`** in the windmill repo so CI picks up the correct EE ref
|
||||
|
||||
## Code Validation (MUST DO)
|
||||
|
||||
After making backend changes, you MUST run `cargo check` and fix all errors and warnings before considering the work done.
|
||||
|
||||
459
backend/Cargo.lock
generated
459
backend/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,13 @@
|
||||
[package]
|
||||
name = "windmill"
|
||||
version = "1.636.0"
|
||||
version = "1.642.0"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"./windmill-object-store",
|
||||
"./windmill-api",
|
||||
"./windmill-api-scripts",
|
||||
"./windmill-api-flows",
|
||||
@@ -75,7 +76,7 @@ members = [
|
||||
exclude = ["./windmill-duckdb-ffi-internal"]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.636.0"
|
||||
version = "1.642.0"
|
||||
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -96,19 +97,19 @@ lto = "thin"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
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"]
|
||||
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"]
|
||||
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"]
|
||||
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"]
|
||||
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"]
|
||||
benchmark = ["windmill-api/benchmark", "windmill-worker/benchmark", "windmill-queue/benchmark", "windmill-common/benchmark", "windmill-api-agent-workers?/benchmark"]
|
||||
embedding = ["windmill-api/embedding"]
|
||||
parquet = ["windmill-api/parquet", "windmill-common/parquet", "windmill-worker/parquet", "dep:object_store"]
|
||||
parquet = ["windmill-api/parquet", "windmill-common/parquet", "windmill-object-store/parquet", "windmill-worker/parquet"]
|
||||
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"]
|
||||
openidconnect = ["windmill-api/openidconnect", "windmill-common/openidconnect", "windmill-object-store/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"]
|
||||
@@ -199,6 +200,7 @@ 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 }
|
||||
@@ -228,7 +230,6 @@ 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
|
||||
@@ -253,6 +254,7 @@ 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
|
||||
@@ -268,6 +270,7 @@ 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" }
|
||||
@@ -477,7 +480,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"
|
||||
native-tls = ">=0.2, <0.2.17"
|
||||
# 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 @@
|
||||
9f6e1e533df7711600ec2b8d5f0c958448db1a20
|
||||
0fede4b1086bc1456be9cc55b203228c979c5c5e
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE worker_ping DROP COLUMN IF EXISTS native_mode;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE worker_ping ADD COLUMN IF NOT EXISTS native_mode BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -16,6 +16,7 @@ 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, ObjectName, ObjectNamePart, SelectItem, TableFactor, TableObject, Value,
|
||||
ValueWithSpan, Visit, Visitor,
|
||||
CopyTarget, Expr, FunctionArg, FunctionArgExpr, ObjectName, ObjectNamePart, SelectItem,
|
||||
TableFactor, TableObject, Value, ValueWithSpan, Visit, Visitor,
|
||||
},
|
||||
dialect::DuckDbDialect,
|
||||
parser::Parser,
|
||||
@@ -125,6 +125,72 @@ 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) {
|
||||
@@ -190,13 +256,19 @@ impl AssetCollector {
|
||||
projection: &[SelectItem],
|
||||
from_tables: &[sqlparser::ast::TableWithJoins],
|
||||
) {
|
||||
// Check if this is a single-table SELECT (to avoid ambiguity)
|
||||
// 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.
|
||||
let single_table = if from_tables.len() == 1 {
|
||||
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
|
||||
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))
|
||||
}
|
||||
self.get_associated_asset_from_obj_name(name, Some(R))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -204,26 +276,48 @@ impl AssetCollector {
|
||||
None
|
||||
};
|
||||
|
||||
// Build a map of table aliases/names to assets for multi-table queries
|
||||
// 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).
|
||||
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 {
|
||||
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()
|
||||
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);
|
||||
}
|
||||
} else {
|
||||
// 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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1271,4 +1365,163 @@ 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,19 +141,7 @@ pub fn parse_db_resource(code: &str) -> Option<String> {
|
||||
cap.map(|x| x.get(1).map(|x| x.as_str().to_string()).unwrap())
|
||||
}
|
||||
|
||||
#[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 use windmill_types::s3::{s3_mode_extension, S3ModeFormat};
|
||||
pub struct S3ModeArgs {
|
||||
pub prefix: Option<String>,
|
||||
pub storage: Option<String>,
|
||||
|
||||
@@ -59,3 +59,6 @@ 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::{
|
||||
reload_custom_tags_setting, Connection, HUB_CACHE_DIR, HUB_RT_CACHE_DIR, TMP_DIR,
|
||||
TMP_LOGS_DIR, WORKER_GROUP,
|
||||
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,
|
||||
},
|
||||
KillpillSender, DEFAULT_HUB_BASE_URL, METRICS_ENABLED,
|
||||
};
|
||||
@@ -108,7 +108,7 @@ use crate::monitor::{
|
||||
};
|
||||
|
||||
#[cfg(feature = "parquet")]
|
||||
use windmill_common::s3_helpers::reload_object_store_setting;
|
||||
use windmill_object_store::reload_object_store_setting;
|
||||
|
||||
const DEFAULT_NUM_WORKERS: usize = 1;
|
||||
const DEFAULT_PORT: u16 = 8000;
|
||||
@@ -485,8 +485,7 @@ 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 WindmillInstance CRDs)");
|
||||
println!(" operator crd Print the WindmillInstance CRD YAML to stdout");
|
||||
println!(" operator Run the Kubernetes operator (watches a ConfigMap)");
|
||||
println!();
|
||||
println!("Environment variables (name = default):");
|
||||
println!(" DATABASE_URL = <required> The Postgres database url.");
|
||||
@@ -633,17 +632,11 @@ 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 controller...");
|
||||
tracing::info!("Database connected. Starting ConfigMap watcher...");
|
||||
windmill_operator::run(db).await?;
|
||||
return Ok(());
|
||||
}
|
||||
@@ -653,6 +646,9 @@ 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()
|
||||
@@ -660,11 +656,21 @@ async fn windmill_main() -> anyhow::Result<()> {
|
||||
.unwrap_or(DEFAULT_NUM_WORKERS as i32)
|
||||
};
|
||||
|
||||
// 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"
|
||||
);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
let server_mode = !std::env::var("DISABLE_SERVER")
|
||||
@@ -924,6 +930,16 @@ 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,8 +39,6 @@ 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,
|
||||
@@ -56,7 +54,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,
|
||||
NPM_CONFIG_REGISTRY_SETTING, NUGET_CONFIG_SETTING, OTEL_SETTING,
|
||||
NPMRC_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,
|
||||
@@ -84,18 +82,20 @@ 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, 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, 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,
|
||||
};
|
||||
|
||||
#[cfg(feature = "parquet")]
|
||||
use windmill_common::s3_helpers::ObjectStoreReload;
|
||||
use windmill_object_store::ObjectStoreReload;
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
use crate::ee_oss::verify_license_key;
|
||||
@@ -239,11 +239,16 @@ 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: DECODED_AGENT_TOKEN
|
||||
.as_ref()
|
||||
.map(|x| x.tags.clone())
|
||||
.unwrap_or_default(),
|
||||
worker_tags,
|
||||
env_vars: load_env_vars(
|
||||
load_whitelist_env_vars_from_env(),
|
||||
&std::collections::HashMap::new(),
|
||||
@@ -257,6 +262,7 @@ pub async fn initial_load(
|
||||
cache_clear: None,
|
||||
additional_python_paths: None,
|
||||
pip_local_dependencies: None,
|
||||
native_mode,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -324,6 +330,7 @@ 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;
|
||||
@@ -706,7 +713,7 @@ async fn send_log_file_to_object_store(
|
||||
}
|
||||
|
||||
#[cfg(feature = "parquet")]
|
||||
let s3_client = windmill_common::s3_helpers::get_object_store().await;
|
||||
let s3_client = windmill_object_store::get_object_store().await;
|
||||
#[cfg(feature = "parquet")]
|
||||
if let Some(s3_client) = s3_client {
|
||||
let path = std::path::Path::new(TMP_WINDMILL_LOGS_SERVICE)
|
||||
@@ -719,7 +726,7 @@ async fn send_log_file_to_object_store(
|
||||
tracing::error!("Error reading log file: {:?}", e);
|
||||
return;
|
||||
}
|
||||
let path = object_store::path::Path::from_url_path(format!(
|
||||
let path = windmill_object_store::object_store_reexports::Path::from_url_path(format!(
|
||||
"{}{hostname}/{highest_file}",
|
||||
windmill_common::tracing_init::LOGS_SERVICE
|
||||
));
|
||||
@@ -1168,7 +1175,7 @@ async fn delete_log_files_from_disk_and_store(
|
||||
_s3_prefix: &str,
|
||||
) {
|
||||
#[cfg(feature = "parquet")]
|
||||
let os = windmill_common::s3_helpers::get_object_store().await;
|
||||
let os = windmill_object_store::get_object_store().await;
|
||||
#[cfg(not(feature = "parquet"))]
|
||||
let os: Option<()> = None;
|
||||
|
||||
@@ -1198,7 +1205,10 @@ async fn delete_log_files_from_disk_and_store(
|
||||
#[cfg(feature = "parquet")]
|
||||
if _should_del_from_store {
|
||||
if let Some(os) = _os2 {
|
||||
let p = object_store::path::Path::from(format!("{}{}", _s3_prefix, path));
|
||||
let p = windmill_object_store::object_store_reexports::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 {
|
||||
@@ -1297,6 +1307,10 @@ 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,
|
||||
@@ -2242,6 +2256,11 @@ 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);
|
||||
|
||||
@@ -2330,7 +2349,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",
|
||||
"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",
|
||||
threshold.to_string()
|
||||
)
|
||||
.fetch_all(db)
|
||||
|
||||
@@ -7,6 +7,7 @@ REVERT="NO"
|
||||
COPY="NO"
|
||||
MOVE_NEW_FILES="NO"
|
||||
EE_CODE_DIR="../windmill-ee-private/"
|
||||
DIR_EXPLICIT="NO"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
@@ -34,6 +35,7 @@ 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
|
||||
;;
|
||||
@@ -53,8 +55,25 @@ if [[ $EE_CODE_DIR == /* ]]; then
|
||||
else
|
||||
EE_CODE_DIR="${root_dirpath}/${EE_CODE_DIR}"
|
||||
fi
|
||||
echo "EE code directory = ${EE_CODE_DIR} | Revert = ${REVERT}"
|
||||
|
||||
# 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"
|
||||
|
||||
474
backend/test_debounce_e2e.sh
Executable file
474
backend/test_debounce_e2e.sh
Executable file
@@ -0,0 +1,474 @@
|
||||
#!/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,8 +104,9 @@ 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,
|
||||
@@ -135,8 +136,9 @@ 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,
|
||||
@@ -167,8 +169,9 @@ 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,
|
||||
@@ -207,8 +210,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,
|
||||
@@ -245,8 +248,9 @@ 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,
|
||||
@@ -276,8 +280,9 @@ 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,
|
||||
@@ -318,8 +323,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,
|
||||
@@ -358,8 +363,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,
|
||||
@@ -398,8 +403,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,
|
||||
@@ -437,8 +442,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,
|
||||
@@ -474,8 +479,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,
|
||||
@@ -516,8 +521,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,
|
||||
@@ -613,8 +618,9 @@ 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,
|
||||
@@ -647,8 +653,9 @@ 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,
|
||||
@@ -693,8 +700,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,
|
||||
@@ -741,8 +748,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,
|
||||
@@ -791,8 +798,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,
|
||||
@@ -836,8 +843,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,
|
||||
@@ -859,11 +866,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, BUN_DEDICATED_WORKER_ARGS, LoaderMode,
|
||||
build_loader, generate_dedicated_worker_wrapper, LoaderMode, BUN_DEDICATED_WORKER_ARGS,
|
||||
BUN_PATH, NODE_BIN_PATH,
|
||||
};
|
||||
|
||||
@@ -934,12 +941,8 @@ 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
|
||||
@@ -992,7 +995,10 @@ 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),
|
||||
@@ -1162,8 +1168,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,
|
||||
@@ -1190,6 +1196,68 @@ 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.
|
||||
@@ -1241,8 +1309,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")
|
||||
}
|
||||
@@ -1257,7 +1325,10 @@ 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");
|
||||
}
|
||||
|
||||
@@ -1271,7 +1342,10 @@ 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");
|
||||
}
|
||||
|
||||
@@ -1304,9 +1378,18 @@ 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");
|
||||
}
|
||||
|
||||
@@ -1330,8 +1413,15 @@ 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)
|
||||
@@ -1347,7 +1437,10 @@ 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
Normal file
10
backend/tests/fixtures/wmill_cli_test.sql
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
-- 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"].cache_clear, Some(5));
|
||||
assert_eq!(config.worker_configs["my_group_name"].extra["cache_clear"], serde_json::json!(5));
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
@@ -280,6 +280,7 @@ async fn test_apply_settings_diff_upserts_only(db: Pool<Postgres>) {
|
||||
m
|
||||
},
|
||||
deletes: vec![],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
apply_settings_diff(&db, &diff).await.unwrap();
|
||||
@@ -303,6 +304,7 @@ 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();
|
||||
@@ -323,6 +325,7 @@ 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();
|
||||
@@ -358,6 +361,7 @@ async fn test_apply_settings_diff_upsert_overwrites(db: Pool<Postgres>) {
|
||||
m
|
||||
},
|
||||
deletes: vec![],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
apply_settings_diff(&db, &diff).await.unwrap();
|
||||
@@ -385,6 +389,7 @@ async fn test_apply_settings_diff_complex_json(db: Pool<Postgres>) {
|
||||
m
|
||||
},
|
||||
deletes: vec![],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
apply_settings_diff(&db, &diff).await.unwrap();
|
||||
@@ -396,7 +401,11 @@ 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()] };
|
||||
SettingsDiff {
|
||||
upserts: BTreeMap::new(),
|
||||
deletes: vec!["does_not_exist".to_string()],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Should not error
|
||||
apply_settings_diff(&db, &diff).await.unwrap();
|
||||
@@ -648,7 +657,11 @@ 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![] };
|
||||
let diff = SettingsDiff {
|
||||
upserts: map.into_iter().collect(),
|
||||
deletes: vec![],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
apply_settings_diff(&db, &diff).await.unwrap();
|
||||
|
||||
@@ -694,6 +707,7 @@ async fn test_idempotent_apply(db: Pool<Postgres>) {
|
||||
m
|
||||
},
|
||||
deletes: vec![],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Apply twice
|
||||
@@ -852,7 +866,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"].cache_clear, Some(7));
|
||||
assert_eq!(config.worker_configs["default"].extra["cache_clear"], serde_json::json!(7));
|
||||
let gpu_auto = config.worker_configs["gpu"].autoscaling.as_ref().unwrap();
|
||||
assert!(gpu_auto.enabled);
|
||||
assert_eq!(gpu_auto.min_workers, Some(0));
|
||||
|
||||
275
backend/tests/nativets_dedicated.rs
Normal file
275
backend/tests/nativets_dedicated.rs
Normal file
@@ -0,0 +1,275 @@
|
||||
/*
|
||||
* Tests for the PrewarmedIsolate used by nativets dedicated workers.
|
||||
*
|
||||
* Run with:
|
||||
* cargo test -p windmill --features "deno_core" --test nativets_dedicated -- --nocapture
|
||||
*/
|
||||
|
||||
#[cfg(feature = "deno_core")]
|
||||
mod prewarmed_isolate_tests {
|
||||
use std::process::Command;
|
||||
use windmill_runtime_nativets::{NativeAnnotation, PrewarmedIsolate};
|
||||
use windmill_worker::{build_loader, LoaderMode, BUN_PATH};
|
||||
|
||||
fn default_annotation() -> NativeAnnotation {
|
||||
NativeAnnotation { useragent: None, proxy: None }
|
||||
}
|
||||
|
||||
/// Bundle a TypeScript script into JS suitable for `PrewarmedIsolate`.
|
||||
///
|
||||
/// Returns `(ts_source, js_bundle, arg_names)`.
|
||||
async fn bundle_script(script: &str) -> (String, String, Vec<String>) {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let dir = temp_dir.path();
|
||||
let dir_str = dir.to_str().unwrap();
|
||||
|
||||
std::fs::write(dir.join("main.ts"), script).unwrap();
|
||||
|
||||
build_loader(
|
||||
dir_str,
|
||||
"http://localhost:8000",
|
||||
"test_token",
|
||||
"test-workspace",
|
||||
"f/test/script",
|
||||
LoaderMode::BrowserBundle,
|
||||
)
|
||||
.await
|
||||
.expect("build_loader failed");
|
||||
|
||||
let output = Command::new(BUN_PATH.as_str())
|
||||
.args(["run", dir.join("node_builder.ts").to_str().unwrap()])
|
||||
.current_dir(dir)
|
||||
.output()
|
||||
.expect("Failed to run bun build");
|
||||
|
||||
if !output.status.success() {
|
||||
panic!(
|
||||
"Bun build failed: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
let ts = std::fs::read_to_string(dir.join("main.ts")).unwrap();
|
||||
let js = std::fs::read_to_string(dir.join("main.js")).unwrap();
|
||||
let parsed = windmill_parser_ts::parse_deno_signature(&ts, true, false, None)
|
||||
.expect("failed to parse signature");
|
||||
let arg_names: Vec<String> = parsed.args.into_iter().map(|a| a.name).collect();
|
||||
(ts, js, arg_names)
|
||||
}
|
||||
|
||||
const TEST_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
|
||||
|
||||
async fn run_prewarmed_test(
|
||||
script: &str,
|
||||
jobs: Vec<serde_json::Value>,
|
||||
) -> Vec<Result<serde_json::Value, String>> {
|
||||
tokio::time::timeout(TEST_TIMEOUT, run_prewarmed_test_inner(script, jobs))
|
||||
.await
|
||||
.expect("test timed out after 30s")
|
||||
}
|
||||
|
||||
async fn run_prewarmed_test_inner(
|
||||
script: &str,
|
||||
jobs: Vec<serde_json::Value>,
|
||||
) -> Vec<Result<serde_json::Value, String>> {
|
||||
windmill_runtime_nativets::setup_deno_runtime().expect("V8 init failed");
|
||||
|
||||
let (_ts, js, arg_names) = bundle_script(script).await;
|
||||
let ann = default_annotation();
|
||||
|
||||
let mut results = Vec::new();
|
||||
|
||||
for job_args in &jobs {
|
||||
let mut isolate =
|
||||
PrewarmedIsolate::spawn("".to_string(), js.clone(), ann.clone(), arg_names.clone());
|
||||
isolate.wait_ready().await.expect("isolate failed to warm");
|
||||
|
||||
let args = serde_json::to_string(job_args).unwrap();
|
||||
let executing = isolate.start_execution(args);
|
||||
let prewarmed_result = executing.wait().await.expect("isolate execution failed");
|
||||
|
||||
match prewarmed_result.result {
|
||||
Ok(raw) => {
|
||||
let value: serde_json::Value =
|
||||
serde_json::from_str(raw.get()).unwrap_or(serde_json::Value::Null);
|
||||
results.push(Ok(value));
|
||||
}
|
||||
Err(e) => {
|
||||
results.push(Err(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_prewarmed_simple() {
|
||||
let script = r#"
|
||||
export function main(x: number, y: number): number {
|
||||
return x + y;
|
||||
}
|
||||
"#;
|
||||
let results = run_prewarmed_test(script, vec![serde_json::json!({"x": 2, "y": 3})]).await;
|
||||
|
||||
assert_eq!(results.len(), 1);
|
||||
assert_eq!(results[0], Ok(serde_json::json!(5)));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_prewarmed_isolation() {
|
||||
let script = r#"
|
||||
let counter = 0;
|
||||
export function main(): number {
|
||||
counter++;
|
||||
return counter;
|
||||
}
|
||||
"#;
|
||||
let results =
|
||||
run_prewarmed_test(script, vec![serde_json::json!({}), serde_json::json!({})]).await;
|
||||
|
||||
assert_eq!(results.len(), 2);
|
||||
// Each job gets a fresh isolate, so counter should be 1 both times
|
||||
assert_eq!(results[0], Ok(serde_json::json!(1)));
|
||||
assert_eq!(results[1], Ok(serde_json::json!(1)));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_prewarmed_error() {
|
||||
let script = r#"
|
||||
export function main(msg: string): never {
|
||||
throw new Error(msg);
|
||||
}
|
||||
"#;
|
||||
let results =
|
||||
run_prewarmed_test(script, vec![serde_json::json!({"msg": "test error"})]).await;
|
||||
|
||||
assert_eq!(results.len(), 1);
|
||||
assert!(results[0].is_err());
|
||||
assert!(
|
||||
results[0].as_ref().unwrap_err().contains("test error"),
|
||||
"Error should contain 'test error', got: {}",
|
||||
results[0].as_ref().unwrap_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_prewarmed_async() {
|
||||
let script = r#"
|
||||
export async function main(x: number): Promise<number> {
|
||||
const val = await Promise.resolve(x * 10);
|
||||
return val + 1;
|
||||
}
|
||||
"#;
|
||||
let results = run_prewarmed_test(script, vec![serde_json::json!({"x": 7})]).await;
|
||||
|
||||
assert_eq!(results.len(), 1);
|
||||
assert_eq!(results[0], Ok(serde_json::json!(71)));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_prewarmed_pipeline() {
|
||||
windmill_runtime_nativets::setup_deno_runtime().expect("V8 init failed");
|
||||
|
||||
let script = r#"
|
||||
export function main(n: number): number {
|
||||
return n * 2;
|
||||
}
|
||||
"#;
|
||||
let (_ts, js, arg_names) = bundle_script(script).await;
|
||||
let ann = default_annotation();
|
||||
|
||||
// Pre-warm first isolate
|
||||
let mut warm =
|
||||
PrewarmedIsolate::spawn("".to_string(), js.clone(), ann.clone(), arg_names.clone());
|
||||
warm.wait_ready()
|
||||
.await
|
||||
.expect("first isolate failed to warm");
|
||||
|
||||
let mut results = Vec::new();
|
||||
|
||||
for i in 1..=3 {
|
||||
let args = serde_json::to_string(&serde_json::json!({"n": i})).unwrap();
|
||||
let executing = warm.start_execution(args);
|
||||
|
||||
// Pipeline: start pre-warming next isolate while current one runs
|
||||
warm =
|
||||
PrewarmedIsolate::spawn("".to_string(), js.clone(), ann.clone(), arg_names.clone());
|
||||
|
||||
let prewarmed_result = executing.wait().await.expect("isolate execution failed");
|
||||
match prewarmed_result.result {
|
||||
Ok(raw) => {
|
||||
let value: serde_json::Value =
|
||||
serde_json::from_str(raw.get()).unwrap_or(serde_json::Value::Null);
|
||||
results.push(value);
|
||||
}
|
||||
Err(e) => panic!("unexpected error: {e}"),
|
||||
}
|
||||
|
||||
warm.wait_ready()
|
||||
.await
|
||||
.expect("next isolate failed to warm");
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
results,
|
||||
vec![
|
||||
serde_json::json!(2),
|
||||
serde_json::json!(4),
|
||||
serde_json::json!(6),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_prewarmed_complex_return() {
|
||||
let script = r#"
|
||||
export function main(name: string, items: number[]): any {
|
||||
return {
|
||||
greeting: `hello ${name}`,
|
||||
sum: items.reduce((a, b) => a + b, 0),
|
||||
items: items.map(x => x * 2),
|
||||
};
|
||||
}
|
||||
"#;
|
||||
let results = run_prewarmed_test(
|
||||
script,
|
||||
vec![serde_json::json!({"name": "world", "items": [1, 2, 3]})],
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(results.len(), 1);
|
||||
assert_eq!(
|
||||
results[0],
|
||||
Ok(serde_json::json!({
|
||||
"greeting": "hello world",
|
||||
"sum": 6,
|
||||
"items": [2, 4, 6],
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_prewarmed_null_undefined() {
|
||||
let script = r#"
|
||||
export function main(returnNull: boolean): any {
|
||||
if (returnNull) {
|
||||
return null;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
"#;
|
||||
let results = run_prewarmed_test(
|
||||
script,
|
||||
vec![
|
||||
serde_json::json!({"returnNull": true}),
|
||||
serde_json::json!({"returnNull": false}),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(results.len(), 2);
|
||||
assert_eq!(results[0], Ok(serde_json::Value::Null));
|
||||
assert_eq!(results[1], Ok(serde_json::Value::Null));
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user