Compare commits

..

72 Commits

Author SHA1 Message Date
Ruben Fiszel
a9bac09b14 all 2026-02-11 18:37:15 +00:00
Ruben Fiszel
607a861b80 all 2026-02-11 18:20:14 +00:00
Ruben Fiszel
5ecfa92ac3 fix: install powershell via dotnet tool for arm64 ci runner
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 16:48:41 +00:00
Ruben Fiszel
a3d5ae5425 all 2026-02-11 16:37:11 +00:00
Ruben Fiszel
d363fa6098 all 2026-02-11 16:28:20 +00:00
Ruben Fiszel
44143e7576 replace leftover common:: references in dependency_map test (#7911)
* fix: replace leftover common:: references in dependency_map test

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: add missing deno_core/mcp features and gate dead code in permissions test

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 16:05:34 +00:00
Ruben Fiszel
4d94d80835 add missing feature-gated deps to api-integration-tests crate
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 15:09:14 +00:00
Ruben Fiszel
c9d5c24c34 chore(main): release 1.631.2 (#7906)
* chore(main): release 1.631.2

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2026-02-11 14:43:13 +00:00
Ruben Fiszel
b11d6ed794 fix(frontend): revert CloseButton refactor that broke tag removal in MultiSelect (#7909)
The recent refactor of CloseButton (from on:close component events to
onClick prop) broke tag removal in MultiSelect/TagsToListenTo. The
refactor changed on:pointerdown (component event) to onPointerdown
(native DOM event), which stopped native pointerdown propagation and
broke the drag tracking in DraggableTags, causing the dropdown to open
on every close button click.

Reverts CloseButton and all callers back to using createEventDispatcher
and on:close.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 14:27:50 +00:00
Ruben Fiszel
ea52a8b8ce refactor: move integration tests to subcrates to reduce recompilation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 14:04:54 +00:00
Ruben Fiszel
b9e0ccaefd chore: split api-scripts and api-flows 2026-02-11 13:27:00 +00:00
Devdatta Talele
5effb87a36 fix: strip unsupported schema fields for Google AI (#7894)
Closes #7759
2026-02-11 13:26:08 +00:00
wendrul
900c76ccad fix: nix flake libz.so for deno_core (#7905) 2026-02-11 13:13:36 +00:00
Ruben Fiszel
ff72b81004 chore(main): release 1.631.1 (#7904)
* chore(main): release 1.631.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2026-02-11 12:45:39 +00:00
Ruben Fiszel
2815cfae1a fix: add kafka-gssapi support to ee builds 2026-02-11 12:40:59 +00:00
Ruben Fiszel
700c61243f chore(main): release 1.631.0 (#7898)
* chore(main): release 1.631.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2026-02-11 12:08:25 +00:00
centdix
f22eb964e4 feat(ai): support 1M context window for Anthropic resources (#7891)
* feat(ai): support 1M context window for Anthropic resources

Add `enable_1m_context` boolean field to Anthropic resource configuration.
When enabled (and not using Vertex AI), sends the `anthropic-beta: context-1m-2025-08-07`
header in both the API proxy layer and the AI agent worker layer.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(ai): add serde alias for enable_1M_context DB field name

The resource_type schema uses `enable_1M_context` (uppercase M) but
serde only matched `enable_1m_context` and `enable1mContext`, causing
the field to always deserialize as false.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 12:01:46 +00:00
hugocasa
754b48cb89 fix: waitJob getJob and streamJob in raw apps (#7901)
* fix: waitJob getJob and streamJob in raw apps

* nits

* use latest ui builder

* fix

---------

Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2026-02-11 11:41:37 +00:00
Pyra
097d9288c5 feat(uv): index resolve strategy (#7885)
* fix

* fix

Signed-off-by: pyranota <pyra@duck.com>

* Update backend/windmill-worker/src/python_executor.rs

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>

* Update backend/windmill-worker/src/python_executor.rs

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>

---------

Signed-off-by: pyranota <pyra@duck.com>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
2026-02-11 11:18:51 +00:00
Ruben Fiszel
92c601860f fix: otel gracefully handle no native ts 2026-02-11 11:15:01 +00:00
Ruben Fiszel
e26e437dd1 refactor: extract 12 leaf crates from windmill-api (#7899)
* feat(backend): extract 12 leaf crates from windmill-api to improve incremental compilation

Extract independent modules from windmill-api (90k LOC monolith) into
separate leaf crates to reduce incremental compilation times. Modules
extracted: assets, configs, debug, flow-conversations, inputs,
npm-proxy, openapi, schedule, settings, workers, agent-workers, and
alerting (from windmill-common).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: add windmill-api-settings dep to root crate, make ee_oss public

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test: add agent_workers integration tests

8 tests covering agent worker lifecycle:
- Simple script execution (bun)
- Script with arguments
- Script with logs (verified in job_logs table)
- Script failure handling
- Complex result (nested objects/arrays)
- Agent token creation via API
- Token creation + Initial/MainLoop ping cycle
- Multiple sequential job execution

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* all

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 10:31:03 +00:00
Ruben Fiszel
9e8d77436b dotNet (#7900) 2026-02-11 10:23:31 +00:00
Ruben Fiszel
7eb239f1e2 fix: worker do not apply migrations anymore but wait for servers to do so 2026-02-11 08:16:06 +00:00
Ruben Fiszel
317ae9196b update refs 2026-02-11 07:12:45 +00:00
Ruben Fiszel
20bb97fc1f reorg mcp_tools 2026-02-11 06:44:10 +00:00
Ruben Fiszel
5e9b4cfa99 nit UI + more tests 2026-02-11 06:27:21 +00:00
Guilhem
31bfccc745 fix(frontend): improve time picker (#7893)
* clean layout

* remove useless toggle

* improve layout

* nit

* Fix input type seconds

* convert secondInputs to svelte 5

* redesign seconds input

* Adapt layout where time input is used

* nit
2026-02-11 00:48:18 +00:00
Ruben Fiszel
0ad4f0232e chore(main): release 1.630.2 (#7897)
* chore(main): release 1.630.2

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2026-02-11 00:29:42 +00:00
Ruben Fiszel
02f4359ba4 bump rust to 1.93.0 2026-02-11 00:02:24 +00:00
Ruben Fiszel
1a109a7797 fix: bump rust version from 1.90.0 to 1.93.0 2026-02-10 23:55:36 +00:00
Ruben Fiszel
3d888c8135 chore(main): release 1.630.1 (#7896)
* chore(main): release 1.630.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2026-02-10 23:45:51 +00:00
Ruben Fiszel
1c4a15bc26 fix compile 2026-02-10 23:34:15 +00:00
Alexander Petric
7147dde511 fix: enforce self-approval check on flow resume owner endpoint (#7886)
* fix: enforce self-approval check on flow resume owner endpoint

* sqlx

* fix test

* fix mcp fields

* test self aproval allowed + other user approves flow
2026-02-10 23:14:40 +00:00
centdix
3b1b4051eb adapt for rmcp update (#7895) 2026-02-10 23:11:19 +00:00
centdix
c3be7376fb internal: fix git actions (#7892)
* fix git actions

* fix

* rm
2026-02-10 23:11:03 +00:00
Ruben Fiszel
2bafaca40d chore(main): release 1.630.0 (#7867)
* chore(main): release 1.630.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2026-02-10 17:58:58 +00:00
Alexander Petric
1037932608 restore worker-hours occupancy calculation for CU billing (#7887)
* fix: restore worker-hours occupancy calculation for CU billing

* ee ref

---------

Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2026-02-10 17:50:55 +00:00
Ruben Fiszel
4c87e7ac2e fix: retry js eval up to 3 times on timeout from slow DB (#7890)
When eval_timeout_quickjs hits the timeout (typically due to slow DB
result retrieval), retry up to 2 more times with a 5s interval between
attempts. Non-timeout errors are returned immediately without retry.

Also extract the eval timeout duration as EVAL_TIMEOUT_MS const (set to
20000ms, up from 10000ms) in windmill-jseval.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Alexander Petric <alpetric@users.noreply.github.com>
2026-02-10 17:50:41 +00:00
centdix
6272cd17a4 feat(aiagent): add prompt caching for Anthropic models (#7878)
* feat: add prompt caching support for Anthropic API

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* exclude vertex

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-10 17:42:49 +00:00
wendrul
6b10416eba add test for protection rulesets (#7889) 2026-02-10 17:35:38 +00:00
Diego Imbert
8a96c90e4b Fix AgGrid modal event handlers + openedModalRow output (#7888)
* Fix broken event handlers in AgGrid Modal buttons

* Add openedModalRow output in AgGrid

* clear openedModalRow
2026-02-10 17:35:26 +00:00
hugocasa
1150eec757 fix(backend): correct early return with stream + prevent delta miss (#7872) 2026-02-10 17:20:14 +00:00
Tammo Ippen
5d29124d9f Add 'on_behalf_of_email' to flow inputs schema (#7854) 2026-02-10 17:00:09 +00:00
wendrul
2851b6b7ca feat: restriction rulesets for workspaces (#7879) 2026-02-10 16:49:25 +00:00
wendrul
25255f9526 Fix nix flake (missing curl for compiling all features) (#7881) 2026-02-10 16:46:06 +00:00
centdix
a7e269f9f3 feat: add workspace search and runnable details tools to AI chat modes (#7874)
* feat: add workspace search and runnable details tools to navigator mode

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: correct uFuzzy search result indexing in workspace search

uFuzzy.search() returns [idxs, info, order] where order contains indices
into idxs, not into the original haystack. The code was using order values
directly as array indices, returning wrong results.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: mutualize search_workspace and get_runnable_details tools

- Move search_workspace tool def + implementation into shared.ts as
  createSearchWorkspaceTool() factory, used by navigator and flow modes
- Move get_runnable_details tool into shared.ts as
  createGetRunnableDetailsTool() factory, used by navigator, flow, and
  script modes
- Replace flow mode's scripts-only search_scripts with search_workspace
  that searches both scripts and flows
- Add search_workspace and get_runnable_details to script mode
- Remove duplicated WorkspaceScriptsSearch class from flow/core.ts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: add get_runnable_details to flow mode system prompt

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: add hard limit on runnable content passed to AI context

Truncate script content and flow value at 20k chars in
get_runnable_details to avoid flooding the context window.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: make search_workspace type param required for strict schema

OpenAI strict mode requires all properties in required array. Make type
a required enum ('all', 'scripts', 'flows') instead of optional.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* cleaning

* nit

* cleaning

* refactor: use shared createSearchWorkspaceTool in app mode

Replace app mode's local list_workspace_runnables tool with the shared
createSearchWorkspaceTool() factory, consistent with navigator, flow,
and script modes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* search by keyword

* cleaning

* fix: document search_workspace and get_runnable_details in script mode system prompt

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: add get_runnable_details tool to app mode

Without it, the AI can find scripts/flows but can't inspect their
schema/content when configuring backend runnables with correct inputs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: race condition in WorkspaceRunnablesSearch workspace caching

Track scriptsWorkspace and flowsWorkspace separately instead of a single
shared workspace field. Previously, initScripts could update the shared
workspace field, causing initFlows to skip re-fetching when the workspace
changed (it saw the workspace already matched), returning stale data.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 16:44:07 +00:00
Ruben Fiszel
af02e200a5 add benchmark variants and fix db pool connection starvation (#7884)
Add new BENCHMARK_KIND variants (sequentialflow, scriptlogs, concurrencylimit,
concurrencykey, mixed, mixed_no_cc) for targeted performance testing. Fix shared
iteration counting across workers using a global atomic counter. Add job_perms
inserts and queue diagnostics for benchmark mode.

Move db connection setup to dedicated module and drop the initial connection pool
before creating the main one, preventing connection starvation when PostgreSQL
max_connections is low.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 16:41:47 +00:00
wendrul
40096e45e3 fix typing of app value for npm check (#7883) 2026-02-10 16:28:50 +00:00
Diego Imbert
fdd7d63311 Fix crash on invalid AI provider (#7880)
* Fix crash on invalid AI provider

* nit handle null
2026-02-10 16:27:48 +00:00
centdix
03eb16a7c6 feat(mcp): add endpoint tools for scripts, flows, apps, and jobs (#7859)
* add endpoints

* feat: add MCP tools for script/flow/app CRUD and run endpoints with field filtering

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: convert enum arrays to description text in MCP tool schemas

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: auto-detect and rename conflicting parameter names across MCP tool schemas

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: use two-pass approach in convert_enums_to_descriptions to preserve dict ordering

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: add MCP instructions to createScript, runScriptByPath, and runFlowByPath

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: add query param exclusion for MCP tools, slim down run endpoints

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: preserve additional top-level keys in allOf schema flattening

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: replace x-mcp-tool-exclude-query-params with x-mcp-tool-include-query-params

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: replace empty {} schemas with valid JSON Schema draft 2020-12 equivalents

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: revert openapi value:{} changes, sanitize empty schemas in generator instead

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2026-02-10 11:30:39 +00:00
Luigi
8e19f9652d Added serpapi import handling (#7871)
Added the handling for serpapi import. The name of the Python library to be installed is google-search-results.

Source: https://pypi.org/project/google-search-results/
2026-02-10 09:59:35 +00:00
Ruben Fiszel
45980f0220 resolve Windows build warnings treated as errors (#7870)
* fix: resolve Windows build warnings treated as errors

- Gate UV_PATH import behind #[cfg(unix)] in python_versions.rs
- Remove unused tokio::time::sleep import in worker.rs (use fully qualified path)
- Fix unused `file` variable warnings in ansible_executor.rs on Windows

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* ci: add Windows cargo check workflow

Runs cargo check with ee_windows features on push to backend/**
using the blacksmith-16vcpu-windows-2025 runner.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* ci: add cargo check step to Windows build, remove separate check workflow

Add a cargo check step with -D warnings before the full build to fail
fast on any warnings. Remove the separate windows-check.yml workflow.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 09:58:56 +00:00
hugocasa
8363ff1eee feat: download encrypted usage (#7804) 2026-02-10 09:50:24 +00:00
Ruben Fiszel
cf596f370a fix: gate Permissions import behind #[cfg(unix)] for Windows build
Move `use std::fs::Permissions` and `use std::os::unix::fs::PermissionsExt`
inside the #[cfg(unix)] block to avoid unused import error on Windows.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 09:02:09 +00:00
Ruben Fiszel
b12304d834 chore(main): release 1.629.1 (#7862)
* chore(main): release 1.629.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2026-02-10 08:44:44 +00:00
Ruben Fiszel
b5f8d931bd ci: add libcurl4-openssl-dev to backend-test workflow
Required by rdkafka-sys 4.10.0 which unconditionally includes curl/curl.h.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 08:39:45 +00:00
Ruben Fiszel
f927c673b5 ci: improve Windows builds with faster runner, caching, and 3h timeout
- Switch to blacksmith-16vcpu-windows-2025 runner
- Replace deprecated actions-rs/toolchain with actions-rust-lang/setup-rust-toolchain with cargo caching
- Increase build timeout from 90min to 180min

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 08:31:27 +00:00
Ruben Fiszel
4f653ca957 adapt to libkafka-changes 2026-02-10 08:06:06 +00:00
Ruben Fiszel
08031640a0 fix: remove unecessary drop index on labeled_jobs_on_jobs 2026-02-10 08:02:38 +00:00
Ruben Fiszel
244c09fcd7 chore(main): release 1.629.0 (#7844)
* chore(main): release 1.629.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2026-02-09 23:03:21 +00:00
Ruben Fiszel
1456f73c16 nit test 2026-02-09 22:51:57 +00:00
Ruben Fiszel
4343b73485 fix: reduce DB pool contention by eliminating dual-connection patterns (#7861) 2026-02-09 22:48:29 +00:00
Ruben Fiszel
b4b3edb64d nit test
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 21:13:52 +00:00
Ruben Fiszel
4631e58c66 test-thread 10 2026-02-09 20:50:23 +00:00
Ruben Fiszel
eb827ce5e8 nit backend tests
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 18:28:01 +00:00
Guilhem
dd421845ba fix(frontend): reorganize workspace settings (#7788)
* Add vertical nav bar to workspace settings

* harmonize settings content titles

* remove sidebar icons

* add background to sidebar

* nit user section

* EEonly display

* Workspace settings general design

* Add schema validation and dirty detection

* Put critical alerts in a separated tab

* separate error success handler

* only enable save when there is some changes

* Fix dirty detection for deployment UI

* Only enable save button when changes for datatables ws storage

* Add setting footer component

* Use new footer setting for saving configs

* nit

* apply setting footer

* improve save button

* nit

* nit

* nit

* make ws app use same pattern as other tabs

* Separate scrolling between sidebar and content

* Gather error handlers

* use universal save button for object storage

* Title sentence case

* nit

* nit

* improve dirty config logic

* nit

* nit

* clean dead code

* Use settings footer for deployment settings

* Git sync settings

* move tabs

* fix dirty stats of error handlers

* nit

* nit
2026-02-09 18:22:22 +00:00
hugocasa
b1d6ac91bd fix: restart after empty branchone + improve UI (#7838)
* improve flow restart UI

* fix: better restart UI + fix restart avec empty branch one
2026-02-09 18:13:18 +00:00
Ruben Fiszel
894d8a94f8 reuse existing transaction in push to reduce pool pressure (#7858)
* fix: reuse existing transaction in push instead of acquiring new connection

In push_inner, fetch_authed_from_permissioned_as was acquiring a new
connection from the pool to fetch job permissions, even though a
transaction was already open. Use fetch_authed_from_permissioned_as_conn
with the existing transaction instead, reducing pool pressure when many
jobs are pushed concurrently.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* improve contention

* improve contention

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 18:07:33 +00:00
Ruben Fiszel
7d37a83d4f nit backend tests 2026-02-09 18:05:14 +00:00
Ruben Fiszel
750926d8da fix form on small screens 2026-02-09 17:57:05 +00:00
Ruben Fiszel
0b0696459e fix missing overflow on form 2026-02-09 17:53:16 +00:00
hugocasa
a3fc27b232 fix(backend): prevent sqs hanging (#7857)
* fix(backend): prevent sqs hanging

* fix dep

* chore: update ee-repo-ref to b1916254951d504db136759f4150a40d3a88a638

This commit updates the EE repository reference after PR #410 was merged in windmill-ee-private.

Previous ee-repo-ref: a5d74260b942eb208cd4b963bd63d74ad5240931

New ee-repo-ref: b1916254951d504db136759f4150a40d3a88a638

Automated by sync-ee-ref workflow.

---------

Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2026-02-09 16:25:18 +00:00
316 changed files with 12870 additions and 3685 deletions

View File

@@ -23,7 +23,7 @@ jobs:
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache: false
toolchain: 1.90.0
toolchain: 1.93.0
- name: cargo check
working-directory: ./backend
timeout-minutes: 16
@@ -39,12 +39,12 @@ jobs:
- name: install xmlsec1 and gssapi
run: |
sudo apt-get update
sudo apt-get install -y libxml2-dev libxmlsec1-dev libkrb5-dev libsasl2-dev mold clang
sudo apt-get install -y libxml2-dev libxmlsec1-dev libkrb5-dev libsasl2-dev libcurl4-openssl-dev mold clang
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache: false
toolchain: 1.90.0
toolchain: 1.93.0
- name: cargo check
working-directory: ./backend
timeout-minutes: 16
@@ -81,7 +81,7 @@ jobs:
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache: false
toolchain: 1.90.0
toolchain: 1.93.0
- name: cargo check
working-directory: ./backend
timeout-minutes: 16
@@ -109,7 +109,7 @@ jobs:
- name: install xmlsec1 and gssapi
run: |
sudo apt-get update
sudo apt-get install -y libxml2-dev libxmlsec1-dev libkrb5-dev libsasl2-dev mold clang
sudo apt-get install -y libxml2-dev libxmlsec1-dev libkrb5-dev libsasl2-dev libcurl4-openssl-dev mold clang
- name: Substitute EE code (EE logic is behind feature flag)
run: |
@@ -118,7 +118,7 @@ jobs:
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache-workspaces: backend
toolchain: 1.90.0
toolchain: 1.93.0
- name: cargo check
timeout-minutes: 16
working-directory: ./backend

View File

@@ -19,7 +19,7 @@ defaults:
jobs:
cargo_test:
runs-on: ubicloud-standard-16
runs-on: blacksmith-32vcpu-ubuntu-2404
services:
postgres:
image: postgres
@@ -50,9 +50,9 @@ jobs:
- uses: denoland/setup-deno@v2
with:
deno-version: v2.x
- uses: actions/setup-go@v2
- uses: actions/setup-go@v6
with:
go-version: 1.21.5
go-version: 1.25.0
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.8
@@ -72,12 +72,14 @@ jobs:
bundler-cache: false
- name: Install PowerShell, mold and clang
run: |
sudo apt-get update && sudo apt-get install -y powershell mold clang
dotnet tool install --global PowerShell
echo "$HOME/.dotnet/tools" >> $GITHUB_PATH
sudo apt-get update && sudo apt-get install -y mold clang libcurl4-openssl-dev
working-directory: /
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache: false
toolchain: 1.90.0
cache: true
toolchain: 1.93.0
- name: Read EE repo commit hash
run: |
echo "ee_repo_ref=$(cat ./ee-repo-ref.txt)" >> "$GITHUB_ENV"
@@ -214,7 +216,7 @@ jobs:
RUST_LOG: "off"
RUST_LOG_STYLE: never
CARGO_NET_GIT_FETCH_WITH_CLI: true
CARGO_BUILD_JOBS: 12
CARGO_BUILD_JOBS: 16
WMDEBUG_FORCE_V0_WORKSPACE_DEPENDENCIES: 1
WMDEBUG_FORCE_RUNNABLE_SETTINGS_V0: 1
WMDEBUG_FORCE_NO_LEGACY_DEBOUNCING_COMPAT: 1
@@ -222,4 +224,4 @@ jobs:
run: |
deno --version && bun -v && node --version && go version && python3 --version && php --version && ruby --version && pwsh --version && dotnet --version
cd windmill-duckdb-ffi-internal && ./build_dev.sh && cd ..
DENO_PATH=$(which deno) BUN_PATH=$(which bun) NODE_BIN_PATH=$(which node) GO_PATH=$(which go) UV_PATH=$(which uv) PHP_PATH=$(which php) COMPOSER_PATH=$(which composer) RUBY_PATH=$(which ruby) RUBY_BUNDLE_PATH=$(which bundle) RUBY_GEM_PATH=$(which gem) POWERSHELL_PATH=$(which pwsh) DOTNET_PATH=$(which dotnet) cargo test --features enterprise,deno_core,duckdb,license,python,rust,scoped_cache,parquet,private,private_registry_test,csharp,php,ruby,mysql,quickjs,mcp --all -- --nocapture --test-threads=16
DENO_PATH=$(which deno) BUN_PATH=$(which bun) NODE_BIN_PATH=$(which node) GO_PATH=$(which go) UV_PATH=$(which uv) PHP_PATH=$(which php) COMPOSER_PATH=$(which composer) RUBY_PATH=$(which ruby) RUBY_BUNDLE_PATH=$(which bundle) RUBY_GEM_PATH=$(which gem) POWERSHELL_PATH=$(which pwsh) DOTNET_PATH=$(which dotnet) cargo test --features enterprise,deno_core,duckdb,license,python,rust,scoped_cache,parquet,private,private_registry_test,csharp,php,ruby,mysql,quickjs,mcp --all -- --nocapture --test-threads=12

View File

@@ -11,7 +11,7 @@ env:
jobs:
cargo_build_windows:
runs-on: windows-latest
runs-on: blacksmith-16vcpu-windows-2025
steps:
- uses: actions/checkout@v4
@@ -30,33 +30,40 @@ jobs:
token: ${{ secrets.WINDMILL_EE_PRIVATE_ACCESS }}
fetch-depth: 0
- name: Setup Rust
uses: actions-rs/toolchain@v1
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: 1.90.0
override: true
cache-workspaces: backend
toolchain: 1.93.0
- name: Substitute EE code
shell: bash
run: |
./backend/substitute_ee_code.sh --copy --dir ./windmill-ee-private
- name: Cargo check (fail fast on warnings)
timeout-minutes: 60
env:
RUSTFLAGS: "-D warnings"
run: |
mkdir frontend/build && cd backend
New-Item -Path . -Name "windmill-api/openapi-deref.yaml" -ItemType "File" -Force
cargo check --features=ee_windows
- name: Cargo build dynamic libraries windows
timeout-minutes: 90
timeout-minutes: 180
run: |
cd backend/windmill-duckdb-ffi-internal
cargo build --release -p windmill_duckdb_ffi_internal
- name: Cargo build binary windows
timeout-minutes: 90
timeout-minutes: 180
run: |
vcpkg.exe install openssl-windows:x64-windows
vcpkg.exe install openssl:x64-windows-static
vcpkg.exe integrate install
$env:VCPKGRS_DYNAMIC=1
$env:OPENSSL_DIR="${Env:VCPKG_INSTALLATION_ROOT}\installed\x64-windows-static"
mkdir frontend/build && cd backend
New-Item -Path . -Name "windmill-api/openapi-deref.yaml" -ItemType "File" -Force
cd backend
cargo build --release --features=ee_windows
- name: Rename binary with corresponding architecture
run: |

View File

@@ -78,18 +78,18 @@ jobs:
- name: install xmlsec1 and gssapi
run: |
sudo apt-get update
sudo apt-get install -y libxml2-dev libxmlsec1-dev libkrb5-dev libsasl2-dev
sudo apt-get install -y libxml2-dev libxmlsec1-dev libkrb5-dev libsasl2-dev libcurl4-openssl-dev mold clang
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache-workspaces: backend
toolchain: 1.90.0
toolchain: 1.93.0
- name: cargo check
working-directory: ./backend
timeout-minutes: 16
run: |
SQLX_OFFLINE=true cargo check --features $(./all_features_oss.sh)
SQLX_OFFLINE=true cargo check --features all_sqlx_features
- name: Run Claude PR Action
uses: anthropics/claude-code-action@v1
@@ -119,7 +119,7 @@ jobs:
- Fix all warnings and errors before proceeding
**Backend Changes:**
- Run: \`cargo check --features $(./all_features_oss.sh)\` in the backend directory
- Run: \`cargo check --features all_sqlx_features\` in the backend directory
- Fix all warnings and errors before proceeding
**Pull Request Creation:**

View File

@@ -62,17 +62,17 @@ jobs:
path: windmill-ee-private
token: ${{ secrets.WINDMILL_EE_PRIVATE_ACCESS }}
# Cache rust dependencies
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
# Setup Rust toolchain
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
workspaces: "./backend -> target"
cache-workspaces: backend
toolchain: 1.93.0
- name: Install xmlsec and gssapi build-time deps
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
pkg-config libxml2-dev libssl-dev libkrb5-dev \
pkg-config libxml2-dev libssl-dev libkrb5-dev libsasl2-dev libcurl4-openssl-dev mold clang \
xmlsec1 libxmlsec1-dev libxmlsec1-openssl
- name: Run update-sqlx script

View File

@@ -13,7 +13,7 @@ env:
jobs:
cargo_build_windows:
runs-on: windows-latest
runs-on: blacksmith-16vcpu-windows-2025
steps:
- uses: actions/checkout@v4
@@ -32,11 +32,10 @@ jobs:
token: ${{ secrets.WINDMILL_EE_PRIVATE_ACCESS }}
fetch-depth: 0
- name: Setup Rust
uses: actions-rs/toolchain@v1
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: 1.90.0
override: true
cache-workspaces: backend
toolchain: 1.93.0
- name: Substitute EE code
shell: bash
@@ -44,13 +43,13 @@ jobs:
./backend/substitute_ee_code.sh --copy --dir ./windmill-ee-private
- name: Cargo build dynamic libraries windows
timeout-minutes: 90
timeout-minutes: 180
run: |
cd backend/windmill-duckdb-ffi-internal
cargo build --release -p windmill_duckdb_ffi_internal
- name: Cargo build windows
timeout-minutes: 90
timeout-minutes: 180
run: |
vcpkg.exe install openssl-windows:x64-windows
vcpkg.exe install openssl:x64-windows-static

1
.gitignore vendored
View File

@@ -19,3 +19,4 @@ backend/target
frontend/node_modules
typescript-client/node_modules
frontend/.svelte-kit
backend/chrome_profiler.json

View File

@@ -1,5 +1,105 @@
# Changelog
## [1.631.2](https://github.com/windmill-labs/windmill/compare/v1.631.1...v1.631.2) (2026-02-11)
### Bug Fixes
* **frontend:** revert CloseButton refactor that broke tag removal in MultiSelect ([#7909](https://github.com/windmill-labs/windmill/issues/7909)) ([b11d6ed](https://github.com/windmill-labs/windmill/commit/b11d6ed7940faddfe74a22b25bcb132527cbcec8))
* nix flake libz.so for deno_core ([#7905](https://github.com/windmill-labs/windmill/issues/7905)) ([900c76c](https://github.com/windmill-labs/windmill/commit/900c76ccad09f58fc6adcfa8151db780249617f6))
* strip unsupported schema fields for Google AI ([#7894](https://github.com/windmill-labs/windmill/issues/7894)) ([5effb87](https://github.com/windmill-labs/windmill/commit/5effb87a36793e20d17e678619091ef458fe8f0a)), closes [#7759](https://github.com/windmill-labs/windmill/issues/7759)
## [1.631.1](https://github.com/windmill-labs/windmill/compare/v1.631.0...v1.631.1) (2026-02-11)
### Bug Fixes
* add kafka-gssapi support to ee builds ([2815cfa](https://github.com/windmill-labs/windmill/commit/2815cfae1a5eefb2c553e893dc926e62ad1df528))
## [1.631.0](https://github.com/windmill-labs/windmill/compare/v1.630.2...v1.631.0) (2026-02-11)
### Features
* **ai:** support 1M context window for Anthropic resources ([#7891](https://github.com/windmill-labs/windmill/issues/7891)) ([f22eb96](https://github.com/windmill-labs/windmill/commit/f22eb964e47defe9922ecdb6ab471dd4ca267952))
* **uv:** index resolve strategy ([#7885](https://github.com/windmill-labs/windmill/issues/7885)) ([097d928](https://github.com/windmill-labs/windmill/commit/097d9288c58076882f1991e2fb33e4441fe332d3))
### Bug Fixes
* **frontend:** improve time picker ([#7893](https://github.com/windmill-labs/windmill/issues/7893)) ([31bfccc](https://github.com/windmill-labs/windmill/commit/31bfccc74588af10fc11fbbb0bc4833d65ff6421))
* otel gracefully handle no native ts ([92c6018](https://github.com/windmill-labs/windmill/commit/92c601860f1e1211aa34838cd08900ba7334a20c))
* waitJob getJob and streamJob in raw apps ([#7901](https://github.com/windmill-labs/windmill/issues/7901)) ([754b48c](https://github.com/windmill-labs/windmill/commit/754b48cb898dffe196339cea1c1598c9e1765cdc))
* worker do not apply migrations anymore but wait for servers to do so ([7eb239f](https://github.com/windmill-labs/windmill/commit/7eb239f1e2eb1b71234a8d4265c7c5813e5861ae))
## [1.630.2](https://github.com/windmill-labs/windmill/compare/v1.630.1...v1.630.2) (2026-02-11)
### Bug Fixes
* bump rust version from 1.90.0 to 1.93.0 ([1a109a7](https://github.com/windmill-labs/windmill/commit/1a109a7797d1a50a0d85f3fff236d707b2cfb81d))
## [1.630.1](https://github.com/windmill-labs/windmill/compare/v1.630.0...v1.630.1) (2026-02-10)
### Bug Fixes
* enforce self-approval check on flow resume owner endpoint ([#7886](https://github.com/windmill-labs/windmill/issues/7886)) ([7147dde](https://github.com/windmill-labs/windmill/commit/7147dde5118d7b3a179e4b310c74b148838b5afe))
## [1.630.0](https://github.com/windmill-labs/windmill/compare/v1.629.1...v1.630.0) (2026-02-10)
### Features
* add workspace search and runnable details tools to AI chat modes ([#7874](https://github.com/windmill-labs/windmill/issues/7874)) ([a7e269f](https://github.com/windmill-labs/windmill/commit/a7e269f9f3c82db0d7e6a70e174ac19d3df730d2))
* **aiagent:** add prompt caching for Anthropic models ([#7878](https://github.com/windmill-labs/windmill/issues/7878)) ([6272cd1](https://github.com/windmill-labs/windmill/commit/6272cd17a4f1300e22e7f0ae27b1a57571deb203))
* download encrypted usage ([#7804](https://github.com/windmill-labs/windmill/issues/7804)) ([8363ff1](https://github.com/windmill-labs/windmill/commit/8363ff1eeef06f284e6d165fbf2dfb190ead573d))
* **mcp:** add endpoint tools for scripts, flows, apps, and jobs ([#7859](https://github.com/windmill-labs/windmill/issues/7859)) ([03eb16a](https://github.com/windmill-labs/windmill/commit/03eb16a7c6c3cd9411840814940d09e22ce23305))
* restriction rulesets for workspaces ([#7879](https://github.com/windmill-labs/windmill/issues/7879)) ([2851b6b](https://github.com/windmill-labs/windmill/commit/2851b6b7caac4a55f5202ace82aba68fd157c52a))
### Bug Fixes
* **backend:** correct early return with stream + prevent delta miss ([#7872](https://github.com/windmill-labs/windmill/issues/7872)) ([1150eec](https://github.com/windmill-labs/windmill/commit/1150eec7571d5828d10b295cb61cca8edfbdffe0))
* gate Permissions import behind #[cfg(unix)] for Windows build ([cf596f3](https://github.com/windmill-labs/windmill/commit/cf596f370ae7cc232ca63f4752d7727a74cd449b))
* retry js eval up to 3 times on timeout from slow DB ([#7890](https://github.com/windmill-labs/windmill/issues/7890)) ([4c87e7a](https://github.com/windmill-labs/windmill/commit/4c87e7ac2e09ec83cfb998a1cebcb9b9c5ef8027))
## [1.629.1](https://github.com/windmill-labs/windmill/compare/v1.629.0...v1.629.1) (2026-02-10)
### Bug Fixes
* remove unecessary drop index on labeled_jobs_on_jobs ([0803164](https://github.com/windmill-labs/windmill/commit/08031640a02ebd5971793942e8534d69f4f71d28))
## [1.629.0](https://github.com/windmill-labs/windmill/compare/v1.628.3...v1.629.0) (2026-02-09)
### Features
* customer portal extra workspace stats ([#7841](https://github.com/windmill-labs/windmill/issues/7841)) ([153dd32](https://github.com/windmill-labs/windmill/commit/153dd32187a3e32e3f26ab88b62195a0f9a359b9))
### Bug Fixes
* adapt mysql and ruby test assertions ([477832d](https://github.com/windmill-labs/windmill/commit/477832dbeeafb88fd16c174d5d1df8ed042e6f31))
* add missing :name param to groups/is_owner route ([fa53a87](https://github.com/windmill-labs/windmill/commit/fa53a87107158c9e8a7e5b522242b0076a78ce46))
* **backend:** prevent sqs hanging ([#7857](https://github.com/windmill-labs/windmill/issues/7857)) ([a3fc27b](https://github.com/windmill-labs/windmill/commit/a3fc27b23224aef2949c19f7c123d28e6cfaf968))
* box push() future to prevent stack overflow in nested async chains ([67c8aef](https://github.com/windmill-labs/windmill/commit/67c8aef9faea5afe8ce330b477f16b4aed1779a5))
* **frontend:** reorganize workspace settings ([#7788](https://github.com/windmill-labs/windmill/issues/7788)) ([dd42184](https://github.com/windmill-labs/windmill/commit/dd421845ba148bba70bcbafbc6a39f3012eb037c))
* improve scheduling reliability in extreme pool contention conditions ([#7825](https://github.com/windmill-labs/windmill/issues/7825)) ([bbb397b](https://github.com/windmill-labs/windmill/commit/bbb397b6ad954052f0bd33cc4ff8897eed66e4db))
* improve tracing behavior with NO_PROXY ([4cce13f](https://github.com/windmill-labs/windmill/commit/4cce13f5228a05da1bbce43bed7e856ce0bcf979))
* incorrect raw app public workspaceStore derived ([edb0d4a](https://github.com/windmill-labs/windmill/commit/edb0d4a05da567b3b0be5d94c9b2856d68ecb0ff))
* increase test thread stack size to 8MB in CI ([5548098](https://github.com/windmill-labs/windmill/commit/5548098e083af76a0b7d6f645a5458592d9c8ddc))
* install mold+clang in Docker for cargo linker config ([99bc383](https://github.com/windmill-labs/windmill/commit/99bc383f9e94a415ff1dcef1c45ccc4c8dab1a9e))
* make V8 runtime init idempotent and auto-initialize before isolate creation ([aa9f3da](https://github.com/windmill-labs/windmill/commit/aa9f3da429da92a059aaabb28481d33b8dacd37b))
* parse Python datetime.datetime and datetime.date type annotations ([#7856](https://github.com/windmill-labs/windmill/issues/7856)) ([ff70a4e](https://github.com/windmill-labs/windmill/commit/ff70a4e9d105cac58c0fb0aba8fbec9875533aa4))
* prevent V8 SIGSEGV by serializing isolate creation and fixing use-after-free ([05106d7](https://github.com/windmill-labs/windmill/commit/05106d7deeda92b7ae0e1708554f6dcb088c4a08))
* reduce DB pool contention by eliminating dual-connection patterns ([#7861](https://github.com/windmill-labs/windmill/issues/7861)) ([4343b73](https://github.com/windmill-labs/windmill/commit/4343b73485843c3b482c21e60052f171ada9b843))
* remove mold linker config that breaks Docker builds ([fea0954](https://github.com/windmill-labs/windmill/commit/fea0954f20f9f7c5a43b25b23df530faeac94999))
* restart after empty branchone + improve UI ([#7838](https://github.com/windmill-labs/windmill/issues/7838)) ([b1d6ac9](https://github.com/windmill-labs/windmill/commit/b1d6ac91bd3af073feac0b31d97f7b4414d8786e))
* use unprotected V8 platform to prevent SIGSEGV on x86_64 Linux ([90d0103](https://github.com/windmill-labs/windmill/commit/90d010347c65086b17f9802dd9a7d2da90dc68eb))
* wmill workspace list to list local profiles ([#7843](https://github.com/windmill-labs/windmill/issues/7843)) ([f924a82](https://github.com/windmill-labs/windmill/commit/f924a8268461c49a0fec26e3216ec9546601b8de))
## [1.628.3](https://github.com/windmill-labs/windmill/compare/v1.628.2...v1.628.3) (2026-02-06)

View File

@@ -1,5 +1,5 @@
ARG DEBIAN_IMAGE=debian:bookworm-slim
ARG RUST_IMAGE=rust:1.90-slim-bookworm
ARG RUST_IMAGE=rust:1.93-slim-bookworm
FROM debian:bookworm-slim AS nsjail
@@ -97,7 +97,7 @@ ARG features=""
COPY --from=planner /windmill/recipe.json recipe.json
RUN apt-get update && apt-get install -y libxml2-dev=2.9.* libxmlsec1-dev=1.2.* libkrb5-dev libsasl2-dev clang=1:14.0-55.* libclang-dev=1:14.0-55.* cmake=3.25.* && \
RUN apt-get update && apt-get install -y libxml2-dev=2.9.* libxmlsec1-dev=1.2.* libkrb5-dev libsasl2-dev libcurl4-openssl-dev clang=1:14.0-55.* libclang-dev=1:14.0-55.* cmake=3.25.* && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*

View File

@@ -12,8 +12,7 @@
"name": "native_trigger_service",
"kind": {
"Enum": [
"nextcloud",
"google"
"nextcloud"
]
}
}

View File

@@ -17,8 +17,7 @@
"name": "native_trigger_service",
"kind": {
"Enum": [
"nextcloud",
"google"
"nextcloud"
]
}
}

View File

@@ -0,0 +1,20 @@
{
"db_name": "PostgreSQL",
"query": "SELECT error_handler->>'path' FROM workspace_settings WHERE workspace_id = 'test-workspace'",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "?column?",
"type_info": "Text"
}
],
"parameters": {
"Left": []
},
"nullable": [
null
]
},
"hash": "12c3101e02f197ad731b66f539fe677ad25520fcc9c5a2378a293122956bed4c"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE workspace_settings\n SET error_handler = '{\"path\": \"script/f/test/error_handler\", \"extra_args\": {\"notify\": true}, \"muted_on_cancel\": true, \"muted_on_user_path\": false}'::jsonb\n WHERE workspace_id = 'test-workspace'\n ",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "18d1a6ac94f2c87d5ea8c48a228f061135be4065dca33fbc429d8b0186c5ccb3"
}

View File

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

View File

@@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "WITH inserted_concurrency_counter AS (\n INSERT INTO concurrency_counter (concurrency_id, job_uuids) \n VALUES ($1, '{}'::jsonb)\n ON CONFLICT DO NOTHING\n )\n INSERT INTO concurrency_key(key, job_id) VALUES ($1, $2)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Uuid"
]
},
"nullable": []
},
"hash": "1bceaf6e9f25745b7f70128054ca81d68f3d56d4782e99e05b4f1cb362683514"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n flow_version.id AS version,\n flow_version.value->>'early_return' as early_return, \n flow_version.value->>'preprocessor_module' IS NOT NULL as has_preprocessor, \n (flow_version.value->>'chat_input_enabled')::boolean as chat_input_enabled, \n flow.tag, \n flow.dedicated_worker, \n flow.on_behalf_of_email, \n flow.edited_by\n FROM \n flow_version\n INNER JOIN flow\n ON flow.path = flow_version.path AND\n flow.workspace_id = flow_version.workspace_id\n WHERE \n flow_version.workspace_id = $1 AND\n flow_version.path = $2 AND\n flow_version.id = $3\n ",
"query": "\n SELECT\n flow_version.id AS version,\n flow_version.value->>'early_return' as early_return,\n flow_version.value->>'preprocessor_module' IS NOT NULL as has_preprocessor,\n (flow_version.value->>'chat_input_enabled')::boolean as chat_input_enabled,\n flow.tag,\n flow.dedicated_worker,\n flow.on_behalf_of_email,\n flow.edited_by\n FROM\n flow_version\n INNER JOIN flow\n ON flow.path = flow_version.path AND\n flow.workspace_id = flow_version.workspace_id\n WHERE\n flow_version.workspace_id = $1 AND\n flow_version.path = $2 AND\n flow_version.id = $3\n ",
"describe": {
"columns": [
{
@@ -62,5 +62,5 @@
false
]
},
"hash": "a7468e9054beed88636786c5495ac3b9d9a6086ae6212ad9237b39a0346d7d26"
"hash": "209dc4c1b91eeab1c12ffcd9f9e16f315c689ca772c736b333dcdf07c8086087"
}

View File

@@ -16,8 +16,7 @@
"name": "native_trigger_service",
"kind": {
"Enum": [
"nextcloud",
"google"
"nextcloud"
]
}
}

View File

@@ -0,0 +1,20 @@
{
"db_name": "PostgreSQL",
"query": "SELECT (error_handler->>'muted_on_cancel')::boolean FROM workspace_settings WHERE workspace_id = 'test-workspace'",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "bool",
"type_info": "Bool"
}
],
"parameters": {
"Left": []
},
"nullable": [
null
]
},
"hash": "25bf3956b4365a4d4f96368bcc6e33817ae284ab26c195eee53d988d74263e86"
}

View File

@@ -11,8 +11,7 @@
"name": "native_trigger_service",
"kind": {
"Enum": [
"nextcloud",
"google"
"nextcloud"
]
}
}

View File

@@ -11,8 +11,7 @@
"name": "native_trigger_service",
"kind": {
"Enum": [
"nextcloud",
"google"
"nextcloud"
]
}
}

View File

@@ -15,8 +15,7 @@
"name": "native_trigger_service",
"kind": {
"Enum": [
"nextcloud",
"google"
"nextcloud"
]
}
}

View File

@@ -0,0 +1,20 @@
{
"db_name": "PostgreSQL",
"query": "SELECT (error_handler->>'muted_on_user_path')::boolean FROM workspace_settings WHERE workspace_id = 'test-workspace'",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "bool",
"type_info": "Bool"
}
],
"parameters": {
"Left": []
},
"nullable": [
null
]
},
"hash": "28b8264a427e8c19be823d2b8e40736a7b7675780672b503d91737cd0c617657"
}

View File

@@ -12,8 +12,7 @@
"name": "native_trigger_service",
"kind": {
"Enum": [
"nextcloud",
"google"
"nextcloud"
]
}
}

View File

@@ -12,8 +12,7 @@
"name": "native_trigger_service",
"kind": {
"Enum": [
"nextcloud",
"google"
"nextcloud"
]
}
}

View File

@@ -16,8 +16,7 @@
"name": "native_trigger_service",
"kind": {
"Enum": [
"nextcloud",
"google"
"nextcloud"
]
}
}
@@ -52,8 +51,7 @@
"name": "native_trigger_service",
"kind": {
"Enum": [
"nextcloud",
"google"
"nextcloud"
]
}
}

View File

@@ -0,0 +1,20 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM job_result_stream_v2\n WHERE job_id NOT IN (SELECT id FROM v2_job_queue)\n AND job_id NOT IN (\n SELECT id FROM v2_job_completed\n WHERE completed_at > NOW() - INTERVAL '60 seconds'\n )\n RETURNING job_id",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "job_id",
"type_info": "Uuid"
}
],
"parameters": {
"Left": []
},
"nullable": [
false
]
},
"hash": "454a611a5a162b2ace137c139bd5383bc7fe142c515dac3edd47991839485e51"
}

View File

@@ -0,0 +1,46 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n workspace_id,\n name,\n rules as \"rules: ProtectionRules\",\n bypass_groups,\n bypass_users\n FROM workspace_protection_rule\n WHERE workspace_id = $1\n ORDER BY name\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "workspace_id",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "name",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "rules: ProtectionRules",
"type_info": "Int4"
},
{
"ordinal": 3,
"name": "bypass_groups",
"type_info": "TextArray"
},
{
"ordinal": 4,
"name": "bypass_users",
"type_info": "TextArray"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false,
false,
false,
false
]
},
"hash": "4cfb35e423a75ca2701f03d5a30a7c0778af5e548254f3e0f29004d7f2058eef"
}

View File

@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock)\n VALUES ('test-workspace', $1, 'f/test/success_script', 'export function main() { return \"ok\"; }', 'deno', 'script', 'test-user', '{}', '', '', '')\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": []
},
"hash": "51f18c817d4014e847486f5a8dbf205ffa0e53d5bd3fdb97777e988389b1d69e"
}

View File

@@ -16,8 +16,7 @@
"name": "native_trigger_service",
"kind": {
"Enum": [
"nextcloud",
"google"
"nextcloud"
]
}
}

View File

@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "WITH inserted_concurrency_counter AS (\n INSERT INTO concurrency_counter (concurrency_id, job_uuids)\n VALUES ($1, '{}'::jsonb)\n ON CONFLICT DO NOTHING\n )\n INSERT INTO concurrency_key(key, job_id) VALUES ($1, $2)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Uuid"
]
},
"nullable": []
},
"hash": "5a9cf9cc229f7b7ddbee2485cda4e9f7a91240a00f10d24d3f28d5b722f68768"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT j.id AS \"id!\", COALESCE(s.flow_status, s.workflow_as_code_status) as flow_status, q.suspend AS \"suspend!\", j.runnable_path as script_path\n FROM v2_job_queue q JOIN v2_job j USING (id) LEFT JOIN v2_job_status s USING (id)\n WHERE j.id = $1\n ",
"query": "\n SELECT j.id AS \"id!\", COALESCE(s.flow_status, s.workflow_as_code_status) as flow_status, q.suspend AS \"suspend!\", j.runnable_path as script_path, j.permissioned_as_email as email\n FROM v2_job_queue q JOIN v2_job j USING (id) LEFT JOIN v2_job_status s USING (id)\n WHERE j.id = $1\n ",
"describe": {
"columns": [
{
@@ -22,6 +22,11 @@
"ordinal": 3,
"name": "script_path",
"type_info": "Varchar"
},
{
"ordinal": 4,
"name": "email",
"type_info": "Varchar"
}
],
"parameters": {
@@ -33,8 +38,9 @@
false,
null,
false,
true
true,
false
]
},
"hash": "485dc289a61a06595acae28d4968f3b6b2aeb6a8aee863dc999d6c8d58397814"
"hash": "5c6e158aee5db3c4bf41a0aedfa5d6c73f5e04ebfd8703dc631bc49f2e797dd4"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO group_ (workspace_id, name, summary, extra_perms)\n VALUES ('test-workspace', 'error_handler', 'The group the error handler acts on behalf of', '{\"u/test-user\": true}')\n ON CONFLICT DO NOTHING\n ",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "5f5bdd667f699a5a1c05e07d805a97afb06252541d06b4785063819c3893b674"
}

View File

@@ -0,0 +1,18 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE workspace_protection_rule\n SET rules = $1, bypass_groups = $2, bypass_users = $3\n WHERE workspace_id = $4 AND name = $5\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int4",
"TextArray",
"TextArray",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "65b12bed9438900518b20dc268d71a2dba6ec66aee2971faef76b6ed56a05b6f"
}

View File

@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock, ws_error_handler_muted)\n VALUES ('test-workspace', $1, 'f/test/muted_failing_script', 'export function main() { throw new Error(\"fail\"); }', 'deno', 'script', 'test-user', '{}', '', '', '', true)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": []
},
"hash": "782c259ccdf71c5e2fa8dcb27515e7c5ff83009774dc15095ed0dbcb0ecdbdde"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE workspace_settings\n SET error_handler = NULL\n WHERE workspace_id = 'test-workspace'\n ",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "79648f03eb50385183c34d57bda1ac984cf6502609df9ee51b7d684b585b8447"
}

View File

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

View File

@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock)\n VALUES ('test-workspace', 3333333333, 'f/test/error_handler', 'export function main() { return \"handled\"; }', 'deno', 'script', 'test-user', '{}', '', '', '')\n ",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "7f98c013682aca6d269ef6ef754a7247b6e2dd4958d0968a3f2eb49cc320e877"
}

View File

@@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE alerts \n SET\n acknowledged = true,\n acknowledged_workspace = CASE\n WHEN $2 THEN\n CASE\n WHEN $1::text IS NOT NULL THEN true\n ELSE acknowledged_workspace\n END\n ELSE true\n END\n WHERE ($1::text IS NOT NULL AND workspace_id = $1)\n OR ($1::text IS NULL)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Bool"
]
},
"nullable": []
},
"hash": "80864158f61adaad8df934acc54ba523c9f17d106298d8781885134d28553d36"
}

View File

@@ -17,8 +17,7 @@
"name": "native_trigger_service",
"kind": {
"Enum": [
"nextcloud",
"google"
"nextcloud"
]
}
}

View File

@@ -17,8 +17,7 @@
"name": "native_trigger_service",
"kind": {
"Enum": [
"nextcloud",
"google"
"nextcloud"
]
}
}

View File

@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT EXISTS(SELECT 1 FROM workspace_protection_rule WHERE workspace_id = $1 AND name = $2)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "exists",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
null
]
},
"hash": "b1b26cb02dcc0748c63411c933e5e6bdfe82466134ba28408a530e22c66656de"
}

View File

@@ -21,8 +21,7 @@
"name": "native_trigger_service",
"kind": {
"Enum": [
"nextcloud",
"google"
"nextcloud"
]
}
}
@@ -72,8 +71,7 @@
"name": "native_trigger_service",
"kind": {
"Enum": [
"nextcloud",
"google"
"nextcloud"
]
}
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO group_ (workspace_id, name, summary, extra_perms)\n VALUES ('test-workspace', 'error_handler', 'Error handler group', '{\"u/test-user\": true}')\n ON CONFLICT DO NOTHING\n ",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "ba0c3dad5d2a77922821c9c6d58e06a65e6666e197f3e59b880470df35c27bec"
}

View File

@@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM job_result_stream_v2 WHERE job_id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": []
},
"hash": "bb46e5dcf5490ef3511faa131ad5693dedf34366e51044ddf30695995d194090"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE workspace_settings\n SET error_handler = '{\"path\": \"script/f/test/error_handler\"}'::jsonb\n WHERE workspace_id = 'test-workspace'\n ",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "bb4cde84157126f29680cb36075848e7cde2ab7c6b7f0861becd4c0b8c330a8d"
}

View File

@@ -0,0 +1,20 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM job_perms\nWHERE job_id NOT IN (SELECT id FROM v2_job_queue)\nRETURNING job_id",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "job_id",
"type_info": "Uuid"
}
],
"parameters": {
"Left": []
},
"nullable": [
false
]
},
"hash": "c825fa5c6e287068aeaad994c0b42b8ad59b9129f032c6b918c27426ab304f2b"
}

View File

@@ -11,8 +11,7 @@
"name": "native_trigger_service",
"kind": {
"Enum": [
"nextcloud",
"google"
"nextcloud"
]
}
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock)\n VALUES ('test-workspace', 5555555555, 'f/test/error_handler', 'export function main() { return \"handled\"; }', 'deno', 'script', 'test-user', '{}', '', '', '')\n ",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "cdc594b30855e9384481f561b2c16200d4e6dacb1eb274513c8f1795235dfe20"
}

View File

@@ -12,8 +12,7 @@
"name": "native_trigger_service",
"kind": {
"Enum": [
"nextcloud",
"google"
"nextcloud"
]
}
}

View File

@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock)\n VALUES ('test-workspace', $1, 'f/test/failing_script', $2, 'deno', 'script', 'test-user', '{}', 'Failing test script', 'A script that always fails', '')\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Text"
]
},
"nullable": []
},
"hash": "d2a9a9af4d00c1c05276c14e6f32044437800a4b0c60aa364ab2f362a4fd9a2c"
}

View File

@@ -0,0 +1,20 @@
{
"db_name": "PostgreSQL",
"query": "SELECT error_handler->'extra_args' FROM workspace_settings WHERE workspace_id = 'test-workspace'",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "?column?",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": []
},
"nullable": [
null
]
},
"hash": "d5f1a2bfbac206a33b991d58f9ff4b8e8e4a3840ea1c65d409576379dede41ed"
}

View File

@@ -0,0 +1,18 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO workspace_protection_rule (workspace_id, name, rules, bypass_groups, bypass_users)\n VALUES ($1, $2, $3, $4, $5)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Int4",
"TextArray",
"TextArray"
]
},
"nullable": []
},
"hash": "dbf2982f43577999dec0c488dfb67f56e5bd0e5fbf8da5132f6fc5a282d2b0e1"
}

View File

@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE alerts\n SET\n acknowledged = true,\n acknowledged_workspace = CASE\n WHEN $2 THEN\n CASE\n WHEN $1::text IS NOT NULL THEN true\n ELSE acknowledged_workspace\n END\n ELSE true\n END\n WHERE ($1::text IS NOT NULL AND workspace_id = $1)\n OR ($1::text IS NULL)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Bool"
]
},
"nullable": []
},
"hash": "e00f7877d7bd33cf81e7bf48b1a37a507ab0a8f8b4bd7a38cf44d181f8f2d940"
}

View File

@@ -0,0 +1,20 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT id\n FROM v2_job\n WHERE workspace_id = 'test-workspace'\n AND permissioned_as_email = 'error_handler@windmill.dev'\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
}
],
"parameters": {
"Left": []
},
"nullable": [
false
]
},
"hash": "e4c9f3bca6bd636c3ee31fc643182ed5ca48a45da895fb992d820682f5169f7c"
}

View File

@@ -21,8 +21,7 @@
"name": "native_trigger_service",
"kind": {
"Enum": [
"nextcloud",
"google"
"nextcloud"
]
}
}
@@ -72,8 +71,7 @@
"name": "native_trigger_service",
"kind": {
"Enum": [
"nextcloud",
"google"
"nextcloud"
]
}
}

View File

@@ -21,8 +21,7 @@
"name": "native_trigger_service",
"kind": {
"Enum": [
"nextcloud",
"google"
"nextcloud"
]
}
}
@@ -72,8 +71,7 @@
"name": "native_trigger_service",
"kind": {
"Enum": [
"nextcloud",
"google"
"nextcloud"
]
}
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "VACUUM v2_job, v2_job_completed, job_result_stream_v2, job_stats, job_logs, concurrency_key, log_file, metrics",
"query": "VACUUM v2_job, v2_job_completed, job_result_stream_v2, job_stats, job_logs, job_perms, concurrency_key, log_file, metrics",
"describe": {
"columns": [],
"parameters": {
@@ -8,5 +8,5 @@
},
"nullable": []
},
"hash": "f8ac5379ecfbff7b8ae75c821680737b249a64c8d9e8f7dbcc46fce98e874571"
"hash": "f0070b36f7c4fc84dc9c23bb6c73d8ba80993a28b2c2e5df70968acf6d7cebe4"
}

View File

@@ -0,0 +1,38 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n id,\n runnable_path,\n permissioned_as_email,\n parent_job\n FROM v2_job\n WHERE workspace_id = 'test-workspace'\n AND permissioned_as_email = 'error_handler@windmill.dev'\n ORDER BY created_at DESC\n LIMIT 1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "runnable_path",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "permissioned_as_email",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "parent_job",
"type_info": "Uuid"
}
],
"parameters": {
"Left": []
},
"nullable": [
false,
true,
false,
true
]
},
"hash": "fdf0a16a72bc2eca2025d64750e2e6a7e7ff90b5ad4b42e6aab53992b447a20e"
}

View File

@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock)\n VALUES ('test-workspace', 1111111111, 'f/test/error_handler', $1, 'deno', 'script', 'test-user', '{}', 'Error handler script', 'Handles failed job completions', '')\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text"
]
},
"nullable": []
},
"hash": "fe17aff543f28cf4d6d09ea7c4c7d18ff6a95a8402468131f5a5f33df91c59e4"
}

View File

@@ -37,14 +37,22 @@ Windmill uses a workspace-based architecture with multiple crates:
- Update database schema with migration if necessary
- Use `sqlx` for database operations with prepared statements
- Use transactions for multi-step operations
- To apply pending migrations: `sqlx migrate run` (never manually run .sql files)
- **Never use `SQLX_OFFLINE=true`** — a live database is always available for compilation
- After all code changes are done, run `./update-sqlx` to regenerate the offline query cache
## Enterprise Features
- Enterprise files use the `*_ee.rs` suffix
- Enterprise source is in `windmill-ee-private` folder (sibling directory), symlinked into each crate's `src/`
- Enterprise source is in `windmill-ee-private` folder (sibling directory at `../../windmill-ee-private`), symlinked into each crate's `src/`
- You can and should modify `windmill-ee-private` directly when needed (e.g., when creating new crates that need EE code, mirror the package structure there)
- Use feature flags: `#[cfg(feature = "enterprise")]`
- Isolate enterprise code in separate modules
## Git Workflow
- **Never push directly to main** — always create a branch and open a pull request
## Testing
- Write unit tests for core functionality

717
backend/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.628.3"
version = "1.631.2"
authors.workspace = true
edition.workspace = true
@@ -9,6 +9,7 @@ resolver = "2"
members = [
"./windmill-api",
"./windmill-api-scripts",
"./windmill-api-flows",
"./windmill-api-users",
"./windmill-api-workspaces",
"./windmill-api-groups",
@@ -26,7 +27,19 @@ members = [
"./windmill-trigger-gcp",
"./windmill-trigger-http",
"./windmill-native-triggers",
"./windmill-alerting",
"./windmill-api-agent-workers",
"./windmill-api-assets",
"./windmill-api-configs",
"./windmill-api-debug",
"./windmill-api-embeddings",
"./windmill-api-flow-conversations",
"./windmill-api-inputs",
"./windmill-api-npm-proxy",
"./windmill-api-openapi",
"./windmill-api-schedule",
"./windmill-api-settings",
"./windmill-api-workers",
"./windmill-store",
"./windmill-queue",
"./windmill-worker",
@@ -54,12 +67,14 @@ members = [
"./parsers/windmill-parser-py",
"./parsers/windmill-parser-py-imports",
"./parsers/windmill-sql-datatype-parser-wasm",
"./parsers/windmill-parser-yaml", "windmill-macros", "parsers/windmill-parser-nu"
"./parsers/windmill-parser-yaml", "windmill-macros", "parsers/windmill-parser-nu",
"./windmill-test-utils",
"./windmill-api-integration-tests",
]
exclude = ["./windmill-duckdb-ffi-internal"]
[workspace.package]
version = "1.628.3"
version = "1.631.2"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"
@@ -80,9 +95,9 @@ lto = "thin"
[features]
default = []
private = ["windmill-api/private", "windmill-autoscaling/private", "windmill-common/private", "windmill-git-sync/private", "windmill-indexer/private", "windmill-queue/private", "windmill-worker/private"]
agent_worker_server = ["windmill-api/agent_worker_server"]
enterprise = ["windmill-worker/enterprise", "windmill-queue/enterprise", "windmill-api/enterprise", "dep:windmill-autoscaling", "windmill-autoscaling/enterprise", "windmill-git-sync/enterprise", "windmill-common/prometheus", "windmill-common/enterprise"]
private = ["windmill-api/private", "windmill-api-agent-workers?/private", "windmill-autoscaling/private", "windmill-common/private", "windmill-git-sync/private", "windmill-indexer/private", "windmill-queue/private", "windmill-worker/private"]
agent_worker_server = ["windmill-api/agent_worker_server", "dep:windmill-api-agent-workers"]
enterprise = ["windmill-worker/enterprise", "windmill-queue/enterprise", "windmill-api/enterprise", "windmill-api-agent-workers?/enterprise", "dep:windmill-autoscaling", "windmill-autoscaling/enterprise", "windmill-git-sync/enterprise", "windmill-common/prometheus", "windmill-common/enterprise"]
local_reports = ["windmill-common/local_reports"]
enterprise_saml = ["windmill-api/enterprise_saml", "oauth2"]
stripe = ["windmill-api/stripe"]
@@ -114,7 +129,7 @@ native_trigger = ["windmill-api/native_trigger"]
sqs_trigger = ["windmill-api/sqs_trigger", "windmill-common/aws_auth", "windmill-api/openidconnect"]
gcp_trigger = ["windmill-api/gcp_trigger"]
smtp = ["windmill-api/smtp", "windmill-common/smtp", "windmill-queue/smtp"]
license = ["windmill-api/license"]
license = ["windmill-api/license", "windmill-api-settings/license"]
oauth2 = ["windmill-api/oauth2"]
zip = ["windmill-api/zip"]
static_frontend = ["windmill-api/static_frontend"]
@@ -160,8 +175,8 @@ oss = ["oss_core", "all_languages", "no_auth"]
ce_rpi = ["ce_core", "all_languages"]
ce = ["ce_rpi", "jemalloc", "dind", "agent_worker_server"]
# Edition meta-features: EE variants
ee = ["ce", "ee_core", "ee_server"]
ee_rhel = ["ce_core", "ee_core", "all_languages"]
ee = ["ce", "ee_core", "ee_server", "kafka-gssapi"]
ee_rhel = ["ce_core", "ee_core", "kafka-gssapi", "all_languages"]
ee_windows = ["ce_core", "ee_core", "all_languages_windows"]
all_sqlx_features = ["all_languages", "enterprise", "enterprise_saml", "embedding", "parquet", "prometheus", "flow_testing",
"openidconnect", "cloud", "jemalloc", "tantivy", "sqlx", "kafka", "kafka-gssapi", "nats", "otel", "dind", "websocket", "http_trigger",
@@ -182,6 +197,8 @@ windmill-queue.workspace = true
windmill-common = { workspace = true, default-features = false }
windmill-git-sync.workspace = true
windmill-api = { workspace = true, default-features = false }
windmill-api-agent-workers = { workspace = true, optional = true }
windmill-api-settings.workspace = true
windmill-worker.workspace = true
windmill-indexer = { workspace = true, optional = true }
windmill-autoscaling = { workspace = true, optional = true }
@@ -225,6 +242,7 @@ serde_json.workspace = true
reqwest.workspace = true
windmill-queue = { workspace = true, features = ["failpoints"] }
windmill-dep-map.workspace = true
windmill-test-utils.workspace = true
axum.workspace = true
serde.workspace = true
windmill-api-client.workspace = true
@@ -253,6 +271,7 @@ windmill-oauth = {path = "./windmill-oauth"}
windmill-macros = {path = "./windmill-macros"}
windmill-api-auth = { path = "./windmill-api-auth" }
windmill-api-scripts = { path = "./windmill-api-scripts" }
windmill-api-flows = { path = "./windmill-api-flows" }
windmill-api-users = { path = "./windmill-api-users" }
windmill-api-workspaces = { path = "./windmill-api-workspaces" }
windmill-api-groups = { path = "./windmill-api-groups" }
@@ -269,7 +288,19 @@ windmill-trigger-sqs = { path = "./windmill-trigger-sqs" }
windmill-trigger-gcp = { path = "./windmill-trigger-gcp" }
windmill-trigger-http = { path = "./windmill-trigger-http" }
windmill-native-triggers = { path = "./windmill-native-triggers" }
windmill-alerting = { path = "./windmill-alerting" }
windmill-api-agent-workers = { path = "./windmill-api-agent-workers" }
windmill-api-assets = { path = "./windmill-api-assets" }
windmill-api-configs = { path = "./windmill-api-configs" }
windmill-api-debug = { path = "./windmill-api-debug" }
windmill-api-embeddings = { path = "./windmill-api-embeddings" }
windmill-api-flow-conversations = { path = "./windmill-api-flow-conversations" }
windmill-api-inputs = { path = "./windmill-api-inputs" }
windmill-api-npm-proxy = { path = "./windmill-api-npm-proxy" }
windmill-api-openapi = { path = "./windmill-api-openapi" }
windmill-api-schedule = { path = "./windmill-api-schedule" }
windmill-api-settings = { path = "./windmill-api-settings" }
windmill-api-workers = { path = "./windmill-api-workers" }
windmill-store = { path = "./windmill-store" }
windmill-parser = { path = "./parsers/windmill-parser" }
windmill-parser-ts = { path = "./parsers/windmill-parser-ts" }
@@ -289,6 +320,7 @@ windmill-parser-php = { path = "./parsers/windmill-parser-php" }
windmill-jseval = { path = "./windmill-jseval" }
windmill-runtime-nativets = { path = "./windmill-runtime-nativets" }
windmill-api-client = { path = "./windmill-api-client" }
windmill-test-utils = { path = "./windmill-test-utils" }
reqwest-retry = "^0"
reqwest-middleware = { version = "^0", features = ["json"] }
@@ -419,7 +451,7 @@ const_format = { version = "0.2.35", features = ["rust_1_64", "rust_1_51"] }
const-str = "0.5"
constant_time_eq = "0.3.1"
rsa = "^0"
aes-gcm = "^0"
aes-gcm = "0.10.3"
async_zip = { version = "0.0.17", features = ["tokio", "tokio-fs", "deflate", "chrono"] }
once_cell = "1.17.1"
dashmap = "6.1.0"
@@ -469,7 +501,7 @@ nkeys = "0.4.4"
nu-parser = { version = "0.101.0", default-features = false }
globset = "0.4.16"
croner = "2.2.0"
rmcp = { version = "^0", features = ["client", "transport-streamable-http-client", "transport-streamable-http-client-reqwest"] }
rmcp = { version = "=0.15.0", features = ["client", "transport-streamable-http-client", "transport-streamable-http-client-reqwest"] }
rquickjs = { version = "0.8", features = ["futures", "parallel", "macro"] }
process-wrap = { version = "8.2.1", features = ["tokio1"] }

View File

@@ -1,21 +0,0 @@
#!/bin/bash
# This script outputs all features except private. Usage :
# > cargo build --features $(./all_features_oss.sh)
# Path to the Cargo.toml file
CARGO_TOML_PATH="./Cargo.toml"
# Extract features from Cargo.toml and output them separated by commas
if [[ -f "$CARGO_TOML_PATH" ]]; then
grep -A 100 '\[features\]' "$CARGO_TOML_PATH" | \
sed -n '/\[features\]/,/^\[/p' | \
grep -E '^[a-zA-Z0-9_-]+' | \
grep -v 'private' | \
grep -v 'benchmark' | \
cut -d' ' -f1 | \
paste -sd ',' -
else
echo "Cargo.toml not found at $CARGO_TOML_PATH"
exit 1
fi

View File

@@ -1 +1 @@
cd2713671bc7ac5762302704ee4ecb81e023301f
ab082a1a65830bf362a32b30fa86741999da091d

View File

@@ -29,7 +29,39 @@ def load_openapi_spec(file_path: str) -> Dict[str, Any]:
print(f"Error loading OpenAPI spec: {e}", file=sys.stderr)
sys.exit(1)
def extract_separate_schemas(parameters: List[Dict[str, Any]], request_body: Optional[Dict[str, Any]], spec: Dict[str, Any], required_fields: Optional[List[str]] = None, base_path: str = "") -> tuple:
def flatten_allof_schema(schema: Dict[str, Any]) -> Dict[str, Any]:
"""Flatten an allOf schema into a single object schema by merging all properties."""
if 'allOf' not in schema:
return schema
merged = {"type": "object", "properties": {}, "required": []}
def collect_from(s: Dict[str, Any]):
if 'allOf' in s:
for item in s['allOf']:
if isinstance(item, dict):
collect_from(item)
if 'properties' in s:
merged['properties'].update(s['properties'])
if 'required' in s and isinstance(s['required'], list):
merged['required'].extend(s['required'])
if 'description' in s and 'description' not in merged:
merged['description'] = s['description']
collect_from(schema)
# Preserve additional top-level keys from the original schema
preserved_keys = {'additionalProperties', 'title', 'nullable', 'default', 'example'}
for key in preserved_keys:
if key in schema and key not in merged:
merged[key] = schema[key]
if not merged['required']:
del merged['required']
return merged
def extract_separate_schemas(parameters: List[Dict[str, Any]], request_body: Optional[Dict[str, Any]], spec: Dict[str, Any], required_fields: Optional[List[str]] = None, base_path: str = "", include_fields: Optional[List[str]] = None, opaque_fields: Optional[List[str]] = None, include_query_params: Optional[List[str]] = None) -> tuple:
"""Extract separate schemas for path parameters, query parameters, and request body."""
path_params_schema = {
"type": "object",
@@ -72,7 +104,7 @@ def extract_separate_schemas(parameters: List[Dict[str, Any]], request_body: Opt
path_params_schema['properties'][param_name] = param_schema
if param_required:
path_params_schema['required'].append(param_name)
elif param_in == 'query':
elif param_in == 'query' and (include_query_params is None or param_name in include_query_params):
query_params_schema['properties'][param_name] = param_schema
if param_required:
query_params_schema['required'].append(param_name)
@@ -80,7 +112,28 @@ def extract_separate_schemas(parameters: List[Dict[str, Any]], request_body: Opt
# Process request body if present
if request_body:
body_schema = extract_request_body_schema(request_body, spec, base_path)
# Flatten allOf schemas into a single object schema for filtering
if body_schema and (include_fields is not None or opaque_fields):
body_schema = flatten_allof_schema(body_schema)
# Apply include_fields filter: only keep listed top-level properties
if body_schema and include_fields is not None and 'properties' in body_schema:
body_schema['properties'] = {
k: v for k, v in body_schema['properties'].items()
if k in include_fields
}
if 'required' in body_schema:
body_schema['required'] = [
r for r in body_schema['required'] if r in include_fields
]
# Apply opaque_fields: simplify listed properties to {"type": "object"}
if body_schema and opaque_fields and 'properties' in body_schema:
for field in opaque_fields:
if field in body_schema['properties']:
body_schema['properties'][field] = {"type": "object"}
# If we have required fields specified and a body schema, update the required array
if body_schema and required_fields:
if 'required' not in body_schema:
@@ -95,11 +148,60 @@ def extract_separate_schemas(parameters: List[Dict[str, Any]], request_body: Opt
# Log warning when a required field is missing from schema properties
print(f"Warning: Required field '{field}' not found in body schema properties", file=sys.stderr)
# Sanitize empty schemas for JSON Schema draft 2020-12 compliance
path_params_schema = sanitize_empty_schemas(path_params_schema)
query_params_schema = sanitize_empty_schemas(query_params_schema)
body_schema = sanitize_empty_schemas(body_schema)
# Convert enums to descriptions for client compatibility
path_params_schema = convert_enums_to_descriptions(path_params_schema)
query_params_schema = convert_enums_to_descriptions(query_params_schema)
body_schema = convert_enums_to_descriptions(body_schema)
# Detect overlapping property names across schemas and rename with suffixes
path_keys = set(path_params_schema['properties'].keys()) if path_params_schema and path_params_schema.get('properties') else set()
query_keys = set(query_params_schema['properties'].keys()) if query_params_schema and query_params_schema.get('properties') else set()
body_keys = set(body_schema['properties'].keys()) if body_schema and body_schema.get('properties') else set()
conflicts = (path_keys & query_keys) | (path_keys & body_keys) | (query_keys & body_keys)
path_field_renames = {}
query_field_renames = {}
body_field_renames = {}
for field in conflicts:
schemas_and_renames = [
(path_params_schema, path_keys, '__path', path_field_renames),
(query_params_schema, query_keys, '__query', query_field_renames),
(body_schema, body_keys, '__body', body_field_renames),
]
for schema, keys, suffix, renames_map in schemas_and_renames:
if field in keys and schema and 'properties' in schema:
new_name = field + suffix
# Rename in properties
schema['properties'][new_name] = schema['properties'].pop(field)
# Update description to clarify the renamed field
if 'description' not in schema['properties'][new_name]:
schema['properties'][new_name] = dict(schema['properties'][new_name])
prop = schema['properties'][new_name]
if isinstance(prop, dict):
existing_desc = prop.get('description', '')
location = suffix.lstrip('_')
if not existing_desc:
prop['description'] = f"({location} parameter)"
else:
prop['description'] = f"{existing_desc} ({location} parameter)"
# Rename in required array
if 'required' in schema and field in schema['required']:
schema['required'] = [new_name if r == field else r for r in schema['required']]
# Store the reverse mapping: renamed -> original
renames_map[new_name] = field
# Return None for empty schemas
path_params_schema = path_params_schema if path_params_schema['properties'] else None
query_params_schema = query_params_schema if query_params_schema['properties'] else None
return (path_params_schema, query_params_schema, body_schema)
path_params_schema = path_params_schema if path_params_schema and path_params_schema.get('properties') else None
query_params_schema = query_params_schema if query_params_schema and query_params_schema.get('properties') else None
return (path_params_schema, query_params_schema, body_schema, path_field_renames, query_field_renames, body_field_renames)
# Cache for loaded external files
_external_file_cache: Dict[str, Dict[str, Any]] = {}
@@ -160,18 +262,25 @@ def resolve_ref(ref_path: str, spec: Dict[str, Any], base_path: str = "") -> tup
return (current if isinstance(current, dict) else None), spec
def resolve_schema_refs(schema: Dict[str, Any], spec: Dict[str, Any], base_path: str = "") -> Dict[str, Any]:
def resolve_schema_refs(schema: Dict[str, Any], spec: Dict[str, Any], base_path: str = "", _visited_refs: Optional[set] = None) -> Dict[str, Any]:
"""Recursively resolve all $ref references in a schema."""
if _visited_refs is None:
_visited_refs = set()
if not isinstance(schema, dict):
return schema
# If this is a $ref, resolve it
if '$ref' in schema:
ref_path = schema['$ref']
if ref_path in _visited_refs:
# Circular reference detected - return empty object to break the cycle
return {"type": "object"}
_visited_refs = _visited_refs | {ref_path}
resolved, resolved_spec = resolve_ref(ref_path, spec, base_path)
if resolved:
# Recursively resolve any refs in the resolved schema using the appropriate spec
return resolve_schema_refs(resolved, resolved_spec, base_path)
return resolve_schema_refs(resolved, resolved_spec, base_path, _visited_refs)
else:
print(f"Warning: Could not resolve $ref: {ref_path}")
return schema
@@ -180,10 +289,10 @@ def resolve_schema_refs(schema: Dict[str, Any], spec: Dict[str, Any], base_path:
resolved_schema = {}
for key, value in schema.items():
if isinstance(value, dict):
resolved_schema[key] = resolve_schema_refs(value, spec, base_path)
resolved_schema[key] = resolve_schema_refs(value, spec, base_path, _visited_refs)
elif isinstance(value, list):
resolved_schema[key] = [
resolve_schema_refs(item, spec, base_path) if isinstance(item, dict) else item
resolve_schema_refs(item, spec, base_path, _visited_refs) if isinstance(item, dict) else item
for item in value
]
else:
@@ -191,6 +300,56 @@ def resolve_schema_refs(schema: Dict[str, Any], spec: Dict[str, Any], base_path:
return resolved_schema
def convert_enums_to_descriptions(schema: Any) -> Any:
"""Recursively convert enum arrays into description text to avoid client compatibility issues."""
if isinstance(schema, list):
return [convert_enums_to_descriptions(item) for item in schema]
if not isinstance(schema, dict):
return schema
result = {}
enum_value = None
# First pass: copy all non-enum keys so 'description' is available before enum processing
for key, value in schema.items():
if key == 'enum':
enum_value = value
else:
result[key] = convert_enums_to_descriptions(value)
# Second pass: process enum using the already-copied description
if enum_value is not None:
values_str = ', '.join(str(v) for v in enum_value)
existing = result.get('description', '')
enum_desc = f"Possible values: {values_str}"
result['description'] = f"{existing}. {enum_desc}" if existing else enum_desc
return result
def sanitize_empty_schemas(schema: Any) -> Any:
"""Replace empty {} schemas with valid JSON Schema draft 2020-12 equivalents.
In OpenAPI, {} means 'any value' but strict JSON Schema validators (e.g. Claude's API)
reject empty objects. This converts them to proper schemas.
"""
if isinstance(schema, list):
return [sanitize_empty_schemas(item) for item in schema]
if not isinstance(schema, dict):
return schema
result = {}
for key, value in schema.items():
if key == 'additionalProperties' and isinstance(value, dict) and len(value) == 0:
result[key] = True
elif key == 'properties' and isinstance(value, dict):
# properties is a map of name -> schema; sanitize each property schema
result[key] = {
k: {"type": "object"} if isinstance(v, dict) and len(v) == 0 else sanitize_empty_schemas(v)
for k, v in value.items()
}
else:
result[key] = sanitize_empty_schemas(value)
return result
def extract_request_body_schema(request_body: Dict[str, Any], spec: Dict[str, Any], base_path: str = "") -> Optional[Dict[str, Any]]:
"""Extract request body schema from OpenAPI requestBody definition and resolve refs."""
if not request_body:
@@ -243,6 +402,9 @@ def find_mcp_tools(spec: Dict[str, Any]) -> List[Dict[str, Any]]:
'parameters': operation.get('parameters', []),
'requestBody': operation.get('requestBody'),
'required_fields': operation.get('x-mcp-required-fields', []),
'include_fields': operation.get('x-mcp-tool-include-fields'),
'opaque_fields': operation.get('x-mcp-tool-opaque-fields'),
'include_query_params': operation.get('x-mcp-tool-include-query-params'),
}
tools.append(tool)
@@ -278,14 +440,18 @@ export const mcpEndpointTools: EndpointTool[] = [];
method = tool['method'].upper()
# Generate separate schemas
path_params_schema, query_params_schema, body_schema = extract_separate_schemas(
tool['parameters'], tool['requestBody'], spec, tool['required_fields'], base_path
path_params_schema, query_params_schema, body_schema, path_field_renames, query_field_renames, body_field_renames = extract_separate_schemas(
tool['parameters'], tool['requestBody'], spec, tool['required_fields'], base_path,
tool.get('include_fields'), tool.get('opaque_fields'), tool.get('include_query_params')
)
# Convert schemas to TypeScript - use 'as const' for better type inference
path_params_ts = json.dumps(path_params_schema, indent=8) if path_params_schema else "undefined"
query_params_ts = json.dumps(query_params_schema, indent=8) if query_params_schema else "undefined"
body_schema_ts = json.dumps(body_schema, indent=8) if body_schema else "undefined"
path_field_renames_ts = json.dumps(path_field_renames, indent=8) if path_field_renames else "undefined"
query_field_renames_ts = json.dumps(query_field_renames, indent=8) if query_field_renames else "undefined"
body_field_renames_ts = json.dumps(body_field_renames, indent=8) if body_field_renames else "undefined"
# Generate tool definition
tool_def = f""" {{
@@ -296,7 +462,10 @@ export const mcpEndpointTools: EndpointTool[] = [];
method: "{method}",
pathParamsSchema: {path_params_ts},
queryParamsSchema: {query_params_ts},
bodySchema: {body_schema_ts}
bodySchema: {body_schema_ts},
pathFieldRenames: {path_field_renames_ts},
queryFieldRenames: {query_field_renames_ts},
bodyFieldRenames: {body_field_renames_ts}
}}"""
tool_definitions.append(tool_def)
@@ -315,6 +484,9 @@ export interface EndpointTool {{
pathParamsSchema?: object;
queryParamsSchema?: object;
bodySchema?: object;
pathFieldRenames?: Record<string, string>;
queryFieldRenames?: Record<string, string>;
bodyFieldRenames?: Record<string, string>;
}}
export const mcpEndpointTools: EndpointTool[] = [
@@ -344,13 +516,17 @@ pub fn all_tools() -> Vec<EndpointTool> {{
method = tool['method'].upper()
# Generate separate schemas
path_params_schema, query_params_schema, body_schema = extract_separate_schemas(
tool['parameters'], tool['requestBody'], spec, tool['required_fields'], base_path
path_params_schema, query_params_schema, body_schema, path_field_renames, query_field_renames, body_field_renames = extract_separate_schemas(
tool['parameters'], tool['requestBody'], spec, tool['required_fields'], base_path,
tool.get('include_fields'), tool.get('opaque_fields'), tool.get('include_query_params')
)
path_params_rust = schema_to_rust_value(path_params_schema)
query_params_rust = schema_to_rust_value(query_params_schema)
body_schema_rust = schema_to_rust_value(body_schema)
path_field_renames_rust = schema_to_rust_value(path_field_renames if path_field_renames else None)
query_field_renames_rust = schema_to_rust_value(query_field_renames if query_field_renames else None)
body_field_renames_rust = schema_to_rust_value(body_field_renames if body_field_renames else None)
# Generate tool definition
tool_def = f""" EndpointTool {{
@@ -362,6 +538,9 @@ pub fn all_tools() -> Vec<EndpointTool> {{
path_params_schema: {path_params_rust},
query_params_schema: {query_params_rust},
body_schema: {body_schema_rust},
path_field_renames: {path_field_renames_rust},
query_field_renames: {query_field_renames_rust},
body_field_renames: {body_field_renames_rust},
}}"""
tool_definitions.append(tool_def)

View File

@@ -0,0 +1,2 @@
-- Drop the workspace_protection_rule table and its indexes
DROP TABLE IF EXISTS workspace_protection_rule;

View File

@@ -0,0 +1,10 @@
-- Add workspace_protection_rule table for fine-grained access control
CREATE TABLE workspace_protection_rule (
workspace_id VARCHAR(50) NOT NULL REFERENCES workspace(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
rules INTEGER NOT NULL,
bypass_groups TEXT[] NOT NULL DEFAULT '{}',
bypass_users TEXT[] NOT NULL DEFAULT '{}',
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
PRIMARY KEY (workspace_id, name)
);

View File

@@ -0,0 +1 @@
-- No-op: cannot restore deleted telemetry data

View File

@@ -0,0 +1,2 @@
-- Delete all saved telemetry data from metrics table
DELETE FROM metrics WHERE id = 'telemetry';

View File

@@ -381,5 +381,6 @@ pub static SHORT_IMPORTS_MAP: PyMap = phf_map! {
"docx" => "python-docx",
"vt" => "vt-py",
"grpc" => "grpcio",
"serpapi" => "google-search-results",
// Add new entry here ^
};

159
backend/src/db_connect.rs Normal file
View File

@@ -0,0 +1,159 @@
use windmill_common::{
error::{self, Error},
get_database_url, DatabaseUrl,
};
pub const DEFAULT_MAX_CONNECTIONS_SERVER: u32 = 50;
pub const DEFAULT_MAX_CONNECTIONS_WORKER: u32 = 5;
pub const DEFAULT_MAX_CONNECTIONS_INDEXER: u32 = 5;
pub async fn initial_connection() -> Result<sqlx::Pool<sqlx::Postgres>, error::Error> {
let connect_options = get_database_url().await?.connect_options().await?;
sqlx::postgres::PgPoolOptions::new()
.max_connections(2)
.connect_with(connect_options)
.await
.map_err(|err| Error::ConnectingToDatabase(err.to_string()))
}
pub async fn connect_db(
server_mode: bool,
indexer_mode: bool,
worker_mode: bool,
#[cfg(feature = "private")] mut killpill_rx: tokio::sync::broadcast::Receiver<()>,
) -> anyhow::Result<sqlx::Pool<sqlx::Postgres>> {
use anyhow::Context;
let database_url = get_database_url().await?;
let max_connections = match std::env::var("DATABASE_CONNECTIONS") {
Ok(n) => n.parse::<u32>().context("invalid DATABASE_CONNECTIONS")?,
Err(_) => {
if server_mode {
DEFAULT_MAX_CONNECTIONS_SERVER
} else if indexer_mode {
DEFAULT_MAX_CONNECTIONS_INDEXER
} else {
DEFAULT_MAX_CONNECTIONS_WORKER
+ std::env::var("NUM_WORKERS")
.ok()
.map(|x| x.parse().ok())
.flatten()
.unwrap_or(1)
- 1
}
}
};
let pool = connect(database_url.clone(), max_connections, worker_mode).await?;
#[cfg(all(feature = "enterprise", feature = "private"))]
let pool2 = pool.clone();
#[cfg(all(feature = "enterprise", feature = "private"))]
if let DatabaseUrl::IamRds(database_url) = database_url {
tokio::spawn(async move {
loop {
tokio::select! {
_ = killpill_rx.recv() => {
break;
}
_ = tokio::time::sleep(std::time::Duration::from_secs(10)) => {
let needs_refresh = {
let read_guard = database_url.read().await;
read_guard.needs_refresh()
};
if needs_refresh {
let new_url = tokio::time::timeout(std::time::Duration::from_secs(10), get_database_url()).await;
match new_url {
Ok(Ok(new_url)) => {
match new_url.connect_options().await {
Ok(connect_options) => {
pool2.set_connect_options(connect_options);
tracing::info!("Refreshed IAM RDS URL successfully");
}
Err(e) => {
tracing::error!("Error getting IAM RDS connect options, retrying in 10s: {}", e);
continue;
}
}
}
Ok(Err(e)) => {
tracing::error!("Error refreshing IAM RDS URL, trying again in 10s: {}", e);
continue;
}
Err(e) => {
tracing::error!("Timeout after 10s refreshing IAM RDS URL, trying again in 10 seconds: {}", e);
continue;
}
}
}
}
}
}
});
}
Ok(pool)
}
pub async fn connect(
database_url: DatabaseUrl,
max_connections: u32,
worker_mode: bool,
) -> Result<sqlx::Pool<sqlx::Postgres>, error::Error> {
use sqlx::Executor;
use std::time::Duration;
let mut pool_options = sqlx::postgres::PgPoolOptions::new()
.min_connections((max_connections / 5).clamp(1, max_connections))
.max_connections(max_connections)
.max_lifetime(Duration::from_secs(30 * 60)); // 30 mins
if worker_mode {
pool_options = pool_options.idle_timeout(Duration::from_secs(60));
}
pool_options
.after_connect(move |conn, _| {
if worker_mode {
Box::pin(async move {
if let Err(e) = conn
.execute(
r#"
SET enable_seqscan = OFF;
SET statement_timeout = '5min';
SET idle_in_transaction_session_timeout = '10min';
SET tcp_keepalives_idle = 300;
SET tcp_keepalives_interval = 60;
SET tcp_keepalives_count = 10;"#,
)
.await
{
tracing::error!("Error setting postgres settings: {}", e);
}
Ok(())
})
} else {
Box::pin(async move {
if let Err(e) = conn
.execute(
r#"
SET statement_timeout = '5min';
SET idle_in_transaction_session_timeout = '10min';
SET tcp_keepalives_idle = 300;
SET tcp_keepalives_interval = 60;
SET tcp_keepalives_count = 10;"#,
)
.await
{
tracing::error!("Error setting postgres settings: {}", e);
}
Ok(())
})
}
})
.connect_with(
database_url
.connect_options()
.await?
.statement_cache_capacity(400),
)
.await
.map_err(|err| Error::ConnectingToDatabase(err.to_string()))
}

View File

@@ -46,8 +46,8 @@ use windmill_common::{
KEEP_JOB_DIR_SETTING, LICENSE_KEY_SETTING, MAVEN_REPOS_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, UV_INDEX_STRATEGY_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,
@@ -103,7 +103,7 @@ use crate::monitor::{
reload_jwt_secret_setting, reload_license_key, reload_npm_config_registry_setting,
reload_otel_tracing_proxy_setting, reload_pip_index_url_setting,
reload_retention_period_setting, reload_scim_token_setting, reload_smtp_config,
reload_worker_config, MonitorIteration,
reload_uv_index_strategy_setting, reload_worker_config, MonitorIteration,
};
#[cfg(feature = "parquet")]
@@ -117,6 +117,7 @@ const BIND_ADDR_ENV: &str = "SERVER_BIND_ADDR";
#[cfg(target_os = "linux")]
mod cgroups;
mod db_connect;
#[cfg(feature = "private")]
pub mod ee;
mod ee_oss;
@@ -665,7 +666,7 @@ async fn windmill_main() -> anyhow::Result<()> {
} else {
println!("Connecting to database...");
let db = windmill_common::initial_connection().await?;
let db = crate::db_connect::initial_connection().await?;
let num_version = sqlx::query_scalar!("SELECT version()").fetch_one(&db).await;
@@ -734,8 +735,12 @@ async fn windmill_main() -> anyhow::Result<()> {
.unwrap_or(false);
if !skip_migration {
// migration code to avoid break
migration_handle = windmill_api::migrate_db(&db, killpill_rx.resubscribe()).await?;
if mode == Mode::Worker {
windmill_api::wait_for_db_migrations(&db, killpill_rx.resubscribe()).await?;
} else {
migration_handle =
windmill_api::migrate_db(&db, killpill_rx.resubscribe()).await?;
}
} else {
tracing::info!("SKIP_MIGRATION set, skipping db migration...")
}
@@ -770,8 +775,12 @@ async fn windmill_main() -> anyhow::Result<()> {
let conn = if mode == Mode::Agent {
conn
} else {
// This time we use a pool of connections
let db = windmill_common::connect_db(
// Drop the initial connection pool before creating the main one.
// With low PostgreSQL max_connections, both pools existing simultaneously
// can exhaust all available connection slots, causing connect_db to hang.
drop(conn);
let db = crate::db_connect::connect_db(
server_mode,
indexer_mode,
worker_mode,
@@ -1555,6 +1564,7 @@ async fn process_notify_event(
SCIM_TOKEN_SETTING => reload_scim_token_setting(conn).await,
EXTRA_PIP_INDEX_URL_SETTING => reload_extra_pip_index_url_setting(conn).await,
PIP_INDEX_URL_SETTING => reload_pip_index_url_setting(conn).await,
UV_INDEX_STRATEGY_SETTING => reload_uv_index_strategy_setting(conn).await,
INSTANCE_PYTHON_VERSION_SETTING => {
reload_instance_python_version_setting(conn).await
}

View File

@@ -57,6 +57,7 @@ use windmill_common::{
JOB_DEFAULT_TIMEOUT_SECS_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, OTEL_TRACING_PROXY_SETTING, PIP_INDEX_URL_SETTING,
UV_INDEX_STRATEGY_SETTING,
POWERSHELL_REPO_PAT_SETTING, POWERSHELL_REPO_URL_SETTING, REQUEST_SIZE_LIMIT_SETTING,
REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING, RETENTION_PERIOD_SECS_SETTING,
SAML_METADATA_SETTING, SCIM_TOKEN_SETTING, TIMEOUT_WAIT_RESULT_SETTING,
@@ -88,7 +89,7 @@ use windmill_worker::{
SameWorkerSender, BUNFIG_INSTALL_SCOPES, INSTANCE_PYTHON_VERSION, JOB_DEFAULT_TIMEOUT,
KEEP_JOB_DIR, MAVEN_REPOS, NO_DEFAULT_MAVEN, NPM_CONFIG_REGISTRY, NUGET_CONFIG,
OTEL_TRACING_PROXY_SETTINGS, PIP_EXTRA_INDEX_URL, PIP_INDEX_URL, POWERSHELL_REPO_PAT,
POWERSHELL_REPO_URL,
POWERSHELL_REPO_URL, UV_INDEX_STRATEGY,
};
#[cfg(feature = "parquet")]
@@ -317,6 +318,7 @@ pub async fn initial_load(
reload_job_default_timeout_setting(&conn).await;
reload_extra_pip_index_url_setting(&conn).await;
reload_pip_index_url_setting(&conn).await;
reload_uv_index_strategy_setting(&conn).await;
reload_npm_config_registry_setting(&conn).await;
reload_bunfig_install_scopes_setting(&conn).await;
reload_instance_python_version_setting(&conn).await;
@@ -1103,7 +1105,6 @@ async fn delete_expired_jobs_batch(
job_retention_secs
);
// Delete related records for this batch
if let Err(e) = sqlx::query!(
"DELETE FROM job_stats WHERE job_id = ANY($1)",
&deleted_jobs
@@ -1251,6 +1252,16 @@ pub async fn reload_pip_index_url_setting(conn: &Connection) {
.await;
}
pub async fn reload_uv_index_strategy_setting(conn: &Connection) {
reload_option_setting_with_tracing(
conn,
UV_INDEX_STRATEGY_SETTING,
"UV_INDEX_STRATEGY",
UV_INDEX_STRATEGY.clone(),
)
.await;
}
pub async fn reload_instance_python_version_setting(conn: &Connection) {
reload_option_setting_with_tracing(
conn,
@@ -1783,6 +1794,19 @@ pub async fn monitor_db(
}
};
let cleanup_job_live_rows_f = async {
if server_mode && !initial_load {
if let Some(db) = conn.as_sql() {
if let Err(e) = cleanup_job_perms_orphaned(&db).await {
tracing::error!("Error cleaning up orphaned job_perms: {:?}", e);
}
if let Err(e) = cleanup_job_result_stream_orphaned_jobs(&db).await {
tracing::error!("Error cleaning up orphaned job_result_stream_v2: {:?}", e);
}
}
}
};
// run every hour (60 minutes / 30 seconds = 120)
let cleanup_worker_group_stats_f = async {
if server_mode && iteration.is_some() && iteration.as_ref().unwrap().should_run(120) {
@@ -1956,6 +1980,7 @@ pub async fn monitor_db(
cleanup_debounce_keys_f,
cleanup_debounce_keys_completed_f,
cleanup_flow_iterator_data_f,
cleanup_job_live_rows_f,
cleanup_worker_group_stats_f,
native_triggers_sync_f,
cleanup_notify_events_f,
@@ -1963,7 +1988,7 @@ pub async fn monitor_db(
}
async fn vacuuming_tables(db: &Pool<Postgres>) -> error::Result<()> {
sqlx::query!("VACUUM v2_job, v2_job_completed, job_result_stream_v2, job_stats, job_logs, concurrency_key, log_file, metrics")
sqlx::query!("VACUUM v2_job, v2_job_completed, job_result_stream_v2, job_stats, job_logs, job_perms, concurrency_key, log_file, metrics")
.execute(db)
.await?;
Ok(())
@@ -3062,6 +3087,43 @@ RETURNING key,job_id
Ok(())
}
async fn cleanup_job_perms_orphaned(db: &DB) -> error::Result<()> {
let result = sqlx::query_scalar!(
"DELETE FROM job_perms
WHERE job_id NOT IN (SELECT id FROM v2_job_queue)
RETURNING job_id"
)
.fetch_all(db)
.await?;
if !result.is_empty() {
tracing::info!("Cleaned up {} orphaned job_perms rows", result.len());
}
Ok(())
}
async fn cleanup_job_result_stream_orphaned_jobs(db: &DB) -> error::Result<()> {
let result = sqlx::query!(
"DELETE FROM job_result_stream_v2
WHERE job_id NOT IN (SELECT id FROM v2_job_queue)
AND job_id NOT IN (
SELECT id FROM v2_job_completed
WHERE completed_at > NOW() - INTERVAL '60 seconds'
)
RETURNING job_id",
)
.fetch_all(db)
.await?;
if result.len() > 0 {
tracing::info!(
"Cleaned up {} orphaned job_result_stream_v2 rows",
result.len()
);
}
Ok(())
}
async fn cleanup_flow_iterator_data_orphaned_jobs(db: &DB) -> error::Result<()> {
let result = sqlx::query!(
"

View File

@@ -0,0 +1,267 @@
#![cfg(all(feature = "private", feature = "agent_worker_server"))]
use windmill_test_utils::*;
use serde_json::json;
use sqlx::{Pool, Postgres};
use windmill_common::{
jobs::{JobPayload, RawCode},
scripts::ScriptLang,
};
fn bun_code(code: &str) -> RawCode {
RawCode {
hash: None,
content: code.to_string(),
path: None,
language: ScriptLang::Bun,
lock: None,
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
concurrency_settings:
windmill_common::runnable_settings::ConcurrencySettings::default().into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
}
}
#[sqlx::test(fixtures("base"))]
async fn test_agent_worker_simple_script(db: Pool<Postgres>) -> anyhow::Result<()> {
let (_client, port, _server) = init_client_agent_mode(db.clone()).await;
let result = RunJob::from(JobPayload::Code(bun_code(
"export function main() { return 42; }",
)))
.run_until_complete(&db, false, port)
.await;
assert!(result.success, "job should succeed");
assert_eq!(result.json_result(), Some(json!(42)));
Ok(())
}
#[sqlx::test(fixtures("base"))]
async fn test_agent_worker_script_with_args(db: Pool<Postgres>) -> anyhow::Result<()> {
let (_client, port, _server) = init_client_agent_mode(db.clone()).await;
let result = RunJob::from(JobPayload::Code(bun_code(
"export function main(x: number, y: number) { return x + y; }",
)))
.arg("x", json!(10))
.arg("y", json!(32))
.run_until_complete(&db, false, port)
.await;
assert!(result.success, "job should succeed");
assert_eq!(result.json_result(), Some(json!(42)));
Ok(())
}
#[sqlx::test(fixtures("base"))]
async fn test_agent_worker_script_with_logs(db: Pool<Postgres>) -> anyhow::Result<()> {
let (_client, port, _server) = init_client_agent_mode(db.clone()).await;
let result = RunJob::from(JobPayload::Code(bun_code(
r#"export function main() {
console.log("hello from agent worker");
console.log("processing step 1");
console.log("processing step 2");
return "done";
}"#,
)))
.run_until_complete(&db, false, port)
.await;
assert!(result.success, "job should succeed");
assert_eq!(result.json_result(), Some(json!("done")));
let logs = sqlx::query_scalar::<_, String>(
"SELECT logs FROM job_logs WHERE job_id = $1 AND workspace_id = 'test-workspace'",
)
.bind(result.id)
.fetch_optional(&db)
.await?;
let logs = logs.expect("logs should exist");
assert!(
logs.contains("hello from agent worker"),
"logs should contain the printed output, got: {logs}"
);
Ok(())
}
#[sqlx::test(fixtures("base"))]
async fn test_agent_worker_script_failure(db: Pool<Postgres>) -> anyhow::Result<()> {
let (_client, port, _server) = init_client_agent_mode(db.clone()).await;
let result = RunJob::from(JobPayload::Code(bun_code(
"export function main() { throw new Error('test error'); }",
)))
.run_until_complete(&db, false, port)
.await;
assert!(!result.success, "job should fail");
Ok(())
}
#[sqlx::test(fixtures("base"))]
async fn test_agent_worker_complex_result(db: Pool<Postgres>) -> anyhow::Result<()> {
let (_client, port, _server) = init_client_agent_mode(db.clone()).await;
let result = RunJob::from(JobPayload::Code(bun_code(
r#"export function main() {
return {
items: [1, 2, 3],
metadata: { key: "value" },
count: 3,
};
}"#,
)))
.run_until_complete(&db, false, port)
.await;
assert!(result.success, "job should succeed");
let json = result.json_result().unwrap();
assert_eq!(json["items"], json!([1, 2, 3]));
assert_eq!(json["metadata"]["key"], json!("value"));
assert_eq!(json["count"], json!(3));
Ok(())
}
#[sqlx::test(fixtures("base"))]
async fn test_agent_worker_token_creation(db: Pool<Postgres>) -> anyhow::Result<()> {
let (client, _port, _server) = init_client_agent_mode(db.clone()).await;
// client.baseurl() already includes /api
let resp = client
.client()
.post(format!(
"{}/agent_workers/create_agent_token",
client.baseurl()
))
.json(&json!({
"worker_group": "lifecycle-test",
"tags": ["bun", "flow", "dependency"],
"exp": usize::MAX
}))
.send()
.await?;
assert!(
resp.status().is_success(),
"create_agent_token should succeed, got: {}",
resp.status()
);
let token = resp.text().await?;
let token = token.trim_matches('"');
assert!(
token.starts_with("jwt_agent_"),
"token should start with jwt_agent_ prefix, got: {token}"
);
Ok(())
}
#[sqlx::test(fixtures("base"))]
async fn test_agent_worker_token_and_ping(db: Pool<Postgres>) -> anyhow::Result<()> {
let (client, port, _server) = init_client_agent_mode(db.clone()).await;
let resp = client
.client()
.post(format!(
"{}/agent_workers/create_agent_token",
client.baseurl()
))
.json(&json!({
"worker_group": "lifecycle-test",
"tags": ["bun", "flow", "dependency"],
"exp": usize::MAX
}))
.send()
.await?;
assert!(resp.status().is_success());
let token = resp.text().await?;
let token = token.trim_matches('"');
let suffix = windmill_common::utils::create_default_worker_suffix("lifecycle-test");
let base_url = format!("http://localhost:{port}");
let http_client =
windmill_common::agent_workers::build_agent_http_client(&suffix, &token, &base_url);
// Initial ping inserts the worker record into the database
let resp = http_client
.client
.post(format!("{}/api/agent_workers/update_ping", base_url))
.json(&json!({
"worker_instance": "test-instance",
"ip": "127.0.0.1",
"tags": ["bun"],
"version": "test",
"vcpus": 4,
"memory": 8192,
"ping_type": "Initial"
}))
.send()
.await?;
assert!(
resp.status().is_success(),
"initial ping should succeed, got: {}",
resp.status()
);
// Verify the ping was recorded in the database
let worker_count = sqlx::query_scalar::<_, i64>(
"SELECT COUNT(*) FROM worker_ping WHERE worker_instance = 'test-instance'",
)
.fetch_one(&db)
.await?;
assert!(worker_count > 0, "worker ping should be recorded in database");
// MainLoop ping updates the existing record
let resp = http_client
.client
.post(format!("{}/api/agent_workers/update_ping", base_url))
.json(&json!({
"tags": ["bun"],
"vcpus": 4,
"memory": 8192,
"jobs_executed": 0,
"ping_type": "MainLoop"
}))
.send()
.await?;
assert!(
resp.status().is_success(),
"main loop ping should succeed, got: {}",
resp.status()
);
Ok(())
}
#[sqlx::test(fixtures("base"))]
async fn test_agent_worker_multiple_jobs_sequential(db: Pool<Postgres>) -> anyhow::Result<()> {
let (_client, port, _server) = init_client_agent_mode(db.clone()).await;
for i in 0..3 {
let result = RunJob::from(JobPayload::Code(bun_code(&format!(
"export function main() {{ return {i}; }}"
))))
.run_until_complete(&db, false, port)
.await;
assert!(result.success, "job {i} should succeed");
assert_eq!(result.json_result(), Some(json!(i)));
}
Ok(())
}

View File

@@ -1,5 +1,4 @@
mod common;
use crate::common::*;
use windmill_test_utils::*;
use sqlx::postgres::Postgres;
use sqlx::Pool;
use windmill_common::jobs::{JobPayload, RawCode};
@@ -860,7 +859,7 @@ export function main() {
// ============================================================================
mod dedicated_worker_protocol {
use crate::common::{parse_dedicated_worker_line, DedicatedWorkerResult};
use windmill_test_utils::{parse_dedicated_worker_line, DedicatedWorkerResult};
use std::io::{BufRead, BufReader, Write};
use std::process::{Command, Stdio};
use windmill_worker::{

View File

@@ -2,8 +2,9 @@ use sqlx::{Pool, Postgres};
use tokio_stream::StreamExt;
use windmill_api_client::types::NewScript;
mod common;
use common::{in_test_worker, init_client, listen_for_completed_jobs, ApiServer};
use windmill_test_utils::{
in_test_worker, init_client, listen_for_completed_jobs, rebuild_dmap, ApiServer,
};
mod dependency_map {
use super::*;
@@ -170,7 +171,7 @@ mod dependency_map {
let (client, _port, _s) = init(db.clone()).await;
assert_dmap(&db, None, CORRECT_DMAP.clone()).await;
// rebuild map
assert!(common::rebuild_dmap(&client).await);
assert!(rebuild_dmap(&client).await);
assert_dmap(&db, None, CORRECT_DMAP.clone()).await;
Ok(())
}
@@ -183,7 +184,7 @@ mod dependency_map {
// Spawn first rebuild
let handle = {
let client = client.clone();
tokio::spawn(async move { common::rebuild_dmap(&client).await })
tokio::spawn(async move { rebuild_dmap(&client).await })
};
// Immidiately spawn another

View File

@@ -0,0 +1,410 @@
use sqlx::{Pool, Postgres};
use windmill_test_utils::*;
/// Test that workspace error handler can be set and removed via database operations
#[cfg(feature = "deno_core")]
#[sqlx::test(fixtures("base"))]
async fn test_error_handler_settings(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
let _server = ApiServer::start(db.clone()).await?;
// Initially error_handler should be NULL
let initial = sqlx::query_scalar!(
r#"SELECT error_handler->>'path' FROM workspace_settings WHERE workspace_id = 'test-workspace'"#
)
.fetch_one(&db)
.await?;
assert!(initial.is_none());
// Set error handler with all options
sqlx::query!(
r#"
UPDATE workspace_settings
SET error_handler = '{"path": "script/f/test/error_handler", "extra_args": {"notify": true}, "muted_on_cancel": true, "muted_on_user_path": false}'::jsonb
WHERE workspace_id = 'test-workspace'
"#
)
.execute(&db)
.await?;
let after_set = sqlx::query_scalar!(
r#"SELECT error_handler->>'path' FROM workspace_settings WHERE workspace_id = 'test-workspace'"#
)
.fetch_one(&db)
.await?;
assert_eq!(
after_set,
Some("script/f/test/error_handler".to_string())
);
// Verify extra_args
let extra_args = sqlx::query_scalar!(
r#"SELECT error_handler->'extra_args' FROM workspace_settings WHERE workspace_id = 'test-workspace'"#
)
.fetch_one(&db)
.await?;
assert!(extra_args.is_some());
// Verify muted_on_cancel
let muted_on_cancel = sqlx::query_scalar!(
r#"SELECT (error_handler->>'muted_on_cancel')::boolean FROM workspace_settings WHERE workspace_id = 'test-workspace'"#
)
.fetch_one(&db)
.await?;
assert_eq!(muted_on_cancel, Some(true));
// Verify muted_on_user_path
let muted_on_user_path = sqlx::query_scalar!(
r#"SELECT (error_handler->>'muted_on_user_path')::boolean FROM workspace_settings WHERE workspace_id = 'test-workspace'"#
)
.fetch_one(&db)
.await?;
assert_eq!(muted_on_user_path, Some(false));
// Remove error handler
sqlx::query!(
r#"
UPDATE workspace_settings
SET error_handler = NULL
WHERE workspace_id = 'test-workspace'
"#
)
.execute(&db)
.await?;
let after_remove = sqlx::query_scalar!(
r#"SELECT error_handler->>'path' FROM workspace_settings WHERE workspace_id = 'test-workspace'"#
)
.fetch_one(&db)
.await?;
assert!(after_remove.is_none());
Ok(())
}
/// Test that a failed job triggers the workspace error handler
#[cfg(all(feature = "deno_core", feature = "enterprise", feature = "private"))]
#[sqlx::test(fixtures("base"))]
async fn test_error_handler_triggered_on_failure(db: Pool<Postgres>) -> anyhow::Result<()> {
use windmill_common::jobs::JobPayload;
use windmill_common::runnable_settings::{ConcurrencySettings, DebouncingSettings};
use windmill_common::scripts::{ScriptHash, ScriptLang};
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
// Create the error handler script
let error_handler_code = r#"
export async function main(path: string, email: string, job_id: string, is_flow: boolean, workspace_id: string, error: any) {
console.log("Error handler called for job:", job_id);
return { handled: true, original_path: path };
}
"#;
sqlx::query!(
r#"
INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock)
VALUES ('test-workspace', 1111111111, 'f/test/error_handler', $1, 'deno', 'script', 'test-user', '{}', 'Error handler script', 'Handles failed job completions', '')
"#,
error_handler_code
)
.execute(&db)
.await?;
// Create a script that will fail
let failing_script_code = "export function main() { throw new Error('intentional failure'); }";
let failing_script_hash: i64 = 2222222222;
sqlx::query!(
r#"
INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock)
VALUES ('test-workspace', $1, 'f/test/failing_script', $2, 'deno', 'script', 'test-user', '{}', 'Failing test script', 'A script that always fails', '')
"#,
failing_script_hash,
failing_script_code
)
.execute(&db)
.await?;
// Set up the error handler in workspace_settings
sqlx::query!(
r#"
UPDATE workspace_settings
SET error_handler = '{"path": "script/f/test/error_handler"}'::jsonb
WHERE workspace_id = 'test-workspace'
"#
)
.execute(&db)
.await?;
// Create the error_handler group
sqlx::query!(
r#"
INSERT INTO group_ (workspace_id, name, summary, extra_perms)
VALUES ('test-workspace', 'error_handler', 'The group the error handler acts on behalf of', '{"u/test-user": true}')
ON CONFLICT DO NOTHING
"#
)
.execute(&db)
.await?;
// Run the failing script
let completed_job = RunJob::from(JobPayload::ScriptHash {
hash: ScriptHash(failing_script_hash),
path: "f/test/failing_script".to_string(),
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
language: ScriptLang::Deno,
priority: None,
apply_preprocessor: false,
concurrency_settings: ConcurrencySettings::default(),
debouncing_settings: DebouncingSettings::default(),
})
.run_until_complete(&db, false, server.addr.port())
.await;
// Verify the job actually failed
assert!(!completed_job.success, "Job should have failed");
let main_job_id = completed_job.id;
// Wait for the error handler job to be created
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
// Verify the error handler job was created
let error_handler_job = sqlx::query!(
r#"
SELECT
id,
runnable_path,
permissioned_as_email,
parent_job
FROM v2_job
WHERE workspace_id = 'test-workspace'
AND permissioned_as_email = 'error_handler@windmill.dev'
ORDER BY created_at DESC
LIMIT 1
"#
)
.fetch_optional(&db)
.await?;
assert!(
error_handler_job.is_some(),
"Error handler job should have been created"
);
let handler_job = error_handler_job.unwrap();
assert_eq!(
handler_job.runnable_path.as_deref(),
Some("f/test/error_handler"),
"Error handler should run the configured script"
);
assert_eq!(
handler_job.permissioned_as_email.as_str(),
"error_handler@windmill.dev",
"Error handler should run as error_handler user"
);
assert_eq!(
handler_job.parent_job,
Some(main_job_id),
"Error handler should have the failed job as parent"
);
Ok(())
}
/// Test that error handler is NOT triggered when ws_error_handler_muted is set on the script
#[cfg(all(feature = "deno_core", feature = "enterprise", feature = "private"))]
#[sqlx::test(fixtures("base"))]
async fn test_error_handler_muted_on_script(db: Pool<Postgres>) -> anyhow::Result<()> {
use windmill_common::jobs::JobPayload;
use windmill_common::runnable_settings::{ConcurrencySettings, DebouncingSettings};
use windmill_common::scripts::{ScriptHash, ScriptLang};
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
// Create the error handler script
sqlx::query!(
r#"
INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock)
VALUES ('test-workspace', 3333333333, 'f/test/error_handler', 'export function main() { return "handled"; }', 'deno', 'script', 'test-user', '{}', '', '', '')
"#,
)
.execute(&db)
.await?;
// Create a failing script with ws_error_handler_muted = true
let failing_script_hash: i64 = 4444444444;
sqlx::query!(
r#"
INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock, ws_error_handler_muted)
VALUES ('test-workspace', $1, 'f/test/muted_failing_script', 'export function main() { throw new Error("fail"); }', 'deno', 'script', 'test-user', '{}', '', '', '', true)
"#,
failing_script_hash,
)
.execute(&db)
.await?;
// Set up the error handler
sqlx::query!(
r#"
UPDATE workspace_settings
SET error_handler = '{"path": "script/f/test/error_handler"}'::jsonb
WHERE workspace_id = 'test-workspace'
"#
)
.execute(&db)
.await?;
sqlx::query!(
r#"
INSERT INTO group_ (workspace_id, name, summary, extra_perms)
VALUES ('test-workspace', 'error_handler', 'Error handler group', '{"u/test-user": true}')
ON CONFLICT DO NOTHING
"#
)
.execute(&db)
.await?;
// Run the muted failing script
let completed_job = RunJob::from(JobPayload::ScriptHash {
hash: ScriptHash(failing_script_hash),
path: "f/test/muted_failing_script".to_string(),
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
language: ScriptLang::Deno,
priority: None,
apply_preprocessor: false,
concurrency_settings: ConcurrencySettings::default(),
debouncing_settings: DebouncingSettings::default(),
})
.run_until_complete(&db, false, server.addr.port())
.await;
assert!(!completed_job.success, "Job should have failed");
// Wait and check that NO error handler job was created
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
let error_handler_job = sqlx::query_scalar!(
r#"
SELECT id
FROM v2_job
WHERE workspace_id = 'test-workspace'
AND permissioned_as_email = 'error_handler@windmill.dev'
"#
)
.fetch_optional(&db)
.await?;
assert!(
error_handler_job.is_none(),
"Error handler should NOT have been triggered for a muted script"
);
Ok(())
}
/// Test that error handler is NOT triggered on successful job completion
#[cfg(all(feature = "deno_core", feature = "enterprise", feature = "private"))]
#[sqlx::test(fixtures("base"))]
async fn test_error_handler_not_triggered_on_success(db: Pool<Postgres>) -> anyhow::Result<()> {
use windmill_common::jobs::JobPayload;
use windmill_common::runnable_settings::{ConcurrencySettings, DebouncingSettings};
use windmill_common::scripts::{ScriptHash, ScriptLang};
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
// Create the error handler script
sqlx::query!(
r#"
INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock)
VALUES ('test-workspace', 5555555555, 'f/test/error_handler', 'export function main() { return "handled"; }', 'deno', 'script', 'test-user', '{}', '', '', '')
"#,
)
.execute(&db)
.await?;
// Create a successful script
let success_script_hash: i64 = 6666666666;
sqlx::query!(
r#"
INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock)
VALUES ('test-workspace', $1, 'f/test/success_script', 'export function main() { return "ok"; }', 'deno', 'script', 'test-user', '{}', '', '', '')
"#,
success_script_hash,
)
.execute(&db)
.await?;
// Set up the error handler
sqlx::query!(
r#"
UPDATE workspace_settings
SET error_handler = '{"path": "script/f/test/error_handler"}'::jsonb
WHERE workspace_id = 'test-workspace'
"#
)
.execute(&db)
.await?;
sqlx::query!(
r#"
INSERT INTO group_ (workspace_id, name, summary, extra_perms)
VALUES ('test-workspace', 'error_handler', 'Error handler group', '{"u/test-user": true}')
ON CONFLICT DO NOTHING
"#
)
.execute(&db)
.await?;
// Run the successful script
let completed_job = RunJob::from(JobPayload::ScriptHash {
hash: ScriptHash(success_script_hash),
path: "f/test/success_script".to_string(),
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
language: ScriptLang::Deno,
priority: None,
apply_preprocessor: false,
concurrency_settings: ConcurrencySettings::default(),
debouncing_settings: DebouncingSettings::default(),
})
.run_until_complete(&db, false, server.addr.port())
.await;
assert!(completed_job.success, "Job should have succeeded");
// Wait and check that NO error handler job was created
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
let error_handler_job = sqlx::query_scalar!(
r#"
SELECT id
FROM v2_job
WHERE workspace_id = 'test-workspace'
AND permissioned_as_email = 'error_handler@windmill.dev'
"#
)
.fetch_optional(&db)
.await?;
assert!(
error_handler_job.is_none(),
"Error handler should NOT have been triggered for a successful job"
);
Ok(())
}

View File

@@ -25,8 +25,7 @@ use windmill_common::{
scripts::ScriptLang,
};
mod common;
use common::*;
use windmill_test_utils::*;
/// Helper to create a FlowModule with default fields
fn flow_module(id: &str, value: FlowModuleValue) -> FlowModule {

View File

@@ -1,5 +1,3 @@
mod common;
mod job_payload {
use serde_json::json;
use sqlx::{Pool, Postgres};
@@ -8,7 +6,7 @@ mod job_payload {
use windmill_common::scripts::{ScriptHash, ScriptLang};
use windmill_common::flow_status::RestartedFrom;
use crate::common::*;
use windmill_test_utils::*;
use windmill_common::min_version::{
MIN_VERSION, MIN_VERSION_IS_AT_LEAST_1_427, MIN_VERSION_IS_AT_LEAST_1_432,
MIN_VERSION_IS_AT_LEAST_1_440,

View File

@@ -7,8 +7,7 @@ use windmill_common::{
scripts::ScriptLang,
};
mod common;
use common::*;
use windmill_test_utils::*;
#[derive(Debug, Deserialize)]
struct ListJobsResponse {

View File

@@ -14,10 +14,7 @@
*/
#[cfg(feature = "deno_core")]
mod common;
#[cfg(feature = "deno_core")]
use common::*;
use windmill_test_utils::*;
#[cfg(feature = "deno_core")]
use futures::StreamExt;

View File

@@ -11,10 +11,7 @@
*/
#[cfg(feature = "deno_core")]
mod common;
#[cfg(feature = "deno_core")]
use common::*;
use windmill_test_utils::*;
#[cfg(feature = "deno_core")]
use std::time::Instant;

View File

@@ -1,5 +1,4 @@
mod common;
use crate::common::*;
use windmill_test_utils::*;
use sqlx::postgres::Postgres;
use sqlx::Pool;
use windmill_common::scripts::ScriptLang;

View File

@@ -1,8 +1,6 @@
mod common;
#[cfg(feature = "deno_core")]
mod retry {
use crate::common::*;
use windmill_test_utils::*;
use serde_json::json;
use sqlx::{Pool, Postgres};
use tokio::io::{AsyncReadExt, AsyncWriteExt};

View File

@@ -1,7 +1,6 @@
use sqlx::{Pool, Postgres};
mod common;
use common::*;
use windmill_test_utils::*;
/// Test that the workspace success handler cache works correctly with 60s TTL
#[cfg(feature = "deno_core")]

View File

@@ -1,11 +1,9 @@
mod common;
mod suspend_resume {
#[cfg(feature = "deno_core")]
use serde_json::json;
#[cfg(feature = "deno_core")]
use crate::common::*;
use windmill_test_utils::*;
#[cfg(feature = "deno_core")]
use futures::{Stream, StreamExt};
@@ -237,6 +235,331 @@ mod suspend_resume {
Ok(())
}
/// Test that self-approval is blocked when self_approval_disabled is true.
///
/// This test verifies that when a flow has an approval step with self_approval_disabled=true,
/// the user who triggered the flow cannot approve it themselves via the owner endpoint
/// (POST /jobs/flow/resume/:id).
///
/// Bug context: The owner endpoint was missing the approval condition check, allowing
/// users to bypass self-approval restrictions by using the UI resume button instead
/// of the HMAC-signed approval link.
#[cfg(feature = "enterprise")]
#[cfg(feature = "deno_core")]
#[sqlx::test(fixtures("base"))]
async fn test_self_approval_disabled_blocks_owner_resume(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
let port = server.addr.port();
// Flow with self_approval_disabled=true on the approval step
let flow_with_self_approval_disabled: FlowValue = serde_json::from_value(json!({
"modules": [{
"id": "a",
"value": {
"type": "rawscript",
"language": "deno",
"content": "export function main() { return 'step1'; }"
},
"suspend": {
"required_events": 1,
"user_auth_required": true,
"self_approval_disabled": true
}
}, {
"id": "b",
"value": {
"type": "rawscript",
"language": "deno",
"content": "export function main() { return 'step2 - after approval'; }"
}
}]
}))
.unwrap();
// Push flow as NON-ADMIN user (test-user-2) - admins bypass self-approval check
// Use a path owned by test-user-2 so require_owner_of_path succeeds
let flow = RunJob::from(JobPayload::RawFlow {
value: flow_with_self_approval_disabled,
path: Some("u/test-user-2/test_approval".to_string()),
restarted_from: None,
})
.push_as(&db, "test-user-2", "test2@windmill.dev")
.await;
let queue = listen_for_queue(&db).await;
let db_ = db.clone();
in_test_worker(
&db,
async move {
let db = db_;
// Wait for flow to suspend at approval step
wait_until_flow_suspends(flow, queue, &db).await;
// Create a token for the same non-admin user who triggered the flow
// This simulates clicking "Resume" in the UI as the flow owner
// Args: db, w_id, owner, label, expires_in, email, job_id, perms, audit_span
let token = windmill_common::auth::create_token_for_owner(
&db,
"test-workspace",
"u/test-user-2",
"test-token",
100,
"test2@windmill.dev",
&Uuid::nil(),
None,
None,
)
.await
.unwrap();
// Try to resume via the owner endpoint (POST /jobs/flow/resume/:id)
// This should FAIL because self_approval_disabled=true and the user
// is the same as the one who triggered the flow
let response = reqwest::Client::new()
.post(format!(
"http://localhost:{port}/api/w/test-workspace/jobs/flow/resume/{flow}"
))
.header("Authorization", format!("Bearer {token}"))
.header("Content-Type", "application/json")
.body("{}")
.send()
.await
.unwrap();
let status = response.status();
// The request should be rejected with 403 Forbidden
// (currently this test FAILS because the bug allows self-approval)
assert!(
status == reqwest::StatusCode::FORBIDDEN,
"Self-approval should be blocked when self_approval_disabled=true. \
Expected 403 Forbidden, got {}. Response: {}",
status,
response.text().await.unwrap_or_default()
);
},
port,
)
.await;
server.close().await.unwrap();
Ok(())
}
/// Test that self-approval WORKS when self_approval_disabled is false (default behavior).
///
/// This is the complementary test to test_self_approval_disabled_blocks_owner_resume.
/// When self_approval_disabled is NOT set, the flow owner should be able to approve
/// their own flow via the owner endpoint.
#[cfg(feature = "enterprise")]
#[cfg(feature = "deno_core")]
#[sqlx::test(fixtures("base"))]
async fn test_self_approval_allowed_when_not_disabled(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
let port = server.addr.port();
// Flow with user_auth_required but WITHOUT self_approval_disabled
let flow_without_self_approval_disabled: FlowValue = serde_json::from_value(json!({
"modules": [{
"id": "a",
"value": {
"type": "rawscript",
"language": "deno",
"content": "export function main() { return 'step1'; }"
},
"suspend": {
"required_events": 1,
"user_auth_required": true
// self_approval_disabled is NOT set (defaults to false)
}
}, {
"id": "b",
"value": {
"type": "rawscript",
"language": "deno",
"content": "export function main() { return 'step2 - after approval'; }"
}
}]
}))
.unwrap();
// Push flow as non-admin user
let flow = RunJob::from(JobPayload::RawFlow {
value: flow_without_self_approval_disabled,
path: Some("u/test-user-2/test_approval_allowed".to_string()),
restarted_from: None,
})
.push_as(&db, "test-user-2", "test2@windmill.dev")
.await;
let queue = listen_for_queue(&db).await;
let db_ = db.clone();
in_test_worker(
&db,
async move {
let db = db_;
// Wait for flow to suspend at approval step
wait_until_flow_suspends(flow, queue, &db).await;
// Create a token for the same user who triggered the flow
let token = windmill_common::auth::create_token_for_owner(
&db,
"test-workspace",
"u/test-user-2",
"test-token",
100,
"test2@windmill.dev",
&Uuid::nil(),
None,
None,
)
.await
.unwrap();
// Try to resume via the owner endpoint - this SHOULD succeed
let response = reqwest::Client::new()
.post(format!(
"http://localhost:{port}/api/w/test-workspace/jobs/flow/resume/{flow}"
))
.header("Authorization", format!("Bearer {token}"))
.header("Content-Type", "application/json")
.body("{}")
.send()
.await
.unwrap();
let status = response.status();
// Self-approval should be allowed when self_approval_disabled is not set
assert!(
status.is_success(),
"Self-approval should be allowed when self_approval_disabled is not set. \
Expected 2xx, got {}. Response: {}",
status,
response.text().await.unwrap_or_default()
);
},
port,
)
.await;
server.close().await.unwrap();
Ok(())
}
/// Test that a DIFFERENT user can approve a flow even when self_approval_disabled is true.
///
/// This verifies that the self_approval_disabled setting only blocks the flow trigger,
/// not other users. A different user should always be able to approve.
#[cfg(feature = "enterprise")]
#[cfg(feature = "deno_core")]
#[sqlx::test(fixtures("base"))]
async fn test_different_user_can_approve_when_self_approval_disabled(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
let port = server.addr.port();
// Flow with self_approval_disabled=true
let flow_with_self_approval_disabled: FlowValue = serde_json::from_value(json!({
"modules": [{
"id": "a",
"value": {
"type": "rawscript",
"language": "deno",
"content": "export function main() { return 'step1'; }"
},
"suspend": {
"required_events": 1,
"user_auth_required": true,
"self_approval_disabled": true
}
}, {
"id": "b",
"value": {
"type": "rawscript",
"language": "deno",
"content": "export function main() { return 'step2 - after approval'; }"
}
}]
}))
.unwrap();
// Push flow as test-user-2
let flow = RunJob::from(JobPayload::RawFlow {
value: flow_with_self_approval_disabled,
path: Some("u/test-user-2/test_approval_by_other".to_string()),
restarted_from: None,
})
.push_as(&db, "test-user-2", "test2@windmill.dev")
.await;
let queue = listen_for_queue(&db).await;
let db_ = db.clone();
in_test_worker(
&db,
async move {
let db = db_;
// Wait for flow to suspend at approval step
wait_until_flow_suspends(flow, queue, &db).await;
// Create a token for a DIFFERENT user (test-user, who is admin)
// This simulates a different person approving the flow
let token = windmill_common::auth::create_token_for_owner(
&db,
"test-workspace",
"u/test-user",
"test-token",
100,
"test@windmill.dev",
&Uuid::nil(),
None,
None,
)
.await
.unwrap();
// Try to resume via the owner endpoint as a different user - this SHOULD succeed
let response = reqwest::Client::new()
.post(format!(
"http://localhost:{port}/api/w/test-workspace/jobs/flow/resume/{flow}"
))
.header("Authorization", format!("Bearer {token}"))
.header("Content-Type", "application/json")
.body("{}")
.send()
.await
.unwrap();
let status = response.status();
// A different user should be able to approve even with self_approval_disabled
assert!(
status.is_success(),
"Different user should be able to approve even with self_approval_disabled=true. \
Expected 2xx, got {}. Response: {}",
status,
response.text().await.unwrap_or_default()
);
},
port,
)
.await;
server.close().await.unwrap();
Ok(())
}
#[cfg(feature = "deno_core")]
#[sqlx::test(fixtures("base"))]
async fn cancel_after_suspend(db: Pool<Postgres>) -> anyhow::Result<()> {

View File

@@ -25,8 +25,7 @@ use windmill_common::{
jobs::{JobPayload, RawCode},
scripts::ScriptLang,
};
mod common;
use common::*;
use windmill_test_utils::*;
#[cfg(feature = "enterprise")]
use futures::StreamExt;
@@ -3343,7 +3342,6 @@ async fn test_duckdb_ffi(db: Pool<Postgres>) -> anyhow::Result<()> {
/// Test that flow substeps with tags that are not available for the workspace fail.
/// This validates that `check_tag_available_for_workspace_internal` is properly called
/// when pushing jobs from worker_flow.
#[cfg(feature = "deno_core")]
#[sqlx::test(fixtures("base"))]
async fn test_flow_substep_tag_availability_check(db: Pool<Postgres>) -> anyhow::Result<()> {
use windmill_common::worker::{
@@ -3385,6 +3383,7 @@ async fn test_flow_substep_tag_availability_check(db: Pool<Postgres>) -> anyhow:
let result =
RunJob::from(JobPayload::RawFlow { value: flow.clone(), path: None, restarted_from: None })
.email("test2@windmill.dev")
.run_until_complete(&db, false, server.addr.port())
.await;

View File

@@ -1,9 +1,8 @@
mod common;
mod workspace_dependencies {
use crate::common::in_test_worker;
use crate::common::init_client;
use crate::common::listen_for_completed_jobs;
use windmill_test_utils::in_test_worker;
use windmill_test_utils::init_client;
use windmill_test_utils::listen_for_completed_jobs;
use sqlx::{Pool, Postgres};
use tokio_stream::StreamExt;
use windmill_common::scripts::ScriptLang;

View File

@@ -10,22 +10,11 @@ if [[ "$(uname)" == "Darwin" ]]; then
# Uncomment the git-based samael dependency
sed -i '' 's/^# \(samael = { git="https:\/\/github.com\/njaremko\/samael", rev="464d015e3ae393e4b5dd00b4d6baa1b617de0dd6", features = \["xmlsec"\] }\)/\1/' Cargo.toml
# Run cargo sqlx prepare with deno_core_mac
# Run cargo sqlx prepare with deno_core_mac
echo "Running cargo sqlx prepare with deno_core_mac..."
cargo sqlx prepare --workspace -- --all-targets --features all_sqlx_features,private,deno_core_mac
else
# Run cargo sqlx prepare
echo "Running cargo sqlx prepare..."
cargo sqlx prepare --workspace -- --all-targets --features all_sqlx_features,private
fi
# Undo the samael changes on macOS
if [[ "$(uname)" == "Darwin" ]]; then
echo "Reverting samael changes..."
# Uncomment the version-based samael dependency
sed -i '' 's/^#samael = { version="0.0.14", features = \["xmlsec"\] }/samael = { version="0.0.14", features = ["xmlsec"] }/' Cargo.toml
# Comment out the git-based samael dependency
sed -i '' 's/^\(samael = { git="https:\/\/github.com\/njaremko\/samael", rev="464d015e3ae393e4b5dd00b4d6baa1b617de0dd6", features = \["xmlsec"\] }\)/# \1/' Cargo.toml
cargo sqlx prepare --workspace -- --all-targets --features all_sqlx_features,ee
fi

Binary file not shown.

View File

@@ -0,0 +1,18 @@
[package]
name = "windmill-alerting"
version.workspace = true
authors.workspace = true
edition.workspace = true
[lib]
name = "windmill_alerting"
path = "src/lib.rs"
[dependencies]
windmill-common = { workspace = true, default-features = false }
axum.workspace = true
chrono.workspace = true
serde.workspace = true
serde_json.workspace = true
sqlx.workspace = true
tracing.workspace = true

View File

@@ -0,0 +1,208 @@
use axum::Json;
use serde::Deserialize;
use windmill_common::error::{self, JsonResult};
use windmill_common::worker::CLOUD_HOSTED;
use windmill_common::DB;
#[derive(serde::Serialize)]
pub struct CriticalAlert {
id: i32,
alert_type: String,
message: String,
created_at: chrono::DateTime<chrono::Utc>,
acknowledged: Option<bool>,
workspace_id: Option<String>,
}
#[derive(Deserialize, Debug)]
pub struct AlertQueryParams {
pub page: Option<i32>,
pub page_size: Option<i32>,
pub acknowledged: Option<bool>,
}
pub async fn get_critical_alerts(
db: DB,
params: AlertQueryParams,
workspace_id: Option<String>,
) -> JsonResult<serde_json::Value> {
let page = params.page.unwrap_or(1).max(1);
let page_size = params.page_size.unwrap_or(10).min(100) as i64;
let offset = ((page - 1) * page_size as i32) as i64;
let total_rows = if let Some(workspace_id) = &workspace_id {
if params.acknowledged.is_none() {
sqlx::query_scalar!(
"SELECT COUNT(*)
FROM alerts
WHERE workspace_id = $1",
workspace_id
)
.fetch_one(&db)
.await?
} else {
sqlx::query_scalar!(
"SELECT COUNT(*)
FROM alerts
WHERE workspace_id = $1 AND COALESCE(acknowledged_workspace, false) = $2",
workspace_id,
params.acknowledged
)
.fetch_one(&db)
.await?
}
} else {
if params.acknowledged.is_none() {
sqlx::query_scalar!(
"SELECT COUNT(*)
FROM alerts"
)
.fetch_one(&db)
.await?
} else {
sqlx::query_scalar!(
"SELECT COUNT(*)
FROM alerts
WHERE COALESCE(acknowledged, false) = $1",
params.acknowledged
)
.fetch_one(&db)
.await?
}
};
let alerts = if let Some(workspace_id) = workspace_id {
if params.acknowledged.is_none() {
sqlx::query_as!(
CriticalAlert,
"SELECT id, alert_type, message, created_at, COALESCE(acknowledged_workspace, false) AS acknowledged, workspace_id
FROM alerts
WHERE workspace_id = $1
ORDER BY created_at DESC
LIMIT $2 OFFSET $3",
workspace_id,
page_size,
offset
)
.fetch_all(&db)
.await?
} else {
sqlx::query_as!(
CriticalAlert,
"SELECT id, alert_type, message, created_at, COALESCE(acknowledged_workspace, false) AS acknowledged, workspace_id
FROM alerts
WHERE workspace_id = $1 AND COALESCE(acknowledged_workspace, false) = $2
ORDER BY created_at DESC
LIMIT $3 OFFSET $4",
workspace_id,
params.acknowledged,
page_size,
offset
)
.fetch_all(&db)
.await?
}
} else {
if params.acknowledged.is_none() {
sqlx::query_as!(
CriticalAlert,
"SELECT id, alert_type, message, created_at, COALESCE(acknowledged, false) AS acknowledged, workspace_id
FROM alerts
ORDER BY created_at DESC
LIMIT $1 OFFSET $2",
page_size,
offset
)
.fetch_all(&db)
.await?
} else {
sqlx::query_as!(
CriticalAlert,
"SELECT id, alert_type, message, created_at, COALESCE(acknowledged, false) AS acknowledged, workspace_id
FROM alerts
WHERE COALESCE(acknowledged, false) = $1
ORDER BY created_at DESC
LIMIT $2 OFFSET $3",
params.acknowledged,
page_size,
offset
)
.fetch_all(&db)
.await?
}
};
let total_rows = total_rows.unwrap_or(0);
let total_pages = ((total_rows as f64) / (page_size as f64)).ceil() as i64;
Ok(Json(serde_json::json!({
"alerts": alerts,
"total_rows": total_rows,
"total_pages": total_pages
})))
}
pub async fn acknowledge_critical_alert(
db: DB,
workspace_id: Option<String>,
id: i32,
) -> error::Result<String> {
sqlx::query!(
"UPDATE alerts
SET
acknowledged = true,
acknowledged_workspace = CASE
WHEN $3 THEN
CASE
WHEN $2::text IS NOT NULL AND workspace_id = $2 THEN true
ELSE acknowledged_workspace
END
ELSE true
END
WHERE id = $1",
id,
workspace_id,
*CLOUD_HOSTED
)
.execute(&db)
.await?;
tracing::info!(
"Acknowledged critical alert with id: {}{}",
id,
workspace_id.map_or_else(|| "".to_string(), |w| format!(" for workspace_id: {}", w))
);
Ok("Critical alert acknowledged".to_string())
}
pub async fn acknowledge_all_critical_alerts(
db: DB,
workspace_id: Option<String>,
) -> error::Result<String> {
sqlx::query!(
"UPDATE alerts
SET
acknowledged = true,
acknowledged_workspace = CASE
WHEN $2 THEN
CASE
WHEN $1::text IS NOT NULL THEN true
ELSE acknowledged_workspace
END
ELSE true
END
WHERE ($1::text IS NOT NULL AND workspace_id = $1)
OR ($1::text IS NULL)",
workspace_id,
*CLOUD_HOSTED
)
.execute(&db)
.await?;
tracing::info!(
"Acknowledged all unacknowledged critical alerts{}",
workspace_id.map_or_else(|| "".to_string(), |w| format!(" for workspace_id: {}", w))
);
Ok("All unacknowledged critical alerts acknowledged".to_string())
}

View File

@@ -0,0 +1,35 @@
[package]
name = "windmill-api-agent-workers"
version.workspace = true
authors.workspace = true
edition.workspace = true
[lib]
name = "windmill_api_agent_workers"
path = "src/lib.rs"
[features]
default = []
enterprise = ["windmill-common/enterprise", "windmill-queue/enterprise"]
private = ["windmill-common/private", "windmill-queue/private"]
python = ["dep:windmill-parser-py-imports"]
benchmark = []
[dependencies]
windmill-api-auth.workspace = true
windmill-common = { workspace = true, default-features = false }
windmill-queue.workspace = true
windmill-worker.workspace = true
axum.workspace = true
lazy_static.workspace = true
chrono.workspace = true
http.workspace = true
hyper.workspace = true
quick_cache.workspace = true
serde.workspace = true
serde_json.workspace = true
sqlx.workspace = true
tokio.workspace = true
tracing.workspace = true
uuid.workspace = true
windmill-parser-py-imports = { workspace = true, optional = true }

View File

@@ -1,6 +1,8 @@
#[cfg(feature = "private")]
mod ee;
#[cfg(feature = "private")]
#[allow(unused)]
pub use crate::agent_workers_ee::*;
pub use ee::*;
/*
* Author: Ruben Fiszel
@@ -11,7 +13,7 @@ pub use crate::agent_workers_ee::*;
*/
#[cfg(not(feature = "private"))]
use crate::db::DB;
use windmill_common::DB;
#[cfg(not(feature = "private"))]
use axum::Router;

View File

@@ -0,0 +1,18 @@
[package]
name = "windmill-api-assets"
version.workspace = true
authors.workspace = true
edition.workspace = true
[lib]
name = "windmill_api_assets"
path = "src/lib.rs"
[dependencies]
windmill-api-auth.workspace = true
windmill-common = { workspace = true, default-features = false }
axum.workspace = true
chrono.workspace = true
serde.workspace = true
serde_json.workspace = true
sqlx.workspace = true

View File

@@ -12,7 +12,7 @@ use windmill_common::{
error::JsonResult,
};
use crate::db::ApiAuthed;
use windmill_api_auth::ApiAuthed;
pub fn workspaced_service() -> Router {
Router::new()

View File

@@ -447,8 +447,9 @@ pub async fn fetch_api_authed_from_permissioned_as(
}
_ => {
tracing::debug!("API authed cache miss for user {}", email);
let authed =
fetch_authed_from_permissioned_as(permissioned_as, email.clone(), w_id, db).await?;
fetch_authed_from_permissioned_as(&permissioned_as, &email, w_id, db).await?;
let api_authed = ApiAuthed {
username: authed.username,

View File

@@ -0,0 +1,29 @@
[package]
name = "windmill-api-configs"
version.workspace = true
authors.workspace = true
edition.workspace = true
[lib]
name = "windmill_api_configs"
path = "src/lib.rs"
[features]
default = []
enterprise = ["dep:windmill-autoscaling"]
private = []
python = []
inline_preview = ["dep:windmill-worker", "dep:itertools"]
[dependencies]
windmill-api-auth.workspace = true
windmill-common = { workspace = true, default-features = false }
windmill-audit.workspace = true
windmill-autoscaling = { workspace = true, optional = true }
windmill-worker = { workspace = true, optional = true }
axum.workspace = true
chrono.workspace = true
itertools = { workspace = true, optional = true }
serde.workspace = true
serde_json.workspace = true
sqlx.workspace = true

View File

@@ -23,7 +23,7 @@ use windmill_common::{
DB,
};
use crate::{db::ApiAuthed, utils::require_devops_role};
use windmill_api_auth::{ApiAuthed, require_devops_role};
pub fn global_service() -> Router {
Router::new()

View File

@@ -0,0 +1,28 @@
[package]
name = "windmill-api-debug"
version.workspace = true
authors.workspace = true
edition.workspace = true
[lib]
name = "windmill_api_debug"
path = "src/lib.rs"
[dependencies]
windmill-api-auth.workspace = true
windmill-common = { workspace = true, default-features = false }
windmill-audit.workspace = true
axum.workspace = true
base64.workspace = true
chrono.workspace = true
ed25519-dalek.workspace = true
hex.workspace = true
lazy_static.workspace = true
rand.workspace = true
serde.workspace = true
serde_json.workspace = true
sha2.workspace = true
sqlx.workspace = true
tokio.workspace = true
tracing.workspace = true
uuid.workspace = true

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