Compare commits

...
17 Commits
Author SHA1 Message Date
Ruben Fiszelandrubenfiszel 81ab777cbb chore(main): release 1.673.0 (#8660)
* chore(main): release 1.673.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2026-04-02 19:41:12 -04:00
hugocasa 61a867f086 Revert "feat: restore bun for dedicated workers, fix dispatch & serialization, cross-workspace deps (#8645)" (#8687)
This reverts commit 619ebb65ce.
2026-04-02 23:09:38 +00:00
Diego Imbert 8581a3300d fix: Run typed pg queries in a single protocol conversation (#8679)
* Run typed pg queries in one protocol conversation

* Update pg queries for db manager to use explicit type syntax

* Unused import
2026-04-02 22:00:38 +00:00
Ruben FiszelandClaude Opus 4.6 ff5fa9f64f fix: poll for preview results to avoid undici headers timeout (#8682)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 20:55:46 +00:00
Ruben FiszelandClaude Opus 4.6 55e8a5cff1 fix: add HMAC signature verification to Slack interactive callback endpoint (#8611)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 20:55:23 +00:00
1a39bd538d add opt-in SMTP click tracking disable for email links (#8665)
* feat: add opt-in SMTP click tracking disable for email links

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: update ee-repo-ref.txt for email clicktracking branch

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: update ee-repo-ref.txt after simplification

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: exclude trailing commas from URL regex in clicktracking

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: update ee-repo-ref to 57dd88faa3b0b354f813385cf3f6a34eca54a4a1

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

Previous ee-repo-ref: 5cf901db7fb0ea169b09564372e444f28e23ac3a

New ee-repo-ref: 57dd88faa3b0b354f813385cf3f6a34eca54a4a1

Automated by sync-ee-ref workflow.

* chore: update ee-repo-ref.txt to include dedicated worker fixes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2026-04-02 20:44:08 +00:00
Ruben FiszelandClaude Opus 4.6 1049c1026b fix Windows compilation errors in MemoryLimitedChild (#8684)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 20:33:23 +00:00
hugocasaandClaude Opus 4.6 619ebb65ce feat: restore bun for dedicated workers, fix dispatch & serialization, cross-workspace deps (#8645)
* feat: restore bun as default runtime for dedicated workers

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: add context comment for bun dedicated worker nodejs migration

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: dedicated worker dispatch for flows + add E2E tests

- Add workspace_id prefix to dedicated worker map lookup keys
- Update ee-repo-ref for dedicated worker path handling fix
- Add spawn_test_worker_dedicated/in_test_worker_dedicated test helpers
- Add 6 E2E tests for dedicated workers:
  - test_dedicated_flow_rawscript (regression for "Script not found" bug)
  - test_dedicated_flow_workspace_script
  - test_dedicated_flow_multiple_steps
  - test_dedicated_standalone_script
  - test_dedicated_runner_group
  - test_dedicated_flow_runners
- Add dedicated_flows.sql fixture with scripts, flows, and worker config

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: always run dependency job for dedicated worker scripts

When a script with dedicated_worker=true is deployed with a pre-computed
lock (e.g. via wmill sync push), no dependency job was created, so the
dedicated worker never detected the update and kept running the old version.

Now dedicated worker scripts always generate a dependency job regardless
of whether a lock is provided. The dependency job runs on the dedicated
worker and triggers a restart so it picks up the new script version.

Fixes #8638

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use serial_test for dedicated worker tests to avoid WORKER_CONFIG races

Dedicated worker tests need non-default worker tags in the global
WORKER_CONFIG. When run in parallel (CI uses --test-threads=10),
multiple tests clobber each other's config. Use #[serial] to ensure
dedicated worker tests run sequentially.

Also load worker config from DB via load_worker_config() instead of
manually setting WORKER_CONFIG fields, ensuring consistency with the
monitor's reload path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: nodejs dedicated worker script_path shadowing + add multi-language E2E tests

Fix script_path shadowing in bun_executor nodejs branch where the wrapper
file path was passed to handle_dedicated_process instead of the logical
path, causing "Script not found" for all //nodejs dedicated workers.

Add E2E tests for dedicated flows in all supported languages:
- test_dedicated_flow_deno
- test_dedicated_flow_python
- test_dedicated_flow_bunnative (V8 PrewarmedIsolate path)
- test_dedicated_flow_bun_nodejs (//nodejs annotation)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: simplify dedicated worker dispatch + add serialization and E2E tests

- Unified lookup: always use {workspace}:{runnable_path} for dedicated
  worker dispatch, replacing the flow_step_id iteration approach
- Added serialization_semaphore parameter to executor start_worker fns
- Added E2E tests: cross-workspace isolation, conflicting flow step IDs,
  preprocessor on dedicated worker
- Added workspace field to RunJob for cross-workspace test support

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: cross-workspace workspace dependencies on workers page

Add two new instance-level endpoints to the configs router:
- GET /configs/list_all_workspace_dependencies
- GET /configs/list_all_dedicated_with_deps

Both require devops role and return data across all workspaces,
enabling the workers page to show a consistent view of which
workspace dependencies exist regardless of which workspace the
user is browsing.

Update DedicatedWorkersSelector to use the new cross-workspace
endpoints with fallback to per-workspace calls for non-devops users.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: update ee-repo-ref to include dedicated worker lookup simplification

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: use branch name for ee-repo-ref (CI can't fetch by SHA from non-default branch)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Update ee-repo-ref.txt with new reference

* sqlx

* fix: revert serialization semaphore, multi-workspace picker, dep conflict warnings

- Remove serialization_semaphore from executor start_worker signatures
- Remove serialization test and fixtures
- Fix DedicatedWorkersSelector to preserve tags from other workspaces
  when toggling in the picker
- Track workspace deps per-workspace for conflict detection
- Show warning when dep exists in another workspace but not the script's
- Group runner groups per-workspace to prevent cross-workspace merging
- Add workspace to dep badge link URL

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: update ee-repo-ref

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: simplify exec protocol — execd: for single-script, exec: for runner groups

Add execd:/execd_preprocess: commands to bun/deno/python wrappers for
single-script dedicated workers (no path needed). Runner groups keep
exec:/exec_preprocess: with path for multi-script disambiguation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: add unit tests for execd:/exec: wrapper protocol

Verify generate_multi_script_wrapper produces both execd: (single-script)
and exec: (runner group) protocol handlers, including preprocessor variants.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Update commit reference in ee-repo-ref.txt

* fix: remove beta badge from squash loop, keep tooltip

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: update protocol tests to use execd: for single-script wrappers

Deno and bun single-script protocol tests now send execd:{args} instead
of exec:{path}:{args}, matching the updated wrapper protocol. Multi-script
(runner group) tests continue to use exec:{path}:{args}.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: remove unused TEST_SCRIPT_PATH in deno protocol tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 19:37:02 +00:00
Ruben FiszelandClaude Opus 4.6 d2d6810db9 feat: add LIMIT_WINDOWS_TO_1CU env var for Windows worker memory limits (#8681)
* feat: add LIMIT_WINDOWS_TO_1CU env var for Windows worker memory limits

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address CI review — stricter env var parsing and SAFETY comment

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 19:31:07 +00:00
Ruben FiszelandClaude Opus 4.6 39af1b75af fix: skip generate-metadata confirmation prompt in non-interactive CI (#8678)
* fix: generate-metadata non-interactive CI and misleading log path

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: add hash consistency tests for workspace deps staleness checks

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 19:28:39 +00:00
Ruben FiszelandClaude Opus 4.6 d569e9e29c fix: resolve race condition where flow sync push reverts to stale version (#8673)
* fix: resolve race condition where flow sync push reverts to stale version

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: add sqlx offline cache for new queries

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add version guard before writing to prevent TOCTOU race

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 19:17:37 +00:00
Ruben FiszelandClaude Opus 4.6 381011a4a8 fix: pass selected language to AI agent when generating flow scripts (#8680)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 19:06:22 +00:00
hugocasaandClaude Opus 4.6 f0437eba19 feat: add endpoint to restart workers in a worker group (#8659)
* feat: add endpoint to restart workers in a worker group

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: regenerate sqlx query cache

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add missing modules field to RawCode in tests and regenerate sqlx cache

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* update sqlx

* fix: use require_devops_role for restart worker group endpoint

Matches the permission level of the clean cache endpoint (update_config),
allowing both superadmin and devops role users.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address review feedback for restart worker group

- Fix OpenAPI description to say "devops role" instead of "superadmin"
- Add dispatch('reload') after restart to refresh worker list

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: only dispatch reload on successful restart

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 17:36:48 +00:00
Ruben FiszelandClaude Opus 4.6 d8edaad99c improve bun bundle error message for syntax errors (#8677)
* fix: improve bun bundle error message for syntax errors like unclosed brackets

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* revert: remove error hint from node_builder.ts wrapper catch blocks

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 16:19:46 +00:00
Ruben FiszelandClaude Opus 4.6 7fd0bf974d fix: respect disabled fields in JSON input mode (#8663)
* fix: respect disabled fields in JSON input mode

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: guard against undefined default in disabled field enforcement

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: show toast when disabled fields are reset to defaults on run

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 14:13:12 +00:00
6a5cfbc159 feat: add Entra ID (Azure Workload Identity) database auth (#8526)
* feat: add Entra ID (Azure Workload Identity) support for database auth

Add support for Azure Workload Identity to authenticate to Azure Database
for PostgreSQL using short-lived Entra ID tokens. Mirrors the existing
AWS IAM RDS auth pattern.

- Extract shared DatabaseParams to db_params.rs for reuse across providers
- Add DatabaseUrl::EntraId variant with token refresh
- Detect "entraid" magic password in DATABASE_URL
- Unified background refresh task for both IAM RDS and Entra ID
- Support sovereign clouds via AZURE_AUTHORITY_HOST env var

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: restore needs_refresh() check in background token refresh task

The unified refresh task was missing the needs_refresh() gate, causing
it to refresh tokens every 10 seconds instead of only when near expiry.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: update ee-repo-ref.txt for Entra ID branch

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: move entraid env var reads inside cfg(private) block

Fixes unused variable warnings in OSS and EE-without-private builds
where -D warnings is enabled.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: update ee-repo-ref to 0e001bab643e449b3310b0692dd3598ee0902ecc

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

Previous ee-repo-ref: 44199013ed0c96680672e718f35124aa34a5d010

New ee-repo-ref: 0e001bab643e449b3310b0692dd3598ee0902ecc

Automated by sync-ee-ref workflow.

* refactor: add needs_refresh() and refresh_if_needed() to DatabaseUrl

Simplify duplicated refresh logic per Claude review suggestion.
Background task and get_database_url() now use shared methods
instead of matching on each variant individually.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2026-04-02 16:00:33 +02:00
Ruben FiszelandClaude Opus 4.5 8c3c97f7a6 fix: sanitize MCP tool schemas for JSON Schema draft 2020-12 compliance (#8666)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-02 13:53:16 +00:00
76 changed files with 1458 additions and 353 deletions
+27
View File
@@ -1,5 +1,32 @@
# Changelog
## [1.673.0](https://github.com/windmill-labs/windmill/compare/v1.672.0...v1.673.0) (2026-04-02)
### Features
* add endpoint to restart workers in a worker group ([#8659](https://github.com/windmill-labs/windmill/issues/8659)) ([f0437eb](https://github.com/windmill-labs/windmill/commit/f0437eba1925a9aa4c430008027d637a0c89ee39))
* add Entra ID (Azure Workload Identity) database auth ([#8526](https://github.com/windmill-labs/windmill/issues/8526)) ([6a5cfbc](https://github.com/windmill-labs/windmill/commit/6a5cfbc159a0ad7925fd7ce5eefc8eaa21bbb70b))
* add LIMIT_WINDOWS_TO_1CU env var for Windows worker memory limits ([#8681](https://github.com/windmill-labs/windmill/issues/8681)) ([d2d6810](https://github.com/windmill-labs/windmill/commit/d2d6810db954114f3333853bd3476cb8fc735f92))
* restore bun for dedicated workers, fix dispatch & serialization, cross-workspace deps ([#8645](https://github.com/windmill-labs/windmill/issues/8645)) ([619ebb6](https://github.com/windmill-labs/windmill/commit/619ebb65ce8dce8264add31c3147919802a8286a))
### Bug Fixes
* add HMAC signature verification to Slack interactive callback endpoint ([#8611](https://github.com/windmill-labs/windmill/issues/8611)) ([55e8a5c](https://github.com/windmill-labs/windmill/commit/55e8a5cff1f185b1dbd332d37b877972efa1ed7d))
* correct raw app flow inputs ([#8667](https://github.com/windmill-labs/windmill/issues/8667)) ([28c0730](https://github.com/windmill-labs/windmill/commit/28c073056c65d4ed1600e39679497e5af964347f))
* pass selected language to AI agent when generating flow scripts ([#8680](https://github.com/windmill-labs/windmill/issues/8680)) ([381011a](https://github.com/windmill-labs/windmill/commit/381011a4a8e48454e9c146c64db502293e646b99))
* poll for preview results to avoid undici headers timeout ([#8682](https://github.com/windmill-labs/windmill/issues/8682)) ([ff5fa9f](https://github.com/windmill-labs/windmill/commit/ff5fa9f64fe4aaf33e06b20f02373894b5df0f95))
* pre-fix trigger edited_by for superadmins not in workspace ([#8669](https://github.com/windmill-labs/windmill/issues/8669)) ([350ffdc](https://github.com/windmill-labs/windmill/commit/350ffdce297ba5b84f9dd247eede6da0c6b0956c))
* resolve race condition where flow sync push reverts to stale version ([#8673](https://github.com/windmill-labs/windmill/issues/8673)) ([d569e9e](https://github.com/windmill-labs/windmill/commit/d569e9e29c588243a90b1cd25f866efb0d178640))
* respect disabled fields in JSON input mode ([#8663](https://github.com/windmill-labs/windmill/issues/8663)) ([7fd0bf9](https://github.com/windmill-labs/windmill/commit/7fd0bf974d2ba2644bb01dd5e9ddc84749e166f5))
* Run typed pg queries in a single protocol conversation ([#8679](https://github.com/windmill-labs/windmill/issues/8679)) ([8581a33](https://github.com/windmill-labs/windmill/commit/8581a3300d056040b7e3ab77d629c74f034c9c97))
* sanitize MCP tool schemas for JSON Schema draft 2020-12 compliance ([#8666](https://github.com/windmill-labs/windmill/issues/8666)) ([8c3c97f](https://github.com/windmill-labs/windmill/commit/8c3c97f7a670d47019cc666219f8187f48499672))
* skip generate-metadata confirmation prompt in non-interactive CI ([#8678](https://github.com/windmill-labs/windmill/issues/8678)) ([39af1b7](https://github.com/windmill-labs/windmill/commit/39af1b75afc8458f85dec4fe51dfaed3d0cb000d))
* strip f/ prefix from folder paths when deploying from workspace forks ([#8662](https://github.com/windmill-labs/windmill/issues/8662)) ([7ab0ea5](https://github.com/windmill-labs/windmill/commit/7ab0ea581d349fbfdb56d22cf9903a90efa045bb))
* support branch-specific folder.meta.yaml in missing-meta check ([#8661](https://github.com/windmill-labs/windmill/issues/8661)) ([c87a6a0](https://github.com/windmill-labs/windmill/commit/c87a6a0f2c1346bf5e21f128d32d89bdca039243))
* validate rd redirect on login with same rules as logout ([#8655](https://github.com/windmill-labs/windmill/issues/8655)) ([bcce627](https://github.com/windmill-labs/windmill/commit/bcce62738791a4e9b9f4dbc64731eef163230172))
## [1.672.0](https://github.com/windmill-labs/windmill/compare/v1.671.0...v1.672.0) (2026-04-01)
@@ -34,7 +34,8 @@
"nu",
"java",
"duckdb",
"ruby"
"ruby",
"rlang"
]
}
}
@@ -76,7 +76,8 @@
"nu",
"java",
"duckdb",
"ruby"
"ruby",
"rlang"
]
}
}
@@ -33,7 +33,8 @@
"nu",
"java",
"duckdb",
"ruby"
"ruby",
"rlang"
]
}
}
@@ -43,7 +43,8 @@
"nu",
"java",
"duckdb",
"ruby"
"ruby",
"rlang"
]
}
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE flow SET versions = array_append(versions, $1) WHERE path = $2 AND workspace_id = $3 AND (versions[array_upper(versions, 1)] IS DISTINCT FROM $1)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "32f26d69b15a74fc96dd585f9eb3c998447ca8b97894bb1a2b238f984c1dc588"
}
@@ -33,7 +33,8 @@
"nu",
"java",
"duckdb",
"ruby"
"ruby",
"rlang"
]
}
}
@@ -44,7 +44,8 @@
"nu",
"java",
"duckdb",
"ruby"
"ruby",
"rlang"
]
}
}
@@ -70,7 +70,8 @@
"nu",
"java",
"duckdb",
"ruby"
"ruby",
"rlang"
]
}
}
@@ -34,7 +34,8 @@
"nu",
"java",
"duckdb",
"ruby"
"ruby",
"rlang"
]
}
}
@@ -40,7 +40,8 @@
"nu",
"java",
"duckdb",
"ruby"
"ruby",
"rlang"
]
}
}
@@ -15,7 +15,7 @@
]
},
"nullable": [
null
true
]
},
"hash": "5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55"
@@ -148,7 +148,8 @@
"nu",
"java",
"duckdb",
"ruby"
"ruby",
"rlang"
]
}
}
@@ -43,7 +43,8 @@
"nu",
"java",
"duckdb",
"ruby"
"ruby",
"rlang"
]
}
}
@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO notify_event (channel, payload) VALUES ('restart_worker_group', $1)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text"
]
},
"nullable": []
},
"hash": "78f7c4efd7be2aad03f0ec67ab882e1f9aaa7141ab75046a71ac789ab659bdc6"
}
@@ -76,7 +76,8 @@
"nu",
"java",
"duckdb",
"ruby"
"ruby",
"rlang"
]
}
}
@@ -41,7 +41,8 @@
"nu",
"java",
"duckdb",
"ruby"
"ruby",
"rlang"
]
}
}
@@ -153,7 +153,8 @@
"nu",
"java",
"duckdb",
"ruby"
"ruby",
"rlang"
]
}
}
@@ -118,7 +118,8 @@
"nu",
"java",
"duckdb",
"ruby"
"ruby",
"rlang"
]
}
}
@@ -83,7 +83,8 @@
"nu",
"java",
"duckdb",
"ruby"
"ruby",
"rlang"
]
}
}
@@ -133,7 +133,8 @@
"nu",
"java",
"duckdb",
"ruby"
"ruby",
"rlang"
]
}
}
@@ -38,7 +38,8 @@
"nu",
"java",
"duckdb",
"ruby"
"ruby",
"rlang"
]
}
}
@@ -153,7 +153,8 @@
"nu",
"java",
"duckdb",
"ruby"
"ruby",
"rlang"
]
}
}
@@ -123,7 +123,8 @@
"nu",
"java",
"duckdb",
"ruby"
"ruby",
"rlang"
]
}
}
@@ -43,7 +43,8 @@
"nu",
"java",
"duckdb",
"ruby"
"ruby",
"rlang"
]
}
}
@@ -40,7 +40,8 @@
"nu",
"java",
"duckdb",
"ruby"
"ruby",
"rlang"
]
}
}
@@ -77,7 +77,8 @@
"nu",
"java",
"duckdb",
"ruby"
"ruby",
"rlang"
]
}
}
@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT value AS \"value!: Json<Box<RawValue>>\" FROM flow WHERE path = $1 AND workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "value!: Json<Box<RawValue>>",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false
]
},
"hash": "c569535d4aded82ffef5e5066e24948c8fcdb8d36913775449b982782c8583e9"
}
@@ -68,7 +68,8 @@
"nu",
"java",
"duckdb",
"ruby"
"ruby",
"rlang"
]
}
}
@@ -77,7 +77,8 @@
"nu",
"java",
"duckdb",
"ruby"
"ruby",
"rlang"
]
}
}
@@ -133,7 +133,8 @@
"nu",
"java",
"duckdb",
"ruby"
"ruby",
"rlang"
]
}
}
@@ -40,7 +40,8 @@
"nu",
"java",
"duckdb",
"ruby"
"ruby",
"rlang"
]
}
}
@@ -38,7 +38,8 @@
"nu",
"java",
"duckdb",
"ruby"
"ruby",
"rlang"
]
}
}
@@ -84,7 +85,8 @@
"nu",
"java",
"duckdb",
"ruby"
"ruby",
"rlang"
]
}
}
@@ -33,7 +33,8 @@
"nu",
"java",
"duckdb",
"ruby"
"ruby",
"rlang"
]
}
}
+170 -126
View File
@@ -2108,7 +2108,7 @@ dependencies = [
"rayon",
"safetensors",
"thiserror 2.0.18",
"yoke 0.8.1",
"yoke 0.8.2",
"zip",
]
@@ -2373,9 +2373,9 @@ dependencies = [
[[package]]
name = "cmov"
version = "0.5.2"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de0758edba32d61d1fd9f4d69491b47604b91ee2f7e6b33de7e54ca4ebe55dc3"
checksum = "3f88a43d011fc4a6876cb7344703e297c71dda42494fee094d5f7c76bf13f746"
[[package]]
name = "codespan-reporting"
@@ -2805,9 +2805,9 @@ dependencies = [
[[package]]
name = "ctutils"
version = "0.4.0"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1005a6d4446f5120ef475ad3d2af2b30c49c2c9c6904258e3bb30219bebed5e4"
checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e"
dependencies = [
"cmov",
]
@@ -7224,22 +7224,23 @@ dependencies = [
[[package]]
name = "icu_collections"
version = "2.1.1"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43"
checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
dependencies = [
"displaydoc",
"potential_utf",
"yoke 0.8.1",
"utf8_iter",
"yoke 0.8.2",
"zerofrom",
"zerovec",
]
[[package]]
name = "icu_locale_core"
version = "2.1.1"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6"
checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
dependencies = [
"displaydoc",
"litemap",
@@ -7250,9 +7251,9 @@ dependencies = [
[[package]]
name = "icu_normalizer"
version = "2.1.1"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599"
checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
dependencies = [
"icu_collections",
"icu_normalizer_data",
@@ -7264,15 +7265,15 @@ dependencies = [
[[package]]
name = "icu_normalizer_data"
version = "2.1.1"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a"
checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
[[package]]
name = "icu_properties"
version = "2.1.2"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec"
checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
dependencies = [
"icu_collections",
"icu_locale_core",
@@ -7284,20 +7285,20 @@ dependencies = [
[[package]]
name = "icu_properties_data"
version = "2.1.2"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af"
checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
[[package]]
name = "icu_provider"
version = "2.1.1"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614"
checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
dependencies = [
"displaydoc",
"icu_locale_core",
"writeable",
"yoke 0.8.1",
"yoke 0.8.2",
"zerofrom",
"zerotrie",
"zerovec",
@@ -8046,9 +8047,9 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.183"
version = "0.2.184"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af"
[[package]]
name = "libffi"
@@ -8204,9 +8205,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
name = "litemap"
version = "0.8.1"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
[[package]]
name = "litrs"
@@ -10427,9 +10428,9 @@ dependencies = [
[[package]]
name = "potential_utf"
version = "0.1.4"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77"
checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
dependencies = [
"zerovec",
]
@@ -12117,6 +12118,15 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ece8e78b2f38ec51c51f5d475df0a7187ba5111b2a28bdc761ee05b075d40a71"
[[package]]
name = "scc"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46e6f046b7fef48e2660c57ed794263155d713de679057f2d0c169bfc6e756cc"
dependencies = [
"sdd",
]
[[package]]
name = "schannel"
version = "0.1.29"
@@ -12222,6 +12232,12 @@ dependencies = [
"untrusted 0.9.0",
]
[[package]]
name = "sdd"
version = "3.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca"
[[package]]
name = "seahash"
version = "4.1.0"
@@ -12558,6 +12574,32 @@ dependencies = [
"serde",
]
[[package]]
name = "serial_test"
version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "911bd979bf1070a3f3aa7b691a3b3e9968f339ceeec89e08c280a8a22207a32f"
dependencies = [
"futures-executor",
"futures-util",
"log",
"once_cell",
"parking_lot",
"scc",
"serial_test_derive",
]
[[package]]
name = "serial_test_derive"
version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a7d91949b85b0d2fb687445e448b40d322b6b3e4af6b44a29b21d9a5f33e6d9"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "sha1"
version = "0.10.6"
@@ -14235,9 +14277,9 @@ dependencies = [
[[package]]
name = "tinystr"
version = "0.8.2"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869"
checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
dependencies = [
"displaydoc",
"zerovec",
@@ -14625,9 +14667,9 @@ dependencies = [
[[package]]
name = "toml_parser"
version = "1.1.1+spec-1.1.0"
version = "1.1.2+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39ca317ebc49f06bd748bfba29533eac9485569dc9bf80b849024b025e814fb9"
checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
dependencies = [
"winnow 1.0.1",
]
@@ -15941,7 +15983,7 @@ dependencies = [
[[package]]
name = "windmill"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"anyhow",
"async-nats",
@@ -15970,6 +16012,7 @@ dependencies = [
"serde_derive",
"serde_json",
"serde_yml",
"serial_test",
"sha1",
"sha2 0.10.9",
"sql-builder",
@@ -16019,7 +16062,7 @@ dependencies = [
[[package]]
name = "windmill-alerting"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"axum 0.8.4",
"chrono",
@@ -16032,7 +16075,7 @@ dependencies = [
[[package]]
name = "windmill-api"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"anyhow",
"argon2",
@@ -16173,7 +16216,7 @@ dependencies = [
[[package]]
name = "windmill-api-agent-workers"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"axum 0.8.4",
"chrono",
@@ -16196,7 +16239,7 @@ dependencies = [
[[package]]
name = "windmill-api-assets"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"axum 0.8.4",
"chrono",
@@ -16209,7 +16252,7 @@ dependencies = [
[[package]]
name = "windmill-api-auth"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"anyhow",
"axum 0.8.4",
@@ -16235,7 +16278,7 @@ dependencies = [
[[package]]
name = "windmill-api-client"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"reqwest 0.12.28",
"serde",
@@ -16245,7 +16288,7 @@ dependencies = [
[[package]]
name = "windmill-api-configs"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"axum 0.8.4",
"chrono",
@@ -16262,7 +16305,7 @@ dependencies = [
[[package]]
name = "windmill-api-debug"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"axum 0.8.4",
"base64 0.22.1",
@@ -16285,7 +16328,7 @@ dependencies = [
[[package]]
name = "windmill-api-embeddings"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"anyhow",
"axum 0.8.4",
@@ -16308,7 +16351,7 @@ dependencies = [
[[package]]
name = "windmill-api-flow-conversations"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"axum 0.8.4",
"chrono",
@@ -16324,7 +16367,7 @@ dependencies = [
[[package]]
name = "windmill-api-flows"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"axum 0.8.4",
"chrono",
@@ -16344,7 +16387,7 @@ dependencies = [
[[package]]
name = "windmill-api-groups"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"axum 0.8.4",
"chrono",
@@ -16364,7 +16407,7 @@ dependencies = [
[[package]]
name = "windmill-api-inputs"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"axum 0.8.4",
"chrono",
@@ -16378,7 +16421,7 @@ dependencies = [
[[package]]
name = "windmill-api-integration-tests"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"anyhow",
"async-nats",
@@ -16409,7 +16452,7 @@ dependencies = [
[[package]]
name = "windmill-api-jobs"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"anyhow",
"axum 0.8.4",
@@ -16434,7 +16477,7 @@ dependencies = [
[[package]]
name = "windmill-api-npm-proxy"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"axum 0.8.4",
"flate2",
@@ -16452,7 +16495,7 @@ dependencies = [
[[package]]
name = "windmill-api-openapi"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"anyhow",
"axum 0.8.4",
@@ -16474,7 +16517,7 @@ dependencies = [
[[package]]
name = "windmill-api-schedule"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"axum 0.8.4",
"chrono",
@@ -16494,7 +16537,7 @@ dependencies = [
[[package]]
name = "windmill-api-scripts"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"axum 0.8.4",
"chrono",
@@ -16524,7 +16567,7 @@ dependencies = [
[[package]]
name = "windmill-api-settings"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"anyhow",
"axum 0.8.4",
@@ -16551,7 +16594,7 @@ dependencies = [
[[package]]
name = "windmill-api-sse"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"lazy_static",
"serde",
@@ -16563,7 +16606,7 @@ dependencies = [
[[package]]
name = "windmill-api-users"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"argon2",
"axum 0.8.4",
@@ -16588,7 +16631,7 @@ dependencies = [
[[package]]
name = "windmill-api-workers"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"axum 0.8.4",
"chrono",
@@ -16602,7 +16645,7 @@ dependencies = [
[[package]]
name = "windmill-api-workspaces"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"axum 0.8.4",
"chrono",
@@ -16634,7 +16677,7 @@ dependencies = [
[[package]]
name = "windmill-audit"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"chrono",
"lazy_static",
@@ -16648,7 +16691,7 @@ dependencies = [
[[package]]
name = "windmill-autoscaling"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"anyhow",
"axum 0.8.4",
@@ -16667,7 +16710,7 @@ dependencies = [
[[package]]
name = "windmill-common"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"aes-gcm",
"aho-corasick",
@@ -16769,7 +16812,7 @@ dependencies = [
[[package]]
name = "windmill-dep-map"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"chrono",
"itertools 0.14.0",
@@ -16788,7 +16831,7 @@ dependencies = [
[[package]]
name = "windmill-git-sync"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"regex",
"serde",
@@ -16803,7 +16846,7 @@ dependencies = [
[[package]]
name = "windmill-indexer"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"anyhow",
"astral-tokio-tar",
@@ -16827,7 +16870,7 @@ dependencies = [
[[package]]
name = "windmill-jseval"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"anyhow",
"futures",
@@ -16844,7 +16887,7 @@ dependencies = [
[[package]]
name = "windmill-macros"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"itertools 0.14.0",
"lazy_static",
@@ -16860,7 +16903,7 @@ dependencies = [
[[package]]
name = "windmill-mcp"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"anyhow",
"async-trait",
@@ -16881,7 +16924,7 @@ dependencies = [
[[package]]
name = "windmill-native-triggers"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"anyhow",
"async-trait",
@@ -16912,7 +16955,7 @@ dependencies = [
[[package]]
name = "windmill-oauth"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"anyhow",
"async-oauth2",
@@ -16936,7 +16979,7 @@ dependencies = [
[[package]]
name = "windmill-object-store"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"anyhow",
"async-stream",
@@ -16970,7 +17013,7 @@ dependencies = [
[[package]]
name = "windmill-operator"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"anyhow",
"futures",
@@ -16988,7 +17031,7 @@ dependencies = [
[[package]]
name = "windmill-parser"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"convert_case 0.6.0",
"serde",
@@ -16997,7 +17040,7 @@ dependencies = [
[[package]]
name = "windmill-parser-bash"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -17009,7 +17052,7 @@ dependencies = [
[[package]]
name = "windmill-parser-csharp"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"anyhow",
"serde_json",
@@ -17021,7 +17064,7 @@ dependencies = [
[[package]]
name = "windmill-parser-go"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"anyhow",
"gosyn",
@@ -17033,7 +17076,7 @@ dependencies = [
[[package]]
name = "windmill-parser-graphql"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -17045,7 +17088,7 @@ dependencies = [
[[package]]
name = "windmill-parser-java"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"anyhow",
"serde_json",
@@ -17057,7 +17100,7 @@ dependencies = [
[[package]]
name = "windmill-parser-nu"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"anyhow",
"nu-parser",
@@ -17068,7 +17111,7 @@ dependencies = [
[[package]]
name = "windmill-parser-php"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -17079,7 +17122,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -17091,7 +17134,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py-asset"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"anyhow",
"rustpython-ast",
@@ -17102,7 +17145,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py-imports"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -17124,7 +17167,7 @@ dependencies = [
[[package]]
name = "windmill-parser-r"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"anyhow",
"serde_json",
@@ -17136,7 +17179,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ruby"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -17150,7 +17193,7 @@ dependencies = [
[[package]]
name = "windmill-parser-rust"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"anyhow",
"convert_case 0.6.0",
@@ -17167,7 +17210,7 @@ dependencies = [
[[package]]
name = "windmill-parser-sql"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -17180,7 +17223,7 @@ dependencies = [
[[package]]
name = "windmill-parser-sql-asset"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"anyhow",
"serde",
@@ -17192,7 +17235,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -17210,7 +17253,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts-asset"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"anyhow",
"serde-wasm-bindgen",
@@ -17226,7 +17269,7 @@ dependencies = [
[[package]]
name = "windmill-parser-wac"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"anyhow",
"rustpython-ast",
@@ -17242,7 +17285,7 @@ dependencies = [
[[package]]
name = "windmill-parser-yaml"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"anyhow",
"serde",
@@ -17253,7 +17296,7 @@ dependencies = [
[[package]]
name = "windmill-queue"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -17290,7 +17333,7 @@ dependencies = [
[[package]]
name = "windmill-runtime-nativets"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"anyhow",
"const_format",
@@ -17328,7 +17371,7 @@ dependencies = [
[[package]]
name = "windmill-sql-datatype-parser-wasm"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"getrandom 0.3.4",
"wasm-bindgen",
@@ -17339,7 +17382,7 @@ dependencies = [
[[package]]
name = "windmill-store"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -17368,7 +17411,7 @@ dependencies = [
[[package]]
name = "windmill-test-utils"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17392,7 +17435,7 @@ dependencies = [
[[package]]
name = "windmill-trigger"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17425,7 +17468,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-email"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17445,7 +17488,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-gcp"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17479,7 +17522,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-http"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17515,7 +17558,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-kafka"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17538,7 +17581,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-mqtt"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17562,7 +17605,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-nats"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"anyhow",
"async-nats",
@@ -17586,7 +17629,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-postgres"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17621,7 +17664,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-sqs"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17649,7 +17692,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-websocket"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17672,7 +17715,7 @@ dependencies = [
[[package]]
name = "windmill-types"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"anyhow",
"bitflags 2.9.4",
@@ -17691,7 +17734,7 @@ dependencies = [
[[package]]
name = "windmill-worker"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"anyhow",
"async-once-cell",
@@ -17795,13 +17838,14 @@ dependencies = [
"windmill-runtime-nativets",
"windmill-types",
"windmill-worker-volumes",
"windows 0.61.3",
"x509-parser 0.16.0",
"yaml-rust",
]
[[package]]
name = "windmill-worker-volumes"
version = "1.672.0"
version = "1.673.0"
dependencies = [
"bytes",
"futures",
@@ -18501,9 +18545,9 @@ dependencies = [
[[package]]
name = "writeable"
version = "0.6.2"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
[[package]]
name = "wtf8"
@@ -18636,12 +18680,12 @@ dependencies = [
[[package]]
name = "yoke"
version = "0.8.1"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954"
checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca"
dependencies = [
"stable_deref_trait",
"yoke-derive 0.8.1",
"yoke-derive 0.8.2",
"zerofrom",
]
@@ -18659,9 +18703,9 @@ dependencies = [
[[package]]
name = "yoke-derive"
version = "0.8.1"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d"
checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
dependencies = [
"proc-macro2",
"quote",
@@ -18691,18 +18735,18 @@ dependencies = [
[[package]]
name = "zerofrom"
version = "0.1.6"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df"
dependencies = [
"zerofrom-derive",
]
[[package]]
name = "zerofrom-derive"
version = "0.1.6"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
dependencies = [
"proc-macro2",
"quote",
@@ -18732,31 +18776,31 @@ dependencies = [
[[package]]
name = "zerotrie"
version = "0.2.3"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851"
checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"
dependencies = [
"displaydoc",
"yoke 0.8.1",
"yoke 0.8.2",
"zerofrom",
]
[[package]]
name = "zerovec"
version = "0.11.5"
version = "0.11.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002"
checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
dependencies = [
"yoke 0.8.1",
"yoke 0.8.2",
"zerofrom",
"zerovec-derive",
]
[[package]]
name = "zerovec-derive"
version = "0.11.2"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3"
checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
dependencies = [
"proc-macro2",
"quote",
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.672.0"
version = "1.673.0"
authors.workspace = true
edition.workspace = true
@@ -85,7 +85,7 @@ members = [
exclude = ["./windmill-duckdb-ffi-internal", "./parsers/windmill-parser-wasm"]
[workspace.package]
version = "1.672.0"
version = "1.673.0"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"
+1 -1
View File
@@ -1 +1 @@
e08a87450627bef9013498e40ee93a47bedda7ee
57dd88faa3b0b354f813385cf3f6a34eca54a4a1
+41 -22
View File
@@ -41,50 +41,69 @@ pub async fn connect_db(
};
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;
{
let needs_token_refresh = matches!(
database_url,
DatabaseUrl::IamRds(_) | DatabaseUrl::EntraId(_)
);
let label = match &database_url {
DatabaseUrl::IamRds(_) => "IAM RDS",
DatabaseUrl::EntraId(_) => "Entra ID",
DatabaseUrl::Static(_) => "",
};
if needs_token_refresh {
let pool2 = pool.clone();
let database_url2 = database_url.clone();
tokio::spawn(async move {
loop {
tokio::select! {
_ = killpill_rx.recv() => {
break;
}
_ = tokio::time::sleep(std::time::Duration::from_secs(10)) => {
if !database_url2.needs_refresh().await {
continue;
}
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");
tracing::info!("Refreshed {label} URL successfully");
}
Err(e) => {
tracing::error!("Error getting IAM RDS connect options, retrying in 10s: {}", e);
tracing::error!(
"Error getting {label} connect options, retrying in 10s: {e}"
);
continue;
}
}
}
Ok(Err(e)) => {
tracing::error!("Error refreshing IAM RDS URL, trying again in 10s: {}", e);
tracing::error!(
"Error refreshing {label} URL, trying again in 10s: {e}"
);
continue;
}
Err(e) => {
tracing::error!("Timeout after 10s refreshing IAM RDS URL, trying again in 10 seconds: {}", e);
tracing::error!(
"Timeout after 10s refreshing {label} URL, trying again in 10s: {e}"
);
continue;
}
}
}
}
}
}
});
});
}
}
Ok(pool)
+6
View File
@@ -1597,6 +1597,12 @@ async fn process_notify_event(
tracing::debug!("config changed but did not target this server/worker");
}
}
"restart_worker_group" => {
if worker_mode && payload == *WORKER_GROUP {
tracing::info!("Restart requested for worker group '{payload}'");
spawn_graceful_killpill(tx, db, 10, "worker group restart requested").await;
}
}
"notify_webhook_change" => {
tracing::info!(
"Webhook change detected, invalidating webhook cache: {}",
+53
View File
@@ -429,6 +429,59 @@ export function main() {
Ok(())
}
#[sqlx::test(fixtures("base"))]
async fn test_bun_job_syntax_error_unclosed_bracket(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
let port = server.addr.port();
// Reproduces the "Unexpected end of file at main.ts:0" error reported
// when a TS file has a missing closing bracket — Bun's bundler gives no
// useful location info.
let content = r#"
export async function main() {
if (true) {
return "hello";
// missing closing bracket for the function
"#
.to_owned();
let job = JobPayload::Code(RawCode {
hash: None,
content,
path: None,
language: ScriptLang::Bun,
lock: None,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
modules: None,
});
let completed = run_job_in_new_worker_until_complete(&db, false, job, port).await;
assert!(!completed.success);
let result = completed
.result
.as_ref()
.and_then(|v| v.get("error"))
.and_then(|v| v.get("message"))
.and_then(|v| v.as_str())
.unwrap_or("");
assert!(
result.contains("Unexpected end of file"),
"should contain bun parser error, got: {result}"
);
assert!(
result.contains("syntax error"),
"should contain helpful hint about syntax errors, got: {result}"
);
Ok(())
}
// ============================================================================
// Annotation Mode Tests
// ============================================================================
+3
View File
@@ -1107,6 +1107,7 @@ main <- function(msg) {
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
modules: None,
}))
.arg("msg", json!("world"))
.run_until_complete(&db, false, port)
@@ -1144,6 +1145,7 @@ main <- function() {
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
modules: None,
}))
.run_until_complete(&db, false, port)
.await
@@ -1180,6 +1182,7 @@ main <- function() {
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
modules: None,
}))
.run_until_complete(&db, false, port)
.await
+24 -3
View File
@@ -12,9 +12,7 @@ use std::{collections::HashMap, time::Duration};
mod ee;
pub mod ee_oss;
#[cfg(feature = "enterprise")]
use windmill_api_auth::require_devops_role;
use windmill_api_auth::{require_super_admin, ApiAuthed};
use windmill_api_auth::{require_devops_role, require_super_admin, ApiAuthed};
use windmill_common::utils::HTTP_CLIENT_PERMISSIVE as HTTP_CLIENT;
use windmill_common::DB;
@@ -102,6 +100,10 @@ pub fn global_service() -> Router {
.route(
"/sync_cached_resource_types",
post(sync_cached_resource_types),
)
.route(
"/restart_worker_group/{worker_group}",
post(restart_worker_group),
);
// Vault integration routes (EE only - requires both private and enterprise features)
@@ -662,6 +664,25 @@ pub async fn send_stats(Extension(db): Extension<DB>, authed: ApiAuthed) -> Resu
Ok("Sent stats".to_string())
}
async fn restart_worker_group(
Extension(db): Extension<DB>,
authed: ApiAuthed,
Path(worker_group): Path<String>,
) -> error::Result<String> {
require_devops_role(&db, &authed.email).await?;
sqlx::query!(
"INSERT INTO notify_event (channel, payload) VALUES ('restart_worker_group', $1)",
worker_group
)
.execute(&db)
.await?;
Ok(format!(
"Restart signal sent to worker group '{worker_group}'"
))
}
#[derive(serde::Serialize)]
pub struct StatsDownload {
pub signature: String,
+23 -1
View File
@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.672.0
version: 1.673.0
title: Windmill API
contact:
@@ -1370,6 +1370,28 @@ paths:
schema:
type: string
/settings/restart_worker_group/{worker_group}:
post:
summary: restart worker group
description: Send a restart signal to all workers in the specified worker group. Workers will gracefully shut down and are expected to be restarted by their supervisor. Requires devops role.
operationId: restartWorkerGroup
tags:
- setting
parameters:
- name: worker_group
in: path
required: true
schema:
type: string
description: the name of the worker group to restart
responses:
"200":
description: restart signal sent
content:
text/plain:
schema:
type: string
/settings/get_stats:
get:
summary: get telemetry stats with HMAC signature (EE only)
+13 -2
View File
@@ -159,13 +159,24 @@ pub async fn check_nb_of_user(db: &DB) -> error::Result<()> {
#[derive(Clone, Debug)]
#[cfg(not(feature = "private"))]
pub struct SlackVerifier {
_mac: HmacSha256,
mac: HmacSha256,
}
#[cfg(not(feature = "private"))]
impl SlackVerifier {
pub fn new<S: AsRef<[u8]>>(secret: S) -> anyhow::Result<SlackVerifier> {
HmacSha256::new_from_slice(secret.as_ref())
.map(|mac| SlackVerifier { _mac: mac })
.map(|mac| SlackVerifier { mac })
.map_err(|_| anyhow::anyhow!("invalid secret"))
}
pub fn verify(&self, ts: &str, body: &str, exp_sig: &str) -> anyhow::Result<()> {
let basestring = format!("v0:{}:{}", ts, body);
let mut mac = self.mac.clone();
mac.update(basestring.as_bytes());
let sig = format!("v0={}", hex::encode(mac.finalize().into_bytes()));
if sig != exp_sig {
Err(anyhow::anyhow!("signature mismatch"))?;
}
Ok(())
}
}
+33 -2
View File
@@ -1,7 +1,9 @@
use axum::{
extract::{Form, Path, Query},
extract::{Path, Query},
Extension,
};
use bytes::Bytes;
use http::HeaderMap;
use hyper::StatusCode;
use reqwest::Client;
use serde::{Deserialize, Serialize};
@@ -119,12 +121,41 @@ struct PrivateMetadata {
hide_cancel: Option<bool>,
}
#[cfg(feature = "oauth2")]
fn verify_slack_callback_signature(headers: &HeaderMap, body: &str) -> Result<(), Error> {
if let Some(sv) = crate::SLACK_SIGNING_SECRET.as_ref() {
let sig = headers
.get("X-Slack-Signature")
.and_then(|v| v.to_str().ok())
.unwrap_or("");
let ts = headers
.get("X-Slack-Request-Timestamp")
.and_then(|v| v.to_str().ok())
.unwrap_or("");
sv.verify(ts, body, sig)
.map_err(|_| Error::BadRequest("Slack signature verification failed".to_string()))?;
}
Ok(())
}
#[cfg(not(feature = "oauth2"))]
fn verify_slack_callback_signature(_headers: &HeaderMap, _body: &str) -> Result<(), Error> {
Ok(())
}
pub async fn slack_app_callback_handler(
authed: Option<ApiAuthed>,
opt_tokened: OptTokened,
Extension(db): Extension<DB>,
Form(form_data): Form<SlackFormData>,
headers: HeaderMap,
body: Bytes,
) -> Result<StatusCode, Error> {
let body_str = String::from_utf8_lossy(&body);
verify_slack_callback_signature(&headers, &body_str)?;
let form_data: SlackFormData = serde_urlencoded::from_bytes(&body)
.map_err(|e| Error::BadRequest(format!("invalid form data: {}", e)))?;
tracing::debug!("Form data: {:#?}", form_data);
let payload: Payload = serde_json::from_str(&form_data.payload)?;
tracing::debug!("Payload: {:#?}", payload);
+45
View File
@@ -0,0 +1,45 @@
use anyhow::Result;
/// Parsed database connection parameters, shared across DB auth providers (IAM RDS, Entra ID, etc.)
#[derive(Debug, Clone)]
pub struct DatabaseParams {
pub hostname: String,
pub port: u64,
pub username: String,
pub database: String,
}
/// Extract database connection parameters from a PostgreSQL URL
pub fn extract_database_params(database_url: &str) -> Result<DatabaseParams> {
let url = url::Url::parse(database_url)
.map_err(|e| anyhow::anyhow!("Failed to parse database URL: {}", e))?;
let hostname = url
.host_str()
.ok_or_else(|| anyhow::anyhow!("Database URL missing hostname"))?
.to_string();
let port = url.port().unwrap_or(5432) as u64;
let username = if url.username().is_empty() {
return Err(anyhow::anyhow!("Database URL missing username"));
} else {
urlencoding::decode(url.username())?.to_string()
};
let database = url
.path()
.trim_start_matches('/')
.split('/')
.next()
.filter(|s| !s.is_empty())
.ok_or_else(|| anyhow::anyhow!("Database URL missing database name"))?
.to_string();
Ok(DatabaseParams {
hostname,
port,
username,
database: urlencoding::decode(&database)?.to_string(),
})
}
+98 -25
View File
@@ -43,7 +43,10 @@ pub mod cache;
pub mod client;
pub mod db;
#[cfg(all(feature = "enterprise", feature = "private"))]
mod db_entra_ee;
#[cfg(all(feature = "enterprise", feature = "private"))]
mod db_iam_ee;
pub mod db_params;
#[cfg(feature = "private")]
pub mod ee;
pub mod ee_oss;
@@ -633,12 +636,14 @@ impl PgDatabase {
pub enum DatabaseUrl {
#[cfg(all(feature = "enterprise", feature = "private"))]
IamRds(std::sync::Arc<tokio::sync::RwLock<db_iam_ee::IamRdsUrl>>),
#[cfg(all(feature = "enterprise", feature = "private"))]
EntraId(std::sync::Arc<tokio::sync::RwLock<db_entra_ee::EntraIdUrl>>),
Static(String),
}
impl DatabaseUrl {
/// Get the database URL as a string.
/// Note: For IAM RDS, this returns the original URL (for metadata extraction).
/// For token-based auth, this returns the original URL (for metadata extraction).
/// For actual database connections, use connect_options() instead.
pub async fn as_str(&self) -> String {
match self {
@@ -647,13 +652,18 @@ impl DatabaseUrl {
let guard = rds_url.read().await;
guard.as_str().to_string()
}
#[cfg(all(feature = "enterprise", feature = "private"))]
DatabaseUrl::EntraId(entra_url) => {
let guard = entra_url.read().await;
guard.as_str().to_string()
}
DatabaseUrl::Static(url) => url.clone(),
}
}
/// Get PgConnectOptions for this database URL.
/// For IAM RDS, this returns options built directly from the token to avoid double-encoding
/// issues with temporary credentials (IRSA/Pod Identity).
/// For token-based auth (IAM RDS, Entra ID), this returns options built directly from the
/// token to avoid double-encoding issues with temporary credentials.
/// For static URLs, this parses the URL string.
pub async fn connect_options(&self) -> Result<sqlx::postgres::PgConnectOptions, Error> {
match self {
@@ -662,6 +672,11 @@ impl DatabaseUrl {
let guard = rds_url.read().await;
Ok(guard.connect_options())
}
#[cfg(all(feature = "enterprise", feature = "private"))]
DatabaseUrl::EntraId(entra_url) => {
let guard = entra_url.read().await;
Ok(guard.connect_options())
}
DatabaseUrl::Static(url) => sqlx::postgres::PgConnectOptions::from_str(url)
.map_err(|e| Error::InternalErr(format!("Failed to parse database URL: {}", e))),
}
@@ -671,9 +686,31 @@ impl DatabaseUrl {
match self {
#[cfg(all(feature = "enterprise", feature = "private"))]
DatabaseUrl::IamRds(rds_url) => rds_url.write().await.refresh().await,
#[cfg(all(feature = "enterprise", feature = "private"))]
DatabaseUrl::EntraId(entra_url) => entra_url.write().await.refresh().await,
DatabaseUrl::Static(_) => Ok(()),
}
}
pub async fn needs_refresh(&self) -> bool {
match self {
#[cfg(all(feature = "enterprise", feature = "private"))]
DatabaseUrl::IamRds(rds_url) => rds_url.read().await.needs_refresh(),
#[cfg(all(feature = "enterprise", feature = "private"))]
DatabaseUrl::EntraId(entra_url) => entra_url.read().await.needs_refresh(),
DatabaseUrl::Static(_) => false,
}
}
/// Double-checked refresh: read-lock to check, then write-lock to refresh if still needed.
pub async fn refresh_if_needed(&self) -> Result<(), Error> {
if self.needs_refresh().await {
self.refresh().await.map_err(|e| {
Error::InternalErr(format!("Failed to refresh database token: {}", e))
})?;
}
Ok(())
}
}
static DATABASE_URL_CACHE: tokio::sync::OnceCell<DatabaseUrl> = tokio::sync::OnceCell::const_new();
@@ -701,7 +738,9 @@ pub async fn get_database_url() -> Result<DatabaseUrl, Error> {
let parsed_url = url::Url::parse(&url)?;
if parsed_url.password().is_some_and(|x| x == "iamrds") {
let password = parsed_url.password().unwrap_or_default();
if password == "iamrds" {
let region = var("AWS_REGION").map_err(|_| {
Error::BadConfig(
"AWS_REGION env var is required for IAM RDS authentication".to_string(),
@@ -731,34 +770,68 @@ pub async fn get_database_url() -> Result<DatabaseUrl, Error> {
"IAM RDS authentication is not enabled in OSS mode".to_string(),
));
}
} else if password == "entraid" {
let tenant_id = var("AZURE_TENANT_ID").map_err(|_| {
Error::BadConfig(
"AZURE_TENANT_ID env var is required for Entra ID authentication"
.to_string(),
)
})?;
tracing::info!(
"entraid mode detected, generating Entra ID URL for tenant: {tenant_id}"
);
#[cfg(all(feature = "enterprise", feature = "private"))]
{
let client_id = var("AZURE_CLIENT_ID").map_err(|_| {
Error::BadConfig(
"AZURE_CLIENT_ID env var is required for Entra ID authentication"
.to_string(),
)
})?;
let federated_token_file =
var("AZURE_FEDERATED_TOKEN_FILE").map_err(|_| {
Error::BadConfig(
"AZURE_FEDERATED_TOKEN_FILE env var is required for Entra ID authentication".to_string(),
)
})?;
let authority_host = var("AZURE_AUTHORITY_HOST")
.unwrap_or_else(|_| "login.microsoftonline.com".to_string());
let entra_url = db_entra_ee::generate_database_url(
&url,
&tenant_id,
&client_id,
&federated_token_file,
&authority_host,
)
.await
.map_err(|e| {
Error::InternalErr(format!(
"Failed to generate Entra ID database URL: {}",
e
))
})?;
tracing::info!("Entra ID URL generated successfully");
Ok::<DatabaseUrl, Error>(DatabaseUrl::EntraId(std::sync::Arc::new(
tokio::sync::RwLock::new(entra_url),
)))
}
#[cfg(not(all(feature = "enterprise", feature = "private")))]
{
return Err(Error::BadConfig(
"Entra ID authentication is not enabled in OSS mode".to_string(),
));
}
} else {
Ok::<DatabaseUrl, Error>(DatabaseUrl::Static(url.to_string()))
}
})
.await?;
// Check if we need to refresh and do so if necessary
#[cfg(all(feature = "enterprise", feature = "private"))]
if let DatabaseUrl::IamRds(ref rds_url_lock) = database_url {
// Check if refresh is needed
let needs_refresh = {
let read_guard = rds_url_lock.read().await;
read_guard.needs_refresh()
};
database_url.refresh_if_needed().await?;
// If refresh is needed, acquire write lock and refresh
if needs_refresh {
let mut write_guard = rds_url_lock.write().await;
// Double-check after acquiring write lock (another task might have refreshed)
if write_guard.needs_refresh() {
write_guard.refresh().await.map_err(|e| {
Error::InternalErr(format!("Failed to refresh IAM token: {}", e))
})?;
}
}
}
// Return the URL string
Ok(database_url.clone())
}
+33 -48
View File
@@ -401,7 +401,7 @@ pub fn build_parameters(columns: &[SimpleColumn], db_type: DbType) -> String {
.map(|(i, col)| {
let base_type = col.datatype.split('(').next().unwrap_or(&col.datatype);
match db_type {
DbType::Postgresql => format!("-- ${} {}", i + 1, col.field),
DbType::Postgresql => format!("-- ${} {} ({})", i + 1, col.field, base_type),
DbType::Mysql => format!("-- :{} ({})", col.field, base_type),
DbType::MsSqlServer => {
format!("-- @p{} {} ({})", i + 1, col.field, base_type)
@@ -765,7 +765,7 @@ pub fn make_select_query(
quicksearch
));
query.push_str(&format!(" ORDER BY {}\n", order_by));
query.push_str(" LIMIT $1::INT OFFSET $2::INT");
query.push_str(" LIMIT $1 OFFSET $2");
Ok(query)
}
DbType::MsSqlServer => {
@@ -1158,13 +1158,11 @@ pub fn make_delete_query(table: &str, columns: &[ColumnDef], db_type: DbType) ->
.map(|(i, c)| {
let qf = qi(&c.field, db_type);
format!(
"(${}::text::{} IS NULL AND {} IS NULL OR {} = ${}::text::{})",
"(${} IS NULL AND {} IS NULL OR {} = ${})",
i + 1,
c.datatype,
qf,
qf,
i + 1,
c.datatype
)
})
.collect::<Vec<_>>()
@@ -1280,7 +1278,7 @@ fn format_insert_values(columns: &[ColumnDef], db_type: DbType, start_index: usi
.enumerate()
.map(|(i, c)| match db_type {
DbType::Mysql => format!(":{}", c.field),
DbType::Postgresql => format!("${}::{}", start_index + i, c.datatype),
DbType::Postgresql => format!("${}", start_index + i),
DbType::MsSqlServer => format!("@p{}", start_index + i),
DbType::Snowflake => "?".to_string(),
DbType::Bigquery => format!("@{}", c.field),
@@ -1458,21 +1456,19 @@ pub fn make_update_query(
.map(|(i, c)| {
let qf = qi(&c.field, db_type);
format!(
"(${}::text::{} IS NULL AND {} IS NULL OR {} = ${}::text::{})",
"(${} IS NULL AND {} IS NULL OR {} = ${})",
i + 2,
c.datatype,
qf,
qf,
i + 2,
c.datatype
)
})
.collect::<Vec<_>>()
.join("\n AND ");
query.push_str(&format!(
"\nUPDATE {} SET {} = $1::text::{} \nWHERE {}\tRETURNING 1",
qt, qcol, column.datatype, conditions
"\nUPDATE {} SET {} = $1 \nWHERE {}\tRETURNING 1",
qt, qcol, conditions
));
}
DbType::Mysql => {
@@ -2706,7 +2702,7 @@ mod tests {
fn test_build_parameters_postgresql() {
let cols = vec![simple_col("limit", "int"), simple_col("offset", "int")];
let result = build_parameters(&cols, DbType::Postgresql);
assert_eq!(result, "-- $1 limit\n-- $2 offset");
assert_eq!(result, "-- $1 limit (int)\n-- $2 offset (int)");
}
#[test]
@@ -2815,11 +2811,11 @@ mod tests {
make_select_query("my_table", &cols, None, DbType::Postgresql, None, None).unwrap();
assert!(result.starts_with(
"-- $1 limit\n-- $2 offset\n-- $3 quicksearch\n-- $4 order_by\n-- $5 is_desc\n"
"-- $1 limit (int)\n-- $2 offset (int)\n-- $3 quicksearch (text)\n-- $4 order_by (text)\n-- $5 is_desc (boolean)\n"
));
assert!(result.contains("SELECT \"id\"::text, \"name\"::text FROM \"my_table\"\n"));
assert!(result.contains("($3 = '' OR CONCAT(\"id\", \"name\") ILIKE '%' || $3 || '%')"));
assert!(result.contains("LIMIT $1::INT OFFSET $2::INT"));
assert!(result.contains("LIMIT $1 OFFSET $2"));
assert!(result.contains("$4 = 'id' AND $5 IS false THEN \"id\"::text"));
assert!(result.contains("$4 = 'name' AND $5 IS true THEN \"name\"::text END) DESC"));
}
@@ -3011,7 +3007,7 @@ mod tests {
let cols = vec![col("id", "int4"), col("name", "text")];
let result = make_count_query(DbType::Postgresql, "my_table", None, &cols).unwrap();
assert!(result.contains("-- $1 quicksearch"));
assert!(result.contains("-- $1 quicksearch (text)"));
assert!(result.contains("SELECT COUNT(*) as count FROM \"my_table\""));
assert!(result.contains("($1 = '' OR CONCAT(\"id\", \"name\") ILIKE '%' || $1 || '%')"));
// Should use WHERE not AND
@@ -3148,13 +3144,10 @@ mod tests {
let cols = vec![col("id", "int4"), col("name", "text")];
let result = make_delete_query("my_table", &cols, DbType::Postgresql);
assert!(result.contains("-- $1 id\n-- $2 name"));
assert!(result.contains("-- $1 id (int4)\n-- $2 name (text)"));
assert!(result.contains("DELETE FROM \"my_table\""));
assert!(result
.contains("($1::text::int4 IS NULL AND \"id\" IS NULL OR \"id\" = $1::text::int4)"));
assert!(result.contains(
"($2::text::text IS NULL AND \"name\" IS NULL OR \"name\" = $2::text::text)"
));
assert!(result.contains("($1 IS NULL AND \"id\" IS NULL OR \"id\" = $1)"));
assert!(result.contains("($2 IS NULL AND \"name\" IS NULL OR \"name\" = $2)"));
assert!(result.contains("RETURNING 1;"));
}
@@ -3221,9 +3214,8 @@ mod tests {
let cols = vec![col("id", "int4"), col("name", "text")];
let result = make_insert_query("my_table", &cols, DbType::Postgresql).unwrap();
assert!(result.contains("-- $1 id\n-- $2 name"));
assert!(result
.contains("INSERT INTO \"my_table\" (\"id\", \"name\") VALUES ($1::int4, $2::text)"));
assert!(result.contains("-- $1 id (int4)\n-- $2 name (text)"));
assert!(result.contains("INSERT INTO \"my_table\" (\"id\", \"name\") VALUES ($1, $2)"));
}
#[test]
@@ -3279,7 +3271,7 @@ mod tests {
make_insert_query("my_table", &[id_col, name_col], DbType::Postgresql).unwrap();
// id should be skipped from insert columns (has nextval default in pg)
assert!(result.contains("INSERT INTO \"my_table\" (\"name\") VALUES ($1::text)"));
assert!(result.contains("INSERT INTO \"my_table\" (\"name\") VALUES ($1)"));
}
#[test]
@@ -3293,9 +3285,7 @@ mod tests {
make_insert_query("my_table", &[id_col, name_col], DbType::Postgresql).unwrap();
// name should be in insert params, id should be in defaults
assert!(
result.contains("INSERT INTO \"my_table\" (\"name\", \"id\") VALUES ($1::text, '42')")
);
assert!(result.contains("INSERT INTO \"my_table\" (\"name\", \"id\") VALUES ($1, '42')"));
}
#[test]
@@ -3308,7 +3298,7 @@ mod tests {
let result =
make_insert_query("my_table", &[id_col, name_col], DbType::Postgresql).unwrap();
assert!(result.contains("VALUES ($1::text, NULL)"));
assert!(result.contains("VALUES ($1, NULL)"));
}
#[test]
@@ -3323,7 +3313,7 @@ mod tests {
make_insert_query("my_table", &[id_col, name_col], DbType::Postgresql).unwrap();
// Column is hidden, not nullable, has db default, no user default -> omit (use db default)
assert!(result.contains("INSERT INTO \"my_table\" (\"name\") VALUES ($1::text)"));
assert!(result.contains("INSERT INTO \"my_table\" (\"name\") VALUES ($1)"));
}
#[test]
@@ -3337,7 +3327,7 @@ mod tests {
make_insert_query("my_table", &[id_col, name_col], DbType::Postgresql).unwrap();
// Always identity should be omitted
assert!(result.contains("INSERT INTO \"my_table\" (\"name\") VALUES ($1::text)"));
assert!(result.contains("INSERT INTO \"my_table\" (\"name\") VALUES ($1)"));
}
#[test]
@@ -3375,13 +3365,12 @@ mod tests {
let where_cols = vec![simple_col("id", "int4"), simple_col("email", "text")];
let result = make_update_query("my_table", &update_col, &where_cols, DbType::Postgresql);
assert!(result.contains("-- $1 value_to_update\n-- $2 id\n-- $3 email"));
assert!(result.contains("UPDATE \"my_table\" SET \"name\" = $1::text::text"));
assert!(result
.contains("($2::text::int4 IS NULL AND \"id\" IS NULL OR \"id\" = $2::text::int4)"));
assert!(result.contains(
"($3::text::text IS NULL AND \"email\" IS NULL OR \"email\" = $3::text::text)"
));
assert!(
result.contains("-- $1 value_to_update (text)\n-- $2 id (int4)\n-- $3 email (text)")
);
assert!(result.contains("UPDATE \"my_table\" SET \"name\" = $1"));
assert!(result.contains("($2 IS NULL AND \"id\" IS NULL OR \"id\" = $2)"));
assert!(result.contains("($3 IS NULL AND \"email\" IS NULL OR \"email\" = $3)"));
assert!(result.contains("RETURNING 1"));
}
@@ -3463,10 +3452,8 @@ mod tests {
let cols = vec![col("a", "int4"), col("b", "text"), col("c", "bool")];
let result = make_delete_query("t", &cols, DbType::Postgresql);
// Check all three conditions are present and joined
assert!(result
.contains("AND ($2::text::text IS NULL AND \"b\" IS NULL OR \"b\" = $2::text::text)"));
assert!(result
.contains("AND ($3::text::bool IS NULL AND \"c\" IS NULL OR \"c\" = $3::text::bool)"));
assert!(result.contains("AND ($2 IS NULL AND \"b\" IS NULL OR \"b\" = $2)"));
assert!(result.contains("AND ($3 IS NULL AND \"c\" IS NULL OR \"c\" = $3)"));
}
#[test]
@@ -3518,7 +3505,7 @@ mod tests {
let result = make_insert_query("my_table", &[name_col, col1], DbType::Postgresql).unwrap();
// Numeric value should not be quoted
assert!(result.contains("VALUES ($1::text, 5)"));
assert!(result.contains("VALUES ($1, 5)"));
}
// -----------------------------------------------------------------------
@@ -3540,7 +3527,7 @@ mod tests {
assert!(result.is_some());
let sql = result.unwrap().unwrap().code;
assert!(sql.contains("SELECT \"id\"::text, \"name\"::text FROM \"my_table\""));
assert!(sql.contains("LIMIT $1::INT OFFSET $2::INT"));
assert!(sql.contains("LIMIT $1 OFFSET $2"));
}
#[test]
@@ -3571,16 +3558,14 @@ mod tests {
fn test_expand_insert_marker() {
let marker = r#"-- WM_INTERNAL_DB_INSERT {"table":"my_table","columns":[{"field":"id","datatype":"int4"},{"field":"name","datatype":"text"}]}"#;
let sql = expand_code(marker, &ScriptLang::Postgresql);
assert!(
sql.contains("INSERT INTO \"my_table\" (\"id\", \"name\") VALUES ($1::int4, $2::text)")
);
assert!(sql.contains("INSERT INTO \"my_table\" (\"id\", \"name\") VALUES ($1, $2)"));
}
#[test]
fn test_expand_update_marker() {
let marker = r#"-- WM_INTERNAL_DB_UPDATE {"table":"my_table","column":{"field":"name","datatype":"text"},"columns":[{"field":"id","datatype":"int4"}]}"#;
let sql = expand_code(marker, &ScriptLang::Postgresql);
assert!(sql.contains("UPDATE \"my_table\" SET \"name\" = $1::text::text"));
assert!(sql.contains("UPDATE \"my_table\" SET \"name\" = $1"));
assert!(sql.contains("RETURNING 1"));
}
+7
View File
@@ -11,6 +11,7 @@ pub struct Smtp {
pub from: String,
pub tls_implicit: Option<bool>,
pub disable_tls: Option<bool>,
pub clicktracking_off: Option<bool>,
}
#[derive(Serialize, Deserialize, PartialEq)]
@@ -22,6 +23,7 @@ pub struct SmtpConfigOpt {
pub smtp_from: Option<String>,
pub smtp_tls_implicit: Option<bool>,
pub smtp_disable_tls: Option<bool>,
pub smtp_clicktracking_off: Option<bool>,
}
pub async fn load_smtp_config(db: &DB) -> error::Result<Option<Smtp>> {
@@ -46,6 +48,7 @@ pub async fn load_smtp_config(db: &DB) -> error::Result<Option<Smtp>> {
from: config
.smtp_from
.unwrap_or_else(|| "noreply@getwindmill.com".to_string()),
clicktracking_off: config.smtp_clicktracking_off,
})
} else {
None
@@ -72,6 +75,9 @@ pub async fn load_smtp_config(db: &DB) -> error::Result<Option<Smtp>> {
.unwrap_or(587),
from: std::env::var("SMTP_FROM")
.unwrap_or_else(|_| "noreply@getwindmill.com".to_string()),
clicktracking_off: std::env::var("SMTP_CLICKTRACKING_OFF")
.ok()
.and_then(|p| p.parse().ok()),
})
} else {
None
@@ -94,6 +100,7 @@ impl Default for SmtpConfigOpt {
smtp_tls_implicit: None,
smtp_username: None,
smtp_disable_tls: None,
smtp_clicktracking_off: None,
}
}
}
+8
View File
@@ -157,6 +157,8 @@ lazy_static::lazy_static! {
pub static ref NATIVE_MODE: bool = std::env::var("NATIVE_MODE").ok().is_some_and(|x| x == "1" || x == "true");
pub static ref LIMIT_WINDOWS_TO_1CU: bool = std::env::var("LIMIT_WINDOWS_TO_1CU").ok().is_some_and(|x| x == "1" || x == "true");
pub static ref CGROUP_V2_PATH_RE: Regex = Regex::new(r#"(?m)^0::(/.*)$"#).unwrap();
pub static ref CGROUP_V2_CPU_RE: Regex = Regex::new(r#"(?m)^(\d+) \S+$"#).unwrap();
pub static ref CGROUP_V1_INACTIVE_FILE_RE: Regex = Regex::new(r#"(?m)^total_inactive_file (\d+)$"#).unwrap();
@@ -1052,6 +1054,9 @@ pub fn get_vcpus() -> Option<i64> {
#[cfg(windows)]
pub fn get_vcpus() -> Option<i64> {
if *LIMIT_WINDOWS_TO_1CU {
return Some(100000); // 1 vCPU
}
let mut sys = System::new();
sys.refresh_cpu_all();
(sys.cpus().len() * 100000).try_into().ok()
@@ -1101,6 +1106,9 @@ pub fn get_memory() -> Option<i64> {
#[cfg(windows)]
pub fn get_memory() -> Option<i64> {
if *LIMIT_WINDOWS_TO_1CU {
return Some(2 * 1024 * 1024 * 1024); // 2 GB
}
let mut sys = System::new();
sys.refresh_memory();
Some(sys.total_memory() as i64)
+199 -5
View File
@@ -42,13 +42,39 @@ pub fn extract_resource_types_from_schema(schema: &SchemaType) -> HashSet<String
/// Transform a JSON schema for maximum MCP client compatibility.
///
/// Some MCP clients (e.g., n8n) have limited JSON Schema support:
/// - `integer` type is not supported (convert to `number`)
/// - invalid non-array `enum` values are removed
/// Ensures schemas conform to JSON Schema draft 2020-12 by:
/// - Converting `integer` type to `number` (some clients don't support integer)
/// - Removing invalid non-array `enum` values
/// - Stripping non-standard keywords (`originalType`, `format` with `resource-*` prefix)
/// - Fixing contradictory schemas (`type: "string"` with `properties` → `type: "object"`)
/// - Removing `default: null` when the type doesn't include `null`
/// - Adding `type: "object"` to empty schemas that have no type
pub fn make_schema_compatible(schema: &mut Value) {
let Value::Object(obj) = schema else { return };
// 1. Convert integer to number
// 1. Strip non-standard keywords that aren't part of JSON Schema
obj.remove("originalType");
// 2. Strip non-standard format values (resource-* is Windmill-internal)
if obj
.get("format")
.and_then(|v| v.as_str())
.is_some_and(|s| s.starts_with("resource-"))
{
obj.remove("format");
}
// 3. Fix contradictory type: if `properties` is present, type must be "object"
if obj.contains_key("properties") {
match obj.get("type").and_then(|v| v.as_str()) {
Some("object") => {}
_ => {
obj.insert("type".to_string(), Value::String("object".to_string()));
}
}
}
// 4. Convert integer to number
if let Some(type_val) = obj.get_mut("type") {
match type_val {
Value::String(s) if s == "integer" => *s = "number".to_string(),
@@ -65,11 +91,28 @@ pub fn make_schema_compatible(schema: &mut Value) {
}
}
// 2. Invalid enum values like `enum: null` are not valid draft 2020-12.
// 5. Remove `default: null` when type doesn't include "null"
if obj.get("default").is_some_and(|v| v.is_null()) {
let type_includes_null = match obj.get("type") {
Some(Value::String(s)) => s == "null",
Some(Value::Array(arr)) => arr.iter().any(|v| v.as_str() == Some("null")),
_ => false,
};
if !type_includes_null {
obj.remove("default");
}
}
// 6. Invalid enum values like `enum: null` are not valid draft 2020-12.
if obj.get("enum").is_some_and(|enum_val| !enum_val.is_array()) {
obj.remove("enum");
}
// 7. Ensure schemas with no type but with properties get type: "object"
if !obj.contains_key("type") && !obj.is_empty() {
obj.insert("type".to_string(), Value::String("object".to_string()));
}
// Recursively process nested schemas
if let Some(Value::Object(props)) = obj.get_mut("properties") {
for value in props.values_mut() {
@@ -174,4 +217,155 @@ mod tests {
json!(["open", "closed"])
);
}
#[test]
fn strips_original_type() {
let mut schema = json!({
"type": "object",
"properties": {
"name": {
"type": "string",
"originalType": "string"
},
"data": {
"type": "string",
"originalType": "bytes"
}
}
});
make_schema_compatible(&mut schema);
assert!(schema["properties"]["name"].get("originalType").is_none());
assert!(schema["properties"]["data"].get("originalType").is_none());
assert_eq!(schema["properties"]["name"]["type"], json!("string"));
}
#[test]
fn strips_resource_format() {
let mut schema = json!({
"type": "object",
"properties": {
"db": {
"type": "string",
"format": "resource-postgresql"
}
}
});
make_schema_compatible(&mut schema);
assert!(schema["properties"]["db"].get("format").is_none());
assert_eq!(schema["properties"]["db"]["type"], json!("string"));
}
#[test]
fn preserves_standard_format() {
let mut schema = json!({
"type": "object",
"properties": {
"email": {
"type": "string",
"format": "email"
}
}
});
make_schema_compatible(&mut schema);
assert_eq!(schema["properties"]["email"]["format"], json!("email"));
}
#[test]
fn fixes_string_type_with_properties() {
let mut schema = json!({
"type": "object",
"properties": {
"config": {
"type": "string",
"format": "resource-record",
"properties": {}
}
}
});
make_schema_compatible(&mut schema);
assert_eq!(schema["properties"]["config"]["type"], json!("object"));
assert!(schema["properties"]["config"].get("format").is_none());
}
#[test]
fn removes_null_default_on_string_type() {
let mut schema = json!({
"type": "object",
"properties": {
"name": {
"type": "string",
"default": null
}
}
});
make_schema_compatible(&mut schema);
assert!(schema["properties"]["name"].get("default").is_none());
}
#[test]
fn preserves_null_default_when_type_includes_null() {
let mut schema = json!({
"type": "object",
"properties": {
"name": {
"type": ["string", "null"],
"default": null
}
}
});
make_schema_compatible(&mut schema);
assert_eq!(schema["properties"]["name"]["default"], json!(null));
}
#[test]
fn preserves_non_null_default() {
let mut schema = json!({
"type": "object",
"properties": {
"name": {
"type": "string",
"default": "hello"
}
}
});
make_schema_compatible(&mut schema);
assert_eq!(schema["properties"]["name"]["default"], json!("hello"));
}
#[test]
fn adds_type_object_to_empty_schema() {
let mut schema = json!({});
make_schema_compatible(&mut schema);
// Empty schema stays empty (no keys = truly empty)
assert_eq!(schema, json!({}));
}
#[test]
fn adds_type_to_schema_with_properties_but_no_type() {
let mut schema = json!({
"properties": {
"value": {}
}
});
make_schema_compatible(&mut schema);
assert_eq!(schema["type"], json!("object"));
}
}
+3
View File
@@ -146,6 +146,9 @@ hyper-tls = { workspace = true, optional = true }
hyper-util = { workspace = true, optional = true }
rcgen = { workspace = true, optional = true }
[target.'cfg(windows)'.dependencies]
windows = { version = "0.61", features = ["Win32_System_JobObjects", "Win32_System_Threading"] }
[dev-dependencies]
tempfile.workspace = true
x509-parser.workspace = true
@@ -6,8 +6,15 @@ try {
external: ["*"],
});
} catch (err) {
const msgs = (err?.errors ?? []).map((e) => e?.message ?? String(e));
msgs.push(err?.message ?? String(err));
const full = msgs.join(" ");
if (full.includes("Unexpected end of file") || full.includes("Unterminated")) {
console.log("Failed to build bundle: syntax error in the script (e.g. unclosed bracket, string, or comment)");
} else {
console.log("Failed to build bundle");
}
console.log(err);
console.log("Failed to build bundle");
process.exit(1);
}
+138 -2
View File
@@ -702,6 +702,120 @@ lazy_static! {
static ref DISABLE_PROCESS_GROUP: bool = std::env::var("DISABLE_PROCESS_GROUP").is_ok();
}
/// 2 GB memory limit in bytes for LIMIT_WINDOWS_TO_1CU
#[cfg(windows)]
const MEMORY_LIMIT_1CU: usize = 2 * 1024 * 1024 * 1024;
/// Wrapper that holds a Windows Job Object handle alongside the child process.
/// The job object enforces memory limits and is closed when the child is dropped.
#[cfg(windows)]
struct MemoryLimitedChild {
inner: Box<dyn TokioChildWrapper>,
_job_handle: Win32JobHandle,
}
#[cfg(windows)]
impl std::fmt::Debug for MemoryLimitedChild {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MemoryLimitedChild").finish()
}
}
/// RAII wrapper for a raw Win32 HANDLE that closes it on drop.
#[cfg(windows)]
struct Win32JobHandle(windows::Win32::Foundation::HANDLE);
// SAFETY: Win32 HANDLEs are plain pointer-sized values with no thread affinity;
// the kernel ref-counts the underlying object, so sending/sharing the handle is safe.
#[cfg(windows)]
unsafe impl Send for Win32JobHandle {}
#[cfg(windows)]
unsafe impl Sync for Win32JobHandle {}
#[cfg(windows)]
impl Drop for Win32JobHandle {
fn drop(&mut self) {
let _ = unsafe { windows::Win32::Foundation::CloseHandle(self.0) };
}
}
#[cfg(windows)]
impl process_wrap::tokio::TokioChildWrapper for MemoryLimitedChild {
fn inner(&self) -> &tokio::process::Child {
self.inner.inner()
}
fn inner_mut(&mut self) -> &mut tokio::process::Child {
self.inner.inner_mut()
}
fn into_inner(self: Box<Self>) -> tokio::process::Child {
self.inner.into_inner()
}
fn start_kill(&mut self) -> std::io::Result<()> {
self.inner.start_kill()
}
fn wait(
&mut self,
) -> Box<dyn std::future::Future<Output = std::io::Result<std::process::ExitStatus>> + Send + '_>
{
self.inner.wait()
}
fn try_wait(&mut self) -> std::io::Result<Option<std::process::ExitStatus>> {
self.inner.try_wait()
}
}
/// Create a Windows Job Object with a memory limit and assign the process to it.
#[cfg(windows)]
fn apply_job_memory_limit(pid: u32, memory_limit: usize) -> Result<Win32JobHandle, std::io::Error> {
use windows::Win32::System::JobObjects::*;
use windows::Win32::System::Threading::{OpenProcess, PROCESS_SET_QUOTA, PROCESS_TERMINATE};
unsafe {
let job = CreateJobObjectW(None, None).map_err(|e| {
std::io::Error::new(std::io::ErrorKind::Other, format!("CreateJobObjectW: {e}"))
})?;
let mut info = JOBOBJECT_EXTENDED_LIMIT_INFORMATION::default();
info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_JOB_MEMORY;
info.JobMemoryLimit = memory_limit;
SetInformationJobObject(
job,
JobObjectExtendedLimitInformation,
&info as *const _ as _,
std::mem::size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32,
)
.map_err(|e| {
let _ = windows::Win32::Foundation::CloseHandle(job);
std::io::Error::new(
std::io::ErrorKind::Other,
format!("SetInformationJobObject: {e}"),
)
})?;
let process_handle = OpenProcess(PROCESS_SET_QUOTA | PROCESS_TERMINATE, false, pid)
.map_err(|e| {
let _ = windows::Win32::Foundation::CloseHandle(job);
std::io::Error::new(
std::io::ErrorKind::Other,
format!("OpenProcess({pid}): {e}"),
)
})?;
let assign_result = AssignProcessToJobObject(job, process_handle);
let _ = windows::Win32::Foundation::CloseHandle(process_handle);
assign_result.map_err(|e| {
let _ = windows::Win32::Foundation::CloseHandle(job);
std::io::Error::new(
std::io::ErrorKind::Other,
format!("AssignProcessToJobObject: {e}"),
)
})?;
Ok(Win32JobHandle(job))
}
}
pub fn build_command_with_isolation(program: &str, args: &[&str]) -> Command {
use tokio::process::Command;
@@ -767,9 +881,31 @@ pub async fn start_child_process(
}
}
return cmd
let child: Box<dyn TokioChildWrapper> = cmd
.spawn()
.map_err(|err| tentatively_improve_error(err.into(), executable));
.map_err(|err| tentatively_improve_error(err.into(), executable))?;
#[cfg(windows)]
if *windmill_common::worker::LIMIT_WINDOWS_TO_1CU {
if let Some(pid) = child.inner().id() {
match apply_job_memory_limit(pid, MEMORY_LIMIT_1CU) {
Ok(job_handle) => {
tracing::info!(
"Applied 2GB memory limit (LIMIT_WINDOWS_TO_1CU) to child process {pid}"
);
return Ok(Box::new(MemoryLimitedChild {
inner: child,
_job_handle: job_handle,
}));
}
Err(e) => {
tracing::warn!("Failed to apply memory limit to child process {pid}: {e}");
}
}
}
}
Ok(child)
}
pub async fn resolve_job_timeout(
+26 -26
View File
@@ -8,7 +8,7 @@ use anyhow::Context;
use base64::{engine, Engine as _};
use chrono::Utc;
use futures::future::BoxFuture;
use futures::{FutureExt, StreamExt, TryFutureExt, TryStreamExt};
use futures::{FutureExt, StreamExt, TryStreamExt};
use itertools::Itertools;
use rust_decimal::{prelude::FromPrimitive, Decimal};
use serde_json::value::RawValue;
@@ -134,45 +134,45 @@ fn do_postgresql_inner<'a>(
let result_f = async move {
let mut res: Vec<Box<serde_json::value::RawValue>> = vec![];
let query_params = query_params
.iter()
.map(|p| &**p as &(dyn ToSql + Sync))
.collect_vec();
let statement = if typed_schema {
// Use query_typed_raw (unnamed prepared statement) when all param types are
// resolved. This avoids named prepared statements ("s0", "s1", ...) which break
// with transaction-mode connection poolers (e.g. PgBouncer/Supabase) since the
// prepare and query can land on different backend connections.
// Fall back to prepare + query_raw for custom/unsupported types.
let rows = if typed_schema {
let typed_params = query_params
.iter()
.zip(param_types.iter())
.map(|(p, t)| (&**p as &(dyn ToSql + Sync), t.clone()));
client
.prepare_typed(&query, &param_types)
.query_typed_raw(&query, typed_params)
.await
.map_err(to_anyhow)?
} else {
client.prepare(&query).await.map_err(to_anyhow)?
let query_params = query_params
.iter()
.map(|p| &**p as &(dyn ToSql + Sync))
.collect_vec();
let statement = client.prepare(&query).await.map_err(to_anyhow)?;
client
.query_raw(&statement, query_params)
.await
.map_err(to_anyhow)?
};
if skip_collect {
client
.execute_raw(&statement, query_params)
.await
.map_err(to_anyhow)?;
futures::pin_mut!(rows);
while rows.try_next().await.map_err(to_anyhow)?.is_some() {}
} else if let Some(ref s3) = s3 {
let rows_stream = client
.query_raw(&statement, query_params)
.map_err(to_anyhow)
.await?
.map_err(to_anyhow)
.map(|row_result| {
row_result.and_then(|row| postgres_row_to_json_value(row).map_err(to_anyhow))
});
let rows_stream = rows.map_err(to_anyhow).map(|row_result| {
row_result.and_then(|row| postgres_row_to_json_value(row).map_err(to_anyhow))
});
let stream = convert_json_line_stream(rows_stream.boxed(), s3.format).await?;
s3.upload(stream.boxed()).await?;
return Ok(vec![to_raw_value(&s3.to_return_s3_obj())]);
} else {
let rows = client
.query_raw(&statement, query_params)
.await
.map_err(to_anyhow)?;
let rows = if first_row_only {
rows.take(1).boxed()
} else {
@@ -429,7 +429,7 @@ pub async fn handle_flow_dependency_job(
// `JobKind::FlowDependencies` job store either:
// - A saved flow version `id` in the `script_hash` column.
// - Preview raw flow in the `queue` or `job` table.
let (mut flow, extras) = match job.runnable_id {
let (mut flow, mut extras) = match job.runnable_id {
Some(ScriptHash(id)) => {
let flow = cache::flow::fetch_version(db, id).await?;
(flow.value().clone(), flow.extras())
@@ -440,6 +440,51 @@ pub async fn handle_flow_dependency_job(
},
};
// When triggered by a relative import (e.g. a dependent script was updated),
// the version captured at job creation time may be stale if the flow was
// updated between job creation and execution. Re-query the latest version
// and read the current flow value from the flow table to avoid overwriting
// a newer flow definition with a stale one.
let version = if triggered_by_relative_import && !skip_flow_update {
let latest_version = sqlx::query_scalar!(
"SELECT id FROM flow_version WHERE path = $1 AND workspace_id = $2 ORDER BY created_at DESC LIMIT 1",
job_path,
job.workspace_id
)
.fetch_optional(db)
.await?;
if let Some(latest_version) = latest_version {
if version != Some(latest_version) {
tracing::info!(
"Flow version changed since dependency job was queued ({:?} -> {}), using latest",
version,
latest_version
);
}
// Read the current flow value from the flow table (not version cache).
// This ensures we have the latest committed state, including any locks
// computed by a concurrent FlowDependencies job from a direct flow update.
let raw_flow_value = sqlx::query_scalar!(
"SELECT value AS \"value!: Json<Box<RawValue>>\" FROM flow WHERE path = $1 AND workspace_id = $2",
job_path,
job.workspace_id
)
.fetch_one(db)
.await?;
let flow_data = cache::FlowData::from_raw(raw_flow_value.0)?;
flow = flow_data.value().clone();
extras = flow_data.extras();
Some(latest_version)
} else {
version
}
} else {
version
};
let mut tx = db.begin().await?;
let mut dependency_map = ScopedDependencyMap::fetch_maybe_rearranged(
@@ -575,6 +620,33 @@ pub async fn handle_flow_dependency_job(
tx = dependency_map.dissolve(tx).await;
// When triggered by a relative import, re-check that our version is still
// the latest before writing. Between reading the flow value and now (module
// locking can take significant time), another job may have created a newer
// version. If so, skip the update — the newer version's dep job will handle it.
if triggered_by_relative_import {
let current_latest = sqlx::query_scalar!(
"SELECT id FROM flow_version WHERE path = $1 AND workspace_id = $2 ORDER BY created_at DESC LIMIT 1",
job_path,
job.workspace_id
)
.fetch_optional(&mut *tx)
.await?;
if current_latest != Some(version) {
tracing::info!(
"Flow version changed during dependency locking ({} -> {:?}), skipping update to avoid overwriting newer version",
version,
current_latest
);
tx.commit().await?;
return Ok(to_raw_value_owned(json!({
"status": "Skipped: newer flow version exists",
"modified_ids": modified_ids,
})));
}
}
sqlx::query!(
"UPDATE flow SET value = $1 WHERE path = $2 AND workspace_id = $3",
&new_flow_value as &Json<Box<RawValue>>,
@@ -627,7 +699,9 @@ pub async fn handle_flow_dependency_job(
// Making new version viewable as the current one.
// This will also trigger `flow_versions_append_trigger` (check _flow_versions_update_notify.up.sql)
// which will invalidate cache for the latest flow versions for all workers.
sqlx::query!("UPDATE flow SET versions = array_append(versions, $1) WHERE path = $2 AND workspace_id = $3",
// Only append if this version isn't already the last element in the array.
// This prevents duplicates when update_flow already appended this version.
sqlx::query!("UPDATE flow SET versions = array_append(versions, $1) WHERE path = $2 AND workspace_id = $3 AND (versions[array_upper(versions, 1)] IS DISTINCT FROM $1)",
version,
&job_path,
&job.workspace_id,
+1 -1
View File
@@ -2,7 +2,7 @@ import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts";
import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts";
import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts";
export const VERSION = "v1.672.0";
export const VERSION = "v1.673.0";
export async function login(email: string, password: string): Promise<string> {
return await windmill.UserService.login({
+21 -26
View File
@@ -15,7 +15,7 @@ import { buildFolderPath, getMetadataFileName, loadNonDottedPathsSetting } from
import { requireLogin } from "../../core/auth.ts";
import { resolveWorkspace, validatePath } from "../../core/context.ts";
import { resolve, track_job } from "../script/script.ts";
import { resolve, track_job, pollForJobResult } from "../script/script.ts";
import { defaultFlowDefinition } from "../../../bootstrap/flow_bootstrap.ts";
import { SyncOptions, mergeConfigWithConfigFile } from "../../core/conf.ts";
import { FSFSElement, elementsToMap, ignoreF } from "../sync/sync.ts";
@@ -574,32 +574,27 @@ async function preview(
log.debug(`Flow value: ${JSON.stringify(localFlow.value, null, 2)}`);
// Run the flow preview
let result;
try {
result = await wmill.runFlowPreviewAndWaitResult({
workspace: workspace.workspaceId,
requestBody: {
value: localFlow.value,
path: flowPath.substring(0, flowPath.indexOf(".flow")).replaceAll(SEP, "/"),
args: input,
},
});
} catch (e: any) {
if (e.body) {
// If a failure_module ran, the body contains its result — not an error
if (e.body.result !== undefined) {
if (opts.silent) {
console.log(JSON.stringify(e.body.result));
} else {
log.info(colors.yellow.bold("Flow failed, error handler result:"));
log.info(JSON.stringify(e.body.result, null, 2));
}
process.exitCode = 1;
return;
}
// Run the flow preview — start the job, then poll for completion
const jobId = await wmill.runFlowPreview({
workspace: workspace.workspaceId,
requestBody: {
value: localFlow.value,
path: flowPath.substring(0, flowPath.indexOf(".flow")).replaceAll(SEP, "/"),
args: input,
},
});
const { result, success } = await pollForJobResult(workspace.workspaceId, jobId);
if (!success) {
if (opts.silent) {
console.log(JSON.stringify(result));
} else {
log.info(colors.yellow.bold("Flow failed, error handler result:"));
log.info(JSON.stringify(result, null, 2));
}
throw e;
process.exitCode = 1;
return;
}
if (opts.silent) {
@@ -333,8 +333,9 @@ async function generateMetadata(
log.info("");
const isInteractive = process.stdin.isTTY ?? false;
if (
!opts.yes &&
!opts.yes && isInteractive &&
!(await Confirm.prompt({
message: "Update metadata?",
default: true,
+37 -3
View File
@@ -1118,6 +1118,27 @@ export async function track_job(workspace: string, id: string) {
}
}
const POLL_INTERVAL_MS = 2000;
export async function pollForJobResult(
workspace: string,
jobId: string,
): Promise<{ result: unknown; success: boolean }> {
while (true) {
const maybeResult = await wmill.getCompletedJobResultMaybe({
workspace,
id: jobId,
getStarted: false,
});
if (maybeResult.completed) {
return { result: maybeResult.result, success: maybeResult.success ?? false };
}
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
}
}
async function show(opts: GlobalOptions, path: string) {
const workspace = await resolveWorkspace(opts);
await requireLogin(opts);
@@ -1532,8 +1553,8 @@ async function preview(
}
}
} else {
// For regular scripts, use the standard preview API
const result = await wmill.runScriptPreviewAndWaitResult({
// For regular scripts, start the preview job then poll for completion
const jobId = await wmill.runScriptPreview({
workspace: workspace.workspaceId,
requestBody: {
content,
@@ -1544,8 +1565,21 @@ async function preview(
},
});
const { result, success } = await pollForJobResult(workspace.workspaceId, jobId);
if (!success) {
if (opts.silent) {
console.log(JSON.stringify(result));
} else {
log.info(colors.red.bold("Preview failed"));
log.info(JSON.stringify(result, null, 2));
}
process.exitCode = 1;
return;
}
if (opts.silent) {
console.log(JSON.stringify(result, null, 2));
console.log(JSON.stringify(result));
} else {
log.info(colors.bold.underline.green("Preview completed"));
log.info(JSON.stringify(result, null, 2));
+1 -1
View File
@@ -78,7 +78,7 @@ export {
token,
};
export const VERSION = "1.672.0";
export const VERSION = "1.673.0";
// Re-exported from constants.ts to maintain backwards compatibility
export { WM_FORK_PREFIX } from "./core/constants.ts";
+1 -1
View File
@@ -1053,12 +1053,12 @@ export async function parseMetadataFile(
}
}
// no metadata file at all. Create it
metadataFilePath = scriptPath + ".script.yaml";
log.info(
(await blueColor())(
`Creating script metadata file for ${metadataFilePath}`
)
);
metadataFilePath = scriptPath + ".script.yaml";
let scriptInitialMetadata = defaultScriptMetadata();
const lockPath = scriptPath + ".script.lock";
scriptInitialMetadata.lock = "!inline " + lockPath;
+139 -1
View File
@@ -385,7 +385,145 @@ lock: ""
});
// =============================================================================
// Test 4: Apps - Create app via API and test filterWorkspaceDependenciesForApp
// Test 4: Hash consistency - generate-metadata (tree) then deprecated check (legacy)
// =============================================================================
test("Workspace deps: hash consistency between tree-based and legacy staleness checks", async () => {
await withTestBackend(async (backend, tempDir) => {
const testWorkspace = {
remote: backend.baseUrl,
workspaceId: backend.workspace,
name: "workspace_deps_hash_consistency_test",
token: backend.token
};
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
includes:
- "**"
excludes: []`, "utf-8");
// Setup workspace deps
await mkdir(`${tempDir}/dependencies`, { recursive: true });
const bunDep = `{"dependencies": {"lodash": "4.17.21"}}`;
await writeFile(`${tempDir}/dependencies/package.json`, bunDep, "utf-8");
// Setup script
await mkdir(`${tempDir}/f/test`, { recursive: true });
const scriptContent = `export async function main() {
return "uses default deps";
}
`;
const scriptMetadata = `summary: "test"
schema:
type: object
properties: {}
lock: ""
`;
await writeFile(`${tempDir}/f/test/my_script.ts`, scriptContent, "utf-8");
await writeFile(`${tempDir}/f/test/my_script.script.yaml`, scriptMetadata, "utf-8");
// Step 1: Run the NEW generate-metadata command (tree-based, stores hash with {} for deps)
const genResult = await backend.runCLICommand(
["generate-metadata", "--yes", "--schema-only"],
tempDir,
"workspace_deps_hash_consistency_test"
);
expect(genResult.code).toEqual(0);
expect(genResult.stdout).toContain("Done. Updated");
// Step 2: Run the DEPRECATED script generate-metadata --dry-run (legacy, checks hash with deps)
// This simulates what sync push does internally — the legacy check path.
// If hashes are inconsistent, this would falsely report the script as stale.
const legacyResult = await backend.runCLICommand(
["script", "generate-metadata", "-i", "f/test/my_script*", "--yes", "--dry-run"],
tempDir,
"workspace_deps_hash_consistency_test"
);
expect(legacyResult.code).toEqual(0);
// The legacy check should NOT find it stale — generate-metadata persisted
// the dep hashes, so getRawWorkspaceDependencies(true) returns {} for the
// legacy path, making the hash formula equivalent.
expect(legacyResult.stdout).toContain("No metadata to update");
});
});
// =============================================================================
// Test 5: Dep change after generate-metadata — legacy check detects staleness
// =============================================================================
test("Workspace deps: dep change after generate-metadata is detected by legacy check", async () => {
await withTestBackend(async (backend, tempDir) => {
const testWorkspace = {
remote: backend.baseUrl,
workspaceId: backend.workspace,
name: "workspace_deps_dep_change_test",
token: backend.token
};
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
includes:
- "**"
excludes: []`, "utf-8");
// Setup workspace deps
await mkdir(`${tempDir}/dependencies`, { recursive: true });
const bunDep = `{"dependencies": {"lodash": "4.17.21"}}`;
await writeFile(`${tempDir}/dependencies/package.json`, bunDep, "utf-8");
// Setup script (no annotation = uses default dep)
await mkdir(`${tempDir}/f/test`, { recursive: true });
const scriptContent = `export async function main() {
return "uses default deps";
}
`;
const scriptMetadata = `summary: "test"
schema:
type: object
properties: {}
lock: ""
`;
await writeFile(`${tempDir}/f/test/my_script.ts`, scriptContent, "utf-8");
await writeFile(`${tempDir}/f/test/my_script.script.yaml`, scriptMetadata, "utf-8");
// Step 1: Run generate-metadata to establish baseline hashes
const genResult = await backend.runCLICommand(
["generate-metadata", "--yes", "--schema-only"],
tempDir,
"workspace_deps_dep_change_test"
);
expect(genResult.code).toEqual(0);
// Step 2: Verify baseline is up-to-date via legacy check
const checkResult = await backend.runCLICommand(
["script", "generate-metadata", "-i", "f/test/my_script*", "--yes", "--dry-run"],
tempDir,
"workspace_deps_dep_change_test"
);
expect(checkResult.code).toEqual(0);
expect(checkResult.stdout).toContain("No metadata to update");
// Step 3: Change the workspace dep
const newBunDep = `{"dependencies": {"lodash": "4.17.22"}}`;
await writeFile(`${tempDir}/dependencies/package.json`, newBunDep, "utf-8");
// Step 4: Legacy check should detect the dep change as stale
// This works because getRawWorkspaceDependencies(true) returns the changed dep
// (its content hash no longer matches the lockfile), and the legacy path
// includes deps in the hash formula.
const afterDepChangeResult = await backend.runCLICommand(
["script", "generate-metadata", "-i", "f/test/my_script*", "--yes", "--dry-run"],
tempDir,
"workspace_deps_dep_change_test"
);
expect(afterDepChangeResult.code).toEqual(0);
expect(afterDepChangeResult.stdout).toContain("my_script");
});
});
// =============================================================================
// Test 6: Apps - Create app via API and test filterWorkspaceDependenciesForApp
// =============================================================================
test("Workspace deps: Apps - filterWorkspaceDependenciesForApp with real app via API", async () => {
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "windmill-components",
"version": "1.672.0",
"version": "1.673.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "windmill-components",
"version": "1.672.0",
"version": "1.673.0",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "windmill-components",
"version": "1.672.0",
"version": "1.673.0",
"scripts": {
"dev": "vite dev",
"build": "vite build",
+29 -2
View File
@@ -38,7 +38,10 @@
export async function run(overrideScheduledForStr?: string | undefined | null) {
let processedArgs: Record<string, any>
try {
processedArgs = await processSecretArgs(args ?? {}, runnable?.schema)
processedArgs = await processSecretArgs(
enforceDisabledDefaults(args ?? {}, true),
runnable?.schema
)
} catch (e) {
sendUserToast('Failed to process sensitive args: ' + e, true)
return
@@ -132,6 +135,30 @@
}
}
function enforceDisabledDefaults(
args: Record<string, any>,
notify: boolean = false
): Record<string, any> {
const schema = runnable?.schema
if (!schema?.properties) return args
const result = { ...args }
const resetKeys: string[] = []
for (const [key, prop] of Object.entries(schema.properties) as [string, any][]) {
if (prop?.disabled && 'default' in prop) {
if (notify && result[key] !== prop.default) {
resetKeys.push(key)
}
result[key] = prop.default
}
}
if (resetKeys.length > 0) {
sendUserToast(
`Disabled field${resetKeys.length > 1 ? 's' : ''} ${resetKeys.map((k) => `'${k}'`).join(', ')} reset to default value${resetKeys.length > 1 ? 's' : ''}`
)
}
return result
}
export function setCode(code: string) {
jsonEditor?.setCode(code)
}
@@ -249,7 +276,7 @@
bind:this={jsonEditor}
on:select={(e) => {
if (e.detail) {
args = e.detail
args = enforceDisabledDefaults(e.detail)
}
}}
updateOnBlur={false}
+53 -1
View File
@@ -6,6 +6,7 @@
RotateCcw,
Settings,
Trash,
Power,
X,
ExternalLink,
FileCode
@@ -14,7 +15,13 @@
import Badge from './common/badge/Badge.svelte'
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
import { ConfigService, WorkspaceService, type WorkerPing, type Workspace } from '$lib/gen'
import {
ConfigService,
SettingService,
WorkspaceService,
type WorkerPing,
type Workspace
} from '$lib/gen'
import ConfirmationModal from './common/confirmationModal/ConfirmationModal.svelte'
import { createEventDispatcher } from 'svelte'
import { sendUserToast } from '$lib/toast'
@@ -264,6 +271,7 @@
})
let openDelete = $state(false)
let openClean = $state(false)
let openRestart = $state(false)
// Compute hashed tags for display (actual tags used by the worker)
let hashedDedicatedTags: Map<string, string> = $state(new Map())
@@ -365,6 +373,32 @@
</div>
</ConfirmationModal>
<ConfirmationModal
open={openRestart}
title="Restart workers"
confirmationText="Restart"
on:canceled={() => {
openRestart = false
}}
on:confirmed={async () => {
try {
await SettingService.restartWorkerGroup({ workerGroup: name })
sendUserToast(`Restart signal sent to worker group '${name}'`)
dispatch('reload')
} catch (e) {
sendUserToast(`Failed to restart worker group: ${e}`, true)
}
openRestart = false
}}
>
<div class="flex flex-col w-full space-y-4">
<span
>Are you sure you want to restart all workers in worker group '{name}'? Workers will be
gracefully shut down and are expected to be restarted by their supervisor.</span
>
</div>
</ConfirmationModal>
<Drawer bind:this={drawer} size="800px">
<DrawerContent
on:close={() => drawer?.closeDrawer()}
@@ -1236,6 +1270,17 @@
>
Clean cache
</Button>
<Button
unifiedSize="sm"
variant="subtle"
on:click={() => {
openRestart = true
}}
startIcon={{ icon: Power }}
destructive
>
Restart workers
</Button>
{:else}
<Dropdown
items={[
@@ -1248,6 +1293,13 @@
disabled: !config,
type: 'delete'
},
{
displayName: 'Restart workers',
action: () => {
openRestart = true
},
type: 'delete'
},
{
displayName: 'Delete config',
action: () => {
@@ -227,6 +227,7 @@ class AIChatManager {
pendingPrompt?: string,
options?: {
closeScriptSettings?: boolean
lang?: ScriptLang | 'bunnative'
}
) {
if (mode === AIMode.SCRIPT && !tryGetCurrentModel()) return
@@ -235,7 +236,7 @@ class AIChatManager {
if (mode === AIMode.SCRIPT) {
const currentModel = getCurrentModel()
const customPrompt = getCombinedCustomPrompt(mode)
const lang = this.scriptEditorOptions?.lang ?? 'bun'
const lang = options?.lang ?? this.scriptEditorOptions?.lang ?? 'bun'
const context = this.contextManager.getSelectedContext()
this.systemMessage = prepareScriptSystemMessage(currentModel, lang, {}, customPrompt)
this.systemMessage.content = this.systemMessage.content
@@ -445,10 +446,7 @@ class AIChatManager {
if (!pendingPrompt) return undefined
this.pendingPrompt = ''
if (this.mode === AIMode.SCRIPT) {
return prepareScriptUserMessage(
pendingPrompt,
this.contextManager.getSelectedContext()
)
return prepareScriptUserMessage(pendingPrompt, this.contextManager.getSelectedContext())
} else if (this.mode === AIMode.FLOW) {
return prepareFlowUserMessage(
pendingPrompt,
@@ -574,9 +572,9 @@ class AIChatManager {
} = {}
) => {
if (options.mode) {
this.changeMode(options.mode)
this.changeMode(options.mode, undefined, { lang: options.lang })
} else {
this.changeMode(this.mode)
this.changeMode(this.mode, undefined, { lang: options.lang })
}
if (options.instructions) {
this.instructions = options.instructions
@@ -145,6 +145,14 @@
}}
options={{ right: 'Disable TLS' }}
/>
<Toggle
id="smtp_clicktracking_off"
{disabled}
bind:checked={$values['smtp_settings'].smtp_clicktracking_off}
size="xs"
options={{ right: 'Disable click tracking on links' }}
/>
</div>
<!-- Test Email -->
+1 -1
View File
@@ -4,7 +4,7 @@ verify_ssl = true
name = "pypi"
[packages]
wmill = ">=1.672.0"
wmill = ">=1.673.0"
sendgrid = "*"
mysql-connector-python = "*"
pymongo = "*"
+1 -1
View File
@@ -1,7 +1,7 @@
openapi: '3.0.3'
info:
version: 1.672.0
version: 1.673.0
title: OpenFlow Spec
contact:
name: Ruben Fiszel
@@ -12,7 +12,7 @@
RootModule = 'WindmillClient.psm1'
# Version number of this module.
ModuleVersion = '1.672.0'
ModuleVersion = '1.673.0'
# Supported PSEditions
# CompatiblePSEditions = @()
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "wmill"
version = "1.672.0"
version = "1.673.0"
description = "A client library for accessing Windmill server wrapping the Windmill client API"
license = "Apache-2.0"
homepage = "https://windmill.dev"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@windmill/windmill",
"version": "1.672.0",
"version": "1.673.0",
"exports": "./src/index.ts",
"publish": {
"exclude": ["!src", "./s3Types.ts", "./sqlUtils.ts", "./client.ts"]
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "windmill-client",
"description": "Windmill SDK client for browsers and Node.js",
"version": "1.672.0",
"version": "1.673.0",
"author": "Ruben Fiszel",
"license": "Apache 2.0",
"sideEffects": false,
+1 -1
View File
@@ -1 +1 @@
1.672.0
1.673.0