From 41d111bf387a934a8267543852c09b9e2bd7f525 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Wed, 26 Aug 2026 00:06:51 +0200 Subject: [PATCH 01/34] chore: emit only line tables for workspace crates in dev builds (#10845) * chore: emit only line tables for workspace crates in dev builds Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01UQiM8pqagY19bnWiMebwGa * docs: correct the CI comments that pinned profile.dev at debug = 2 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01UQiM8pqagY19bnWiMebwGa * docs: scope the windows debuginfo comment to the crates that job builds Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01UQiM8pqagY19bnWiMebwGa --------- Co-authored-by: Claude Opus 5 (1M context) --- .github/workflows/backend-test-windows.yml | 17 ++++++++--------- .github/workflows/backend-test.yml | 14 +++++++------- backend/Cargo.toml | 4 ++++ 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/.github/workflows/backend-test-windows.yml b/.github/workflows/backend-test-windows.yml index c43e4ece79..e2e7318ab6 100644 --- a/.github/workflows/backend-test-windows.yml +++ b/.github/workflows/backend-test-windows.yml @@ -179,15 +179,14 @@ jobs: # binary link spikes several hundred MB of transient I/O. Capping at # 8 trades ~25% wall time for headroom on the ~75GB runner disk. CARGO_BUILD_JOBS: 8 - # backend/Cargo.toml leaves profile.dev at the default debug = 2 for - # the (large) windmill workspace crates; that debuginfo is emitted - # into every object file and embedded in each test binary, and on - # windows-msvc also spawns the mspdbsrv.exe PDB type server. Across a - # full --all --features build it is the dominant consumer of the - # ~63GB free on the runner disk (LNK1180 / disk-full during linking). - # CI needs no debug info, so drop it entirely for the dev/test - # profiles here. debug = 0 supersedes the previous split-debuginfo=off - # knob (no debuginfo => no .pdb and no LNK1318 type-server limit). + # backend/Cargo.toml keeps line tables on profile.dev for the (large) + # windmill workspace crates; that debuginfo is emitted into every + # object file and embedded in each test binary, and on windows-msvc + # also spawns the mspdbsrv.exe PDB type server. Across the worker + # crates' test build it drives the peak on the ~63GB free of the + # runner disk (LNK1180 / disk-full during linking). CI reads no + # backtraces, so drop it entirely for the dev/test profiles here: + # debug = 0 means no .pdb and no LNK1318 type-server limit. CARGO_PROFILE_DEV_DEBUG: "0" CARGO_PROFILE_TEST_DEBUG: "0" # Tests' poll-time stack frames (deep nested async fn chains in diff --git a/.github/workflows/backend-test.yml b/.github/workflows/backend-test.yml index 5b6a461668..cdd0e2a212 100644 --- a/.github/workflows/backend-test.yml +++ b/.github/workflows/backend-test.yml @@ -268,13 +268,13 @@ jobs: # overhead and extra disk. Off here (kept on for local dev via # .cargo/config.toml). Matches backend-test-windows.yml. CARGO_INCREMENTAL: "0" - # backend/Cargo.toml leaves profile.dev at the default debug = 2 for - # the (large) windmill workspace crates; that debug info is emitted - # into every object file and embedded in each test binary. Across the - # full --all --features build it is the dominant memory/disk consumer - # when mold links the windmill-api-integration-tests binary, tipping - # the runner over (lost runner reported as a canceled step). CI needs - # no debug info, so drop it entirely for the dev/test profiles here. + # backend/Cargo.toml keeps line tables on profile.dev for the (large) + # windmill workspace crates; that debug info is emitted into every + # object file and embedded in each test binary. Across the full + # --all --features build it drives the memory/disk peak when mold + # links the windmill-api-integration-tests binary, tipping the runner + # over (lost runner reported as a canceled step). CI reads no + # backtraces, so drop it entirely for the dev/test profiles here. # (test profile inherits dev, but the workspace crates link in as # dev-profile deps, so both must be set.) CI-only; local dev builds # are unaffected. diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 50a5c34b34..6dd9e452b8 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -100,6 +100,10 @@ path = "./src/main.rs" opt-level = 0 incremental = true split-debuginfo = "unpacked" +# Type and variable DWARF is the single largest thing in target/ and nothing in the dev loop +# reads it; backtraces only need the line tables, which this keeps. Raise to `true` when you +# actually need to inspect variables in gdb/lldb. +debug = "line-tables-only" [profile.dev.package."*"] debug = false From 5dc43c400a8d8131eb77a4e91267c1cf37594c27 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Wed, 26 Aug 2026 00:31:21 +0200 Subject: [PATCH 02/34] report the EE gate instead of a 500 on restart flow at step (#10846) Claude-Session: https://claude.ai/code/session_01CbayDTXcGCTYuE9m56BRag Co-authored-by: Claude Opus 5 (1M context) --- .../tests/jobs_authed.rs | 19 +++++++++++++++---- backend/windmill-api/src/jobs.rs | 15 ++++++++------- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/backend/windmill-api-integration-tests/tests/jobs_authed.rs b/backend/windmill-api-integration-tests/tests/jobs_authed.rs index cd60275afc..c3da2f3fb9 100644 --- a/backend/windmill-api-integration-tests/tests/jobs_authed.rs +++ b/backend/windmill-api-integration-tests/tests/jobs_authed.rs @@ -286,13 +286,24 @@ async fn test_jobs_authed_reachability(db: Pool) -> anyhow::Result<()> "GET /jobs/result_by_id", ); + // Sent the way the generated client sends it. A handler whose `Path` tuple has drifted from + // the route is rejected by axum before it runs, which surfaces as a routing error rather + // than the handler's own answer, so reaching the handler is what this pins. let resp = authed(client().post(format!("{base}/restart/f/{fake}"))) + .json(&json!({ "step_id": "a" })) .send() .await?; - assert_route_reachable( - resp.status().as_u16(), - &resp.text().await?, - "POST /jobs/restart/f", + let status = resp.status().as_u16(); + let body = resp.text().await?; + assert_route_reachable(status, &body, "POST /jobs/restart/f"); + assert!( + !body.contains("path arguments"), + "POST /jobs/restart/f never reached its handler: {status} {body}", + ); + #[cfg(not(feature = "enterprise"))] + assert!( + body.contains("only available in enterprise version"), + "POST /jobs/restart/f must report the enterprise gate outside EE: {status} {body}", ); let resp = authed(client().post(format!("{base}/run/workflow_as_code/{fake}/main"))) diff --git a/backend/windmill-api/src/jobs.rs b/backend/windmill-api/src/jobs.rs index 513341b6c1..a30cffbfbf 100644 --- a/backend/windmill-api/src/jobs.rs +++ b/backend/windmill-api/src/jobs.rs @@ -6559,17 +6559,18 @@ pub async fn run_flow_by_version_inner( Ok((uuid, early_return, has_failure_module)) } +/// Path parameters of `POST /w/{workspace}/jobs/restart/f/{job_id}`, shared by the CE and EE +/// handlers. Axum only checks the tuple against the route at request time and rejects a +/// mismatch with an opaque 500 before the handler runs, so both must be declared from here: +/// an arity that drifts from the route hides the handler behind what reads as a broken route. +type RestartFlowPath = Path<(String, Uuid)>; + #[cfg(not(feature = "enterprise"))] pub async fn restart_flow( _authed: ApiAuthed, Extension(_db): Extension, Extension(_user_db): Extension, - Path((_w_id, _job_id, _step_id, _branch_or_iteration_n)): Path<( - String, - Uuid, - String, - Option, - )>, + Path((_w_id, _job_id)): RestartFlowPath, Query(_run_query): Query, ) -> error::Result<(StatusCode, String)> { return Err(Error::BadRequest( @@ -6798,7 +6799,7 @@ pub async fn restart_flow( authed: ApiAuthed, Extension(db): Extension, Extension(user_db): Extension, - Path((w_id, job_id)): Path<(String, Uuid)>, + Path((w_id, job_id)): RestartFlowPath, Query(run_query): Query, Json(RestartFlowRequestBody { step_id, From 46582245926a7f8ea961bcd125a58fbfba3530cf Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Wed, 26 Aug 2026 00:38:09 +0200 Subject: [PATCH 03/34] fix(debugger): parse bun 1.4's UUID inspector token (#10828) * fix(debugger): parse bun 1.4's UUID inspector token Bun 1.4 changed the inspector URL's token to a hyphenated UUID. The stderr scraper matched `[a-z0-9]+`, so it stopped at the first hyphen and connected to a truncated path, which the inspector answers with 404. Every TypeScript debug session has failed to attach since the 1.4.0 bump, taking the windmill-extra integration tests with it. Match the whole path, and only once its line is newline-terminated: a stderr chunk can end mid-URL and would otherwise be read as a complete, truncated URL. A close before the handshake completes is now reported as the connection failure it is, rather than as a finished script, and the debuggee is reaped - --inspect-wait blocks until a debugger attaches, so a failed attach leaked a bun process per session. On the test client, queue events that arrive before their waiter registers: the server sends 'initialized' immediately behind the 'initialize' response, which the client could drop and then time out waiting for. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01XMizaQRcnWRd79t5wWhjBN * fix(debugger): keep the first terminated event's result on launch failure A socket that drops after the handshake opens but mid-command-sequence reports the termination from onclose, carrying the script result, and then fails the launch. Sending a second terminated from the failure path overwrote that result with an error-only event. Guard the send the way every other emit site in the file does, leaving the reaping unconditional. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01XMizaQRcnWRd79t5wWhjBN * fix(debugger): report an inspector drop during setup as the failure it is The setup commands run over an open socket and none of them reject when it drops - sendInspectorCommand only has its own timer - so a drop between the upgrade and Inspector.initialized was reported as a clean termination, and the error surfaced up to 10s later or, once the duplicate was guarded, not at all. Draw the line at execution actually starting rather than at the socket opening, so those failures terminate with the connection error, immediately and once. Pair the "Failed to start Bun" output with the terminated event it explains, so a run that already reported its result cannot also be told it failed to launch. Prove the inspector URL complete with whitespace rather than an end-of-line: trailing text on the banner line would otherwise stall the parse for 10s. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01XMizaQRcnWRd79t5wWhjBN * fix(debugger): mark execution started only once the start command is answered Inspector.initialized is what starts the script, so setting the flag before awaiting its reply left a drop during that round trip looking like a clean termination - the same silent failure, narrowed to one command. Its reply precedes any close on the socket, so the continuation still runs before onclose and a real run is not misread as a failure. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01XMizaQRcnWRd79t5wWhjBN --------- Co-authored-by: Claude Opus 5 (1M context) --- debugger/dap_websocket_server_bun.ts | 50 +++++++++++++++++++++++++--- docker/test_windmill_extra.ts | 35 +++++++++++++------ 2 files changed, 69 insertions(+), 16 deletions(-) diff --git a/debugger/dap_websocket_server_bun.ts b/debugger/dap_websocket_server_bun.ts index 3b5072b1e6..d1e0ea1f14 100644 --- a/debugger/dap_websocket_server_bun.ts +++ b/debugger/dap_websocket_server_bun.ts @@ -1651,8 +1651,17 @@ export class DebugSession { try { await this.startBunProcess(cwd) } catch (error) { - this.sendEvent('output', { category: 'stderr', output: `Failed to start Bun: ${error}\n` }) - this.sendEvent('terminated', { error: String(error) }) + // A launch failure is reported here, a finished script from onclose; whichever gets + // there first owns the terminated event, so a client that already has a result is + // never told afterwards that the launch failed. + if (!this.terminatedSent) { + this.terminatedSent = true + this.sendEvent('output', { category: 'stderr', output: `Failed to start Bun: ${error}\n` }) + this.sendEvent('terminated', { error: String(error) }) + } + // --inspect-wait blocks until a debugger attaches, so a bun we failed to attach to + // waits forever unless it is reaped here. + await this.cleanup() } } @@ -1956,9 +1965,12 @@ export class DebugSession { const text = decoder.decode(value) buffer += text - // Look for the WebSocket URL in Bun's inspector output - // Format: "ws://127.0.0.1:9229/xxxxx" - const wsMatch = buffer.match(/ws:\/\/[\d.]+:\d+\/[a-z0-9]+/i) + // Look for the WebSocket URL in Bun's inspector banner, e.g. + // " ws://127.0.0.1:9229/848c719d-a52e-4610-8e94-99cd60f34af9". + // The token's alphabet is Bun's to change (it became a hyphenated UUID in 1.4), so + // take the whole path, and only once whitespace proves it complete: a stderr chunk + // can end mid-URL, and connecting to a truncated path gets a 404 from the inspector. + const wsMatch = buffer.match(/ws:\/\/[\d.]+:\d+\/\S+(?=\s)/) if (wsMatch && this.inspectorWsUrlPromise) { const wsUrl = wsMatch[0] logger.info(`Found inspector WebSocket URL in stderr: ${wsUrl}`) @@ -1988,12 +2000,22 @@ export class DebugSession { return new Promise((resolve, reject) => { this.inspectorWs = new WebSocket(wsUrl) + // A close before the script is running is a failed connection, not a finished script, + // and the two are reported to the client in opposite ways. The socket opening is not + // the line: the setup commands below run over an open socket and none of them reject + // when it drops (sendInspectorCommand only has its own timer), so a drop mid-setup + // would otherwise be indistinguishable from a clean exit. + let opened = false + let executionStarted = false + let handshakeError: string | null = null + const timeout = setTimeout(() => { reject(new Error('Inspector connection timeout')) }, 5000) this.inspectorWs.onopen = async () => { clearTimeout(timeout) + opened = true logger.info('Connected to inspector') try { @@ -2030,6 +2052,11 @@ export class DebugSession { logger.info('Starting script execution with Inspector.initialized...') await this.sendInspectorCommand('Inspector.initialized', {}) + // Only past its reply is a later close a finished script rather than a lost + // connection. The reply precedes any close on this socket, so the continuation + // runs first and a real run is never misread as a failure. + executionStarted = true + resolve() } catch (error) { reject(error) @@ -2042,12 +2069,25 @@ export class DebugSession { this.inspectorWs.onerror = (error) => { logger.error('Inspector WebSocket error:', error) + if (!opened) { + handshakeError = (error as ErrorEvent)?.message || String(error) + } } this.inspectorWs.onclose = () => { logger.info('Inspector WebSocket closed') this.inspectorWs = null + if (!executionStarted) { + clearTimeout(timeout) + reject( + new Error( + `Inspector connection failed: ${handshakeError ?? (opened ? 'closed before setup completed' : 'closed before the handshake completed')}` + ) + ) + return + } + // When inspector closes, the script has ended - send terminated event if (!this.terminatedSent) { this.terminatedSent = true diff --git a/docker/test_windmill_extra.ts b/docker/test_windmill_extra.ts index 46ffce54c9..5fd1a74108 100644 --- a/docker/test_windmill_extra.ts +++ b/docker/test_windmill_extra.ts @@ -206,7 +206,11 @@ class DAPTestClient { private events: DAPMessage[] = [] private output: string[] = [] private result: unknown = undefined - private eventHandlers = new Map void)[]>() + private eventWaiters = new Map void)[]>() + // An event that arrives before its waiter is registered is queued rather than dropped: the + // server sends 'initialized' right behind the 'initialize' response, and 'terminated' can + // land before the launch call the test awaits has even returned. + private bufferedEvents = new Map() async connect(endpoint: string): Promise { const url = `ws://${HOST}:${DEBUGGER_PORT}${endpoint}` @@ -273,9 +277,13 @@ class DAPTestClient { this.result = msg.body.result } - const handlers = this.eventHandlers.get(msg.event!) || [] - for (const handler of handlers) { - handler(msg) + const waiters = this.eventWaiters.get(msg.event!) + if (waiters && waiters.length > 0) { + waiters.shift()!(msg) + } else { + const buffered = this.bufferedEvents.get(msg.event!) || [] + buffered.push(msg) + this.bufferedEvents.set(msg.event!, buffered) } } } catch { @@ -313,23 +321,27 @@ class DAPTestClient { } waitForEvent(eventName: string, timeout = 10000): Promise { + const buffered = this.bufferedEvents.get(eventName) + if (buffered && buffered.length > 0) { + return Promise.resolve(buffered.shift()!) + } + return new Promise((resolve, reject) => { + const waiters = this.eventWaiters.get(eventName) || [] + this.eventWaiters.set(eventName, waiters) + const timer = setTimeout(() => { + const idx = waiters.indexOf(handler) + if (idx >= 0) waiters.splice(idx, 1) reject(new Error(`Timeout waiting for event: ${eventName}`)) }, timeout) const handler = (event: DAPMessage) => { clearTimeout(timer) - const handlers = this.eventHandlers.get(eventName) || [] - const idx = handlers.indexOf(handler) - if (idx >= 0) handlers.splice(idx, 1) resolve(event) } - if (!this.eventHandlers.has(eventName)) { - this.eventHandlers.set(eventName, []) - } - this.eventHandlers.get(eventName)!.push(handler) + waiters.push(handler) }) } @@ -379,6 +391,7 @@ class DAPTestClient { this.output = [] this.result = undefined this.events = [] + this.bufferedEvents.clear() } } From 9fa8159ad16204cab52fd18a34a48ebf13f800f6 Mon Sep 17 00:00:00 2001 From: hugocasa Date: Wed, 26 Aug 2026 00:41:15 +0200 Subject: [PATCH 04/34] fix: migrate slack resource-connect oauth to v2 (#10836) * fix: migrate slack resource-connect oauth to v2 Co-Authored-By: Claude Opus 5 * fix: keep slack scopes one per entry, as every other provider does Co-Authored-By: Claude Opus 5 --------- Co-authored-by: Claude Opus 5 --- backend/oauth_connect.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/oauth_connect.json b/backend/oauth_connect.json index cec49861cd..cb874eb2c7 100644 --- a/backend/oauth_connect.json +++ b/backend/oauth_connect.json @@ -16,9 +16,9 @@ "scopes": ["repository"] }, "slack": { - "auth_url": "https://slack.com/oauth/authorize", - "token_url": "https://slack.com/api/oauth.access", - "scopes": ["chat:write:user", "users:read", "users:read.email"] + "auth_url": "https://slack.com/oauth/v2/authorize", + "token_url": "https://slack.com/api/oauth.v2.access", + "scopes": ["chat:write", "chat:write.public", "channels:join", "files:write"] }, "supabase_wizard": { "auth_url": "https://api.supabase.com/v1/oauth/authorize", From 8a6dc27236aca67f0efe941d9606b787c2305ea8 Mon Sep 17 00:00:00 2001 From: Diego Imbert <70353967+diegoimbert@users.noreply.github.com> Date: Wed, 26 Aug 2026 00:47:24 +0200 Subject: [PATCH 05/34] feat: configurable expiry for presigned s3 public url signatures (#10835) * feat: configurable expiry for presigned s3 public url signatures Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_015JdZFeMXLGfeFNiQgx9QvA * fix: describe expiry_secs clamping in the spec and pin the bounds in a test Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_015JdZFeMXLGfeFNiQgx9QvA * fix: omit null expiry_secs from the python sdk sign request Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_015JdZFeMXLGfeFNiQgx9QvA --------- Co-authored-by: Claude Opus 5 (1M context) --- backend/tests/sign_s3_objects_authz.rs | 91 ++++++++++++++++++- backend/windmill-api/openapi.yaml | 4 + backend/windmill-api/src/apps.rs | 15 ++- cli/src/guidance/skills.gen.ts | 52 +++++++---- python-client/wmill/wmill/client.py | 62 ++++++++++--- system_prompts/auto-generated/prompts.ts | 28 ++++-- system_prompts/auto-generated/script.md | 28 ++++-- system_prompts/auto-generated/sdks/python.md | 16 +++- .../auto-generated/sdks/typescript.md | 12 ++- .../skills/write-script-bun/SKILL.md | 12 ++- .../skills/write-script-bunnative/SKILL.md | 12 ++- .../skills/write-script-deno/SKILL.md | 12 ++- .../skills/write-script-python3/SKILL.md | 16 +++- typescript-client/client.d.ts | 16 +++- typescript-client/client.ts | 27 ++++-- 15 files changed, 322 insertions(+), 81 deletions(-) diff --git a/backend/tests/sign_s3_objects_authz.rs b/backend/tests/sign_s3_objects_authz.rs index a2baf9d449..511ef39ab5 100644 --- a/backend/tests/sign_s3_objects_authz.rs +++ b/backend/tests/sign_s3_objects_authz.rs @@ -17,7 +17,11 @@ //! so two test functions sharing the one fixture workspace serve each other's stale — by then //! deleted — filesystem root. //! -//! Advanced S3 permissions are an enterprise feature, so this test requires the +//! A second test pins the `expiry_secs` bounds: the signature's `exp` follows the caller's +//! request, defaults to 12h, and is clamped to [60s, 7d]. It only mints signatures and never +//! fetches through the proxy, so it never populates or reads that cache. +//! +//! Advanced S3 permissions are an enterprise feature, so these tests require the //! `enterprise` + `private` + `parquet` features. #![cfg(all(feature = "enterprise", feature = "private", feature = "parquet"))] @@ -175,3 +179,88 @@ async fn test_sign_s3_objects_enforces_read_authz(db: Pool) -> anyhow: Ok(()) } + +/// `exp` is signed into the HMAC message, so the only way a caller can influence +/// it is through `expiry_secs` — pin the default and both clamp bounds. +#[sqlx::test(fixtures("base"))] +async fn test_sign_s3_objects_expiry_secs(db: Pool) -> anyhow::Result<()> { + initialize_tracing().await; + + let server = ApiServer::start(db.clone()).await?; + let port = server.addr.port(); + let base = format!("http://localhost:{port}/api/w/test-workspace"); + + let storage_dir = tempfile::tempdir()?; + configure_lfs(&db, &storage_dir.path().to_string_lossy()).await?; + + async fn signed_exp(base: &str, body: serde_json::Value) -> anyhow::Result { + let resp = authed( + client().post(format!("{base}/apps/sign_s3_objects")), + "SECRET_TOKEN", + ) + .json(&body) + .send() + .await?; + let status = resp.status(); + let signed: serde_json::Value = resp.json().await?; + assert!(status.is_success(), "sign must succeed: {status} {signed}"); + let presigned = signed[0]["presigned"] + .as_str() + .expect("sign must return a presigned string"); + let exp = presigned + .split('&') + .find_map(|kv| kv.strip_prefix("exp=")) + .expect("presigned string must carry exp"); + Ok(exp.parse::()?) + } + + let key = json!([{ "s3": "allowed/file.txt" }]); + // The handler stamps `now` itself, so assert on a window rather than an exact value. + // Keep the window well under the 60s lower bound, or an unclamped 1s would pass. + let ttl_around = |exp: i64| exp - chrono::Utc::now().timestamp(); + let tolerance = 30; + + let default_ttl = ttl_around(signed_exp(&base, json!({ "s3_objects": key.clone() })).await?); + assert!( + (43200 - tolerance..=43200).contains(&default_ttl), + "omitting expiry_secs must keep the 12h default, got {default_ttl}s" + ); + + let honored = ttl_around( + signed_exp( + &base, + json!({ "s3_objects": key.clone(), "expiry_secs": 300 }), + ) + .await?, + ); + assert!( + (300 - tolerance..=300).contains(&honored), + "expiry_secs must be honored verbatim inside the bounds, got {honored}s" + ); + + let clamped_low = ttl_around( + signed_exp( + &base, + json!({ "s3_objects": key.clone(), "expiry_secs": 1 }), + ) + .await?, + ); + assert!( + (60 - tolerance..=60).contains(&clamped_low), + "expiry_secs below 60s must clamp up to 60s, got {clamped_low}s" + ); + + let clamped_high = ttl_around( + signed_exp( + &base, + json!({ "s3_objects": key.clone(), "expiry_secs": 99_999_999 }), + ) + .await?, + ); + assert!( + (604800 - tolerance..=604800).contains(&clamped_high), + "expiry_secs above 7d must clamp down to 7d, got {clamped_high}s" + ); + + Ok(()) +} diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 1fe3423316..d88c633106 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -13468,6 +13468,10 @@ paths: type: array items: $ref: "#/components/schemas/S3Object" + expiry_secs: + type: integer + format: int64 + description: how long the signature stays valid, in seconds. Defaults to 43200 (12h) and is clamped server-side to [60, 604800] (1 minute to 7 days). required: - s3_objects responses: diff --git a/backend/windmill-api/src/apps.rs b/backend/windmill-api/src/apps.rs index 0e64d1cba2..7bc995844d 100644 --- a/backend/windmill-api/src/apps.rs +++ b/backend/windmill-api/src/apps.rs @@ -4112,10 +4112,18 @@ struct S3DeleteTokenClaims { pub exp: usize, } +#[cfg(feature = "parquet")] +const SIGN_S3_DEFAULT_EXPIRY_SECS: i64 = 12 * 60 * 60; +#[cfg(feature = "parquet")] +const SIGN_S3_MIN_EXPIRY_SECS: i64 = 60; +#[cfg(feature = "parquet")] +const SIGN_S3_MAX_EXPIRY_SECS: i64 = 7 * 24 * 60 * 60; + #[cfg(feature = "parquet")] #[derive(Deserialize)] struct S3TokenRequestBody { s3_objects: Vec, + expiry_secs: Option, } #[cfg(feature = "parquet")] async fn sign_s3_objects( @@ -4126,6 +4134,12 @@ async fn sign_s3_objects( ) -> Result>> { let workspace_key = get_workspace_key(&w_id, &db).await?; + let expiry_secs = body + .expiry_secs + .unwrap_or(SIGN_S3_DEFAULT_EXPIRY_SECS) + .clamp(SIGN_S3_MIN_EXPIRY_SECS, SIGN_S3_MAX_EXPIRY_SECS); + let exp = (chrono::Utc::now() + chrono::Duration::seconds(expiry_secs)).timestamp(); + let futures = body.s3_objects.into_iter().map(|s3_object| async { // The signature this mints is a transferable bearer capability: `validate_s3_signature` // only checks the HMAC and expiry, so anyone who obtains the string can read this key. @@ -4156,7 +4170,6 @@ async fn sign_s3_objects( ) .await?; - let exp = (chrono::Utc::now() + chrono::Duration::hours(12)).timestamp(); let message = format!( "file_key={}&exp={}{}", s3_object.s3.clone(), diff --git a/cli/src/guidance/skills.gen.ts b/cli/src/guidance/skills.gen.ts index 0f57c21463..b5cb3417f4 100644 --- a/cli/src/guidance/skills.gen.ts +++ b/cli/src/guidance/skills.gen.ts @@ -848,31 +848,35 @@ async deleteS3File(s3object: S3Object, workspace: string | undefined = undefined /** * Sign S3 objects to be used by anonymous users in public apps * @param s3objects s3 objects to sign + * @param expirySecs how long the signature stays valid, in seconds (default 43200 = 12h, clamped to [60, 604800]) * @returns signed s3 objects */ -async signS3Objects(s3objects: S3Object[]): Promise +async signS3Objects(s3objects: S3Object[], { expirySecs }: { expirySecs?: number } = {}): Promise /** * Sign S3 object to be used by anonymous users in public apps * @param s3object s3 object to sign + * @param expirySecs how long the signature stays valid, in seconds (default 43200 = 12h, clamped to [60, 604800]) * @returns signed s3 object */ -async signS3Object(s3object: S3Object): Promise +async signS3Object(s3object: S3Object, { expirySecs }: { expirySecs?: number } = {}): Promise /** * Generate a presigned public URL for an array of S3 objects. * If an S3 object is not signed yet, it will be signed first. * @param s3Objects s3 objects to sign + * @param expirySecs how long the signature stays valid, in seconds (default 43200 = 12h, clamped to [60, 604800]) * @returns list of signed public URLs */ -async getPresignedS3PublicUrls(s3Objects: S3Object[], { baseUrl }: { baseUrl?: string } = {}): Promise +async getPresignedS3PublicUrls(s3Objects: S3Object[], { baseUrl, expirySecs }: { baseUrl?: string; expirySecs?: number } = {}): Promise /** * Generate a presigned public URL for an S3 object. If the S3 object is not signed yet, it will be signed first. * @param s3Object s3 object to sign + * @param expirySecs how long the signature stays valid, in seconds (default 43200 = 12h, clamped to [60, 604800]) * @returns signed public URL */ -async getPresignedS3PublicUrl(s3Objects: S3Object, { baseUrl }: { baseUrl?: string } = {}): Promise +async getPresignedS3PublicUrl(s3Objects: S3Object, { baseUrl, expirySecs }: { baseUrl?: string; expirySecs?: number } = {}): Promise /** * Get URLs needed for resuming a flow after this step @@ -1631,31 +1635,35 @@ async deleteS3File(s3object: S3Object, workspace: string | undefined = undefined /** * Sign S3 objects to be used by anonymous users in public apps * @param s3objects s3 objects to sign + * @param expirySecs how long the signature stays valid, in seconds (default 43200 = 12h, clamped to [60, 604800]) * @returns signed s3 objects */ -async signS3Objects(s3objects: S3Object[]): Promise +async signS3Objects(s3objects: S3Object[], { expirySecs }: { expirySecs?: number } = {}): Promise /** * Sign S3 object to be used by anonymous users in public apps * @param s3object s3 object to sign + * @param expirySecs how long the signature stays valid, in seconds (default 43200 = 12h, clamped to [60, 604800]) * @returns signed s3 object */ -async signS3Object(s3object: S3Object): Promise +async signS3Object(s3object: S3Object, { expirySecs }: { expirySecs?: number } = {}): Promise /** * Generate a presigned public URL for an array of S3 objects. * If an S3 object is not signed yet, it will be signed first. * @param s3Objects s3 objects to sign + * @param expirySecs how long the signature stays valid, in seconds (default 43200 = 12h, clamped to [60, 604800]) * @returns list of signed public URLs */ -async getPresignedS3PublicUrls(s3Objects: S3Object[], { baseUrl }: { baseUrl?: string } = {}): Promise +async getPresignedS3PublicUrls(s3Objects: S3Object[], { baseUrl, expirySecs }: { baseUrl?: string; expirySecs?: number } = {}): Promise /** * Generate a presigned public URL for an S3 object. If the S3 object is not signed yet, it will be signed first. * @param s3Object s3 object to sign + * @param expirySecs how long the signature stays valid, in seconds (default 43200 = 12h, clamped to [60, 604800]) * @returns signed public URL */ -async getPresignedS3PublicUrl(s3Objects: S3Object, { baseUrl }: { baseUrl?: string } = {}): Promise +async getPresignedS3PublicUrl(s3Objects: S3Object, { baseUrl, expirySecs }: { baseUrl?: string; expirySecs?: number } = {}): Promise /** * Get URLs needed for resuming a flow after this step @@ -2508,31 +2516,35 @@ async deleteS3File(s3object: S3Object, workspace: string | undefined = undefined /** * Sign S3 objects to be used by anonymous users in public apps * @param s3objects s3 objects to sign + * @param expirySecs how long the signature stays valid, in seconds (default 43200 = 12h, clamped to [60, 604800]) * @returns signed s3 objects */ -async signS3Objects(s3objects: S3Object[]): Promise +async signS3Objects(s3objects: S3Object[], { expirySecs }: { expirySecs?: number } = {}): Promise /** * Sign S3 object to be used by anonymous users in public apps * @param s3object s3 object to sign + * @param expirySecs how long the signature stays valid, in seconds (default 43200 = 12h, clamped to [60, 604800]) * @returns signed s3 object */ -async signS3Object(s3object: S3Object): Promise +async signS3Object(s3object: S3Object, { expirySecs }: { expirySecs?: number } = {}): Promise /** * Generate a presigned public URL for an array of S3 objects. * If an S3 object is not signed yet, it will be signed first. * @param s3Objects s3 objects to sign + * @param expirySecs how long the signature stays valid, in seconds (default 43200 = 12h, clamped to [60, 604800]) * @returns list of signed public URLs */ -async getPresignedS3PublicUrls(s3Objects: S3Object[], { baseUrl }: { baseUrl?: string } = {}): Promise +async getPresignedS3PublicUrls(s3Objects: S3Object[], { baseUrl, expirySecs }: { baseUrl?: string; expirySecs?: number } = {}): Promise /** * Generate a presigned public URL for an S3 object. If the S3 object is not signed yet, it will be signed first. * @param s3Object s3 object to sign + * @param expirySecs how long the signature stays valid, in seconds (default 43200 = 12h, clamped to [60, 604800]) * @returns signed public URL */ -async getPresignedS3PublicUrl(s3Objects: S3Object, { baseUrl }: { baseUrl?: string } = {}): Promise +async getPresignedS3PublicUrl(s3Objects: S3Object, { baseUrl, expirySecs }: { baseUrl?: string; expirySecs?: number } = {}): Promise /** * Get URLs needed for resuming a flow after this step @@ -4225,19 +4237,23 @@ def delete_s3_object(s3object: S3Object | str, s3_resource_path: str | None = No # # Args: # s3_objects: List of S3 objects to sign +# expiry_secs: How long the signature stays valid, in seconds +# (defaults to 43200 = 12h, clamped to [60, 604800]) # # Returns: # List of signed S3 objects -def sign_s3_objects(s3_objects: list[S3Object | str]) -> list[S3Object] +def sign_s3_objects(s3_objects: list[S3Object | str], expiry_secs: int | None = None) -> list[S3Object] # Sign a single S3 object for use by anonymous users in public apps. # # Args: # s3_object: S3 object to sign +# expiry_secs: How long the signature stays valid, in seconds +# (defaults to 43200 = 12h, clamped to [60, 604800]) # # Returns: # Signed S3 object -def sign_s3_object(s3_object: S3Object | str) -> S3Object +def sign_s3_object(s3_object: S3Object | str, expiry_secs: int | None = None) -> S3Object # Generate presigned public URLs for an array of S3 objects. # If an S3 object is not signed yet, it will be signed first. @@ -4245,6 +4261,8 @@ def sign_s3_object(s3_object: S3Object | str) -> S3Object # Args: # s3_objects: List of S3 objects to sign # base_url: Optional base URL for the presigned URLs (defaults to WM_BASE_URL) +# expiry_secs: How long the signatures stay valid, in seconds +# (defaults to 43200 = 12h, clamped to [60, 604800]) # # Returns: # List of signed public URLs @@ -4252,7 +4270,7 @@ def sign_s3_object(s3_object: S3Object | str) -> S3Object # Example: # >>> s3_objs = [S3Object(s3="/path/to/file1.txt"), S3Object(s3="/path/to/file2.txt")] # >>> urls = client.get_presigned_s3_public_urls(s3_objs) -def get_presigned_s3_public_urls(s3_objects: list[S3Object | str], base_url: str | None = None) -> list[str] +def get_presigned_s3_public_urls(s3_objects: list[S3Object | str], base_url: str | None = None, expiry_secs: int | None = None) -> list[str] # Generate a presigned public URL for an S3 object. # If the S3 object is not signed yet, it will be signed first. @@ -4260,6 +4278,8 @@ def get_presigned_s3_public_urls(s3_objects: list[S3Object | str], base_url: str # Args: # s3_object: S3 object to sign # base_url: Optional base URL for the presigned URL (defaults to WM_BASE_URL) +# expiry_secs: How long the signature stays valid, in seconds +# (defaults to 43200 = 12h, clamped to [60, 604800]) # # Returns: # Signed public URL @@ -4267,7 +4287,7 @@ def get_presigned_s3_public_urls(s3_objects: list[S3Object | str], base_url: str # Example: # >>> s3_obj = S3Object(s3="/path/to/file.txt") # >>> url = client.get_presigned_s3_public_url(s3_obj) -def get_presigned_s3_public_url(s3_object: S3Object | str, base_url: str | None = None) -> str +def get_presigned_s3_public_url(s3_object: S3Object | str, base_url: str | None = None, expiry_secs: int | None = None) -> str # Get the current user information. # diff --git a/python-client/wmill/wmill/client.py b/python-client/wmill/wmill/client.py index 9400dabdc7..3c0ce050fb 100644 --- a/python-client/wmill/wmill/client.py +++ b/python-client/wmill/wmill/client.py @@ -31,6 +31,15 @@ logger = logging.getLogger("windmill_client") JobStatus = Literal["RUNNING", "WAITING", "COMPLETED"] +def _sign_s3_objects_body(s3_objects: list, expiry_secs: int | None) -> dict: + # `expiry_secs` is optional but not nullable in the spec, so omit it rather than + # sending an explicit null a validating gateway would reject. + body: dict = {"s3_objects": s3_objects} + if expiry_secs is not None: + body["expiry_secs"] = expiry_secs + return body + + class Windmill: """Windmill client for interacting with the Windmill API.""" @@ -1044,37 +1053,45 @@ class Windmill: except Exception as e: raise Exception("Could not delete file from S3") from e - def sign_s3_objects(self, s3_objects: list[S3Object | str]) -> list[S3Object]: + def sign_s3_objects( + self, s3_objects: list[S3Object | str], expiry_secs: int | None = None + ) -> list[S3Object]: """Sign S3 objects for use by anonymous users in public apps. Args: s3_objects: List of S3 objects to sign + expiry_secs: How long the signature stays valid, in seconds + (defaults to 43200 = 12h, clamped to [60, 604800]) Returns: List of signed S3 objects """ return self.post( - f"/w/{self.workspace}/apps/sign_s3_objects", json={"s3_objects": list(map(parse_s3_object, s3_objects))} + f"/w/{self.workspace}/apps/sign_s3_objects", + json=_sign_s3_objects_body(list(map(parse_s3_object, s3_objects)), expiry_secs), ).json() - def sign_s3_object(self, s3_object: S3Object | str) -> S3Object: + def sign_s3_object(self, s3_object: S3Object | str, expiry_secs: int | None = None) -> S3Object: """Sign a single S3 object for use by anonymous users in public apps. Args: s3_object: S3 object to sign + expiry_secs: How long the signature stays valid, in seconds + (defaults to 43200 = 12h, clamped to [60, 604800]) Returns: Signed S3 object """ return self.post( f"/w/{self.workspace}/apps/sign_s3_objects", - json={"s3_objects": [s3_object]}, + json=_sign_s3_objects_body([s3_object], expiry_secs), ).json()[0] def get_presigned_s3_public_urls( self, s3_objects: list[S3Object | str], base_url: str | None = None, + expiry_secs: int | None = None, ) -> list[str]: """ Generate presigned public URLs for an array of S3 objects. @@ -1083,6 +1100,8 @@ class Windmill: Args: s3_objects: List of S3 objects to sign base_url: Optional base URL for the presigned URLs (defaults to WM_BASE_URL) + expiry_secs: How long the signatures stay valid, in seconds + (defaults to 43200 = 12h, clamped to [60, 604800]) Returns: List of signed public URLs @@ -1104,7 +1123,7 @@ class Windmill: if s3_objs_to_sign: signed_s3_objs = self.sign_s3_objects( - [s3_obj for s3_obj, _ in s3_objs_to_sign] + [s3_obj for s3_obj, _ in s3_objs_to_sign], expiry_secs ) for i, (_, original_index) in enumerate(s3_objs_to_sign): s3_objs[original_index] = parse_s3_object(signed_s3_objs[i]) @@ -1123,6 +1142,7 @@ class Windmill: self, s3_object: S3Object | str, base_url: str | None = None, + expiry_secs: int | None = None, ) -> str: """ Generate a presigned public URL for an S3 object. @@ -1131,6 +1151,8 @@ class Windmill: Args: s3_object: S3 object to sign base_url: Optional base URL for the presigned URL (defaults to WM_BASE_URL) + expiry_secs: How long the signature stays valid, in seconds + (defaults to 43200 = 12h, clamped to [60, 604800]) Returns: Signed public URL @@ -1139,7 +1161,7 @@ class Windmill: >>> s3_obj = S3Object(s3="/path/to/file.txt") >>> url = client.get_presigned_s3_public_url(s3_obj) """ - urls = self.get_presigned_s3_public_urls([s3_object], base_url) + urls = self.get_presigned_s3_public_urls([s3_object], base_url, expiry_secs) return urls[0] def _get_public_base_url(self) -> str: @@ -1814,27 +1836,38 @@ def delete_s3_object( @init_global_client -def sign_s3_objects(s3_objects: list[S3Object | str]) -> list[S3Object]: +def sign_s3_objects(s3_objects: list[S3Object | str], expiry_secs: int | None = None) -> list[S3Object]: """ Sign S3 objects to be used by anonymous users in public apps Returns a list of signed s3 tokens + + Args: + s3_objects: List of S3 objects to sign + expiry_secs: How long the signatures stay valid, in seconds + (defaults to 43200 = 12h, clamped to [60, 604800]) """ - return _client.sign_s3_objects(s3_objects) + return _client.sign_s3_objects(s3_objects, expiry_secs) @init_global_client -def sign_s3_object(s3_object: S3Object| str) -> S3Object: +def sign_s3_object(s3_object: S3Object| str, expiry_secs: int | None = None) -> S3Object: """ Sign S3 object to be used by anonymous users in public apps Returns a signed s3 object + + Args: + s3_object: S3 object to sign + expiry_secs: How long the signature stays valid, in seconds + (defaults to 43200 = 12h, clamped to [60, 604800]) """ - return _client.sign_s3_object(s3_object) + return _client.sign_s3_object(s3_object, expiry_secs) @init_global_client def get_presigned_s3_public_urls( s3_objects: list[S3Object | str], base_url: str | None = None, + expiry_secs: int | None = None, ) -> list[str]: """ Generate presigned public URLs for an array of S3 objects. @@ -1843,6 +1876,8 @@ def get_presigned_s3_public_urls( Args: s3_objects: List of S3 objects to sign base_url: Optional base URL for the presigned URLs (defaults to WM_BASE_URL) + expiry_secs: How long the signatures stay valid, in seconds + (defaults to 43200 = 12h, clamped to [60, 604800]) Returns: List of signed public URLs @@ -1853,13 +1888,14 @@ def get_presigned_s3_public_urls( >>> s3_objs = [S3Object(s3="/path/to/file1.txt"), S3Object(s3="/path/to/file2.txt")] >>> urls = wmill.get_presigned_s3_public_urls(s3_objs) """ - return _client.get_presigned_s3_public_urls(s3_objects, base_url) + return _client.get_presigned_s3_public_urls(s3_objects, base_url, expiry_secs) @init_global_client def get_presigned_s3_public_url( s3_object: S3Object | str, base_url: str | None = None, + expiry_secs: int | None = None, ) -> str: """ Generate a presigned public URL for an S3 object. @@ -1868,6 +1904,8 @@ def get_presigned_s3_public_url( Args: s3_object: S3 object to sign base_url: Optional base URL for the presigned URL (defaults to WM_BASE_URL) + expiry_secs: How long the signature stays valid, in seconds + (defaults to 43200 = 12h, clamped to [60, 604800]) Returns: Signed public URL @@ -1878,7 +1916,7 @@ def get_presigned_s3_public_url( >>> s3_obj = S3Object(s3="/path/to/file.txt") >>> url = wmill.get_presigned_s3_public_url(s3_obj) """ - return _client.get_presigned_s3_public_url(s3_object, base_url) + return _client.get_presigned_s3_public_url(s3_object, base_url, expiry_secs) @init_global_client diff --git a/system_prompts/auto-generated/prompts.ts b/system_prompts/auto-generated/prompts.ts index d6fe7689f9..2c3b585c20 100644 --- a/system_prompts/auto-generated/prompts.ts +++ b/system_prompts/auto-generated/prompts.ts @@ -1546,31 +1546,35 @@ async deleteS3File(s3object: S3Object, workspace: string | undefined = undefined /** * Sign S3 objects to be used by anonymous users in public apps * @param s3objects s3 objects to sign + * @param expirySecs how long the signature stays valid, in seconds (default 43200 = 12h, clamped to [60, 604800]) * @returns signed s3 objects */ -async signS3Objects(s3objects: S3Object[]): Promise +async signS3Objects(s3objects: S3Object[], { expirySecs }: { expirySecs?: number } = {}): Promise /** * Sign S3 object to be used by anonymous users in public apps * @param s3object s3 object to sign + * @param expirySecs how long the signature stays valid, in seconds (default 43200 = 12h, clamped to [60, 604800]) * @returns signed s3 object */ -async signS3Object(s3object: S3Object): Promise +async signS3Object(s3object: S3Object, { expirySecs }: { expirySecs?: number } = {}): Promise /** * Generate a presigned public URL for an array of S3 objects. * If an S3 object is not signed yet, it will be signed first. * @param s3Objects s3 objects to sign + * @param expirySecs how long the signature stays valid, in seconds (default 43200 = 12h, clamped to [60, 604800]) * @returns list of signed public URLs */ -async getPresignedS3PublicUrls(s3Objects: S3Object[], { baseUrl }: { baseUrl?: string } = {}): Promise +async getPresignedS3PublicUrls(s3Objects: S3Object[], { baseUrl, expirySecs }: { baseUrl?: string; expirySecs?: number } = {}): Promise /** * Generate a presigned public URL for an S3 object. If the S3 object is not signed yet, it will be signed first. * @param s3Object s3 object to sign + * @param expirySecs how long the signature stays valid, in seconds (default 43200 = 12h, clamped to [60, 604800]) * @returns signed public URL */ -async getPresignedS3PublicUrl(s3Objects: S3Object, { baseUrl }: { baseUrl?: string } = {}): Promise +async getPresignedS3PublicUrl(s3Objects: S3Object, { baseUrl, expirySecs }: { baseUrl?: string; expirySecs?: number } = {}): Promise /** * Get URLs needed for resuming a flow after this step @@ -2169,19 +2173,23 @@ def delete_s3_object(s3object: S3Object | str, s3_resource_path: str | None = No # # Args: # s3_objects: List of S3 objects to sign +# expiry_secs: How long the signature stays valid, in seconds +# (defaults to 43200 = 12h, clamped to [60, 604800]) # # Returns: # List of signed S3 objects -def sign_s3_objects(s3_objects: list[S3Object | str]) -> list[S3Object] +def sign_s3_objects(s3_objects: list[S3Object | str], expiry_secs: int | None = None) -> list[S3Object] # Sign a single S3 object for use by anonymous users in public apps. # # Args: # s3_object: S3 object to sign +# expiry_secs: How long the signature stays valid, in seconds +# (defaults to 43200 = 12h, clamped to [60, 604800]) # # Returns: # Signed S3 object -def sign_s3_object(s3_object: S3Object | str) -> S3Object +def sign_s3_object(s3_object: S3Object | str, expiry_secs: int | None = None) -> S3Object # Generate presigned public URLs for an array of S3 objects. # If an S3 object is not signed yet, it will be signed first. @@ -2189,6 +2197,8 @@ def sign_s3_object(s3_object: S3Object | str) -> S3Object # Args: # s3_objects: List of S3 objects to sign # base_url: Optional base URL for the presigned URLs (defaults to WM_BASE_URL) +# expiry_secs: How long the signatures stay valid, in seconds +# (defaults to 43200 = 12h, clamped to [60, 604800]) # # Returns: # List of signed public URLs @@ -2196,7 +2206,7 @@ def sign_s3_object(s3_object: S3Object | str) -> S3Object # Example: # >>> s3_objs = [S3Object(s3="/path/to/file1.txt"), S3Object(s3="/path/to/file2.txt")] # >>> urls = client.get_presigned_s3_public_urls(s3_objs) -def get_presigned_s3_public_urls(s3_objects: list[S3Object | str], base_url: str | None = None) -> list[str] +def get_presigned_s3_public_urls(s3_objects: list[S3Object | str], base_url: str | None = None, expiry_secs: int | None = None) -> list[str] # Generate a presigned public URL for an S3 object. # If the S3 object is not signed yet, it will be signed first. @@ -2204,6 +2214,8 @@ def get_presigned_s3_public_urls(s3_objects: list[S3Object | str], base_url: str # Args: # s3_object: S3 object to sign # base_url: Optional base URL for the presigned URL (defaults to WM_BASE_URL) +# expiry_secs: How long the signature stays valid, in seconds +# (defaults to 43200 = 12h, clamped to [60, 604800]) # # Returns: # Signed public URL @@ -2211,7 +2223,7 @@ def get_presigned_s3_public_urls(s3_objects: list[S3Object | str], base_url: str # Example: # >>> s3_obj = S3Object(s3="/path/to/file.txt") # >>> url = client.get_presigned_s3_public_url(s3_obj) -def get_presigned_s3_public_url(s3_object: S3Object | str, base_url: str | None = None) -> str +def get_presigned_s3_public_url(s3_object: S3Object | str, base_url: str | None = None, expiry_secs: int | None = None) -> str # Get the current user information. # diff --git a/system_prompts/auto-generated/script.md b/system_prompts/auto-generated/script.md index 9b976e347f..0b67fa0027 100644 --- a/system_prompts/auto-generated/script.md +++ b/system_prompts/auto-generated/script.md @@ -1738,31 +1738,35 @@ async deleteS3File(s3object: S3Object, workspace: string | undefined = undefined /** * Sign S3 objects to be used by anonymous users in public apps * @param s3objects s3 objects to sign + * @param expirySecs how long the signature stays valid, in seconds (default 43200 = 12h, clamped to [60, 604800]) * @returns signed s3 objects */ -async signS3Objects(s3objects: S3Object[]): Promise +async signS3Objects(s3objects: S3Object[], { expirySecs }: { expirySecs?: number } = {}): Promise /** * Sign S3 object to be used by anonymous users in public apps * @param s3object s3 object to sign + * @param expirySecs how long the signature stays valid, in seconds (default 43200 = 12h, clamped to [60, 604800]) * @returns signed s3 object */ -async signS3Object(s3object: S3Object): Promise +async signS3Object(s3object: S3Object, { expirySecs }: { expirySecs?: number } = {}): Promise /** * Generate a presigned public URL for an array of S3 objects. * If an S3 object is not signed yet, it will be signed first. * @param s3Objects s3 objects to sign + * @param expirySecs how long the signature stays valid, in seconds (default 43200 = 12h, clamped to [60, 604800]) * @returns list of signed public URLs */ -async getPresignedS3PublicUrls(s3Objects: S3Object[], { baseUrl }: { baseUrl?: string } = {}): Promise +async getPresignedS3PublicUrls(s3Objects: S3Object[], { baseUrl, expirySecs }: { baseUrl?: string; expirySecs?: number } = {}): Promise /** * Generate a presigned public URL for an S3 object. If the S3 object is not signed yet, it will be signed first. * @param s3Object s3 object to sign + * @param expirySecs how long the signature stays valid, in seconds (default 43200 = 12h, clamped to [60, 604800]) * @returns signed public URL */ -async getPresignedS3PublicUrl(s3Objects: S3Object, { baseUrl }: { baseUrl?: string } = {}): Promise +async getPresignedS3PublicUrl(s3Objects: S3Object, { baseUrl, expirySecs }: { baseUrl?: string; expirySecs?: number } = {}): Promise /** * Get URLs needed for resuming a flow after this step @@ -2361,19 +2365,23 @@ def delete_s3_object(s3object: S3Object | str, s3_resource_path: str | None = No # # Args: # s3_objects: List of S3 objects to sign +# expiry_secs: How long the signature stays valid, in seconds +# (defaults to 43200 = 12h, clamped to [60, 604800]) # # Returns: # List of signed S3 objects -def sign_s3_objects(s3_objects: list[S3Object | str]) -> list[S3Object] +def sign_s3_objects(s3_objects: list[S3Object | str], expiry_secs: int | None = None) -> list[S3Object] # Sign a single S3 object for use by anonymous users in public apps. # # Args: # s3_object: S3 object to sign +# expiry_secs: How long the signature stays valid, in seconds +# (defaults to 43200 = 12h, clamped to [60, 604800]) # # Returns: # Signed S3 object -def sign_s3_object(s3_object: S3Object | str) -> S3Object +def sign_s3_object(s3_object: S3Object | str, expiry_secs: int | None = None) -> S3Object # Generate presigned public URLs for an array of S3 objects. # If an S3 object is not signed yet, it will be signed first. @@ -2381,6 +2389,8 @@ def sign_s3_object(s3_object: S3Object | str) -> S3Object # Args: # s3_objects: List of S3 objects to sign # base_url: Optional base URL for the presigned URLs (defaults to WM_BASE_URL) +# expiry_secs: How long the signatures stay valid, in seconds +# (defaults to 43200 = 12h, clamped to [60, 604800]) # # Returns: # List of signed public URLs @@ -2388,7 +2398,7 @@ def sign_s3_object(s3_object: S3Object | str) -> S3Object # Example: # >>> s3_objs = [S3Object(s3="/path/to/file1.txt"), S3Object(s3="/path/to/file2.txt")] # >>> urls = client.get_presigned_s3_public_urls(s3_objs) -def get_presigned_s3_public_urls(s3_objects: list[S3Object | str], base_url: str | None = None) -> list[str] +def get_presigned_s3_public_urls(s3_objects: list[S3Object | str], base_url: str | None = None, expiry_secs: int | None = None) -> list[str] # Generate a presigned public URL for an S3 object. # If the S3 object is not signed yet, it will be signed first. @@ -2396,6 +2406,8 @@ def get_presigned_s3_public_urls(s3_objects: list[S3Object | str], base_url: str # Args: # s3_object: S3 object to sign # base_url: Optional base URL for the presigned URL (defaults to WM_BASE_URL) +# expiry_secs: How long the signature stays valid, in seconds +# (defaults to 43200 = 12h, clamped to [60, 604800]) # # Returns: # Signed public URL @@ -2403,7 +2415,7 @@ def get_presigned_s3_public_urls(s3_objects: list[S3Object | str], base_url: str # Example: # >>> s3_obj = S3Object(s3="/path/to/file.txt") # >>> url = client.get_presigned_s3_public_url(s3_obj) -def get_presigned_s3_public_url(s3_object: S3Object | str, base_url: str | None = None) -> str +def get_presigned_s3_public_url(s3_object: S3Object | str, base_url: str | None = None, expiry_secs: int | None = None) -> str # Get the current user information. # diff --git a/system_prompts/auto-generated/sdks/python.md b/system_prompts/auto-generated/sdks/python.md index 0c5a0f37d7..f30f1a0018 100644 --- a/system_prompts/auto-generated/sdks/python.md +++ b/system_prompts/auto-generated/sdks/python.md @@ -313,19 +313,23 @@ def delete_s3_object(s3object: S3Object | str, s3_resource_path: str | None = No # # Args: # s3_objects: List of S3 objects to sign +# expiry_secs: How long the signature stays valid, in seconds +# (defaults to 43200 = 12h, clamped to [60, 604800]) # # Returns: # List of signed S3 objects -def sign_s3_objects(s3_objects: list[S3Object | str]) -> list[S3Object] +def sign_s3_objects(s3_objects: list[S3Object | str], expiry_secs: int | None = None) -> list[S3Object] # Sign a single S3 object for use by anonymous users in public apps. # # Args: # s3_object: S3 object to sign +# expiry_secs: How long the signature stays valid, in seconds +# (defaults to 43200 = 12h, clamped to [60, 604800]) # # Returns: # Signed S3 object -def sign_s3_object(s3_object: S3Object | str) -> S3Object +def sign_s3_object(s3_object: S3Object | str, expiry_secs: int | None = None) -> S3Object # Generate presigned public URLs for an array of S3 objects. # If an S3 object is not signed yet, it will be signed first. @@ -333,6 +337,8 @@ def sign_s3_object(s3_object: S3Object | str) -> S3Object # Args: # s3_objects: List of S3 objects to sign # base_url: Optional base URL for the presigned URLs (defaults to WM_BASE_URL) +# expiry_secs: How long the signatures stay valid, in seconds +# (defaults to 43200 = 12h, clamped to [60, 604800]) # # Returns: # List of signed public URLs @@ -340,7 +346,7 @@ def sign_s3_object(s3_object: S3Object | str) -> S3Object # Example: # >>> s3_objs = [S3Object(s3="/path/to/file1.txt"), S3Object(s3="/path/to/file2.txt")] # >>> urls = client.get_presigned_s3_public_urls(s3_objs) -def get_presigned_s3_public_urls(s3_objects: list[S3Object | str], base_url: str | None = None) -> list[str] +def get_presigned_s3_public_urls(s3_objects: list[S3Object | str], base_url: str | None = None, expiry_secs: int | None = None) -> list[str] # Generate a presigned public URL for an S3 object. # If the S3 object is not signed yet, it will be signed first. @@ -348,6 +354,8 @@ def get_presigned_s3_public_urls(s3_objects: list[S3Object | str], base_url: str # Args: # s3_object: S3 object to sign # base_url: Optional base URL for the presigned URL (defaults to WM_BASE_URL) +# expiry_secs: How long the signature stays valid, in seconds +# (defaults to 43200 = 12h, clamped to [60, 604800]) # # Returns: # Signed public URL @@ -355,7 +363,7 @@ def get_presigned_s3_public_urls(s3_objects: list[S3Object | str], base_url: str # Example: # >>> s3_obj = S3Object(s3="/path/to/file.txt") # >>> url = client.get_presigned_s3_public_url(s3_obj) -def get_presigned_s3_public_url(s3_object: S3Object | str, base_url: str | None = None) -> str +def get_presigned_s3_public_url(s3_object: S3Object | str, base_url: str | None = None, expiry_secs: int | None = None) -> str # Get the current user information. # diff --git a/system_prompts/auto-generated/sdks/typescript.md b/system_prompts/auto-generated/sdks/typescript.md index df0ff4786f..1269a3e497 100644 --- a/system_prompts/auto-generated/sdks/typescript.md +++ b/system_prompts/auto-generated/sdks/typescript.md @@ -303,31 +303,35 @@ async deleteS3File(s3object: S3Object, workspace: string | undefined = undefined /** * Sign S3 objects to be used by anonymous users in public apps * @param s3objects s3 objects to sign + * @param expirySecs how long the signature stays valid, in seconds (default 43200 = 12h, clamped to [60, 604800]) * @returns signed s3 objects */ -async signS3Objects(s3objects: S3Object[]): Promise +async signS3Objects(s3objects: S3Object[], { expirySecs }: { expirySecs?: number } = {}): Promise /** * Sign S3 object to be used by anonymous users in public apps * @param s3object s3 object to sign + * @param expirySecs how long the signature stays valid, in seconds (default 43200 = 12h, clamped to [60, 604800]) * @returns signed s3 object */ -async signS3Object(s3object: S3Object): Promise +async signS3Object(s3object: S3Object, { expirySecs }: { expirySecs?: number } = {}): Promise /** * Generate a presigned public URL for an array of S3 objects. * If an S3 object is not signed yet, it will be signed first. * @param s3Objects s3 objects to sign + * @param expirySecs how long the signature stays valid, in seconds (default 43200 = 12h, clamped to [60, 604800]) * @returns list of signed public URLs */ -async getPresignedS3PublicUrls(s3Objects: S3Object[], { baseUrl }: { baseUrl?: string } = {}): Promise +async getPresignedS3PublicUrls(s3Objects: S3Object[], { baseUrl, expirySecs }: { baseUrl?: string; expirySecs?: number } = {}): Promise /** * Generate a presigned public URL for an S3 object. If the S3 object is not signed yet, it will be signed first. * @param s3Object s3 object to sign + * @param expirySecs how long the signature stays valid, in seconds (default 43200 = 12h, clamped to [60, 604800]) * @returns signed public URL */ -async getPresignedS3PublicUrl(s3Objects: S3Object, { baseUrl }: { baseUrl?: string } = {}): Promise +async getPresignedS3PublicUrl(s3Objects: S3Object, { baseUrl, expirySecs }: { baseUrl?: string; expirySecs?: number } = {}): Promise /** * Get URLs needed for resuming a flow after this step diff --git a/system_prompts/auto-generated/skills/write-script-bun/SKILL.md b/system_prompts/auto-generated/skills/write-script-bun/SKILL.md index 9d19e4bf85..74ef8f1908 100644 --- a/system_prompts/auto-generated/skills/write-script-bun/SKILL.md +++ b/system_prompts/auto-generated/skills/write-script-bun/SKILL.md @@ -474,31 +474,35 @@ async deleteS3File(s3object: S3Object, workspace: string | undefined = undefined /** * Sign S3 objects to be used by anonymous users in public apps * @param s3objects s3 objects to sign + * @param expirySecs how long the signature stays valid, in seconds (default 43200 = 12h, clamped to [60, 604800]) * @returns signed s3 objects */ -async signS3Objects(s3objects: S3Object[]): Promise +async signS3Objects(s3objects: S3Object[], { expirySecs }: { expirySecs?: number } = {}): Promise /** * Sign S3 object to be used by anonymous users in public apps * @param s3object s3 object to sign + * @param expirySecs how long the signature stays valid, in seconds (default 43200 = 12h, clamped to [60, 604800]) * @returns signed s3 object */ -async signS3Object(s3object: S3Object): Promise +async signS3Object(s3object: S3Object, { expirySecs }: { expirySecs?: number } = {}): Promise /** * Generate a presigned public URL for an array of S3 objects. * If an S3 object is not signed yet, it will be signed first. * @param s3Objects s3 objects to sign + * @param expirySecs how long the signature stays valid, in seconds (default 43200 = 12h, clamped to [60, 604800]) * @returns list of signed public URLs */ -async getPresignedS3PublicUrls(s3Objects: S3Object[], { baseUrl }: { baseUrl?: string } = {}): Promise +async getPresignedS3PublicUrls(s3Objects: S3Object[], { baseUrl, expirySecs }: { baseUrl?: string; expirySecs?: number } = {}): Promise /** * Generate a presigned public URL for an S3 object. If the S3 object is not signed yet, it will be signed first. * @param s3Object s3 object to sign + * @param expirySecs how long the signature stays valid, in seconds (default 43200 = 12h, clamped to [60, 604800]) * @returns signed public URL */ -async getPresignedS3PublicUrl(s3Objects: S3Object, { baseUrl }: { baseUrl?: string } = {}): Promise +async getPresignedS3PublicUrl(s3Objects: S3Object, { baseUrl, expirySecs }: { baseUrl?: string; expirySecs?: number } = {}): Promise /** * Get URLs needed for resuming a flow after this step diff --git a/system_prompts/auto-generated/skills/write-script-bunnative/SKILL.md b/system_prompts/auto-generated/skills/write-script-bunnative/SKILL.md index 5803909032..0c7b6aecde 100644 --- a/system_prompts/auto-generated/skills/write-script-bunnative/SKILL.md +++ b/system_prompts/auto-generated/skills/write-script-bunnative/SKILL.md @@ -474,31 +474,35 @@ async deleteS3File(s3object: S3Object, workspace: string | undefined = undefined /** * Sign S3 objects to be used by anonymous users in public apps * @param s3objects s3 objects to sign + * @param expirySecs how long the signature stays valid, in seconds (default 43200 = 12h, clamped to [60, 604800]) * @returns signed s3 objects */ -async signS3Objects(s3objects: S3Object[]): Promise +async signS3Objects(s3objects: S3Object[], { expirySecs }: { expirySecs?: number } = {}): Promise /** * Sign S3 object to be used by anonymous users in public apps * @param s3object s3 object to sign + * @param expirySecs how long the signature stays valid, in seconds (default 43200 = 12h, clamped to [60, 604800]) * @returns signed s3 object */ -async signS3Object(s3object: S3Object): Promise +async signS3Object(s3object: S3Object, { expirySecs }: { expirySecs?: number } = {}): Promise /** * Generate a presigned public URL for an array of S3 objects. * If an S3 object is not signed yet, it will be signed first. * @param s3Objects s3 objects to sign + * @param expirySecs how long the signature stays valid, in seconds (default 43200 = 12h, clamped to [60, 604800]) * @returns list of signed public URLs */ -async getPresignedS3PublicUrls(s3Objects: S3Object[], { baseUrl }: { baseUrl?: string } = {}): Promise +async getPresignedS3PublicUrls(s3Objects: S3Object[], { baseUrl, expirySecs }: { baseUrl?: string; expirySecs?: number } = {}): Promise /** * Generate a presigned public URL for an S3 object. If the S3 object is not signed yet, it will be signed first. * @param s3Object s3 object to sign + * @param expirySecs how long the signature stays valid, in seconds (default 43200 = 12h, clamped to [60, 604800]) * @returns signed public URL */ -async getPresignedS3PublicUrl(s3Objects: S3Object, { baseUrl }: { baseUrl?: string } = {}): Promise +async getPresignedS3PublicUrl(s3Objects: S3Object, { baseUrl, expirySecs }: { baseUrl?: string; expirySecs?: number } = {}): Promise /** * Get URLs needed for resuming a flow after this step diff --git a/system_prompts/auto-generated/skills/write-script-deno/SKILL.md b/system_prompts/auto-generated/skills/write-script-deno/SKILL.md index 32691fdba8..930f078a58 100644 --- a/system_prompts/auto-generated/skills/write-script-deno/SKILL.md +++ b/system_prompts/auto-generated/skills/write-script-deno/SKILL.md @@ -476,31 +476,35 @@ async deleteS3File(s3object: S3Object, workspace: string | undefined = undefined /** * Sign S3 objects to be used by anonymous users in public apps * @param s3objects s3 objects to sign + * @param expirySecs how long the signature stays valid, in seconds (default 43200 = 12h, clamped to [60, 604800]) * @returns signed s3 objects */ -async signS3Objects(s3objects: S3Object[]): Promise +async signS3Objects(s3objects: S3Object[], { expirySecs }: { expirySecs?: number } = {}): Promise /** * Sign S3 object to be used by anonymous users in public apps * @param s3object s3 object to sign + * @param expirySecs how long the signature stays valid, in seconds (default 43200 = 12h, clamped to [60, 604800]) * @returns signed s3 object */ -async signS3Object(s3object: S3Object): Promise +async signS3Object(s3object: S3Object, { expirySecs }: { expirySecs?: number } = {}): Promise /** * Generate a presigned public URL for an array of S3 objects. * If an S3 object is not signed yet, it will be signed first. * @param s3Objects s3 objects to sign + * @param expirySecs how long the signature stays valid, in seconds (default 43200 = 12h, clamped to [60, 604800]) * @returns list of signed public URLs */ -async getPresignedS3PublicUrls(s3Objects: S3Object[], { baseUrl }: { baseUrl?: string } = {}): Promise +async getPresignedS3PublicUrls(s3Objects: S3Object[], { baseUrl, expirySecs }: { baseUrl?: string; expirySecs?: number } = {}): Promise /** * Generate a presigned public URL for an S3 object. If the S3 object is not signed yet, it will be signed first. * @param s3Object s3 object to sign + * @param expirySecs how long the signature stays valid, in seconds (default 43200 = 12h, clamped to [60, 604800]) * @returns signed public URL */ -async getPresignedS3PublicUrl(s3Objects: S3Object, { baseUrl }: { baseUrl?: string } = {}): Promise +async getPresignedS3PublicUrl(s3Objects: S3Object, { baseUrl, expirySecs }: { baseUrl?: string; expirySecs?: number } = {}): Promise /** * Get URLs needed for resuming a flow after this step diff --git a/system_prompts/auto-generated/skills/write-script-python3/SKILL.md b/system_prompts/auto-generated/skills/write-script-python3/SKILL.md index afa5eda299..274ffb343e 100644 --- a/system_prompts/auto-generated/skills/write-script-python3/SKILL.md +++ b/system_prompts/auto-generated/skills/write-script-python3/SKILL.md @@ -498,19 +498,23 @@ def delete_s3_object(s3object: S3Object | str, s3_resource_path: str | None = No # # Args: # s3_objects: List of S3 objects to sign +# expiry_secs: How long the signature stays valid, in seconds +# (defaults to 43200 = 12h, clamped to [60, 604800]) # # Returns: # List of signed S3 objects -def sign_s3_objects(s3_objects: list[S3Object | str]) -> list[S3Object] +def sign_s3_objects(s3_objects: list[S3Object | str], expiry_secs: int | None = None) -> list[S3Object] # Sign a single S3 object for use by anonymous users in public apps. # # Args: # s3_object: S3 object to sign +# expiry_secs: How long the signature stays valid, in seconds +# (defaults to 43200 = 12h, clamped to [60, 604800]) # # Returns: # Signed S3 object -def sign_s3_object(s3_object: S3Object | str) -> S3Object +def sign_s3_object(s3_object: S3Object | str, expiry_secs: int | None = None) -> S3Object # Generate presigned public URLs for an array of S3 objects. # If an S3 object is not signed yet, it will be signed first. @@ -518,6 +522,8 @@ def sign_s3_object(s3_object: S3Object | str) -> S3Object # Args: # s3_objects: List of S3 objects to sign # base_url: Optional base URL for the presigned URLs (defaults to WM_BASE_URL) +# expiry_secs: How long the signatures stay valid, in seconds +# (defaults to 43200 = 12h, clamped to [60, 604800]) # # Returns: # List of signed public URLs @@ -525,7 +531,7 @@ def sign_s3_object(s3_object: S3Object | str) -> S3Object # Example: # >>> s3_objs = [S3Object(s3="/path/to/file1.txt"), S3Object(s3="/path/to/file2.txt")] # >>> urls = client.get_presigned_s3_public_urls(s3_objs) -def get_presigned_s3_public_urls(s3_objects: list[S3Object | str], base_url: str | None = None) -> list[str] +def get_presigned_s3_public_urls(s3_objects: list[S3Object | str], base_url: str | None = None, expiry_secs: int | None = None) -> list[str] # Generate a presigned public URL for an S3 object. # If the S3 object is not signed yet, it will be signed first. @@ -533,6 +539,8 @@ def get_presigned_s3_public_urls(s3_objects: list[S3Object | str], base_url: str # Args: # s3_object: S3 object to sign # base_url: Optional base URL for the presigned URL (defaults to WM_BASE_URL) +# expiry_secs: How long the signature stays valid, in seconds +# (defaults to 43200 = 12h, clamped to [60, 604800]) # # Returns: # Signed public URL @@ -540,7 +548,7 @@ def get_presigned_s3_public_urls(s3_objects: list[S3Object | str], base_url: str # Example: # >>> s3_obj = S3Object(s3="/path/to/file.txt") # >>> url = client.get_presigned_s3_public_url(s3_obj) -def get_presigned_s3_public_url(s3_object: S3Object | str, base_url: str | None = None) -> str +def get_presigned_s3_public_url(s3_object: S3Object | str, base_url: str | None = None, expiry_secs: int | None = None) -> str # Get the current user information. # diff --git a/typescript-client/client.d.ts b/typescript-client/client.d.ts index 1fa1ed1ca6..1e4448300e 100644 --- a/typescript-client/client.d.ts +++ b/typescript-client/client.d.ts @@ -200,37 +200,45 @@ export declare function writeS3File( /** * Sign S3 objects to be used by anonymous users in public apps * @param s3objects s3 objects to sign + * @param expirySecs how long the signature stays valid, in seconds (default 43200 = 12h, clamped to [60, 604800]) * @returns signed s3 objects */ export declare function signS3Objects( - s3objects: S3Object[] + s3objects: S3Object[], + { expirySecs }?: { expirySecs?: number } ): Promise; /** * Sign S3 object to be used by anonymous users in public apps * @param s3object s3 object to sign + * @param expirySecs how long the signature stays valid, in seconds (default 43200 = 12h, clamped to [60, 604800]) * @returns signed s3 object */ -export declare function signS3Object(s3object: S3Object): Promise; +export declare function signS3Object( + s3object: S3Object, + { expirySecs }?: { expirySecs?: number } +): Promise; /** * Generate a presigned public URL for an array of S3 objects. * If an S3 object is not signed yet, it will be signed first. * @param s3Objects s3 objects to sign + * @param expirySecs how long the signature stays valid, in seconds (default 43200 = 12h, clamped to [60, 604800]) * @returns list of signed public URLs */ export declare function getPresignedS3PublicUrls( s3Objects: S3Object[], - { baseUrl }: { baseUrl?: string } + { baseUrl, expirySecs }: { baseUrl?: string; expirySecs?: number } ): Promise; /** * Generate a presigned public URL for an S3 object. If the S3 object is not signed yet, it will be signed first. * @param s3Object s3 object to sign + * @param expirySecs how long the signature stays valid, in seconds (default 43200 = 12h, clamped to [60, 604800]) * @returns signed public URL */ export declare function getPresignedS3PublicUrl( s3Objects: S3Object, - { baseUrl }: { baseUrl?: string } + { baseUrl, expirySecs }: { baseUrl?: string; expirySecs?: number } ): Promise; /** diff --git a/typescript-client/client.ts b/typescript-client/client.ts index f7be5e5a6d..105641eee4 100644 --- a/typescript-client/client.ts +++ b/typescript-client/client.ts @@ -1057,15 +1057,18 @@ export async function deleteS3File( /** * Sign S3 objects to be used by anonymous users in public apps * @param s3objects s3 objects to sign + * @param expirySecs how long the signature stays valid, in seconds (default 43200 = 12h, clamped to [60, 604800]) * @returns signed s3 objects */ export async function signS3Objects( - s3objects: S3Object[] + s3objects: S3Object[], + { expirySecs }: { expirySecs?: number } = {} ): Promise { const signedKeys = await AppService.signS3Objects({ workspace: getWorkspace(), requestBody: { s3_objects: s3objects.map(parseS3Object), + expiry_secs: expirySecs, }, }); return signedKeys; @@ -1073,10 +1076,14 @@ export async function signS3Objects( /** * Sign S3 object to be used by anonymous users in public apps * @param s3object s3 object to sign + * @param expirySecs how long the signature stays valid, in seconds (default 43200 = 12h, clamped to [60, 604800]) * @returns signed s3 object */ -export async function signS3Object(s3object: S3Object): Promise { - const [signedObject] = await signS3Objects([s3object]); +export async function signS3Object( + s3object: S3Object, + { expirySecs }: { expirySecs?: number } = {} +): Promise { + const [signedObject] = await signS3Objects([s3object], { expirySecs }); return signedObject; } @@ -1084,11 +1091,12 @@ export async function signS3Object(s3object: S3Object): Promise { * Generate a presigned public URL for an array of S3 objects. * If an S3 object is not signed yet, it will be signed first. * @param s3Objects s3 objects to sign + * @param expirySecs how long the signature stays valid, in seconds (default 43200 = 12h, clamped to [60, 604800]) * @returns list of signed public URLs */ export async function getPresignedS3PublicUrls( s3Objects: S3Object[], - { baseUrl }: { baseUrl?: string } = {} + { baseUrl, expirySecs }: { baseUrl?: string; expirySecs?: number } = {} ): Promise { baseUrl ??= getPublicBaseUrl(); @@ -1100,7 +1108,8 @@ export async function getPresignedS3PublicUrls( .filter(([s3Obj, _]) => s3Obj.presigned === undefined); if (s3ObjsToSign.length > 0) { const signedS3Objs = await signS3Objects( - s3ObjsToSign.map(([s3Obj, _]) => s3Obj) + s3ObjsToSign.map(([s3Obj, _]) => s3Obj), + { expirySecs } ); for (let i = 0; i < s3ObjsToSign.length; i++) { const [_, originalIndex] = s3ObjsToSign[i]; @@ -1120,13 +1129,17 @@ export async function getPresignedS3PublicUrls( /** * Generate a presigned public URL for an S3 object. If the S3 object is not signed yet, it will be signed first. * @param s3Object s3 object to sign + * @param expirySecs how long the signature stays valid, in seconds (default 43200 = 12h, clamped to [60, 604800]) * @returns signed public URL */ export async function getPresignedS3PublicUrl( s3Objects: S3Object, - { baseUrl }: { baseUrl?: string } = {} + { baseUrl, expirySecs }: { baseUrl?: string; expirySecs?: number } = {} ): Promise { - const [s3Object] = await getPresignedS3PublicUrls([s3Objects], { baseUrl }); + const [s3Object] = await getPresignedS3PublicUrls([s3Objects], { + baseUrl, + expirySecs, + }); return s3Object; } From 6b73145e7220232601538b801ebc9dc73fe79bbb Mon Sep 17 00:00:00 2001 From: AlexRV12 <71396855+AlexRV12@users.noreply.github.com> Date: Wed, 26 Aug 2026 00:47:46 +0200 Subject: [PATCH 06/34] fix(frontend): follow the operating workspace in step input forms (#10834) * fix(frontend): follow the operating workspace in step input forms Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): rethrow auth errors and wire remaining variable pickers Co-Authored-By: Claude Opus 5 (1M context) * refactor(frontend): use runed watch for picker workspace reloads Co-Authored-By: Claude Opus 5 (1M context) --------- Co-authored-by: Claude Opus 5 (1M context) --- .../lib/components/EditableSchemaForm.svelte | 6 ++ .../InputTransformSchemaForm.svelte | 43 ++++++++---- frontend/src/lib/components/ItemPicker.svelte | 68 +++++++++++++++---- frontend/src/lib/components/SchemaForm.svelte | 6 ++ .../content/FlowEnvironmentVariables.svelte | 6 ++ 5 files changed, 102 insertions(+), 27 deletions(-) diff --git a/frontend/src/lib/components/EditableSchemaForm.svelte b/frontend/src/lib/components/EditableSchemaForm.svelte index 1e83887522..67dfeed0b8 100644 --- a/frontend/src/lib/components/EditableSchemaForm.svelte +++ b/frontend/src/lib/components/EditableSchemaForm.svelte @@ -15,6 +15,7 @@ import PropertyEditor from './schema/PropertyEditor.svelte' import SimpleEditor from './SimpleEditor.svelte' import { createEventDispatcher, untrack } from 'svelte' + import { watch } from 'runed' import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte' import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte' import Label from './Label.svelte' @@ -173,6 +174,11 @@ let itemPicker: ItemPicker | undefined = $state(undefined) let variableEditor: VariableEditor | undefined = $state(undefined) + watch( + () => ws, + () => itemPicker?.reloadItems() + ) + let keys: string[] = $state( (Array.isArray(schema?.order) ? [...schema.order] diff --git a/frontend/src/lib/components/InputTransformSchemaForm.svelte b/frontend/src/lib/components/InputTransformSchemaForm.svelte index 034f891116..d0d29b3111 100644 --- a/frontend/src/lib/components/InputTransformSchemaForm.svelte +++ b/frontend/src/lib/components/InputTransformSchemaForm.svelte @@ -1,9 +1,10 @@ - -
{ - const btn = menubarEl?.querySelector('[data-melt-menubar-trigger]') - if (btn instanceof HTMLElement) btn.click() - }} -> - - {#snippet children({ createMenu })} - (showExtraTriggers = false)}> - {#snippet triggr({ trigger })} - - {/snippet} - {#snippet children({ item })} -
- {#each favoriteLinks ?? [] as favorite (favorite.href)} - - - {#if favorite.kind == 'script'} - - {:else if favorite.kind == 'flow'} - - {:else if favorite.kind == 'app' || favorite.kind == 'raw_app'} - - {:else if favorite.kind == 'asset'} - - {/if} - - - {favorite.label} - - - {/each} + + {#snippet children({ createMenu })} + (showExtraTriggers = false)} + > + {#snippet triggr({ trigger, pinned })} + + {/snippet} + {#snippet children({ item })} +
+ {#each favoriteLinks ?? [] as favorite (favorite.href)} + + + {#if favorite.kind == 'script'} + + {:else if favorite.kind == 'flow'} + + {:else if favorite.kind == 'app' || favorite.kind == 'raw_app'} + + {:else if favorite.kind == 'asset'} + + {/if} + + + {favorite.label} + + + {/each} +
+ + {#each mainMenuLinks as menuLink (menuLink.href ?? menuLink.label)} + + {/each} + +
+
+ + + Account settings +
- {#each mainMenuLinks as menuLink (menuLink.href ?? menuLink.label)} - - {/each} - -
-
- - - Account settings - -
- -
- { - if (!document.documentElement.classList.contains('dark')) { - document.documentElement.classList.add('dark') - window.localStorage.setItem('dark-mode', 'dark') - } else { - document.documentElement.classList.remove('dark') - window.localStorage.setItem('dark-mode', 'light') - } - }} - lightMode - class={twMerge( - 'w-full flex gap-3.5 px-2 py-2', - sidebarClasses.hoverBg, - sidebarClasses.text - )} - {item} - > - {#if darkMode} - - {:else} - - {/if} - Switch theme - - clearWorkspaceFromStorage()} - lightMode - class={twMerge( - 'flex gap-3.5 px-2 py-2', - sidebarClasses.hoverBg, - sidebarClasses.text - )} - {item} - > - - All workspaces - - - {#if $superadmin} - - - Instance settings - +
+ { + if (!document.documentElement.classList.contains('dark')) { + document.documentElement.classList.add('dark') + window.localStorage.setItem('dark-mode', 'dark') + } else { + document.documentElement.classList.remove('dark') + window.localStorage.setItem('dark-mode', 'light') + } + }} + lightMode + class={twMerge( + 'w-full flex gap-3.5 px-2 py-2', + 'transition-colors', + sidebarClasses.text, + 'data-[highlighted]:bg-surface-hover data-[highlighted]:text-primary' + )} + {item} + > + {#if darkMode} + + {:else} + {/if} + Switch theme + + clearWorkspaceFromStorage()} + lightMode + class={twMerge( + 'flex gap-3.5 px-2 py-2', + 'transition-colors', + sidebarClasses.text, + 'data-[highlighted]:bg-surface-hover data-[highlighted]:text-primary' + )} + {item} + > + + All workspaces + + {#if $superadmin} logout()} + href="#superadmin-settings" class={twMerge( - 'flex flex-row gap-3.5 items-center px-2 py-2 w-full', - 'text-primary text-xs', - 'hover:bg-surface-hover cursor-pointer', + 'flex flex-row gap-3.5 items-center px-2 py-2 ', + 'text-secondary text-xs', + 'cursor-pointer', 'data-[highlighted]:bg-surface-hover data-[highlighted]:text-primary' )} {item} > - - Sign out + + Instance settings -
-
- {#snippet renderSecondMenuLinks(menuLinks: SecondMenuLink[])} - {#each menuLinks as menuLink (menuLink.href ?? menuLink.label)} - - {menuLink.label} - - {/each} - {/snippet} - {#if secondMenuLinks.length || secondMenuTriggerLinks.length || extraTriggerLinks.length} -
- {#if secondMenuLinks.length}
{@render renderSecondMenuLinks(secondMenuLinks)}
{/if} - {#if secondMenuTriggerLinks.length}
{@render renderSecondMenuLinks(secondMenuTriggerLinks)}
{/if} - {#if extraTriggerLinks.length}
- -
{ - e.stopPropagation() - showExtraTriggers = !showExtraTriggers - }} + {/if} + + logout()} + class={twMerge( + 'flex flex-row gap-3.5 items-center px-2 py-2 w-full', + 'text-primary text-xs', + 'cursor-pointer', + 'data-[highlighted]:bg-surface-hover data-[highlighted]:text-primary' + )} + {item} + > + + Sign out + +
+
+ {#snippet renderSecondMenuLinks(menuLinks: SecondMenuLink[])} + {#each menuLinks as menuLink (menuLink.href ?? menuLink.label)} + + {menuLink.label} + + {/each} + {/snippet} + {#if secondMenuLinks.length || secondMenuTriggerLinks.length || extraTriggerLinks.length} +
+ {#if secondMenuLinks.length}
{@render renderSecondMenuLinks(secondMenuLinks)}
{/if} + {#if secondMenuTriggerLinks.length}
{@render renderSecondMenuLinks(secondMenuTriggerLinks)}
{/if} + {#if extraTriggerLinks.length}
+ +
{ + // This row expands the list below it instead of acting on the selection, and + // melt keeps the menu open only for a click it sees as defaultPrevented. + // Svelte delegates onclick to the root, which runs after melt's own listener, + // and a capture listener on the item itself would be ordered only by + // registration, so an ancestor's capture phase is what reliably wins. + e.preventDefault() + showExtraTriggers = !showExtraTriggers + }} + > + More triggers -
- {#if showExtraTriggers} - {#each extraTriggerLinks as menuLink (menuLink.href)} - - {menuLink.label} - - {/each} - {/if} -
{/if} -
- {/if} - {#if $enterpriseLicense} - - {/if} -
+ +
+ {#if showExtraTriggers} + {#each extraTriggerLinks as menuLink (menuLink.href)} + + {menuLink.label} + + {/each} + {/if} +
{/if} +
+ {/if} + {#if $enterpriseLicense} + + {/if}
- {/snippet} -
- {/snippet} -
-
+
+ {/snippet} + + {/snippet} + From 46c363ffa4bc72bef6b367ece4bdbeef5e0eadc9 Mon Sep 17 00:00:00 2001 From: hugocasa Date: Wed, 26 Aug 2026 00:49:13 +0200 Subject: [PATCH 10/34] fix: require admin on workspace tarball settings export (#10817) * fix: require admin on workspace tarball settings export Co-Authored-By: Claude Opus 5 * fix: name the refused flag in the settings export error Co-Authored-By: Claude Opus 5 --------- Co-authored-by: Claude Opus 5 --- backend/tests/workspace_export.rs | 76 ++++++++++++++++++- backend/windmill-api/src/workspaces_export.rs | 18 +++-- 2 files changed, 86 insertions(+), 8 deletions(-) diff --git a/backend/tests/workspace_export.rs b/backend/tests/workspace_export.rs index 8568b8ff24..5894ca8394 100644 --- a/backend/tests/workspace_export.rs +++ b/backend/tests/workspace_export.rs @@ -1,6 +1,6 @@ use sqlx::postgres::Postgres; use sqlx::Pool; -use windmill_test_utils::{initialize_tracing, ApiServer}; +use windmill_test_utils::{initialize_tracing, set_jwt_secret, ApiServer}; /// Integration test: exercises every explicit-column query in `tarball_workspace`. /// @@ -287,3 +287,77 @@ async fn test_tarball_export_gates_values_on_item_scopes(db: Pool) -> Ok(()) } + +/// `settings.json` carries the admin-managed integration config that `get_settings` +/// is admin-only for (the webhook URL, ai_config, git_sync, handler extra_args), so +/// `include_settings` takes the same admin check as `get_settings` rather than +/// riding on the route's `workspaces:read`. Git sync exports settings through the +/// same route, so the gate must still admit its system identity. +#[sqlx::test(fixtures("base"))] +async fn test_tarball_export_settings_are_admin_only(db: Pool) -> anyhow::Result<()> { + initialize_tracing().await; + set_jwt_secret().await; + let server = ApiServer::start(db.clone()).await?; + let base_url = format!("http://localhost:{}", server.addr.port()); + + sqlx::query( + r#"UPDATE workspace_settings + SET webhook = 'https://hook.example/?token=WEBHOOK_SECRET', + ai_config = '{"providers":{"openai":{"api_key":"AI_CONFIG_SECRET"}}}'::jsonb + WHERE workspace_id = 'test-workspace'"#, + ) + .execute(&db) + .await?; + + let export = async |token: &str| -> anyhow::Result<(u16, String)> { + let resp = reqwest::Client::new() + .get(format!( + "{base_url}/api/w/test-workspace/workspaces/tarball?include_settings=true&settings_version=v2" + )) + .bearer_auth(token) + .send() + .await?; + let status = resp.status().as_u16(); + // Lossy: a successful export is a tar, not UTF-8. Only the values matter here. + Ok(( + status, + String::from_utf8_lossy(&resp.bytes().await?).into_owned(), + )) + }; + + // SECRET_TOKEN_2 belongs to test-user-2, a non-admin member of test-workspace. + let (status, body) = export("SECRET_TOKEN_2").await?; + assert_eq!(status, 403, "non-admin exported settings: {body}"); + + let (status, body) = export("SECRET_TOKEN").await?; + assert_eq!(status, 200, "admin denied settings: {body}"); + assert!( + body.contains("WEBHOOK_SECRET") && body.contains("AI_CONFIG_SECRET"), + "admin got no settings" + ); + + // Git sync pushes the workspace to the repo by exporting it under + // `superadmin_sync@windmill.dev`, which belongs to no workspace: the job token + // it runs with is the export's only admin claim. + let sync_email = windmill_common::users::SUPERADMIN_SYNC_EMAIL; + let sync_token = windmill_common::auth::create_token_for_owner( + &db, + "test-workspace", + sync_email, + "git-sync", + 300, + sync_email, + &uuid::Uuid::new_v4(), + None, + None, + ) + .await?; + let (status, body) = export(&sync_token).await?; + assert_eq!(status, 200, "git-sync identity denied settings: {body}"); + assert!( + body.contains("WEBHOOK_SECRET"), + "git-sync identity got no settings" + ); + + Ok(()) +} diff --git a/backend/windmill-api/src/workspaces_export.rs b/backend/windmill-api/src/workspaces_export.rs index 9116021626..05cd354cd4 100644 --- a/backend/windmill-api/src/workspaces_export.rs +++ b/backend/windmill-api/src/workspaces_export.rs @@ -643,6 +643,16 @@ pub(crate) async fn tarball_workspace( windmill_api_auth::forbid_scoped_token_workspace_key(&authed)?; } + // settings.json carries the admin-managed integration config that `get_settings` + // is admin-only for (ai_config, the webhook URL, git_sync, handler extra_args), + // so it takes the same check. Not a per-field redaction: fields silently dropped + // from settings.json come back as null on the next `wmill sync push`. + if include_settings.unwrap_or(false) && !authed.is_admin { + return Err(Error::PermissionDenied( + "include_settings requires workspace admin".to_string(), + )); + } + // The route is gated by workspaces:read, but the tarball also carries the item // values that the per-item routes gate on their own domain (get_resource_value, // get_variable). A whole-workspace export cannot be confined to a path, so it @@ -1626,13 +1636,7 @@ pub(crate) async fn tarball_workspace( slack_name: row.slack_name.clone(), slack_command_script: row.slack_command_script.clone(), slack_oauth_client_id: row.slack_oauth_client_id.clone(), - // Mirror the non-admin redaction in `get_settings`: the OAuth - // client secret is admin-only and must not leak via tarball. - slack_oauth_client_secret: if authed.is_admin { - row.slack_oauth_client_secret.clone() - } else { - None - }, + slack_oauth_client_secret: row.slack_oauth_client_secret.clone(), }; serde_json::to_value(settings) .map(|v| serde_json::to_string_pretty(&v).ok()) From 72763c9ba582c4e55b0f1ebfe17fe1435353bdaa Mon Sep 17 00:00:00 2001 From: Guilhem Date: Wed, 26 Aug 2026 00:49:48 +0200 Subject: [PATCH 11/34] tighten spacing between login email and password fields (#10811) Claude-Session: https://claude.ai/code/session_01BmEVHF8afJmgv6saBRYN6w Co-authored-by: Claude Opus 5 (1M context) --- frontend/src/lib/components/Login.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/lib/components/Login.svelte b/frontend/src/lib/components/Login.svelte index 9c4666bf4c..d1dc3ec7a1 100644 --- a/frontend/src/lib/components/Login.svelte +++ b/frontend/src/lib/components/Login.svelte @@ -732,7 +732,7 @@ contact@windmill.dev

{/if} -
+
From ffdf17ef8dc5575dd92d62d0d0ba887c1e378576 Mon Sep 17 00:00:00 2001 From: hugocasa Date: Wed, 26 Aug 2026 08:23:25 +0200 Subject: [PATCH 12/34] fix: force HTTP router rebuild on trigger-change notification (#10849) * fix: force HTTP router rebuild on trigger-change notification Co-Authored-By: Claude Opus 5 * fix: coalesce http trigger change events into one forced rebuild Co-Authored-By: Claude Opus 5 * fix: retry the coalesced http router rebuild when it fails Co-Authored-By: Claude Opus 5 * fix: mark http routers stale when a forced rebuild fails Co-Authored-By: Claude Opus 5 * fix: keep the router invalidation across an in-flight rebuild Co-Authored-By: Claude Opus 5 --------- Co-authored-by: Claude Opus 5 --- backend/src/main.rs | 50 +++++++++------ .../windmill-api/src/triggers/http/handler.rs | 4 +- backend/windmill-trigger-http/src/lib.rs | 41 ++++++++++-- .../tests/refresh_routers.rs | 62 +++++++++++++++++++ 4 files changed, 130 insertions(+), 27 deletions(-) create mode 100644 backend/windmill-trigger-http/tests/refresh_routers.rs diff --git a/backend/src/main.rs b/backend/src/main.rs index 10d1434f98..f8b150383a 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -1434,21 +1434,30 @@ Windmill Community Edition {GIT_VERSION} // Poll for new events from notify_event table match windmill_common::notify_events::poll_notify_events(&db, last_event_id).await { Ok(events) => { + let mut http_trigger_change_handled = false; for event in events { if !*windmill_common::QUIET_LOGS { tracing::info!("Processing notify event: channel={}, payload={}", event.channel, event.payload); } - process_notify_event( - &event.channel, - &event.payload, - &db, - &conn, - &tx, - server_mode, - worker_mode, - #[cfg(feature = "parquet")] - disable_s3_store, - ).await; + let is_http_trigger_change = event.channel == "notify_http_trigger_change"; + // Every changed http_trigger row emits its own event and each one forces + // a full router rebuild, but the batch's first successful rebuild already + // read every row the batch committed. A failed rebuild leaves the flag + // clear so the next event in the batch retries it. + if !(is_http_trigger_change && http_trigger_change_handled) { + let handled = process_notify_event( + &event.channel, + &event.payload, + &db, + &conn, + &tx, + server_mode, + worker_mode, + #[cfg(feature = "parquet")] + disable_s3_store, + ).await; + http_trigger_change_handled |= is_http_trigger_change && handled; + } last_event_id = last_event_id.max(event.id); } } @@ -1670,6 +1679,9 @@ Windmill Community Edition {GIT_VERSION} /// Process a single notify event from the polling-based event system. /// This replaces the old PgListener notification handling. +/// +/// Returns `false` when the event still needs handling. Only the HTTP router rebuild reports +/// that, because the poll loop coalesces those events and must not swallow the retry. #[allow(unused_variables)] async fn process_notify_event( channel: &str, @@ -1680,7 +1692,7 @@ async fn process_notify_event( server_mode: bool, worker_mode: bool, #[cfg(feature = "parquet")] disable_s3_store: bool, -) { +) -> bool { match channel { "notify_config_change" => { if payload == "server" && server_mode { @@ -1825,17 +1837,14 @@ async fn process_notify_event( #[cfg(feature = "http_trigger")] "notify_http_trigger_change" => { tracing::info!("HTTP trigger change detected: {}", payload); - match windmill_api::triggers::http::refresh_routers(db).await { - Ok((true, _)) => { + match windmill_api::triggers::http::refresh_routers(db, true).await { + Ok(_) => { tracing::info!("Refreshed HTTP routers (trigger change)"); } - Ok((false, _)) => { - tracing::warn!( - "Should have refreshed HTTP routers (trigger change) but did not" - ); - } Err(err) => { tracing::error!("Error refreshing HTTP routers (trigger change): {err:#}"); + windmill_api::triggers::http::invalidate_routers(); + return false; } }; } @@ -2059,7 +2068,7 @@ async fn process_notify_event( tracing::error!(error = %e, "Could not reload http route workspaced route setting"); } #[cfg(feature = "http_trigger")] - match windmill_api::triggers::http::refresh_routers(db).await { + match windmill_api::triggers::http::refresh_routers(db, false).await { Ok((true, _)) => { tracing::info!( "Refreshed HTTP routers (http workspaced route setting change)" @@ -2179,6 +2188,7 @@ async fn process_notify_event( tracing::warn!("Unknown notification channel: {}", channel); } } + true } fn display_config(envs: &[&str]) { diff --git a/backend/windmill-api/src/triggers/http/handler.rs b/backend/windmill-api/src/triggers/http/handler.rs index 37442c8111..e58aab44a2 100644 --- a/backend/windmill-api/src/triggers/http/handler.rs +++ b/backend/windmill-api/src/triggers/http/handler.rs @@ -123,7 +123,9 @@ async fn get_http_route_trigger( let routers_cache = if routers_cache.routers.is_empty() { tracing::warn!("HTTP routers are not loaded, loading from db"); - let (_, routers_cache) = refresh_routers(db).await?; + // refresh_routers takes the write lock, so holding this read guard across it deadlocks. + drop(routers_cache); + let (_, routers_cache) = refresh_routers(db, false).await?; routers_cache } else { routers_cache diff --git a/backend/windmill-trigger-http/src/lib.rs b/backend/windmill-trigger-http/src/lib.rs index f89247dd16..b78276c28d 100644 --- a/backend/windmill-trigger-http/src/lib.rs +++ b/backend/windmill-trigger-http/src/lib.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::sync::atomic::{AtomicU64, Ordering}; use quick_cache::sync::Cache; use serde::{Deserialize, Serialize}; @@ -27,9 +28,12 @@ lazy_static::lazy_static! { pub static ref HTTP_ROUTERS_CACHE: RwLock = RwLock::new(RoutersCache { routers: HashMap::new(), version: 0, + invalidations: 0, }); } +static HTTP_ROUTERS_INVALIDATIONS: AtomicU64 = AtomicU64::new(0); + #[derive(Debug, Deserialize, Clone)] pub struct TriggerRoute { pub path: String, @@ -56,6 +60,10 @@ pub struct TriggerRoute { pub struct RoutersCache { pub routers: HashMap>, pub version: i64, + /// `HTTP_ROUTERS_INVALIDATIONS` as of the moment these rows were read. A rebuild that + /// started before an invalidation publishes a count behind the current one, which is what + /// stops it from passing its own stale rows off as covering that invalidation. + invalidations: u64, } #[derive(Serialize, Deserialize, sqlx::Type, Debug, Clone, Copy, Hash, Eq, PartialEq)] @@ -223,12 +231,24 @@ pub fn validate_authentication_method( } } -pub async fn refresh_routers(db: &DB) -> Result<(bool, RwLockReadGuard<'_, RoutersCache>)> { +/// `force` rebuilds unconditionally. `nextval` on `http_trigger_version_seq` runs inside the +/// writing transaction and sequences are non-transactional, so another session can cache the +/// bumped version against still-uncommitted rows, after which every version-gated refresh is a +/// no-op. Force when reacting to a bump that could have been observed before its own rows were. +pub async fn refresh_routers( + db: &DB, + force: bool, +) -> Result<(bool, RwLockReadGuard<'_, RoutersCache>)> { + let invalidations = HTTP_ROUTERS_INVALIDATIONS.load(Ordering::Relaxed); let version = sqlx::query_scalar!("SELECT last_value FROM http_trigger_version_seq",) .fetch_one(db) .await?; let routers_cache = HTTP_ROUTERS_CACHE.read().await; - if routers_cache.version == 0 || version > routers_cache.version { + if force + || routers_cache.version == 0 + || version > routers_cache.version + || invalidations != routers_cache.invalidations + { drop(routers_cache); let mut routers = HashMap::new(); @@ -274,7 +294,8 @@ pub async fn refresh_routers(db: &DB) -> Result<(bool, RwLockReadGuard<'_, Route .await?; let mut router = matchit::Router::new(); - let http_route_workspaced = HTTP_ROUTE_WORKSPACED_ROUTE.load(std::sync::atomic::Ordering::Relaxed); + let http_route_workspaced = + HTTP_ROUTE_WORKSPACED_ROUTE.load(std::sync::atomic::Ordering::Relaxed); for trigger in triggers { let full_path = @@ -306,7 +327,7 @@ pub async fn refresh_routers(db: &DB) -> Result<(bool, RwLockReadGuard<'_, Route } let mut routers_cache = HTTP_ROUTERS_CACHE.write().await; - *routers_cache = RoutersCache { routers, version }; + *routers_cache = RoutersCache { routers, version, invalidations }; Ok((true, routers_cache.downgrade())) } else { @@ -315,11 +336,19 @@ pub async fn refresh_routers(db: &DB) -> Result<(bool, RwLockReadGuard<'_, Route } } +/// Record that the cache no longer covers everything committed, so the next refresh rebuilds +/// whatever the version says. The routes already loaded keep being served in the meantime. Use +/// after a forced refresh fails: its change is inside the cached version, so nothing else would +/// retry it. +pub fn invalidate_routers() { + HTTP_ROUTERS_INVALIDATIONS.fetch_add(1, Ordering::Relaxed); +} + pub async fn refresh_routers_loop( db: &DB, mut killpill_rx: tokio::sync::broadcast::Receiver<()>, ) -> () { - match refresh_routers(db).await { + match refresh_routers(db, false).await { Ok(_) => { tracing::info!("Loaded HTTP routers"); } @@ -335,7 +364,7 @@ pub async fn refresh_routers_loop( break; } _ = tokio::time::sleep(std::time::Duration::from_secs(60)) => { - match refresh_routers(&db).await { + match refresh_routers(&db, false).await { Ok((true, _)) => { tracing::info!("Refreshed HTTP routers"); } diff --git a/backend/windmill-trigger-http/tests/refresh_routers.rs b/backend/windmill-trigger-http/tests/refresh_routers.rs new file mode 100644 index 0000000000..f47da2ee0e --- /dev/null +++ b/backend/windmill-trigger-http/tests/refresh_routers.rs @@ -0,0 +1,62 @@ +use sqlx::{Pool, Postgres}; +use windmill_trigger_http::{invalidate_routers, refresh_routers, HttpMethod, RoutersCache}; + +async fn insert_trigger(db: &Pool, path: &str, route_path: &str) { + sqlx::query( + "INSERT INTO http_trigger ( + path, route_path, route_path_key, script_path, is_flow, workspace_id, edited_by, + permissioned_as, http_method, authentication_method, request_type, is_static_website, + workspaced_route, wrap_body, raw_string, mode + ) VALUES ($1, $2, $2, 'f/test/handler', false, 'test-workspace', 'test-user', + 'u/test-user', 'get', 'none', 'async', false, false, false, false, 'enabled')", + ) + .bind(path) + .bind(route_path) + .execute(db) + .await + .expect("insert http_trigger"); +} + +fn routes(cache: &RoutersCache, path: &str) -> bool { + cache.routers[&HttpMethod::Get].at(path).is_ok() +} + +// A trigger row can commit without advancing http_trigger_version_seq past what the cache +// already holds, because `nextval` runs ahead of the commit it belongs to. The version gate +// cannot see such a row; only forcing, or an invalidation, recovers the route. +#[sqlx::test(migrations = "../migrations")] +async fn rebuilds_a_change_the_cached_version_does_not_cover(db: Pool) { + insert_trigger(&db, "f/test/first", "first").await; + let (rebuilt, cache) = refresh_routers(&db, false).await.unwrap(); + assert!(rebuilt); + assert!(routes(&cache, "/first")); + drop(cache); + + insert_trigger(&db, "f/test/second", "second").await; + + let (rebuilt, cache) = refresh_routers(&db, false).await.unwrap(); + assert!(!rebuilt, "an unchanged version must not rebuild"); + assert!(!routes(&cache, "/second")); + drop(cache); + + let (rebuilt, cache) = refresh_routers(&db, true).await.unwrap(); + assert!(rebuilt, "force must rebuild whatever the version says"); + assert!(routes(&cache, "/second")); + drop(cache); + + // A forced refresh that failed leaves its change inside the cached version, so the periodic + // version-gated refresh has to rebuild on the invalidation alone. + insert_trigger(&db, "f/test/third", "third").await; + invalidate_routers(); + + let (rebuilt, cache) = refresh_routers(&db, false).await.unwrap(); + assert!( + rebuilt, + "an invalidation must rebuild through the version gate" + ); + assert!(routes(&cache, "/third")); + drop(cache); + + let (rebuilt, _) = refresh_routers(&db, false).await.unwrap(); + assert!(!rebuilt, "a served invalidation must not rebuild forever"); +} From b8bf539c3fe2b4db9c74dd73f04b3029287acdc6 Mon Sep 17 00:00:00 2001 From: hugocasa Date: Wed, 26 Aug 2026 20:24:34 +0200 Subject: [PATCH 13/34] fix(cli): keep svelte component styles in the raw-app bundle (#10838) * fix(cli): keep svelte component styles in the raw-app bundle Co-Authored-By: Claude Opus 5 * test: fold svelte style guard into the plugin test file Co-Authored-By: Claude Opus 5 * docs: record the editor-parity constraint on the svelte css option Co-Authored-By: Claude Opus 5 * test(cli): pin esbuild's service cwd before any test file chdirs esbuild's node API captures process.cwd() when its module is first imported and spawns its service with that cwd on every (re)start. createBundle stops the service after each bundle, so the cwd is reused across the whole run. Several test files chdir into a temp dir and delete it afterwards. The first one to bundle therefore pinned the service to a directory that stopped existing, and the next test to reach esbuild died with The service was stopped: ENOENT: no such file or directory, posix_spawn '.../@esbuild/linux-x64/bin/esbuild' The binary is present; ENOENT is posix_spawn rejecting the missing cwd. Which file tripped it depended on bun's readdir order, so renaming an unrelated test file was enough to surface it. Importing esbuild from the preload pins the service to a cwd that outlives the run, independent of file ordering. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01G88YF3sZFnJZUvTLVjqhZc --------- Co-authored-by: Claude Opus 5 Co-authored-by: Ruben Fiszel --- cli/src/commands/app/bundle.ts | 9 +++- ....ts => raw_app_svelte_plugin_unit.test.ts} | 44 +++++++++++++++++-- cli/test/setup.ts | 6 +++ 3 files changed, 54 insertions(+), 5 deletions(-) rename cli/test/{raw_app_svelte_module_unit.test.ts => raw_app_svelte_plugin_unit.test.ts} (76%) diff --git a/cli/src/commands/app/bundle.ts b/cli/src/commands/app/bundle.ts index 0ebbdaa076..286e878cfb 100644 --- a/cli/src/commands/app/bundle.ts +++ b/cli/src/commands/app/bundle.ts @@ -191,7 +191,14 @@ function createSveltePlugin(appDir: string): any { // Convert Svelte syntax to JavaScript try { - const { js, warnings } = svelte.compile(source, { filename }); + // The raw-app editor's in-browser bundler compiles with + // `css: "injected"`, so this must too, or the same app renders + // styled there and unstyled once the CLI builds it: Svelte's default + // ("external") hands the +`, + "styles_entry.ts": `import Styled from './Styled.svelte'; +export default Styled; +`, + }); + + const js = await bundle("styles_entry.ts"); + + const scopeClass = js.match(/

Date: Wed, 26 Aug 2026 20:46:31 +0200 Subject: [PATCH 14/34] fix: recover from unresolvable AI session links instead of a dead end (#10854) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(frontend): delete the open AI session by its stable id `session` is a $derived lookup into the session list, so it resolves to undefined as soon as the entry is dropped. Nothing reads it after the removal today, so this is latent rather than a live bug, but the delete handler is async and the id is already available as a prop that stays valid for the whole teardown. Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): recover from unresolvable AI session links instead of a dead end Sessions live only in IndexedDB, keyed in the URL by `session_name`, so a link that resolves in one browser resolves to nothing in another. That hit a dead-end "Session not found" page whose only way out was a button — and it also caught a session of the user's own that had simply never been touched, since an untouched session is never persisted. Redirect instead: land on an empty session (reusing one that already exists, else creating one), replace the URL so back doesn't return to the broken link, and explain the swap in one dismissible notice above the composer. Never land on an existing conversation, which would read as a successful load. The notice explains one arrival, so it is spent the moment the arrival ends: a first message sent, the session deselected, or the page left. Deleting the open session removes it before the handler's own navigation lands — across HTTP when a fork goes with it — so that teardown is gated, otherwise recovery claims the gap and reports the session the user just deleted as missing. Co-Authored-By: Claude Opus 5 (1M context) --------- Co-authored-by: Claude Opus 5 (1M context) --- .../components/sessions/SessionPicker.svelte | 47 ++++---- .../components/sessions/SessionWrapper.svelte | 105 +++++++++++++----- .../sessions/sessionRecoveryNotice.svelte.ts | 18 +++ .../sessions/sessionState.svelte.ts | 57 ++++++++-- .../components/sessions/sessionState.test.ts | 105 ++++++++++++++++++ .../(root)/(logged)/sessions/+page.svelte | 85 ++++++++++---- 6 files changed, 340 insertions(+), 77 deletions(-) create mode 100644 frontend/src/lib/components/sessions/sessionRecoveryNotice.svelte.ts diff --git a/frontend/src/lib/components/sessions/SessionPicker.svelte b/frontend/src/lib/components/sessions/SessionPicker.svelte index 67b53aeff4..87462e301b 100644 --- a/frontend/src/lib/components/sessions/SessionPicker.svelte +++ b/frontend/src/lib/components/sessions/SessionPicker.svelte @@ -36,6 +36,7 @@ setNewSessionWorkspace, setSessionArchived, syncWorkspaceTo, + withOpenSessionTeardown, type Session } from './sessionState.svelte' import { unreadCountFor } from './sessionUnread.svelte' @@ -571,8 +572,8 @@ // After deleting the open session, land somewhere usable: the newest remaining // session, else a fresh one. The page derives the visible session from the - // `session_name` query, so leaving the URL on a deleted session would render - // its not-found state instead of a ready-to-type composer. + // `session_name` query, so leaving the URL on a deleted session would fall + // through to recovery and open a blank one rather than their recent work. async function openReplacementSession() { const next = sessionState.sessions[0] if (next) await activate(next) @@ -590,9 +591,11 @@ if (ids.length === 0) return const current = sessionState.currentSessionId const wasActive = !!current && ids.includes(current) - for (const id of ids) removeSession(id) - exitSelectionMode() - if (wasActive) await openReplacementSession() + await withOpenSessionTeardown(async () => { + for (const id of ids) removeSession(id) + exitSelectionMode() + if (wasActive) await openReplacementSession() + }) } async function handleConfirmedDelete() { @@ -608,23 +611,25 @@ deleteAlsoFork = false if (!session) return const wasActive = sessionState.currentSessionId === session.id - removeSession(session.id) - if (forkToDelete) { - try { - await WorkspaceService.deleteWorkspace({ workspace: forkToDelete }) - await deleteSessionsForWorkspace(forkToDelete) - sendUserToast(`Deleted forked workspace ${forkToDelete}`) - await reconcileAfterWorkspaceChange() - } catch (e: any) { - sendUserToast(`Failed to delete fork ${forkToDelete}: ${e?.body ?? e}`, true) + await withOpenSessionTeardown(async () => { + removeSession(session.id) + if (forkToDelete) { + try { + await WorkspaceService.deleteWorkspace({ workspace: forkToDelete }) + await deleteSessionsForWorkspace(forkToDelete) + sendUserToast(`Deleted forked workspace ${forkToDelete}`) + await reconcileAfterWorkspaceChange() + } catch (e: any) { + sendUserToast(`Failed to delete fork ${forkToDelete}: ${e?.body ?? e}`, true) + } } - } - // If the deleted fork was the active workspace, fall back to its parent - // so the user isn't stranded on a workspace that no longer exists. - if (forkToDelete && forkParentId && $workspaceStore === forkToDelete) { - syncWorkspaceTo(forkParentId) - } - if (wasActive) await openReplacementSession() + // If the deleted fork was the active workspace, fall back to its parent + // so the user isn't stranded on a workspace that no longer exists. + if (forkToDelete && forkParentId && $workspaceStore === forkToDelete) { + syncWorkspaceTo(forkParentId) + } + if (wasActive) await openReplacementSession() + }) } function focusAt(index: number) { diff --git a/frontend/src/lib/components/sessions/SessionWrapper.svelte b/frontend/src/lib/components/sessions/SessionWrapper.svelte index 52d9e476fb..b56b72c2be 100644 --- a/frontend/src/lib/components/sessions/SessionWrapper.svelte +++ b/frontend/src/lib/components/sessions/SessionWrapper.svelte @@ -1,5 +1,5 @@ {#snippet externalLinkHint()} @@ -315,6 +342,32 @@
Session not found
{:else} {#snippet inputPreface()} + {#if showRecoveryNotice} + + + +
+
+ + + We couldn't find that session +
+
+ {/if} {#if !hasFirstUserMessage} {/if} @@ -329,9 +382,7 @@ and reconcile would re-archive a workspace-archived one anyway. When the workspace is unavailable the SessionChangesBar below shows the move/discard banner instead (its actions are the real recovery path). --> -
+
This session is archived diff --git a/frontend/src/lib/components/sessions/sessionRecoveryNotice.svelte.ts b/frontend/src/lib/components/sessions/sessionRecoveryNotice.svelte.ts new file mode 100644 index 0000000000..635bd68601 --- /dev/null +++ b/frontend/src/lib/components/sessions/sessionRecoveryNotice.svelte.ts @@ -0,0 +1,18 @@ +import { SvelteSet } from 'svelte/reactivity' + +// Session ids opened as a stand-in for a `session_name` this browser doesn't +// hold. Kept in memory rather than on the Session record: persisted, the notice +// would replay on every reload of a session the user has since made their own. +const recovered = new SvelteSet() + +export function markSessionRecovered(id: string): void { + recovered.add(id) +} + +export function isSessionRecovered(id: string): boolean { + return recovered.has(id) +} + +export function clearSessionRecovered(id: string): void { + recovered.delete(id) +} diff --git a/frontend/src/lib/components/sessions/sessionState.svelte.ts b/frontend/src/lib/components/sessions/sessionState.svelte.ts index d8ef56b273..505af7a43a 100644 --- a/frontend/src/lib/components/sessions/sessionState.svelte.ts +++ b/frontend/src/lib/components/sessions/sessionState.svelte.ts @@ -21,6 +21,7 @@ import { import { getLocalSetting, storeLocalSetting } from '$lib/utils' import { logFeatureUsage } from '$lib/utils/featureUsage' import { workspaceRootId } from './sessionScope.svelte' +import { clearSessionRecovered } from './sessionRecoveryNotice.svelte' import { type DBSchema, type IDBPDatabase } from 'idb' import { userScopedDb } from '$lib/userScopedDb' import { deleteItemsForSession } from '../copilot/chat/files/attachedFilesDB' @@ -343,8 +344,8 @@ export function setSessionDraftPrompt(sessionId: string, text: string): void { if ((s.draftPrompt ?? '') === text) return // Keep `transient` (means "in-memory only") set until the flush persists the // draft, so hydrateSessions preserves it across a reconcile inside this window; - // isReusableBlank, not `transient`, is what stops createSession reusing a typed - // draft. Only the IndexedDB write is debounced. + // isDiscardableDraft, not `transient`, is what stops createSession reusing a + // typed draft. Only the IndexedDB write is debounced. s.draftPrompt = text clearTimeout(draftPromptFlushHandles.get(sessionId)) draftPromptFlushHandles.set( @@ -748,30 +749,52 @@ export function requestComposerFocus(): void { composerFocusRequest.nonce++ } -// An untouched in-memory blank that `+` may reuse/discard. `draftPrompt === -// undefined` (never edited), not falsiness: a draft typed then erased to '' still -// has a pending flush and is a real session, so it must survive both. Every other +// An untouched in-memory blank that `+` may reuse and createSession may silently +// drop. `draftPrompt === undefined` (never edited), not falsiness: a draft typed +// then erased to '' still has a pending flush and is a real session. Every other // touch clears `transient` synchronously, so only the draft prompt needs checking. -function isReusableBlank(s: Session): boolean { +function isDiscardableDraft(s: Session): boolean { return !!s.transient && s.draftPrompt === undefined } +// Somewhere empty to put the user, for a URL naming a session this browser +// doesn't hold. Only `transient` makes "empty" trustworthy: chat seeding and +// attached-file persistence both key off `!transient` and leave every field +// below untouched, so a persisted session can hold a conversation regardless. +export function findEmptyLandingSession(): Session | undefined { + return sessionState.sessions.find( + (s) => + !!s.transient && + !s.archived && + !s.workspace_id && + // Falsiness, not `=== undefined`: we only navigate into the session, so a + // draft erased back to '' is still an empty composer to land on. + !s.draftPrompt?.trim() && + !s.pending_fork && + sessionInCurrentFamily(s) + ) +} + export function createSession(): Session { // Reuse an existing untouched draft from the active family rather than pile a // blank entry on every `+`, so several pending sessions can still be built up // in parallel, one touch at a time. A cross-family leftover blank is dropped // instead of reused (reusing it would act on that family). const reusable = sessionState.sessions.find( - (s) => isReusableBlank(s) && sessionInCurrentFamily(s) + (s) => isDiscardableDraft(s) && sessionInCurrentFamily(s) ) if (reusable) { sessionState.currentSessionId = reusable.id + // The blank recovery just landed on is exactly what this reuses, so `+` + // would otherwise hand back a session still carrying the recovery notice: + // asking for a new session must not be answered with "we couldn't find it". + clearSessionRecovered(reusable.id) // Reusing an already-active draft doesn't change currentSessionId, so ask // the composer to focus explicitly — the caller still navigates/redirects. requestComposerFocus() return reusable } - sessionState.sessions = sessionState.sessions.filter((s) => !isReusableBlank(s)) + sessionState.sessions = sessionState.sessions.filter((s) => !isDiscardableDraft(s)) const existingNumbers = sessionState.sessions .map((s) => /^session-(\d+)$/.exec(s.name)?.[1]) .map((n) => (n ? parseInt(n, 10) : 0)) @@ -1087,6 +1110,24 @@ export function setSessionArchived(id: string, archived: boolean) { persistTouched(s) } +// A counter rather than a flag: an inner teardown finishing must not reopen the +// gate while an outer one is still running. Released in a finally, so a delete +// that throws can't wedge it shut. +let openSessionTeardowns = $state(0) + +export function isTearingDownOpenSession(): boolean { + return openSessionTeardowns > 0 +} + +export async function withOpenSessionTeardown(run: () => Promise): Promise { + openSessionTeardowns++ + try { + return await run() + } finally { + openSessionTeardowns-- + } +} + export function deleteSession(id: string) { const s = sessionState.sessions.find((x) => x.id === id) if (!s) return diff --git a/frontend/src/lib/components/sessions/sessionState.test.ts b/frontend/src/lib/components/sessions/sessionState.test.ts index 7732c44746..1a54c86e78 100644 --- a/frontend/src/lib/components/sessions/sessionState.test.ts +++ b/frontend/src/lib/components/sessions/sessionState.test.ts @@ -4,14 +4,22 @@ import { commitSessionWorkspace, createSession, decideSessionLifecycle, + findEmptyLandingSession, isForkSession, + isTearingDownOpenSession, renameSession, sessionInCurrentFamily, setGeneratedSessionSummary, setSessionDraftPrompt, sessionState, + withOpenSessionTeardown, type Session } from './sessionState.svelte' +import { + clearSessionRecovered, + isSessionRecovered, + markSessionRecovered +} from './sessionRecoveryNotice.svelte' import { enterpriseLicense, usersWorkspaceStore, @@ -362,6 +370,28 @@ describe('createSession — reuses an untouched draft, family-scoped', () => { } }) + it('clears the recovery notice off the draft it reuses, so `+` is not answered with "not found"', () => { + const restore = withTwoFamilies('rootA') + const prevCurrent = sessionState.currentSessionId + const landed = session({ + id: 'recovered-blank', + name: 'session-903', + pending_workspace_id: 'forkA', + transient: true + }) + sessionState.sessions.push(landed) + markSessionRecovered(landed.id) + try { + expect(createSession().id).toBe('recovered-blank') + expect(isSessionRecovered('recovered-blank')).toBe(false) + } finally { + clearSessionRecovered('recovered-blank') + sessionState.sessions = sessionState.sessions.filter((s) => s.id !== 'recovered-blank') + sessionState.currentSessionId = prevCurrent + restore() + } + }) + it('drops an untouched draft left over from another family and starts in the active workspace', () => { const restore = withTwoFamilies('rootB') const prevCurrent = sessionState.currentSessionId @@ -488,3 +518,78 @@ describe('createSession — reuses an untouched draft, family-scoped', () => { } }) }) + +describe('findEmptyLandingSession — where an unresolvable session link lands', () => { + it('takes an untouched draft', () => { + const restore = withTwoFamilies('forkA') + const blank = session({ + id: 'landing-blank', + name: 'session-910', + pending_workspace_id: 'forkA', + transient: true + }) + sessionState.sessions.push(blank) + try { + expect(findEmptyLandingSession()?.id).toBe('landing-blank') + } finally { + sessionState.sessions = sessionState.sessions.filter((s) => s.id !== 'landing-blank') + restore() + } + }) + + it('passes over a persisted session, onto which chat seeding can graft a conversation', () => { + const restore = withTwoFamilies('forkA') + // ensureChatIdsSeeded assigns untagged legacy chats to `!transient` sessions + // and initRuntime loads them, without touching a field checked here. + const abandoned = session({ + id: 'landing-abandoned', + name: 'session-910', + pending_workspace_id: 'forkA' + }) + const others = sessionState.sessions + sessionState.sessions = [abandoned] + try { + expect(findEmptyLandingSession()).toBeUndefined() + } finally { + sessionState.sessions = others + restore() + } + }) + + it('passes over a session that has been sent, so recovery never reopens a conversation', () => { + const restore = withTwoFamilies('forkA') + const sent = session({ id: 'landing-sent', name: 'session-911', workspace_id: 'forkA' }) + // Sole candidate, so `undefined` pins the exclusion: `not.toBe` would also + // pass on any unrelated session the shared module state happens to hold. + const others = sessionState.sessions + sessionState.sessions = [sent] + try { + expect(findEmptyLandingSession()).toBeUndefined() + } finally { + sessionState.sessions = others + restore() + } + }) +}) + +describe('withOpenSessionTeardown — the gate that holds recovery off during a delete', () => { + it('stays shut until the outermost teardown finishes', async () => { + let innerDone = false + await withOpenSessionTeardown(async () => { + await withOpenSessionTeardown(async () => {}) + innerDone = true + expect(isTearingDownOpenSession()).toBe(true) + }) + expect(innerDone).toBe(true) + expect(isTearingDownOpenSession()).toBe(false) + }) + + it('reopens when the teardown throws, so a failed delete cannot wedge recovery shut', async () => { + await expect( + withOpenSessionTeardown(async () => { + throw new Error('fork deletion failed') + }) + ).rejects.toThrow('fork deletion failed') + expect(isTearingDownOpenSession()).toBe(false) + }) +}) diff --git a/frontend/src/routes/(root)/(logged)/sessions/+page.svelte b/frontend/src/routes/(root)/(logged)/sessions/+page.svelte index b6f5029616..fbf045fe19 100644 --- a/frontend/src/routes/(root)/(logged)/sessions/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/sessions/+page.svelte @@ -1,5 +1,5 @@ {#if deployHub.session} + { + const target = discardTarget + discardTarget = undefined + if (target && target === deployHub.session) await target.discardUpdate() + }} + onCanceled={() => (discardTarget = undefined)} + > + + Discard this update? Everything pushed for it, including recordings made for it, is deleted + and cannot be recovered. Your published project is unaffected. + + {#key deployHub.session} {@const s = deployHub.session}
@@ -178,8 +202,10 @@
  • 1 ? 'opacity-60' : ''}> {stepNum > 1 ? '✓' : '1.'} - Bundle your project — creates a draft - on the Hub with every selected script, flow, app and resource from this folder. + Bundle your project — sends every + selected script, flow, app and resource from this folder to the Hub{s.liveOnHub + ? ' as an update' + : ' as a draft'}.
  • 2 ? 'opacity-60' : 'opacity-40'} @@ -235,7 +261,7 @@ startIcon={{ icon: Cloud }} onclick={openBundle} > - Create Hub draft ({s.selectedItems.length}) + {s.liveOnHub ? 'Bundle update' : 'Create Hub draft'} ({s.selectedItems.length}) {:else if s.phase === 'draft'} + {/if} + {/if} + {#if s.liveOnHub && s.phase === 'draft'} + {/if}
  • @@ -270,8 +319,10 @@ {#if s.phase === 'predeploy'}
    - Bundling creates a draft project on the Hub from the selected scripts, flows and - apps of {s.selectedFolder}/. + Bundling creates {s.liveOnHub + ? 'an update to your Hub project' + : 'a draft project on the Hub'} from the selected scripts, flows and apps of + {s.selectedFolder}/. {s.selectedItems.length} of {s.filteredWorkspaceItems.length} items selected.
    @@ -351,6 +402,31 @@ {/if}
    {/if} + {#if s.liveOnHub && s.phase !== 'live' && s.phase !== 'under_review'} + + Visitors keep seeing the published version, with its stars, forks and comments, + until this update is approved. Approving replaces it in place; discarding leaves it + exactly as it is. + + {/if} + {#if s.pipelineReplayMayBeStale && s.phase === 'draft'} + + This update carries the cascade recorded for the version that is live, and at least + one item has changed since. Record it again below, or visitors will replay the old + run as though it were this version. + + {/if} + {#if s.rejectionReason && s.phase === 'draft'} + + {s.rejectionReason} + + {/if} {#if s.phase === 'draft'}
    @@ -477,18 +553,21 @@
    {/if} {#if s.phase === 'under_review'} -
    - -
    - Locked while under review - - The Windmill team is reviewing this submission. Editing, recording, and sharing - actions are disabled. Estimated turnaround: 1-2 business days. - -
    -
    + The Windmill team is reviewing your project. Submission is locked until they answer + — no new version can be sent to the Hub, and no recording added to this one. + Estimated turnaround: 1-2 business days{#if s.hubSupportsUpdates}; cancel the + submission to get back to it sooner{/if}.{#if s.liveOnHub} + Visitors keep seeing the published version meanwhile, with its stars, forks and + comments; approving replaces it in place.{/if} Your folder itself is untouched — keep + editing your scripts and flows as usual. + {/if} {#if s.phase === 'draft'} {@const recordedCount = s.recordableItems.filter((i) => i.rec === 'recorded').length} @@ -668,7 +747,11 @@ Waiting for the Windmill team to review the submission. {:else} - Iterate further by starting a new draft. + + {s.liveOnHub + ? 'Publish an update to change it — this stays live until the update is approved.' + : 'Iterate further by starting a new draft.'} + {/if}
    {/snippet} diff --git a/frontend/src/lib/components/workspaceSettings/deployToHubSession.svelte.ts b/frontend/src/lib/components/workspaceSettings/deployToHubSession.svelte.ts index 5cc7f0d1d7..c421553a7c 100644 --- a/frontend/src/lib/components/workspaceSettings/deployToHubSession.svelte.ts +++ b/frontend/src/lib/components/workspaceSettings/deployToHubSession.svelte.ts @@ -198,8 +198,31 @@ export class DeployToHubSession { // Whether the Hub currently has a custom logo for this project (from // rehydration) — drives the "Remove current logo" affordance. hubHasRemoteLogo = $state(false) + // A pipeline recording is attached on the Hub. An update inherits the published + // one, which is only a demo of the new version if nothing it runs changed. + hubHasPipelineRecording = $state(false) + // The Hub's own verdict: this update runs different content from the published + // version. False when there is no update in flight. + hubItemsChanged = $state(false) + // The attached pipeline recording is the published version's, copied when this + // update started, rather than one recorded for it. Authoritative across reloads, + // unlike `pipelineRecorded`, which only remembers this session. + hubPipelineRecordingInherited = $state(false) effectiveSlug = $state('') hubItemIds = $state>({}) + // Set once the project is published: everything the wizard shows from here on + // describes an update to it, and the published version keeps serving until that + // update is approved. `phase` is the update's own status, not the project's. + liveOnHub = $state(false) + /** This Hub knows about pending updates — it answers rehydration with a `live` + * key. An older one takes a project offline to republish and has neither the + * withdraw nor the discard endpoint, so the actions built on them stay hidden. */ + hubSupportsUpdates = $state(false) + // A reviewer's verdict on the current draft, shown so the publisher knows what + // to fix before resubmitting. + rejectionReason = $state(undefined) + discardingUpdate = $state(false) + withdrawing = $state(false) // Best-effort data table migrations for the bundle, editable in the drawer and // pushed on deploy. Regenerated when the bundle drawer opens. @@ -228,6 +251,10 @@ export class DeployToHubSession { submitting = $state(false) syncing = $state(false) + // Set from the Hub's answer to the draft request: this push went into an update + // rather than over the published project. + #publishedAsUpdate = false + // Intra-session tokens: latest call wins among competing calls on this session. #triggerLoadTok = 0 #recordRunTok = 0 @@ -316,6 +343,18 @@ export class DeployToHubSession { ) pipelineScriptPathSet = $derived(new Set(this.pipelineScriptPaths)) isPipelineProject = $derived(this.pipelineScriptPaths.length > 0) + /** The pipeline replay this update carries came from the published version, and + * something it runs has changed since — so it is a recording of another version. + * `hubItemsChanged` is the Hub comparing content, not a guess from which items + * carry recordings: an item nobody ever recorded has not changed. */ + pipelineReplayMayBeStale = $derived( + this.liveOnHub && + this.isPipelineProject && + this.hubHasPipelineRecording && + this.hubPipelineRecordingInherited && + this.hubItemsChanged && + !this.pipelineRecorded + ) hubSlug = $derived(this.effectiveSlug || sanitizeSlug(this.hubName)) relevantTriggers = $derived.by(() => { @@ -573,6 +612,17 @@ export class DeployToHubSession { this.hubSummary = p.summary ?? '' this.hubReadme = p.readme ?? '' this.hubHasRemoteLogo = p.has_logo === true + this.hubHasPipelineRecording = p.has_pipeline_recording === true + this.hubItemsChanged = p.items_changed === true + this.hubPipelineRecordingInherited = p.pipeline_recording_inherited === true + this.rejectionReason = p.rejection_reason ?? undefined + // `live` is a key this Hub always sends — null unless an update is in + // flight, in which case the fields above describe that update and the + // project itself is still published. Its absence means a Hub old enough to + // still take a project offline while it re-publishes, so the wizard must + // not promise otherwise. + this.hubSupportsUpdates = 'live' in p + this.liveOnHub = this.hubSupportsUpdates && (p.live?.approved === true || p.status === 'live') this.phase = p.status === 'live' ? 'live' : p.status === 'under_review' ? 'under_review' : 'draft' const ids: Record = {} @@ -898,6 +948,9 @@ export class DeployToHubSession { try { const parsed = JSON.parse(text) if (typeof parsed?.slug === 'string') returnedSlug = parsed.slug + // The Hub decides this: publishing over an approved project goes into a + // pending update instead, and the project keeps serving meanwhile. + this.#publishedAsUpdate = parsed?.pending_revision === true } catch {} if (!returnedSlug) { sendUserToast(`Hub did not return a slug. Aborting publish to avoid path drift.`, true) @@ -1252,8 +1305,13 @@ export class DeployToHubSession { // UI stuck in `predeploy`; rehydrate then upgrades to authoritative state. this.draftItems = itemsSnapshot.map((i) => ({ ...i, rec: 'none' })) this.phase = 'draft' + const asUpdate = this.#publishedAsUpdate await this.rehydrateFromHub() - sendUserToast(`Draft created on the Hub. Add recordings before submitting for review.`) + sendUserToast( + asUpdate + ? `Update ready on the Hub. Your published project stays live until it is approved.` + : `Draft created on the Hub. Add recordings before submitting for review.` + ) } finally { this.deploying = false } @@ -1313,12 +1371,82 @@ export class DeployToHubSession { } } + /** Go back to picking items, to publish again. Local only — nothing reaches the + * Hub until the bundle is confirmed, and where the Hub supports updates the + * published version keeps serving even then. */ startNewDraft = () => { this.draftItems = [] this.recordings = {} + this.rejectionReason = undefined + // All of it belongs to the update just finished, not the one starting. The + // captured cascade especially: left in place, the next update could save a + // replay of the version it replaces. Bumping the token first abandons a run + // still in flight, which would otherwise write its result back over this. + this.#pipelineRunTok++ + this.pipelineRecorded = false + this.pipelineRecordingResult = undefined + this.pipelineRunState = 'idle' + this.pipelineRunError = undefined this.phase = 'predeploy' } + /** Take the submission back out of review. Everything pushed for it is kept, so + * it can be fixed and submitted again. */ + cancelSubmission = async () => { + if (this.withdrawing) return + const slug = this.effectiveSlug + if (!slug) return + this.withdrawing = true + try { + const res = await fetch( + `/api/w/${this.workspace}/hub/projects/${encodeURIComponent(slug)}/withdraw${this.#folderQs()}`, + { method: 'POST', credentials: 'include' } + ) + if (!res.ok) { + sendUserToast(`Could not cancel the submission: ${await res.text()}`, true) + return + } + if (this.#disposed) return + this.phase = 'draft' + await this.rehydrateFromHub() + sendUserToast(`Submission cancelled. Everything you pushed is still here.`) + } catch (e: any) { + sendUserToast(`Could not cancel the submission: ${e?.message ?? e}`, true) + } finally { + this.withdrawing = false + } + } + + /** Throw away an update in progress and go back to what is published. */ + discardUpdate = async () => { + if (this.discardingUpdate) return + const slug = this.effectiveSlug + if (!slug) return + this.discardingUpdate = true + try { + const res = await fetch( + `/api/w/${this.workspace}/hub/projects/${encodeURIComponent(slug)}/discard_update${this.#folderQs()}`, + { method: 'POST', credentials: 'include' } + ) + if (!res.ok) { + sendUserToast(`Could not discard the update: ${await res.text()}`, true) + return + } + if (this.#disposed) return + this.draftItems = [] + this.recordings = {} + this.deploymentStatus = {} + this.rejectionReason = undefined + this.phase = 'live' + await this.rehydrateFromHub() + sendUserToast(`Update discarded. The published project is unchanged.`) + } catch (e: any) { + sendUserToast(`Could not discard the update: ${e?.message ?? e}`, true) + } finally { + this.discardingUpdate = false + } + } + /** Reset record-drawer state and load the target's schema. */ async openRecord(it: DeployItem) { const tok = ++this.#recordRunTok From af15a73b8b74ab2e8fa4150ffb364dc319445c52 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Wed, 26 Aug 2026 22:14:27 +0200 Subject: [PATCH 16/34] chore: move the compose stack to postgres 18 (#10827) * chore: move the compose stack to postgres 18 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Ha6ovKdT9XRoj5FyVe7fEt * fix: dump the whole cluster in the postgres 18 upgrade recipe Windmill creates instance datatable, DuckLake and wm_fork_* databases in the same cluster as windmill, so a single-database pg_dump followed by removing the volume loses them silently. Dump the cluster with pg_dumpall instead, which also carries the roles the RLS policies are granted to, with their passwords. Also wait on the healthcheck before restoring, stop services generically rather than by name, and ANALYZE after the restore. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Ha6ovKdT9XRoj5FyVe7fEt * fix: analyze every restored database and check the restore for errors ANALYZE is per-database, so the sibling datatable/DuckLake/wm_fork_* databases the recipe now restores were left with no planner statistics; vacuumdb --all covers them. psql does not stop on error and the old volume is gone by that point, so the restore needs an explicit grep rather than a trusted exit code. Also note that logical replication slots are never dumped, so a Postgres trigger reading a database in this cluster comes back disabled until it is re-saved. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Ha6ovKdT9XRoj5FyVe7fEt * fix: drop the bootstrapped windmill database before the restore POSTGRES_DB creates an empty windmill database, so the dump's own CREATE DATABASE for it fails and its objects load into the entrypoint's database instead, keeping the new cluster's encoding and collation rather than the dumped ones. Sibling databases are created by the dump and so were never affected. Dropping it first makes the restore reproduce the source cluster exactly, and leaves one expected error instead of two. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Ha6ovKdT9XRoj5FyVe7fEt * docs: move the postgres 18 upgrade runbook out of the compose file A step-by-step runbook in a config file needed corrections in three consecutive review rounds, which is the argument for keeping it somewhere it can be fixed once. The comment keeps only the constraint a reader has to know before touching the mount, plus a link. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Ha6ovKdT9XRoj5FyVe7fEt * docs: point the postgres 18 upgrade note at windmill.dev GitHub gists are owned by user accounts, never organisations, so a gist is the wrong home for the only migration instructions every self-hosted operator gets. The procedure now lives in the self-host docs page instead. Depends on windmill-labs/windmilldocs#1704 merging and deploying first. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Ha6ovKdT9XRoj5FyVe7fEt --------- Co-authored-by: Claude Opus 5 (1M context) --- docker-compose.yml | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index a801a0ce7c..bd1e060f22 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,15 +8,28 @@ x-logging: &default-logging compress: "true" services: + ## UPGRADING FROM POSTGRES 16: db_data holds a cluster 18 cannot read, so the + ## container exits with an explanatory error rather than coming up blank. Migrating + ## means dumping the WHOLE cluster (pg_dumpall), never just the windmill database: + ## Windmill keeps datatable, DuckLake and wm_fork_* databases beside it and grants + ## its RLS policies to cluster-level roles, and a single-database dump loses both + ## silently. Full procedure, and why 16 is still a valid choice until Nov 2028: + ## https://www.windmill.dev/docs/advanced/self_host#upgrade-postgresql-to-18 db: deploy: # To use an external database, set replicas to 0 and set DATABASE_URL to the external database url in the .env file replicas: 1 - image: postgres:16 + image: postgres:18 shm_size: 1g restart: unless-stopped volumes: - - db_data:/var/lib/postgresql/data + # From 18 on the official image keeps the cluster in a major-version + # subdirectory (/var/lib/postgresql/18/docker), so the mount has to be the + # parent directory: that is what lets pg_upgrade see an old and a new + # cluster inside a single mount point. Mounting the pre-18 .../data path + # instead makes the image exit rather than start, which is what turns a + # stale 16 cluster into a loud failure instead of an empty instance. + - db_data:/var/lib/postgresql expose: - 5432 environment: From f131c3920f50f9fa18cd637eac39609495999aef Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Wed, 26 Aug 2026 23:08:36 +0200 Subject: [PATCH 17/34] fix: keep connection string query parameters under token auth (#10859) * fix: keep connection string query parameters under token auth * refactor: fold the database url parsing into one connect-options helper * docs: state the narrower invariant on base_connect_options * chore: update ee-repo-ref to 212cc7d61ec38580d4a70d9ac38d7a2cc9daf409 This commit updates the EE repository reference after PR #746 was merged in windmill-ee-private. Previous ee-repo-ref: a15d08345d7e42526c28382079ad1f575a2d1674 New ee-repo-ref: 212cc7d61ec38580d4a70d9ac38d7a2cc9daf409 Automated by sync-ee-ref workflow. --------- Co-authored-by: windmill-internal-app[bot] --- backend/ee-repo-ref.txt | 2 +- backend/windmill-common/src/db_params.rs | 45 ------------------------ backend/windmill-common/src/lib.rs | 19 +++++++--- 3 files changed, 15 insertions(+), 51 deletions(-) delete mode 100644 backend/windmill-common/src/db_params.rs diff --git a/backend/ee-repo-ref.txt b/backend/ee-repo-ref.txt index 8f96f52924..bdbf08e187 100644 --- a/backend/ee-repo-ref.txt +++ b/backend/ee-repo-ref.txt @@ -1 +1 @@ -d6aef91c0f7ba556befbf4addeb7674d4a9dd819 \ No newline at end of file +212cc7d61ec38580d4a70d9ac38d7a2cc9daf409 diff --git a/backend/windmill-common/src/db_params.rs b/backend/windmill-common/src/db_params.rs deleted file mode 100644 index d52700376d..0000000000 --- a/backend/windmill-common/src/db_params.rs +++ /dev/null @@ -1,45 +0,0 @@ -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 { - 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(), - }) -} diff --git a/backend/windmill-common/src/lib.rs b/backend/windmill-common/src/lib.rs index 89db0ee769..cf0be2bbd8 100644 --- a/backend/windmill-common/src/lib.rs +++ b/backend/windmill-common/src/lib.rs @@ -42,7 +42,6 @@ pub mod db; mod db_entra_ee; #[cfg(all(feature = "enterprise", feature = "private"))] mod db_iam_ee; -pub mod db_params; pub mod dbt_manifest; pub mod deploy_origin; #[cfg(feature = "private")] @@ -1479,6 +1478,17 @@ pub async fn create_custom_instance_database( Ok(()) } +/// Connection options parsed from a database URL. +/// +/// The only place a database URL becomes `PgConnectOptions`. Providers that mint the password +/// themselves override it on these and keep the rest: options assembled field by field instead +/// would drop every query parameter, `sslmode` and `sslrootcert` above all, leaving the +/// connection on sqlx's default TLS policy rather than the operator's. +pub fn base_connect_options(database_url: &str) -> Result { + sqlx::postgres::PgConnectOptions::from_str(database_url) + .map_err(|e| Error::InternalErr(format!("Failed to parse database URL: {}", e))) +} + #[derive(Clone)] pub enum DatabaseUrl { #[cfg(all(feature = "enterprise", feature = "private"))] @@ -1509,8 +1519,8 @@ impl DatabaseUrl { } /// Get PgConnectOptions for this database URL. - /// 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 token-based auth (IAM RDS, Entra ID), this returns options carrying the current + /// token, set on the builder to avoid double-encoding temporary credentials. /// For static URLs, this parses the URL string. pub async fn connect_options(&self) -> Result { match self { @@ -1524,8 +1534,7 @@ impl DatabaseUrl { 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))), + DatabaseUrl::Static(url) => base_connect_options(url), } } From 8b80b09f33d311f0881678577ca6004c12d97c22 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Wed, 26 Aug 2026 23:35:44 +0200 Subject: [PATCH 18/34] fix: restrict filesystem workspace storage to debug builds (#10864) * fix: restrict filesystem workspace storage to debug builds Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Q7p2VbtYqaXHGaAskgwVk5 * chore: update ee-repo-ref to b58ad414b098d3d7787001a352bfbb13e43a335f This commit updates the EE repository reference after PR #747 was merged in windmill-ee-private. Previous ee-repo-ref: 1b4dada77a8fe2224579c643550c63b1ac2616de New ee-repo-ref: b58ad414b098d3d7787001a352bfbb13e43a335f Automated by sync-ee-ref workflow. --------- Co-authored-by: Claude Opus 5 (1M context) Co-authored-by: windmill-internal-app[bot] --- backend/AGENTS.md | 8 ++- backend/ee-repo-ref.txt | 2 +- .../windmill-api-workspaces/src/workspaces.rs | 17 ++++++ backend/windmill-common/src/workspaces.rs | 26 +++++++++ backend/windmill-object-store/src/lib.rs | 1 + backend/windmill-worker/src/common.rs | 1 + .../workspaceSettings/StorageSettings.svelte | 57 ++++++++++++------- 7 files changed, 87 insertions(+), 25 deletions(-) diff --git a/backend/AGENTS.md b/backend/AGENTS.md index 0d9639c6c0..7bd84dfca3 100644 --- a/backend/AGENTS.md +++ b/backend/AGENTS.md @@ -131,9 +131,11 @@ minimal explicit set for dev. ## Workspace object storage in dev — use the local filesystem For a dev workspace you don't need MinIO/S3: use the built-in **`FilesystemStorage`** large-file -storage (a root path on local disk). It is intentionally hidden from the settings-UI storage -dropdown (dev-only), so set it via the API. Requires the backend built with `parquet` (+ `private` -for the real S3 helpers, + `enterprise` if you want advanced permission rules enforced): +storage (a root path on local disk). It is a **debug-build affordance only** — every site that +builds a filesystem object store calls `ensure_filesystem_storage_allowed`, so release builds +refuse it, and the settings UI never offers it — so set it via the API on a `cargo run`/`cargo +test` binary. Requires the backend built with `parquet` (+ `private` for the real S3 helpers, ++ `enterprise` if you want advanced permission rules enforced): ```bash curl -X POST "$BASE/api/w//workspaces/edit_large_file_storage_config" \ diff --git a/backend/ee-repo-ref.txt b/backend/ee-repo-ref.txt index bdbf08e187..4767454f25 100644 --- a/backend/ee-repo-ref.txt +++ b/backend/ee-repo-ref.txt @@ -1 +1 @@ -212cc7d61ec38580d4a70d9ac38d7a2cc9daf409 +b58ad414b098d3d7787001a352bfbb13e43a335f diff --git a/backend/windmill-api-workspaces/src/workspaces.rs b/backend/windmill-api-workspaces/src/workspaces.rs index e04e1c1ab9..3b3ea2713c 100644 --- a/backend/windmill-api-workspaces/src/workspaces.rs +++ b/backend/windmill-api-workspaces/src/workspaces.rs @@ -1970,6 +1970,23 @@ async fn edit_large_file_storage_config( ))); } + if !windmill_common::workspaces::filesystem_storage_allowed() { + let named = std::iter::once(("primary storage", &lfs_config.large_file_storage)).chain( + lfs_config + .secondary_storage + .iter() + .map(|(name, storage)| (name.as_str(), storage)), + ); + for (name, storage) in named { + if matches!(storage, LargeFileStorage::FilesystemStorage(_)) { + return Err(Error::BadRequest(format!( + "{name}: {}", + windmill_common::workspaces::FILESYSTEM_STORAGE_DEV_ONLY_MSG + ))); + } + } + } + let serialized_lfs_config = serde_json::to_value::(lfs_config) .map_err(|err| Error::internal_err(err.to_string()))?; diff --git a/backend/windmill-common/src/workspaces.rs b/backend/windmill-common/src/workspaces.rs index 5f558d325a..ae3e6546cd 100644 --- a/backend/windmill-common/src/workspaces.rs +++ b/backend/windmill-common/src/workspaces.rs @@ -2193,6 +2193,32 @@ pub fn lfs_entry_storage_ref(entry: &serde_json::Value) -> Option { Some(format!("{typ}:{path}")) } +pub const FILESYSTEM_STORAGE_DEV_ONLY_MSG: &str = + "Filesystem storage is only available in development builds of Windmill: it points the \ + workspace at a directory on the server's own disk rather than at a resource. Use an S3, \ + Azure Blob or Google Cloud Storage backend instead."; + +/// A filesystem workspace storage names a directory on the server's own disk, so it hands whoever +/// configures it — a workspace admin, or any member who can write a `filesystem` resource — +/// whatever the server process can reach, and it only resolves when server and workers share that +/// disk. It is there so local development can skip MinIO, hence debug builds only. Instance object +/// storage on local disk is a separate, superadmin-only setting and stays allowed everywhere. +pub fn filesystem_storage_allowed() -> bool { + cfg!(debug_assertions) +} + +/// Guards every site that builds an `ObjectStoreResource::Filesystem`, so nothing downstream can +/// reach a local-disk store: a stored config outlives the build that accepted it, and the resource +/// route never passes through the workspace-storage settings at all. +pub fn ensure_filesystem_storage_allowed() -> Result<()> { + if !filesystem_storage_allowed() { + return Err(Error::BadRequest( + FILESYSTEM_STORAGE_DEV_ONLY_MSG.to_string(), + )); + } + Ok(()) +} + /// Resolve a `$res:`/`$var:` reference tree to its concrete value (recursively, secrets /// decrypted). No permission checks — trusted server-side callers only; never echo the result /// to a user. diff --git a/backend/windmill-object-store/src/lib.rs b/backend/windmill-object-store/src/lib.rs index 0f919f1048..cc588d485f 100644 --- a/backend/windmill-object-store/src/lib.rs +++ b/backend/windmill-object-store/src/lib.rs @@ -1171,6 +1171,7 @@ pub fn lfs_to_object_store_resource( Ok(ObjectStoreResource::Gcs(gcs_resource)) } LargeFileStorage::FilesystemStorage(fs) => { + windmill_common::workspaces::ensure_filesystem_storage_allowed()?; Ok(ObjectStoreResource::Filesystem(FilesystemSettings { root_path: fs.root_path.clone(), })) diff --git a/backend/windmill-worker/src/common.rs b/backend/windmill-worker/src/common.rs index 6ee22fd882..f6391eab07 100644 --- a/backend/windmill-worker/src/common.rs +++ b/backend/windmill-worker/src/common.rs @@ -1640,6 +1640,7 @@ pub(crate) async fn get_workspace_s3_resource_path( ) } Some(LargeFileStorage::FilesystemStorage(fs)) => { + windmill_common::workspaces::ensure_filesystem_storage_allowed()?; return Ok(Some( windmill_object_store::ObjectStoreResource::Filesystem( windmill_object_store::FilesystemSettings { root_path: fs.root_path.clone() }, diff --git a/frontend/src/lib/components/workspaceSettings/StorageSettings.svelte b/frontend/src/lib/components/workspaceSettings/StorageSettings.svelte index 1e6c990819..a2ccf0b958 100644 --- a/frontend/src/lib/components/workspaceSettings/StorageSettings.svelte +++ b/frontend/src/lib/components/workspaceSettings/StorageSettings.svelte @@ -44,6 +44,14 @@ onDiscard?: () => void } = $props() + const creatableStorageTypes = [ + { value: 's3', label: 'S3' }, + { value: 'azure_blob', label: 'Azure Blob' }, + { value: 's3_aws_oidc', label: 'AWS OIDC' }, + { value: 'azure_workload_identity', label: 'Azure Workload Identity' }, + { value: 'gcloud_storage', label: 'Google Cloud Storage' } + ] + let advancedPermissionModalState: | { open: false } | { open: true; storage: S3ResourceSettingsItem } = $state({ open: false }) @@ -294,31 +302,38 @@
    - {#if tableRow[1].resourceType === 'filesystem'} - +
    + - {/if} + {#if tableRow[1].resourceType === 'filesystem'} + + Filesystem storage points the workspace at a directory on the server's own + disk. Only development builds of Windmill accept it — switch this storage to + S3, Azure Blob or Google Cloud Storage to configure it here. + + {/if} +
    {#if tableRow[1].resourceType === 'filesystem'} From 69320b28f615b897a92f580bd5961c41e5c29951 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Thu, 27 Aug 2026 00:39:03 +0200 Subject: [PATCH 19/34] perf: index the suspended-job resume test instead of filtering it (#10863) * perf: index the suspended-job resume test instead of filtering it Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01SEUq14Wz4cC2NzcRyo6CNj * fix: keep the legacy suspended index until the replacement is recorded Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01SEUq14Wz4cC2NzcRyo6CNj * perf: drop the redundant suspend_until column from the suspended index Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01SEUq14Wz4cC2NzcRyo6CNj --------- Co-authored-by: Claude Opus 5 (1M context) --- ...9_queue_suspended_resume_at_index.down.sql | 1 + ...939_queue_suspended_resume_at_index.up.sql | 20 +++++ ...queue_suspended_drop_legacy_index.down.sql | 3 + ...6_queue_suspended_drop_legacy_index.up.sql | 6 ++ backend/tests/suspended_pull_index.rs | 77 +++++++++++++++++++ backend/windmill-api/src/db.rs | 8 ++ backend/windmill-common/src/worker.rs | 8 +- 7 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 backend/migrations/20260826202939_queue_suspended_resume_at_index.down.sql create mode 100644 backend/migrations/20260826202939_queue_suspended_resume_at_index.up.sql create mode 100644 backend/migrations/20260826214706_queue_suspended_drop_legacy_index.down.sql create mode 100644 backend/migrations/20260826214706_queue_suspended_drop_legacy_index.up.sql create mode 100644 backend/tests/suspended_pull_index.rs diff --git a/backend/migrations/20260826202939_queue_suspended_resume_at_index.down.sql b/backend/migrations/20260826202939_queue_suspended_resume_at_index.down.sql new file mode 100644 index 0000000000..fe727f6761 --- /dev/null +++ b/backend/migrations/20260826202939_queue_suspended_resume_at_index.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS queue_suspended_v2; diff --git a/backend/migrations/20260826202939_queue_suspended_resume_at_index.up.sql b/backend/migrations/20260826202939_queue_suspended_resume_at_index.up.sql new file mode 100644 index 0000000000..6d91a64d5f --- /dev/null +++ b/backend/migrations/20260826202939_queue_suspended_resume_at_index.up.sql @@ -0,0 +1,20 @@ +-- Serves the suspended-job pull in windmill-common/src/worker.rs, whose resume test is the +-- indexed CASE expression. Two things about the shape are load-bearing: +-- * (priority DESC NULLS LAST, created_at) leads, so the scan yields that query's ORDER BY +-- and stops at the first match rather than sorting. +-- * the index is dropped before it is built rather than relying on IF NOT EXISTS. The +-- OVERRIDDEN_MIGRATIONS rewrite in windmill-api/src/db.rs runs these CONCURRENTLY, and an +-- interrupted concurrent build leaves the index present but invalid, which IF NOT EXISTS +-- would then skip rebuilding. Retiring the index this replaces is left to the migration +-- that follows, so this one can only ever be replayed while that index is still there to +-- cover the rebuild. +DROP INDEX IF EXISTS queue_suspended_v2; + +CREATE INDEX IF NOT EXISTS queue_suspended_v2 + ON v2_job_queue ( + priority DESC NULLS LAST, + created_at, + (CASE WHEN suspend <= 0 THEN '-infinity'::timestamptz ELSE suspend_until END), + tag + ) + WHERE suspend_until IS NOT NULL; diff --git a/backend/migrations/20260826214706_queue_suspended_drop_legacy_index.down.sql b/backend/migrations/20260826214706_queue_suspended_drop_legacy_index.down.sql new file mode 100644 index 0000000000..011105927a --- /dev/null +++ b/backend/migrations/20260826214706_queue_suspended_drop_legacy_index.down.sql @@ -0,0 +1,3 @@ +CREATE INDEX IF NOT EXISTS queue_suspended + ON v2_job_queue (priority DESC NULLS LAST, created_at, suspend_until, suspend, tag) + WHERE suspend_until IS NOT NULL; diff --git a/backend/migrations/20260826214706_queue_suspended_drop_legacy_index.up.sql b/backend/migrations/20260826214706_queue_suspended_drop_legacy_index.up.sql new file mode 100644 index 0000000000..b8851b99e0 --- /dev/null +++ b/backend/migrations/20260826214706_queue_suspended_drop_legacy_index.up.sql @@ -0,0 +1,6 @@ +-- Retires the index queue_suspended_v2 replaces. Separate from the migration that builds it +-- so that one is only ever replayed while this index still exists: sqlx records a migration +-- only after all its statements run, so a process that dies before the record is written +-- replays the build, and its leading DROP would otherwise be destroying the sole usable +-- index rather than an interrupted build. +DROP INDEX IF EXISTS queue_suspended; diff --git a/backend/tests/suspended_pull_index.rs b/backend/tests/suspended_pull_index.rs new file mode 100644 index 0000000000..4b31f4d45e --- /dev/null +++ b/backend/tests/suspended_pull_index.rs @@ -0,0 +1,77 @@ +//! Pins the plan of the suspended-job pull. Its resume test degrades silently: once the +//! query expression and `queue_suspended_v2` stop matching, Postgres still returns the right +//! job, just by falling back to a heap filter and fetching one tuple per suspended row on +//! every worker poll. No functional test can see that, so assert on the plan instead. + +use serde_json::Value; +use sqlx::{Pool, Postgres}; +use windmill_common::worker::make_suspended_pull_query; + +/// Depth-first walk of an `EXPLAIN (FORMAT JSON)` plan tree. +fn nodes(plan: &Value, out: &mut Vec) { + out.push(plan.clone()); + for child in plan["Plans"].as_array().unwrap_or(&vec![]) { + nodes(child, out); + } +} + +#[sqlx::test(fixtures("base"))] +async fn suspended_pull_tests_resume_time_inside_the_index( + db: Pool, +) -> anyhow::Result<()> { + sqlx::query( + "INSERT INTO v2_job_queue (id, workspace_id, created_at, scheduled_for, running, suspend, suspend_until, tag) + SELECT gen_random_uuid(), 'test-workspace', now() - make_interval(secs => i), + now(), true, 1 + (i % 3), now() + interval '7 day', 'flow' + FROM generate_series(1, 2000) i", + ) + .execute(&db) + .await?; + sqlx::query("ANALYZE v2_job_queue").execute(&db).await?; + + // Both plans are cheap on a 2000-row table, and which one wins there says nothing + // about a queue with a large suspended backlog. Force the index path, which is the + // one production takes, and assert on how it evaluates the resume test. + let mut conn = db.acquire().await?; + sqlx::query("SET enable_seqscan = off") + .execute(&mut *conn) + .await?; + let version: String = sqlx::query_scalar("SELECT version()") + .fetch_one(&mut *conn) + .await?; + // FORMAT JSON rather than the default: `Index Cond` and `Filter` are separate keys on the + // node, so this does not ride on EXPLAIN's line layout staying put across a major bump. + let explained: Value = sqlx::query_scalar(&format!( + "EXPLAIN (FORMAT JSON) {}", + make_suspended_pull_query(&["flow".to_string()]) + )) + .bind("test-worker") + .fetch_one(&mut *conn) + .await?; + + let mut all = vec![]; + nodes(&explained[0]["Plan"], &mut all); + let pretty = serde_json::to_string_pretty(&explained)?; + let scan = all + .iter() + .find(|n| n["Index Name"] == "queue_suspended_v2") + .unwrap_or_else(|| { + panic!("suspended pull did not scan queue_suspended_v2 on {version}:\n{pretty}") + }); + // Only `Index Cond` is checked against the index tuple, so that is where the resume test + // has to land — as a `Filter` it would cost a heap fetch per suspended row. The residual + // `suspend_until IS NOT NULL` filter is not that: it is always true for rows the partial + // index holds, and only ever runs on the row LIMIT 1 already fetched. + let cond = scan["Index Cond"].as_str().unwrap_or_else(|| { + panic!("no Index Cond on the suspended pull scan on {version}:\n{pretty}") + }); + assert!( + cond.contains("CASE WHEN"), + "resume test is not an index condition on {version}:\n{pretty}" + ); + assert!( + !scan["Filter"].as_str().unwrap_or("").contains("CASE WHEN"), + "resume test fell back to a heap filter on {version}:\n{pretty}" + ); + Ok(()) +} diff --git a/backend/windmill-api/src/db.rs b/backend/windmill-api/src/db.rs index 04d21c7b53..d781e229d2 100644 --- a/backend/windmill-api/src/db.rs +++ b/backend/windmill-api/src/db.rs @@ -102,6 +102,12 @@ lazy_static::lazy_static! { (20260727151319, include_str!( "../../migrations/20260727151319_draft_only_listing_indexes.up.sql" ).replace("CREATE INDEX", "CREATE INDEX CONCURRENTLY")), + (20260826202939, include_str!( + "../../migrations/20260826202939_queue_suspended_resume_at_index.up.sql" + ).replace("CREATE INDEX", "CREATE INDEX CONCURRENTLY").replace("DROP INDEX", "DROP INDEX CONCURRENTLY")), + (20260826214706, include_str!( + "../../migrations/20260826214706_queue_suspended_drop_legacy_index.up.sql" + ).replace("DROP INDEX", "DROP INDEX CONCURRENTLY")), ].into_iter().collect(); } @@ -228,6 +234,8 @@ impl Migrate for CustomMigrator { // CONCURRENTLY operations cannot run inside a transaction block // or a multi-statement query (PostgreSQL requires top-level execution). // Split into individual statements and execute each separately. + // The split is naive, so a `;` anywhere in an overridden migration — + // inside a comment or a string literal included — splits mid-statement. for stmt in migration_sql.split(';') { let stmt = stmt.trim(); if !stmt.is_empty() diff --git a/backend/windmill-common/src/worker.rs b/backend/windmill-common/src/worker.rs index 7945e73b45..84284759c6 100644 --- a/backend/windmill-common/src/worker.rs +++ b/backend/windmill-common/src/worker.rs @@ -734,11 +734,17 @@ fn format_pull_query(peek: String) -> String { r } +// The `CASE` is `suspend <= 0 OR suspend_until <= now()` written as one indexable +// expression, equivalent only under the `suspend_until IS NOT NULL` guard. It must stay in +// sync with `queue_suspended_v2` (migration 20260826202939): if it no longer matches, the +// test silently reverts to a heap filter over every suspended row on every worker poll. pub fn make_suspended_pull_query(tags: &[String]) -> String { format_pull_query(format!( "SELECT id FROM v2_job_queue - WHERE suspend_until IS NOT NULL AND (suspend <= 0 OR suspend_until <= now()) AND tag IN ({}) + WHERE suspend_until IS NOT NULL + AND (CASE WHEN suspend <= 0 THEN '-infinity'::timestamptz ELSE suspend_until END) <= now() + AND tag IN ({}) ORDER BY priority DESC NULLS LAST, created_at FOR UPDATE SKIP LOCKED LIMIT 1", From 52ca19e9aeebda821b8744a4b6ff83b26ce3a71e Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Thu, 27 Aug 2026 10:15:23 +0200 Subject: [PATCH 20/34] chore(main): release 1.797.0 (#10848) * chore(main): release 1.797.0 * Apply automatic changes --------- Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com> --- .release-please-manifest.json | 2 +- CHANGELOG.md | 29 +++ backend/Cargo.lock | 206 +++++++++--------- backend/Cargo.toml | 4 +- .../parsers/windmill-parser-wasm/Cargo.lock | 48 ++-- .../parsers/windmill-parser-wasm/Cargo.toml | 2 +- backend/windmill-api/openapi.yaml | 2 +- benchmarks/lib.ts | 2 +- cli/src/core/constants.ts | 2 +- frontend/package-lock.json | 4 +- frontend/package.json | 2 +- lsp/Pipfile | 2 +- openflow.openapi.yaml | 2 +- .../WindmillClient/WindmillClient.psd1 | 2 +- python-client/wmill/pyproject.toml | 2 +- typescript-client/jsr.json | 2 +- typescript-client/package.json | 2 +- version.txt | 2 +- windmill-yaml-validator/package-lock.json | 4 +- windmill-yaml-validator/package.json | 2 +- 20 files changed, 180 insertions(+), 143 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index ae93300697..15ef7898c2 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.796.0" + ".": "1.797.0" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 947e63f5c6..77b3cff7d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,34 @@ # Changelog +## [1.797.0](https://github.com/windmill-labs/windmill/compare/v1.796.0...v1.797.0) (2026-08-26) + + +### Features + +* configurable expiry for presigned s3 public url signatures ([#10835](https://github.com/windmill-labs/windmill/issues/10835)) ([8a6dc27](https://github.com/windmill-labs/windmill/commit/8a6dc27236aca67f0efe941d9606b787c2305ea8)) +* **frontend:** flag the fork-compare datatable schema diff as legacy ([#10829](https://github.com/windmill-labs/windmill/issues/10829)) ([07c77ea](https://github.com/windmill-labs/windmill/commit/07c77ead7425f1877372d358d867445a4c525c96)) +* keep a Hub project live while an update is under review ([#10814](https://github.com/windmill-labs/windmill/issues/10814)) ([c04b570](https://github.com/windmill-labs/windmill/commit/c04b5705745c36ecbb3a551ac59459218d2e3807)) + + +### Bug Fixes + +* **cli:** keep svelte component styles in the raw-app bundle ([#10838](https://github.com/windmill-labs/windmill/issues/10838)) ([b8bf539](https://github.com/windmill-labs/windmill/commit/b8bf539c3fe2b4db9c74dd73f04b3029287acdc6)) +* **debugger:** parse bun 1.4's UUID inspector token ([#10828](https://github.com/windmill-labs/windmill/issues/10828)) ([4658224](https://github.com/windmill-labs/windmill/commit/46582245926a7f8ea961bcd125a58fbfba3530cf)) +* force HTTP router rebuild on trigger-change notification ([#10849](https://github.com/windmill-labs/windmill/issues/10849)) ([ffdf17e](https://github.com/windmill-labs/windmill/commit/ffdf17ef8dc5575dd92d62d0d0ba887c1e378576)) +* **frontend:** follow the operating workspace in step input forms ([#10834](https://github.com/windmill-labs/windmill/issues/10834)) ([6b73145](https://github.com/windmill-labs/windmill/commit/6b73145e7220232601538b801ebc9dc73fe79bbb)) +* **frontend:** key the GitHub App installation selector on installation_id ([#10831](https://github.com/windmill-labs/windmill/issues/10831)) ([78331fd](https://github.com/windmill-labs/windmill/commit/78331fda8b290a2d9a5dd92b8362ff32c8b39432)) +* **frontend:** operator menu opens on hover, pins on click ([#10824](https://github.com/windmill-labs/windmill/issues/10824)) ([665f83e](https://github.com/windmill-labs/windmill/commit/665f83e1f438e34d006429889d51a5fb6a6b6176)) +* keep connection string query parameters under token auth ([#10859](https://github.com/windmill-labs/windmill/issues/10859)) ([f131c39](https://github.com/windmill-labs/windmill/commit/f131c3920f50f9fa18cd637eac39609495999aef)) +* migrate slack resource-connect oauth to v2 ([#10836](https://github.com/windmill-labs/windmill/issues/10836)) ([9fa8159](https://github.com/windmill-labs/windmill/commit/9fa8159ad16204cab52fd18a34a48ebf13f800f6)) +* recover from unresolvable AI session links instead of a dead end ([#10854](https://github.com/windmill-labs/windmill/issues/10854)) ([e38c449](https://github.com/windmill-labs/windmill/commit/e38c449007f27b952808cba5aa812441f2ce5946)) +* require admin on workspace tarball settings export ([#10817](https://github.com/windmill-labs/windmill/issues/10817)) ([46c363f](https://github.com/windmill-labs/windmill/commit/46c363ffa4bc72bef6b367ece4bdbeef5e0eadc9)) +* restrict filesystem workspace storage to debug builds ([#10864](https://github.com/windmill-labs/windmill/issues/10864)) ([8b80b09](https://github.com/windmill-labs/windmill/commit/8b80b09f33d311f0881678577ca6004c12d97c22)) + + +### Performance Improvements + +* index the suspended-job resume test instead of filtering it ([#10863](https://github.com/windmill-labs/windmill/issues/10863)) ([69320b2](https://github.com/windmill-labs/windmill/commit/69320b28f615b897a92f580bd5961c41e5c29951)) + ## [1.796.0](https://github.com/windmill-labs/windmill/compare/v1.795.0...v1.796.0) (2026-08-24) diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 2721fc7283..fcc8daa23f 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -1729,7 +1729,7 @@ dependencies = [ "clang-sys", "itertools 0.13.0", "log", - "prettyplease", + "prettyplease 0.2.37", "proc-macro2", "quote", "regex", @@ -1749,7 +1749,7 @@ dependencies = [ "clang-sys", "itertools 0.13.0", "log", - "prettyplease", + "prettyplease 0.2.37", "proc-macro2", "quote", "regex", @@ -1940,34 +1940,32 @@ dependencies = [ [[package]] name = "bon" -version = "3.9.3" +version = "3.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a602c73c7b0148ec6d12af6fd5cc7a46e2eacc8878271a999abac56eed12f561" +checksum = "9e3fac94a66da67200398458a25412bcc3f9b6443b5119a6cad9cf3ccfcd8cc6" dependencies = [ "bon-macros", - "rustversion", ] [[package]] name = "bon-macros" -version = "3.9.3" +version = "3.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dee98b0db6a962de883bf5d20362dee4d7ca0d12fe39a7c6c73c844e1cd7c1f" +checksum = "d4654961ad0494e4774c5c60b4cb4cd0ae9b9d92d039d901638b1dba97ebebf5" dependencies = [ - "darling 0.23.0", + "darling 0.24.1", "ident_case", - "prettyplease", + "prettyplease 0.3.0", "proc-macro2", "quote", - "rustversion", - "syn 2.0.119", + "syn 3.0.4", ] [[package]] name = "borsh" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a88b7ea17d208c4193f2c1e6de3c35fe71f98c96982d5ced308bdcc749ff6e1f" +checksum = "553c5d846a6ba5150c65e3b1b8ec073bcf1abc20f9b7220de384a4443ea4e20a" dependencies = [ "borsh-derive", "bytes", @@ -1976,15 +1974,15 @@ dependencies = [ [[package]] name = "borsh-derive" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8f347189c62a579b8cd5f80714efa178f52e461dc2e6d701d264f5ff22e566c" +checksum = "12cdfe656708a01f89b451a7d36466e6fe6c414de0aa18fc54f864f6f9ca9f56" dependencies = [ "once_cell", "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.4", ] [[package]] @@ -2476,9 +2474,9 @@ checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "combine" -version = "4.6.7" +version = "4.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" dependencies = [ "bytes", "memchr", @@ -9442,6 +9440,16 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "prettyplease" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bfe0f4c752e450fc2faf62654f1c134747922825d5b04ca717b8874f41a40c0" +dependencies = [ + "proc-macro2", + "syn 3.0.4", +] + [[package]] name = "primeorder" version = "0.13.6" @@ -14251,9 +14259,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.25.0" +version = "1.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f053576934f05a761a402421fbbe3d425d9366f75f978806a037b3ca481abecc" +checksum = "b5772d71c9be8a8a6ac2117d949c5b224c1b72241bb611d9a3012edcf8af7812" dependencies = [ "getrandom 0.4.3", "js-sys", @@ -14663,7 +14671,7 @@ dependencies = [ [[package]] name = "windmill" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "async-nats", @@ -14748,7 +14756,7 @@ dependencies = [ [[package]] name = "windmill-ai" -version = "1.796.0" +version = "1.797.0" dependencies = [ "async-stream", "async-trait", @@ -14781,7 +14789,7 @@ dependencies = [ [[package]] name = "windmill-alerting" -version = "1.796.0" +version = "1.797.0" dependencies = [ "axum 0.8.9", "chrono", @@ -14794,7 +14802,7 @@ dependencies = [ [[package]] name = "windmill-api" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "argon2", @@ -14934,7 +14942,7 @@ dependencies = [ [[package]] name = "windmill-api-agent-workers" -version = "1.796.0" +version = "1.797.0" dependencies = [ "axum 0.8.9", "chrono", @@ -14957,7 +14965,7 @@ dependencies = [ [[package]] name = "windmill-api-assets" -version = "1.796.0" +version = "1.797.0" dependencies = [ "axum 0.8.9", "chrono", @@ -14974,7 +14982,7 @@ dependencies = [ [[package]] name = "windmill-api-auth" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "axum 0.8.9", @@ -15000,7 +15008,7 @@ dependencies = [ [[package]] name = "windmill-api-client" -version = "1.796.0" +version = "1.797.0" dependencies = [ "reqwest 0.12.28", "serde", @@ -15010,7 +15018,7 @@ dependencies = [ [[package]] name = "windmill-api-configs" -version = "1.796.0" +version = "1.797.0" dependencies = [ "axum 0.8.9", "chrono", @@ -15027,7 +15035,7 @@ dependencies = [ [[package]] name = "windmill-api-debug" -version = "1.796.0" +version = "1.797.0" dependencies = [ "axum 0.8.9", "base64 0.22.1", @@ -15049,7 +15057,7 @@ dependencies = [ [[package]] name = "windmill-api-embeddings" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "axum 0.8.9", @@ -15072,7 +15080,7 @@ dependencies = [ [[package]] name = "windmill-api-flow-conversations" -version = "1.796.0" +version = "1.797.0" dependencies = [ "axum 0.8.9", "chrono", @@ -15088,7 +15096,7 @@ dependencies = [ [[package]] name = "windmill-api-flows" -version = "1.796.0" +version = "1.797.0" dependencies = [ "axum 0.8.9", "chrono", @@ -15110,7 +15118,7 @@ dependencies = [ [[package]] name = "windmill-api-groups" -version = "1.796.0" +version = "1.797.0" dependencies = [ "axum 0.8.9", "chrono", @@ -15131,7 +15139,7 @@ dependencies = [ [[package]] name = "windmill-api-inputs" -version = "1.796.0" +version = "1.797.0" dependencies = [ "axum 0.8.9", "chrono", @@ -15145,7 +15153,7 @@ dependencies = [ [[package]] name = "windmill-api-integration-tests" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "async-nats", @@ -15180,7 +15188,7 @@ dependencies = [ [[package]] name = "windmill-api-jobs" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "axum 0.8.9", @@ -15205,7 +15213,7 @@ dependencies = [ [[package]] name = "windmill-api-npm-proxy" -version = "1.796.0" +version = "1.797.0" dependencies = [ "axum 0.8.9", "chrono", @@ -15233,7 +15241,7 @@ dependencies = [ [[package]] name = "windmill-api-openapi" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "axum 0.8.9", @@ -15255,7 +15263,7 @@ dependencies = [ [[package]] name = "windmill-api-schedule" -version = "1.796.0" +version = "1.797.0" dependencies = [ "axum 0.8.9", "chrono", @@ -15275,7 +15283,7 @@ dependencies = [ [[package]] name = "windmill-api-scripts" -version = "1.796.0" +version = "1.797.0" dependencies = [ "axum 0.8.9", "chrono", @@ -15313,7 +15321,7 @@ dependencies = [ [[package]] name = "windmill-api-settings" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "axum 0.8.9", @@ -15341,7 +15349,7 @@ dependencies = [ [[package]] name = "windmill-api-sse" -version = "1.796.0" +version = "1.797.0" dependencies = [ "lazy_static", "serde", @@ -15353,7 +15361,7 @@ dependencies = [ [[package]] name = "windmill-api-users" -version = "1.796.0" +version = "1.797.0" dependencies = [ "argon2", "axum 0.8.9", @@ -15377,7 +15385,7 @@ dependencies = [ [[package]] name = "windmill-api-workers" -version = "1.796.0" +version = "1.797.0" dependencies = [ "axum 0.8.9", "chrono", @@ -15391,7 +15399,7 @@ dependencies = [ [[package]] name = "windmill-api-workspaces" -version = "1.796.0" +version = "1.797.0" dependencies = [ "axum 0.8.9", "chrono", @@ -15426,7 +15434,7 @@ dependencies = [ [[package]] name = "windmill-audit" -version = "1.796.0" +version = "1.797.0" dependencies = [ "chrono", "lazy_static", @@ -15440,7 +15448,7 @@ dependencies = [ [[package]] name = "windmill-autoscaling" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "axum 0.8.9", @@ -15459,7 +15467,7 @@ dependencies = [ [[package]] name = "windmill-common" -version = "1.796.0" +version = "1.797.0" dependencies = [ "aes-gcm", "aho-corasick", @@ -15563,7 +15571,7 @@ dependencies = [ [[package]] name = "windmill-dep-map" -version = "1.796.0" +version = "1.797.0" dependencies = [ "chrono", "itertools 0.14.0", @@ -15582,7 +15590,7 @@ dependencies = [ [[package]] name = "windmill-git-sync" -version = "1.796.0" +version = "1.797.0" dependencies = [ "regex", "serde", @@ -15597,7 +15605,7 @@ dependencies = [ [[package]] name = "windmill-indexer" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "astral-tokio-tar", @@ -15621,7 +15629,7 @@ dependencies = [ [[package]] name = "windmill-jseval" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "futures", @@ -15638,7 +15646,7 @@ dependencies = [ [[package]] name = "windmill-macros" -version = "1.796.0" +version = "1.797.0" dependencies = [ "itertools 0.14.0", "lazy_static", @@ -15654,7 +15662,7 @@ dependencies = [ [[package]] name = "windmill-mcp" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "async-trait", @@ -15675,7 +15683,7 @@ dependencies = [ [[package]] name = "windmill-native-triggers" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "async-trait", @@ -15706,7 +15714,7 @@ dependencies = [ [[package]] name = "windmill-oauth" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "arc-swap", @@ -15731,7 +15739,7 @@ dependencies = [ [[package]] name = "windmill-object-store" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "async-stream", @@ -15765,7 +15773,7 @@ dependencies = [ [[package]] name = "windmill-operator" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "futures", @@ -15783,7 +15791,7 @@ dependencies = [ [[package]] name = "windmill-parser" -version = "1.796.0" +version = "1.797.0" dependencies = [ "convert_case 0.6.0", "serde", @@ -15792,7 +15800,7 @@ dependencies = [ [[package]] name = "windmill-parser-bash" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "lazy_static", @@ -15804,7 +15812,7 @@ dependencies = [ [[package]] name = "windmill-parser-csharp" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "serde_json", @@ -15816,7 +15824,7 @@ dependencies = [ [[package]] name = "windmill-parser-go" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "gosyn", @@ -15828,7 +15836,7 @@ dependencies = [ [[package]] name = "windmill-parser-graphql" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "lazy_static", @@ -15840,7 +15848,7 @@ dependencies = [ [[package]] name = "windmill-parser-java" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "serde_json", @@ -15852,7 +15860,7 @@ dependencies = [ [[package]] name = "windmill-parser-nu" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "nu-parser", @@ -15863,7 +15871,7 @@ dependencies = [ [[package]] name = "windmill-parser-php" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "itertools 0.14.0", @@ -15874,7 +15882,7 @@ dependencies = [ [[package]] name = "windmill-parser-py" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "itertools 0.14.0", @@ -15886,7 +15894,7 @@ dependencies = [ [[package]] name = "windmill-parser-py-asset" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "rustpython-ast", @@ -15897,7 +15905,7 @@ dependencies = [ [[package]] name = "windmill-parser-py-imports" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "async-recursion", @@ -15919,7 +15927,7 @@ dependencies = [ [[package]] name = "windmill-parser-r" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "serde_json", @@ -15931,7 +15939,7 @@ dependencies = [ [[package]] name = "windmill-parser-ruby" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "lazy_static", @@ -15945,7 +15953,7 @@ dependencies = [ [[package]] name = "windmill-parser-rust" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "convert_case 0.6.0", @@ -15962,7 +15970,7 @@ dependencies = [ [[package]] name = "windmill-parser-sql" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "lazy_static", @@ -15975,7 +15983,7 @@ dependencies = [ [[package]] name = "windmill-parser-sql-asset" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "serde", @@ -15987,7 +15995,7 @@ dependencies = [ [[package]] name = "windmill-parser-ts" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "lazy_static", @@ -16005,7 +16013,7 @@ dependencies = [ [[package]] name = "windmill-parser-ts-asset" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "serde-wasm-bindgen", @@ -16021,7 +16029,7 @@ dependencies = [ [[package]] name = "windmill-parser-wac" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "rustpython-ast", @@ -16037,7 +16045,7 @@ dependencies = [ [[package]] name = "windmill-parser-yaml" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "lazy_static", @@ -16051,7 +16059,7 @@ dependencies = [ [[package]] name = "windmill-queue" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "async-recursion", @@ -16090,7 +16098,7 @@ dependencies = [ [[package]] name = "windmill-runtime-nativets" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "const_format", @@ -16130,7 +16138,7 @@ dependencies = [ [[package]] name = "windmill-sql-datatype-parser-wasm" -version = "1.796.0" +version = "1.797.0" dependencies = [ "getrandom 0.3.4", "wasm-bindgen", @@ -16141,7 +16149,7 @@ dependencies = [ [[package]] name = "windmill-store" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "async-recursion", @@ -16176,7 +16184,7 @@ dependencies = [ [[package]] name = "windmill-test-utils" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "async-trait", @@ -16200,7 +16208,7 @@ dependencies = [ [[package]] name = "windmill-trigger" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "async-trait", @@ -16233,7 +16241,7 @@ dependencies = [ [[package]] name = "windmill-trigger-amqp" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "async-trait", @@ -16260,7 +16268,7 @@ dependencies = [ [[package]] name = "windmill-trigger-azure" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "async-trait", @@ -16293,7 +16301,7 @@ dependencies = [ [[package]] name = "windmill-trigger-email" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "async-trait", @@ -16313,7 +16321,7 @@ dependencies = [ [[package]] name = "windmill-trigger-gcp" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "async-trait", @@ -16347,7 +16355,7 @@ dependencies = [ [[package]] name = "windmill-trigger-http" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "async-trait", @@ -16383,7 +16391,7 @@ dependencies = [ [[package]] name = "windmill-trigger-kafka" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "async-trait", @@ -16406,7 +16414,7 @@ dependencies = [ [[package]] name = "windmill-trigger-mqtt" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "async-trait", @@ -16430,7 +16438,7 @@ dependencies = [ [[package]] name = "windmill-trigger-nats" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "async-nats", @@ -16454,7 +16462,7 @@ dependencies = [ [[package]] name = "windmill-trigger-postgres" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "async-trait", @@ -16489,7 +16497,7 @@ dependencies = [ [[package]] name = "windmill-trigger-sqs" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "async-trait", @@ -16517,7 +16525,7 @@ dependencies = [ [[package]] name = "windmill-trigger-websocket" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "async-trait", @@ -16542,7 +16550,7 @@ dependencies = [ [[package]] name = "windmill-types" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "bitflags 2.13.1", @@ -16561,7 +16569,7 @@ dependencies = [ [[package]] name = "windmill-worker" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "async-once-cell", @@ -16678,7 +16686,7 @@ dependencies = [ [[package]] name = "windmill-worker-volumes" -version = "1.796.0" +version = "1.797.0" dependencies = [ "bytes", "futures", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 6dd9e452b8..887b7591f6 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "windmill" -version = "1.796.0" +version = "1.797.0" authors.workspace = true edition.workspace = true @@ -88,7 +88,7 @@ members = [ exclude = ["./windmill-duckdb-ffi-internal", "./parsers/windmill-parser-wasm"] [workspace.package] -version = "1.796.0" +version = "1.797.0" authors = ["Ruben Fiszel "] edition = "2021" diff --git a/backend/parsers/windmill-parser-wasm/Cargo.lock b/backend/parsers/windmill-parser-wasm/Cargo.lock index 2f2fafedf4..54909e961b 100644 --- a/backend/parsers/windmill-parser-wasm/Cargo.lock +++ b/backend/parsers/windmill-parser-wasm/Cargo.lock @@ -6191,7 +6191,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windmill-common" -version = "1.796.0" +version = "1.797.0" dependencies = [ "aho-corasick", "anyhow", @@ -6274,7 +6274,7 @@ dependencies = [ [[package]] name = "windmill-macros" -version = "1.796.0" +version = "1.797.0" dependencies = [ "proc-macro2", "quote", @@ -6286,7 +6286,7 @@ dependencies = [ [[package]] name = "windmill-parser" -version = "1.796.0" +version = "1.797.0" dependencies = [ "convert_case", "serde", @@ -6295,7 +6295,7 @@ dependencies = [ [[package]] name = "windmill-parser-bash" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "lazy_static", @@ -6307,7 +6307,7 @@ dependencies = [ [[package]] name = "windmill-parser-csharp" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "serde_json", @@ -6319,7 +6319,7 @@ dependencies = [ [[package]] name = "windmill-parser-go" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "gosyn", @@ -6331,7 +6331,7 @@ dependencies = [ [[package]] name = "windmill-parser-graphql" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "lazy_static", @@ -6343,7 +6343,7 @@ dependencies = [ [[package]] name = "windmill-parser-java" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "serde_json", @@ -6355,7 +6355,7 @@ dependencies = [ [[package]] name = "windmill-parser-nu" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "nu-parser", @@ -6366,7 +6366,7 @@ dependencies = [ [[package]] name = "windmill-parser-php" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "itertools 0.14.0", @@ -6377,7 +6377,7 @@ dependencies = [ [[package]] name = "windmill-parser-py" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "itertools 0.14.0", @@ -6389,7 +6389,7 @@ dependencies = [ [[package]] name = "windmill-parser-py-asset" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "rustpython-ast", @@ -6400,7 +6400,7 @@ dependencies = [ [[package]] name = "windmill-parser-py-imports" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "async-recursion", @@ -6422,7 +6422,7 @@ dependencies = [ [[package]] name = "windmill-parser-r" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "serde_json", @@ -6434,7 +6434,7 @@ dependencies = [ [[package]] name = "windmill-parser-ruby" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "lazy_static", @@ -6448,7 +6448,7 @@ dependencies = [ [[package]] name = "windmill-parser-rust" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "convert_case", @@ -6465,7 +6465,7 @@ dependencies = [ [[package]] name = "windmill-parser-sql" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "lazy_static", @@ -6478,7 +6478,7 @@ dependencies = [ [[package]] name = "windmill-parser-sql-asset" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "serde", @@ -6490,7 +6490,7 @@ dependencies = [ [[package]] name = "windmill-parser-ts" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "lazy_static", @@ -6508,7 +6508,7 @@ dependencies = [ [[package]] name = "windmill-parser-ts-asset" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "serde-wasm-bindgen", @@ -6524,7 +6524,7 @@ dependencies = [ [[package]] name = "windmill-parser-wac" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "rustpython-ast", @@ -6540,7 +6540,7 @@ dependencies = [ [[package]] name = "windmill-parser-wasm" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "getrandom 0.2.17", @@ -6572,7 +6572,7 @@ dependencies = [ [[package]] name = "windmill-parser-yaml" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "lazy_static", @@ -6586,7 +6586,7 @@ dependencies = [ [[package]] name = "windmill-types" -version = "1.796.0" +version = "1.797.0" dependencies = [ "anyhow", "bitflags", diff --git a/backend/parsers/windmill-parser-wasm/Cargo.toml b/backend/parsers/windmill-parser-wasm/Cargo.toml index 826e279c39..6f91233e28 100644 --- a/backend/parsers/windmill-parser-wasm/Cargo.toml +++ b/backend/parsers/windmill-parser-wasm/Cargo.toml @@ -12,7 +12,7 @@ resolver = "2" members = ["."] [workspace.package] -version = "1.796.0" +version = "1.797.0" edition = "2021" authors = ["Ruben Fiszel "] diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 2ca23e79b7..908056bcda 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -1,7 +1,7 @@ openapi: "3.0.3" info: - version: 1.796.0 + version: 1.797.0 title: Windmill API contact: diff --git a/benchmarks/lib.ts b/benchmarks/lib.ts index 55734e52e8..519ca0c803 100644 --- a/benchmarks/lib.ts +++ b/benchmarks/lib.ts @@ -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.796.0"; +export const VERSION = "v1.797.0"; export async function login(email: string, password: string): Promise { return await windmill.UserService.login({ diff --git a/cli/src/core/constants.ts b/cli/src/core/constants.ts index 2f80eb41fc..4b2e3d659c 100644 --- a/cli/src/core/constants.ts +++ b/cli/src/core/constants.ts @@ -10,4 +10,4 @@ export const WM_FORK_PREFIX = "wm-fork"; // (e.g. utils.ts) can read it without importing main.ts and creating a circular // dependency (main → workspace → utils → main) that triggers a TDZ. // Re-exported from main.ts for backwards compatibility. -export const VERSION = "1.796.0"; +export const VERSION = "1.797.0"; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 6d72964274..c73a7c41e7 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "@windmill-labs/components", - "version": "1.796.0", + "version": "1.797.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@windmill-labs/components", - "version": "1.796.0", + "version": "1.797.0", "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { diff --git a/frontend/package.json b/frontend/package.json index 1d984c8587..f94ff691ae 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "@windmill-labs/components", - "version": "1.796.0", + "version": "1.797.0", "scripts": { "dev": "vite dev", "dev:ui-builder": "mv static/ui_builder static/ui_builder.dev-disabled 2>/dev/null || true ; trap 'mv static/ui_builder.dev-disabled static/ui_builder 2>/dev/null || true' EXIT ; vite dev", diff --git a/lsp/Pipfile b/lsp/Pipfile index e0b64e8222..8ae25a1e5b 100644 --- a/lsp/Pipfile +++ b/lsp/Pipfile @@ -4,7 +4,7 @@ verify_ssl = true name = "pypi" [packages] -wmill = ">=1.796.0" +wmill = ">=1.797.0" sendgrid = "*" mysql-connector-python = "*" pymongo = "*" diff --git a/openflow.openapi.yaml b/openflow.openapi.yaml index 5f1882d56d..6ca54afbb2 100644 --- a/openflow.openapi.yaml +++ b/openflow.openapi.yaml @@ -1,7 +1,7 @@ openapi: '3.0.3' info: - version: 1.796.0 + version: 1.797.0 title: OpenFlow Spec contact: name: Ruben Fiszel diff --git a/powershell-client/WindmillClient/WindmillClient.psd1 b/powershell-client/WindmillClient/WindmillClient.psd1 index f8cb751a07..583d84a8b2 100644 --- a/powershell-client/WindmillClient/WindmillClient.psd1 +++ b/powershell-client/WindmillClient/WindmillClient.psd1 @@ -12,7 +12,7 @@ RootModule = 'WindmillClient.psm1' # Version number of this module. - ModuleVersion = '1.796.0' + ModuleVersion = '1.797.0' # Supported PSEditions # CompatiblePSEditions = @() diff --git a/python-client/wmill/pyproject.toml b/python-client/wmill/pyproject.toml index cc20fab3ea..7c522c5c06 100644 --- a/python-client/wmill/pyproject.toml +++ b/python-client/wmill/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "wmill" -version = "1.796.0" +version = "1.797.0" description = "A client library for accessing Windmill server wrapping the Windmill client API" license = "Apache-2.0" homepage = "https://windmill.dev" diff --git a/typescript-client/jsr.json b/typescript-client/jsr.json index 767ff7ba9d..0b3fa2e0a3 100644 --- a/typescript-client/jsr.json +++ b/typescript-client/jsr.json @@ -1,6 +1,6 @@ { "name": "@windmill/windmill", - "version": "1.796.0", + "version": "1.797.0", "exports": "./src/index.ts", "publish": { "exclude": ["!src", "./s3Types.ts", "./sqlUtils.ts", "./client.ts", "./wacError.ts"] diff --git a/typescript-client/package.json b/typescript-client/package.json index 1e1ea142a9..5295f7dc8b 100644 --- a/typescript-client/package.json +++ b/typescript-client/package.json @@ -1,7 +1,7 @@ { "name": "windmill-client", "description": "Windmill SDK client for browsers and Node.js", - "version": "1.796.0", + "version": "1.797.0", "author": "Ruben Fiszel", "license": "Apache 2.0", "homepage": "https://github.com/windmill-labs/windmill/tree/main/typescript-client#readme", diff --git a/version.txt b/version.txt index d7d21894db..23df753fdd 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.796.0 +1.797.0 diff --git a/windmill-yaml-validator/package-lock.json b/windmill-yaml-validator/package-lock.json index 5f4baec7db..72051d74d4 100644 --- a/windmill-yaml-validator/package-lock.json +++ b/windmill-yaml-validator/package-lock.json @@ -1,12 +1,12 @@ { "name": "windmill-yaml-validator", - "version": "1.796.0", + "version": "1.797.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "windmill-yaml-validator", - "version": "1.796.0", + "version": "1.797.0", "license": "Apache 2.0", "dependencies": { "@stoplight/yaml": "^4.3.0", diff --git a/windmill-yaml-validator/package.json b/windmill-yaml-validator/package.json index bea35194bd..b3e0677aad 100644 --- a/windmill-yaml-validator/package.json +++ b/windmill-yaml-validator/package.json @@ -1,6 +1,6 @@ { "name": "windmill-yaml-validator", - "version": "1.796.0", + "version": "1.797.0", "description": "YAML validator for Windmill flow, schedule, and trigger files", "main": "dist/index.js", "types": "dist/index.d.ts", From 29133398f99cd2dd5b33057ee9df4492d82e067a Mon Sep 17 00:00:00 2001 From: Guilhem Date: Thu, 27 Aug 2026 10:39:36 +0200 Subject: [PATCH 21/34] feat: a wizard for importing a hub project, and finishing what the import cannot (#10729) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(frontend): guided setup wizard for data tables On Cloud a data table cannot use the Windmill instance database, so a new workspace hit a dead end: an alert telling the user to go find a PostgreSQL resource somewhere else. Setting one up meant three disconnected places, and the connection could only be tested after the config had already been saved. Adds a three-step wizard (choose a database -> set it up -> name it) reached from the data tables settings page: - Supabase: signs in via the existing supabase_wizard OAuth client and creates the project from inside Windmill. Because db_pass is an input to project creation, Windmill sets the password and the user never visits a dashboard. - Your own database: picks an existing postgresql resource, or adds one with a connection string through the form that already supports it. - Windmill database: hands back to the inline row editor, since instance databases are provisioned by a superadmin. Verifying access is no longer a step the user takes: Continue runs the check and passing it is what advances the wizard, so a database that cannot create tables never reaches the workspace config. Co-Authored-By: Claude Opus 5 (1M context) * chore: pin ee-repo-ref to the Supabase provisioning endpoints Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): do not claim the database is ready when its check failed Co-Authored-By: Claude Opus 5 (1M context) * fix: address review findings on the data table wizard - The Supabase create branch advanced on `provisioning === 4` without consulting the check it had just run, so a role that cannot create tables could reach Finish. It now blocks and offers Try again. - Retrying no longer mints a fresh secret variable + resource each time: the credentials are only re-created when the password actually changed. - The generated password is captured before the create call rather than after, since a throw there can still leave a project behind. - On a failed provision the project list is refreshed, so the just-created project can be picked up from the other tab instead of provisioning a second. - Finish refuses a name that already belongs to another data table, which previously repointed it at the new database. - Secrets go to the acting user's namespace instead of a literal `u/admin/`. - The progress list no longer ticks "Created on Supabase" before the request is sent, and does not claim the database is ready when its check failed. - The wizard's resume state is cleared when it closes, so reopening after an abandoned OAuth round trip is not stuck on step 2. - The OAuth callback shares the session-storage key rather than repeating it. - SupabaseConnect uses the shared provisioning helpers instead of a fork. - Restores the doc comment displaced onto TestDataTableResourceQuery. Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): simplify Alert layout and balance its vertical padding The body was rendered by two near-duplicate branches, each wrapping the text in an extra div only to hang a margin on it, and the margins disagreed: the collapsible branch spaced above with mt-2, the static one below with mb-2. Since isCollapsed defaults to true, every non-collapsible alert took the static branch, so titled alerts read as 24px of space below the text against 16px above -- visibly off-centre -- with the title and body flush against each other. Collapse both branches into one and drop the margins; the container's own padding now sets top and bottom equally, with a small gap under the title row. Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): only offer Supabase when its OAuth client is configured The wizard offered the Supabase card unconditionally, so on an instance whose superadmin never configured a supabase_wizard client -- or whose backend is built without the oauth2 feature, which compiles the whole /api/oauth router out -- the card dead-ended at a 404. Gate it on listOauthConnects, the same check ApiConnectForm already makes, fetched on open so configuring the client mid-session does not require a reload. Also drop the Supabase project ref from the existing-project cards: it is an opaque identifier that means nothing outside Supabase's own dashboard URLs. Show the region instead, plus a status word when the project is not healthy, since a paused project is the one case where the connection check fails for a reason unrelated to the password. Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): run the Supabase OAuth leg in a popup A full-page redirect unmounts the wizard, so anything the user does on Supabase's side -- signing in, confirming an email, browsing their dashboard -- leaves them with nothing pointing back at Windmill, and the wizard had to park its state in sessionStorage to survive the trip. Open the connect endpoint in a popup instead. The modal stays on screen throughout and the callback hands the token back through postMessage rather than navigating. The parked-state path stays as the fallback for browsers that block the popup. Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): scope the connection check to the choice that produced it A failed check stayed on screen when the user switched Supabase mode or picked a different provider, so a fresh tab opened showing an error about a database it had nothing to do with. Clear the report and the error on both switches; re-clicking the tab already selected leaves an error the user is reading in place. Also polish the Supabase step: project cards get the provider-card treatment (icon, p-3, flex column) instead of a hand-rolled variant whose block layout left more padding above the name than below; form labels settle on text-emphasis; and the signup link sits under the primary button for anyone who does not have an account yet. Drop the "free" badge and the "Free on Supabase" line -- every option in the wizard is free, so neither told the user anything -- and say what the Supabase card actually does now that connecting an existing project is the default. Co-Authored-By: Claude Opus 5 (1M context) * refactor(frontend): one setup checklist and one Supabase step for every host The data table wizard, the instance database modal and the resource drawer each had their own version of the same two interactions, and they had already begun to drift: the wizard's Supabase resource shape was rebuilt by hand in the drawer, and the instance checks rendered with no notion of a step being in flight. SetupChecklist replaces LoggedWizardResult, whose only consumer was the instance modal. It adds the running state that component lacked, so a list driven by an endpoint that reports nothing until it returns still shows where it is. Both the instance checks and the Supabase provisioning stages render through it. SupabaseProjectStep owns picking or creating a project, and useSupabaseOauth owns the popup leg. Each host keeps only what is genuinely its own: the wizard saves a variable and resource then verifies the connection, the resource drawer fills in its own form. Both trigger authorization themselves, so a host can offer it a screen earlier than the step does. The lists load behind a spinner because which mode to open on depends on whether the account has projects; deciding that after rendering flipped the toggle under the user. Adds a kitchen_sink playground for the checklist so the animation and every failure position can be exercised without a backend, a superadmin, or a Supabase account. Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): tidy the resource drawer around the Supabase entry point Connect Supabase was a hand-styled anchor carrying Supabase's brand hex values rather than a Button, and it sat in a row whose other controls had settled on unifiedSize md. Making it a Button meant SupabaseIcon had to satisfy IconType, so it now takes `size` (deriving height/width from it) alongside the string props its other callers pass. The manual resource form spaced every field 32px apart and WhitelistIp added another 16px of its own, which read as a gap rather than a rhythm. One gap of 16px, with the form itself given a little more separation from the description above it. Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): stop Supabase resources coming up modified when first opened Resource forms fill in every unset property from the schema as soon as they render, so a postgresql resource saved without region, root_certificate_pem and use_iam_auth was dirty -- and had saved a draft -- the first time anyone looked at it. Write them with the rest of the value. SupabaseConnect also rebuilt the resource shape by hand instead of using the shared helper, which is how the pooler host format ended up in two places. Co-Authored-By: Claude Opus 5 (1M context) * feat(backend): record where a data table came from and whether setup finished edit_datatable_config replaces the whole datatables map and DataTable does not deny unknown fields, so anything the request omits is dropped without a word. origin and setup_incomplete would have been erased by any unrelated save; preserve_unmanaged_datatable_fields carries them -- and migrations_enabled, which had the same problem inline -- forward for entries that already exist, following renames. setup_incomplete is what lets a row be recorded before the resource it points at exists, so the wizard can write nothing until the user finishes. There is deliberately no intermediate state: the setup runs entirely in the browser, so nothing server-side could advance one. datatable_health probes every data table at once for the settings page and skips the incomplete ones, whose resource_path resolves to nothing yet. set_datatable_setup patches a single entry instead of resending the map. test_datatable_connection_value checks a connection the caller has not saved anywhere, which the wizard needs before it has written a resource. Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): make destructive default and subtle buttons read red Both variants were neutral until the pointer arrived, then filled solid red: nothing marked the button as destructive until you were already on it. They now carry red text at rest, with a faded red border on default and a light red wash on hover, which is what the legacy red border style in the same file had always done. Three call sites passed color="red" alongside a design-system variant. getStyleClass returns before colour is read for accent, accent-secondary, default and subtle, so the delete-migration control, its modal confirm and the import-database button had all been rendering neutral. They pass destructive now. The dropdown variant strips the button's own border, and matched border-border-light literally -- a class the destructive style no longer contains. Co-Authored-By: Claude Opus 5 (1M context) * feat(frontend): rebuild data table setup around a read-only row The wizard gathers intent over two steps, reviews it on a third and writes nothing until Finish, so a billable Supabase project is created only once the user has seen what will happen. runSetup is also the retry: every step probes for its own result before doing anything, so running it again on a half-finished data table resumes instead of duplicating. Its steps are keyed rather than dispatched on their titles, where rewording one changed what it did. The settings row stops being an editable form with a dirty/save cycle. It carries the name, where the database came from, a health dot and two actions; everything rare moved into the gear panel, which also offers Finish setup for a data table whose wizard never completed. Manage is ExploreAssetButton, the control the ducklake list already uses, and the row and panel both link out to the underlying resource. supabaseResourceValue no longer assembles the pooler host from the region. aws-0-.pooler.supabase.com is wrong for any project Supabase allocated elsewhere, so the host, user and port come from the pooler config endpoint. Two data tables sharing one database also share _wm_migrations, which is probed unqualified, so the review step warns when the database being connected is already behind another data table. SupabaseConnect is deleted. The resource drawer uses the shared project step restricted to existing projects: creating one is a billed action and belongs in the wizard, which has somewhere to report what it did. The kitchen_sink checklist playground goes with it. Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): fall back to a direct Supabase connection when the pooler cannot be read Reading a project's Supavisor config needs the database_pooling_config_read scope, which an instance's Supabase OAuth app may never have been granted. No retry recovers from that, and the wizard treated it as fatal: the user was left with an error and no way to finish connecting a project that was otherwise fine. resolveSupabaseConnection replaces the bare pooler read everywhere it happened. Asking for session pooling and failing now yields a direct connection plus the reason, which supabaseResourceValue already knew how to write. Nothing about the fallback is silent -- direct is IPv6-only, which is the whole reason session pooling is the default -- so the wizard warns on its review step and the resource drawer says so in its toast. The row is recorded before credentials are saved, so an origin claiming session pooling has to be corrected once a direct host is what gets written; the run patches it through set_datatable_setup rather than leaving the panel to report a mode nothing uses. Co-Authored-By: Claude Opus 5 (1M context) * feat(frontend): open the database behind a data table, and say when it cannot write Every database in the list now opens the surface that owns its credentials. A postgres one opens its resource in the editor drawer; a Windmill instance one opens the instance modal, which is where its setup checks, password rotation and drop already lived. Both are reachable from the row and from the panel's provenance list, and the provider icon moved inside the button so the whole thing is one target. CustomInstanceDbWizardModal targeted #content unconditionally, which put it underneath the panel drawer that now opens it. It takes a target, and the panel portals it to the body. The status column gains a third state. The probe reports privileges but nothing gated the dot on them, so a data table whose role cannot create tables showed as Connected and only failed when someone ran a migration. It reads "Limited permissions" instead, and opens the panel on the report carrying the GRANTs that fix it -- the settings page has already probed, so the panel takes that report rather than asking the user to run Test connection over work already done. fullyPrivileged is exported from the report component so the dot and the report cannot disagree about what counts as healthy. Co-Authored-By: Claude Opus 5 (1M context) * revert(frontend): keep the data tables settings table as it was The settings table and the setup wizard are two changes that only shared a file. Splitting them makes each reviewable: this branch keeps the wizard, and the read-only row, gear panel, health probe and clickable databases move to their own branch. The rows go back to the editable form with its pickers and save footer, still opening the wizard from Add a database. DataTableSettingsPanel, dataTableHealth and dataTableOrigin had no other consumers and go with them; the connection report stays, because the wizard shows it too. DataTableSettingsType keeps `origin`: the wizard writes it, and the review step reads it back to warn when two data tables would share one database and therefore one _wm_migrations table. Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): confirm before dismissing the data table wizard mid-setup Closing was guarded while a run was in flight and unguarded before one, which is backwards: a run leaves a row to resume from, whereas a backdrop click on the review step threw away the project, the pasted password and the folder with nothing to recover them from. Backdrop, Escape and the close button now go through one path that asks first. It only asks when there is something to lose -- no provider chosen yet, or a run that already produced a result, closes immediately -- so the dialog does not become something to click through. Continue in the background still leaves in one click; that exit was always the deliberate one. Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): stop the wizard claiming the resource folder controls who can use a data table "Who can use this database" was wrong. Every path that resolves a datatable:// reference -- both executors and the agent-worker endpoint -- reads the resource unchecked, by workspace and name. A resource in u/admin is usable by everyone's scripts. The folder governs who can see and edit the connection, and who can reference the resource directly in a SQL step; neither is who can use the data table. The wizard was contradicting the tab's own description two screens later. The folder select and name field become one Path picker, the same one the resource, variable and script forms use, so the review step reads as a resource path rather than a permission choice. Its initialPath is snapshotted when the step opens: Path seeds itself from it, and a live value fights the typing. Finish now also gates on Path's error, so a taken or malformed path stops the run before it writes anything. The button that opens all this says "Add a data table" -- the data table is what you get; the database is a detail chosen along the way. Co-Authored-By: Claude Opus 5 (1M context) * revert(frontend): move the destructive button restyle out of the wizard PR This reverts 3881e4d8ea. Making default and subtle destructive buttons red at rest changes every existing caller of the prop -- the workspace integrations, AI skills, workspace creation and the instance database drop -- so it is a design-system change, and the call sites it fixed are the migrations list and the database manager. None of that is the setup wizard. Nothing on this branch passes destructive any more, so it leaves with no loose ends. Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): make the wizard stepper navigate the steps it already offers Stepper dispatches a click and paints cursor-pointer on every reached step, but the wizard never listened, so the breadcrumbs invited a click and did nothing. They now reach any step already passed, in either direction: going back to check something should not cost the progress, which means tracking the furthest step reached rather than the current one. Forward movement still only happens through the primary action, so a step is never reachable without having been validated -- and changing the intent revokes the steps ahead of it, or Finish could run against a review built from something the user has since edited. The five places that cleared the probe on an edit now do both through one call. During a run nothing is reachable, and the stepper says so rather than showing a pointer over steps that will not respond. Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): restore the data tables description lost in the branch split The rewritten description went into DataTableSettings.svelte shortly before that file was restored wholesale to its pre-rebuild state, so it left with the row rework it had nothing to do with. The tab went back to describing the plumbing -- a fully managed PostgreSQL database, reachable from the SDK -- which never answered the question a new user actually has: why this rather than a Postgres resource. It leads with what a data table is, then the two things a resource cannot do -- nobody needs the credentials to query it, and the name can be pointed at another database without editing anything that uses it -- and closes with what Windmill runs on top. Both middle claims are the ones every resolution path backs up: datatable:// resolves by workspace and name, unchecked. Co-Authored-By: Claude Opus 5 (1M context) * fix(backend): say what is missing when a $res: or $var: reference does not resolve Both interpolations fetched with fetch_one and mapped the error through to_anyhow, so a reference to something deleted surfaced as "no rows returned by a query that expected to return at least one row @workspaces.rs:2169". It names neither the kind of thing that was missing nor its path, and it is what a data table pointing at a deleted resource reports. They now fetch_optional and return NotFound naming the path, and datatable resolution adds the data table on the way out: the caller asked for one by name, and a bare "resource f/x/y does not exist" leaves them to work out which of them points at it. The health probe is new, so this string had only just become something users read. Co-Authored-By: Claude Opus 5 (1M context) * feat(frontend): gate the data table wizard behind a dev flag The wizard only appears with `dataTableWizard` set in localStorage; without it the settings page keeps the inline-row flow it had before this branch, down to the empty-state copy and the "New Data Table" button, and the wizard component is not mounted at all. The existing e2e suite drives that button, so the default-off flag is also what keeps it green. Step 2 of "your own database" becomes one list rather than a segmented control: the workspace's Postgres resources, then a New resource card that expands in place. A connection string is not an alternative to a resource, it is how one is written, and the old layout taught otherwise. The card holds the same connection as a string or as fields and carries values across when you switch, so `parse` and `compose` have to be inverses -- hence the percent-encoding on both sides, which also fixes a password containing `@` silently corrupting in the resource form. The Supabase step now uses the same shape. Names and paths are checked as they are typed rather than at the end of a run that may have created a billed project first: the data table name against the charset `edit_datatable_config` enforces, the instance database name against what `setup_custom_instance_db` will accept, and the resource path against both the resource and variable namespaces, since the run writes to both and both writes upsert. `test_datatable_connection_value` refuses `$var:`/`$res:` in its body. It feeds `transform_json_value_unchecked`, which resolves references with no permission check of its own, so an admin could otherwise have had the API server decrypt any workspace secret and hand it to a host the same request chose -- without the audit trail a variable read leaves. Callers testing something unsaved hold the literal value already. Alert, SetupChecklist and postgresConnectionString change for everyone, not just behind the flag: body-only alerts no longer reserve an empty title row, the checklist can nest the checks a step is made of, and the connection-string parser is shared with the resource form. Co-Authored-By: Claude Opus 5 (1M context) * chore: pin ee-repo-ref to the EE branch merged with EE main The Supabase proxies the wizard calls are still unmerged, so the ref cannot be an EE main commit yet; it now names that branch merged with EE main rather than the branch alone, which was nine commits behind and would have been built against a CE main it never saw. Co-Authored-By: Claude Opus 5 (1M context) * feat(frontend): gate the supabase resource path behind the dev flag * test(frontend): pin connection string parsing to libpq behaviour * fix(frontend): keep the supabase resource link off the popup callback path * refactor(frontend): load the supabase resource dialog only behind the flag * fix(frontend): refuse a resource path the wizard run does not own * fix(frontend): let a failed data table setup be corrected without losing what it made * fix(frontend): let a failed setup reuse the resource path it claimed Co-Authored-By: Claude Opus 5 (1M context) * fix(backend): record the two data table connection tests in the audit log Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): use Section for the data table wizard advanced group Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): read connection strings the way libpq does Co-Authored-By: Claude Opus 5 (1M context) * fix(backend): pin the ee ref back to a commit this branch can build Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): keep a failed setup's claims across the redirect and rollback Co-Authored-By: Claude Opus 5 (1M context) * fix(backend): probe a data table with the auth mode the worker will use Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): keep every part of a connection string through the round trip Co-Authored-By: Claude Opus 5 (1M context) * refactor(frontend): give a setup run one record of what it created Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): mark a resource claim by edited_at, not its creator Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): mark every claim by revision, and keep an unconfirmed project's secret Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): refuse to test or save behind a connection string that will not parse Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): refuse a connection string carrying options the resource cannot hold Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): allowlist the connection-string parameters a resource can honour Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): guard every created Supabase project, not just the last one Co-Authored-By: Claude Opus 5 (1M context) * feat: three-step wizard for importing a hub project Importing used to be a single page that inherited whatever workspace happened to be active, with no way to say where the project should go — the hub cannot know, since it only ever links to *an* instance. `/projects/import` now asks: which kind of destination, which workspace, then imports. Nothing is created, switched or written until the last step runs. The wizard's state is a plain value in the URL (`importWizard/plan.ts`), so the back button, the stepper and the Back control are the same operation, and none of them can strand a half-created workspace — there is no state anywhere else to unwind. `importWizard/execution.svelte.ts` is the only code that acts on a plan: it runs create → fetch → import as an observable task list, reuses what already succeeded when retried, and offers to delete the workspace it created if the run stops early. Its UI needs — the data table migration review — are injected, so it holds no components. The old `/projects/install` becomes a redirect: hubs upgrade on their own schedule and a self-hosted one may keep pointing at it for a long time. Co-Authored-By: Claude Opus 5 (1M context) * fix: let the import wizard survive sign-in and a missing workspace Signing in with `rd=/projects/import?hub=...` dropped the destination: the login redirect only honours `rd` verbatim for `/user/workspaces`, so anyone with more than one workspace landed on the workspace picker instead — the page the wizard exists to replace, asking the question it was about to ask. Both copies of that logic now allow the wizard through. The root layout's "no workspace selected" redirect skips the wizard too. It picks the destination itself and may end in a workspace that does not exist yet, so bouncing it to the picker forces the very choice it is there to make. Co-Authored-By: Claude Opus 5 (1M context) * chore: bench page for the import project card /kitchen_sink/import_project_card renders the card against fixtures — a real project, an oversized one, a minimal one — so its layout can be judged without a hub running or an import in flight. Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): do not warn about renaming an item that does not exist yet Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): make the review step read as one list of what will exist Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): keep the picked Supabase project across the redirect, reject connect_timeout Co-Authored-By: Claude Opus 5 (1M context) * fix: check the data table connection from a worker, not the API server The wizard's connection check ran on the API server through two endpoints added for it. That server is a different machine with a different identity, so the answer was about the API server rather than about the worker that will run the queries: a host reachable from one is not necessarily reachable from the other, and IAM RDS and Azure workload identity authenticate as whichever process opens the connection. Run the privilege query as a preview job instead. A job goes through the worker's Postgres executor, which is where `PgAuthMode::of` already picks the authentication mode, and it takes either a resource value or a `$res:` path exactly as a Postgres step does. Postgres composes the suggested GRANT statements through `format('%I')`, so identifier quoting stays where it is already implemented. Removes `test_datatable_resource_connection` and `test_datatable_connection_value`, and `connect_as_the_worker_would` with them. Co-Authored-By: Claude Opus 5 (1M context) * refactor: fold check_datatable_connection back into its only caller The helper was split out so the two connection-test endpoints could share a body. Those endpoints are gone, leaving one caller. Co-Authored-By: Claude Opus 5 (1M context) * revert: keep the data table connection check schema inline It was lifted into components so three endpoints could share it. Two of those are gone, so it is back to one user and the extraction changes nothing. Co-Authored-By: Claude Opus 5 (1M context) * fix: restore openapi.yaml to the branch point The previous commit restored main's tip rather than the merge base, which carried three unrelated main-only changes into this branch: the resource mcp_tools truncation fields, the execution_mode description, and a version bump. Co-Authored-By: Claude Opus 5 (1M context) * refactor(frontend): drop four effects from the data table wizard Each was doing work a derived, a load callback or a real entry point does better. - The name conflict is kept with the name it was raised for and derived from it. As an effect it was correct only because it never read what it wrote: the pre-flight sets the message and the effect does not re-trigger, so adding a read would have cleared it the instant it appeared. The message now also comes back if the taken name is retyped, which is what the server will say. - The default resource selection is seeded inside the fetcher that loads the list, where "has the fetch settled" cannot be asked wrong. - Reset-on-open becomes an exported open(), called by the settings page, so a fresh run is set up by the act of opening rather than by a flag emulating mount. - The OAuth connects and the folder list become resources; supabaseAvailable and folders are derived from them. defaultFolder takes the list rather than reading it, so the fetch can seed off its own result. Leaves the debounced path check, which is async with an out-of-order guard. Co-Authored-By: Claude Opus 5 (1M context) * refactor(frontend): drop three effects from the Supabase branch - useSupabaseOauth reports success as onAuthed, alongside the failures it already reported. SupabaseResourceConnect was watching `authed` to find out; it takes the callback instead, keeping the guard that stops an authorization started elsewhere on the page from opening its dialog. - SupabaseProjectStep loads its orgs and projects through a resource keyed on the token, so the `loaded` latch goes and re-authorizing reloads rather than keeping the lists from the expired session. - SetupChecklist records what the user toggled and derives the open state from it, a failed step defaulting to open. Recording the open state instead needed an effect to force it, and that effect re-ran on every progress update, so a description closed while anything was still ticking reopened. A close now holds for the life of the checklist, including across Try again. Leaves the message listener, which subscribes to another window. Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): confine the modal restyle to the wizard, and trim the comments The wider side padding and lighter dialog heading were changing all 17 Modal2 dialogs to suit this one flow. They move behind an opt-in `formStyling`, taken by the three dialogs this branch owns; every other Modal2 renders as it did. Also drops two comments that cited a design approval rather than a constraint, and shortens the blocks that had grown past the four lines AGENTS.md asks for. Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): use the accent token for the wizard's links `text-blue-500` is the marketing blue `#3B82F6`, which brand-guidelines.md rules out in the app interface. Co-Authored-By: Claude Opus 5 (1M context) * chore: point ee-repo-ref at the EE branch head Picks up EE main, which the branch now needs, and the Supabase proxy auth fix. Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): read sslmode by name, and stop decrypting a secret to date it - `sslmode` was found by searching the query text, so it also matched inside another parameter's value: `?application_name=sslmode=disable` passed the allowlist on the parameter name and then parsed as a request to turn TLS off, which both the wizard and the resource form saved and probed. Parsed with `URLSearchParams` by exact name, with a test. - `secretMark` read the variable with `decryptSecret` defaulted to true, so every write decrypted a secret nothing reads and recorded the decryption -- including someone else's on the retry about to refuse it. It wants only `edited_at`, which is returned either way. - The probe gave up at 15s while the worker allows its Postgres connect 20s, so a host that accepts the connection and never answers was cancelled and reported as a missing worker rather than a failed connection. - The create-mode region and project name did not report an intent change, so renaming a project after a name collision left the failure naming the old one. - Two comments described the code as it was before the claim mark became a revision, and a doc comment outlived the field it documented. Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): read connection parameters the way libpq does One reader for both the parser and the allowlist, since they disagreed about what a string says in two ways that both ended in a weaker connection than was pasted: - `URLSearchParams.get` takes the first of a repeated parameter and libpq takes the last, so `?sslmode=disable&sslmode=require` was read as `disable`. - The allowlist folded the parameter name and the parser did not, so `?SslMode=verify-full` was refused by neither and honoured by neither, and saved as the `require` default. The parked Supabase run is now handed to `open()` rather than read back off the `resume` prop it was just assigned to, so restoring it does not depend on when that prop reaches the component. Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): keep connection parameter names case-sensitive libpq does not fold them: `?SslMode=disable` is rejected as an invalid URI query parameter rather than read as `sslmode`, which a local server confirms. Folding made Windmill accept and honour a string Postgres itself refuses; naming the parameter instead tells the user why it cannot be stored. The last-value-wins rule for a repeated parameter is unchanged, and matches what the same server does. Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): seed the Supabase organization from the project it selects The loader took `orgs[0]` independently of the project it seeded, so an account whose first project sits outside its first organization had the review step name an organization the database does not belong to. Picking a project by hand already derives it; the seeding now does the same. Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): let the probe report an empty search_path instead of failing on it `format('%I', NULL)` raises rather than returning NULL, so a role whose search_path names no valid schema failed the whole privilege query and was reported as an unreachable database. That is the one case `fix_search_path` exists to name, and it never reached the user. Verified against a local server with `SET search_path = ''`. Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): say which of the two refusals a connection string hit Making parameter names case-sensitive gave `unsupportedConnectionParam` two reasons to refuse, and the single message explained only one. `?SslMode=` was answered with "Windmill cannot store SslMode on a Postgres resource", which is false twice over: sslmode is exactly what the resource stores, and the string asks for nothing because Postgres rejects the URI. It now names the spelling when the parameter is one we keep, and the storage limit otherwise. The folder-list guard also still read the `resume` prop that `open(parked)` was changed to stop trusting, so the resumed path now comes from whatever `reset` was handed. Co-Authored-By: Claude Opus 5 (1M context) * fix(frontend): leave the Supabase organization unset when the lookup misses Falling back to the first organization named one the seeded project is not in, since `supabaseSummary` prefers `intent.org` over the project's own. Unset, it falls through to the project's organization identifier — the right one, spelled as a slug rather than a name. Co-Authored-By: Claude Opus 5 (1M context) * fix: harden the import wizard and put it on the design system Review fixes, then the parts of the wizard that were hand-built where the design system already had an answer. Correctness: - Hub SVGs are sanitised with DOMPurify before `{@html}`. The earlier comment claimed the markup came from the hub's own icon package rather than user input, which the custom-URL feature makes false: the hub is whatever address the user typed. - The run owns navigation while it is in flight. The stepper refuses to move, `beforeNavigate` cancels browser back/forward, and unmounting resolves a pending migration review so the executor cannot hang waiting on a component that is gone. - The folder edited on the last step reaches the executor, so a retry after changing it imports where the field now says. - `validateWorkspaceId` and the workspace-entry pair (`listUserWorkspaces` then `switchWorkspace`) are extracted, so the wizard and the real create form cannot drift on what an id is or on what entering a workspace means. Design system: - The destination tiles are `RadioCard`, which gains `showRadio` and a snippet `description`; the wizard turns the glyph off because the border and tint already say which one is picked. `RadioCard` now also carries `role="radio"` and `aria-checked`, which it had neither of, and marks its selection with `surface-accent-selected` — the token `FileExplorer`, `TriggersTable` and `RunnableRow` all use for the chosen row. - Form labels follow `brand-guidelines.md` — sentence case, real `
    {#if step.substeps?.length} -
    - +
    +
    {/if}
    diff --git a/frontend/src/lib/components/workspaceSettings/AddDataTableWizard.svelte b/frontend/src/lib/components/workspaceSettings/AddDataTableWizard.svelte index 2e19cbea82..3495bdbee0 100644 --- a/frontend/src/lib/components/workspaceSettings/AddDataTableWizard.svelte +++ b/frontend/src/lib/components/workspaceSettings/AddDataTableWizard.svelte @@ -21,6 +21,8 @@ FolderService, OauthService, ResourceService, + UserService, + type User, VariableService, WorkspaceService } from '$lib/gen' @@ -85,6 +87,38 @@ customInstanceDbs: ResourceReturn confirmationModal: ConfirmationModalHandle defaultInstanceDbName: () => string + /** Name to open with, when the caller needs a table of a particular name rather + * than whatever the user picks — the import wizard configures the one a project's + * migrations target. + * + * Locked when `onFinishAlso` is also given, because that work targets this name and + * nothing carries an edit through to it: renaming `main` to `other` would create + * `other`, then run the migrations against `main`, fail, and leave a data table + * nobody asked for. Editable without one, where the name is only a name. */ + initialName?: string + /** Where the dialog portals to. `#content` is the app shell's scroll container, + * which only exists inside the `(logged)` layout; a page reparented out of it + * (the hub import wizard) has to say `body` or the portal finds nothing and the + * dialog never appears. */ + modalTarget?: string + /** What the caller does once the table exists, named on the final button so the + * user is told before pressing it — the import wizard runs the project's + * migrations, which is otherwise invisible until it has already happened. */ + finishAlso?: string + /** The work `finishAlso` names. Run as the last checklist step, so it reports + * where the rest of the run does instead of starting after the dialog closes. + * Throwing marks that step failed; the table itself is already made either way. */ + onFinishAlso?: () => Promise + /** The workspace everything here is created in and checked against. + * + * Defaults to `$workspaceStore`, which is right for the settings page — it is the + * workspace being looked at. The import wizard is the exception: its page is + * reparented out of `(logged)`, so nothing re-runs the layout's workspace + * persistence, and after a reload the store still names whatever workspace the + * user came from while the plan in the URL names the destination. Left ambient, + * this would create the data table in one workspace and run the project's + * migrations in the other. */ + workspace?: string } let { @@ -95,9 +129,62 @@ onDone, customInstanceDbs, confirmationModal, - defaultInstanceDbName + defaultInstanceDbName, + initialName, + modalTarget = '#content', + finishAlso, + onFinishAlso, + workspace: workspaceProp }: Props = $props() + /** + * The caller needs this exact table, and has follow-up work bound to its name. + * + * Captured when the dialog opens rather than read live: `initialName` is the caller's + * `wizardFor`, which it clears from `onDone` — and that fires after a *failed* run too, + * while the dialog stays up offering Back. Reading it live releases the lock exactly when + * the user is most likely to edit, which is the divergence the lock exists to stop. + */ + let nameLocked = $state(false) + + /** Every write and every check goes through this, never `$workspaceStore` directly. */ + const targetWorkspace = $derived(workspaceProp ?? $workspaceStore ?? '') + + /** + * Who the caller is *in the destination*, which is not who `$userStore` describes. + * + * `$userStore` is the membership of the workspace the app is in. Routing the API calls + * elsewhere without routing this leaves the username behind: after a reload on the import + * wizard's step 4 it names the workspace the user came from, and a resource path built + * from it lands on `u/` inside the destination — failing an ownership check, + * or for an admin, quietly putting database credentials in another member's namespace. + */ + let targetUser = $state(undefined) + const aimedElsewhere = $derived(!!workspaceProp && workspaceProp !== $workspaceStore) + const ambientUsername = $derived($userStore?.username ?? '') + const targetUsername = $derived(aimedElsewhere ? (targetUser?.username ?? '') : ambientUsername) + /** The destination's membership could not be read, so nothing here knows who the user is. */ + let membershipFailed = $state(false) + + async function loadTargetUser(): Promise { + const ws = workspaceProp + if (!ws || ws === $workspaceStore) { + targetUser = undefined + membershipFailed = false + return + } + try { + targetUser = await UserService.whoami({ workspace: ws }) + membershipFailed = false + } catch { + // Recorded rather than swallowed: an unknown username silently becomes `admin` in + // the default path, which is the wrong namespace to write credentials into. Setup + // is blocked instead. + targetUser = undefined + membershipFailed = true + } + } + const STEPS = ['Choose a database', 'Set it up', 'Review'] let wiz: WizardState = $state( @@ -146,7 +233,7 @@ } clearTimeout(variableCheck) variableCheck = setTimeout(async () => { - const taken = await VariableService.existsVariable({ workspace: $workspaceStore!, path }) + const taken = await VariableService.existsVariable({ workspace: targetWorkspace, path }) // Two checks can be in flight at once and resolve out of order. A `false` for a path // nobody is on any more would clear the error guarding the one about to be written; // a `true` would disable Finish over a path this run stopped caring about. @@ -163,7 +250,7 @@ * in flight when Finish is pressed. */ async function pathConflictMessage(path: string): Promise { - const workspace = $workspaceStore! + const workspace = targetWorkspace // Each namespace answers to its own claim. Holding the secret says nothing about who owns // the resource beside it, so one claim must not wave the other's check through. const [variable, resource] = await Promise.all([ @@ -178,11 +265,14 @@ let maxStep = $state(1) function defaultProjectName(): string { - return `windmill-${$workspaceStore ?? 'workspace'}` + return `windmill-${targetWorkspace || 'workspace'}` } function defaultTableName(): string { - return existingNames.includes('main') ? `${$workspaceStore ?? 'data'}_datatable` : 'main' + // A caller that needs a specific name wins over the usual "main, unless taken": + // the import wizard's migrations only apply to a table of the name they target. + if (initialName) return initialName + return existingNames.includes('main') ? `${targetWorkspace || 'data'}_datatable` : 'main' } // Takes the list rather than reading it, so the fetch that loads it can seed off its own @@ -190,7 +280,7 @@ function defaultFolder(list: string[] = folders): string { // The first folder this admin can write to, so the resource lands somewhere the team // can find and repair. A workspace with no folders falls back to the personal space. - return list.length ? `f/${list[0]}` : `u/${$userStore?.username ?? 'admin'}` + return list.length ? `f/${list[0]}` : `u/${targetUsername || 'admin'}` } // A row this run wrote and could not take back out is still its own: `removeRow` reports @@ -236,7 +326,7 @@ ) const pgResources = resource( - () => (opened && wiz.provider === 'resource' ? ($workspaceStore ?? '') : ''), + () => (opened && wiz.provider === 'resource' ? targetWorkspace : ''), async (workspace) => { if (!workspace) return undefined const list = await ResourceService.listResource({ workspace, resourceType: 'postgresql' }) @@ -330,7 +420,7 @@ ) const folderNames = resource( - () => (opened ? ($workspaceStore ?? '') : ''), + () => (opened ? targetWorkspace : ''), async (workspace) => { if (!workspace) return [] const all = await FolderService.listFolderNames({ workspace }) @@ -374,7 +464,11 @@ let resumedPath = $state(undefined) function reset(from: WizardResume | undefined) { + nameLocked = !!initialName && !!onFinishAlso resumedPath = from?.resourcePath + // A pending confirmation that never settled leaves `dismissing` true, and `finally` + // cannot clear what never resolves — so a fresh open always starts dismissable. + dismissing = false wiz = newWizardState({ name: from?.name || defaultTableName(), projectName: from?.projectName || defaultProjectName(), @@ -391,6 +485,7 @@ createdProjects = [] nameConflictFor = undefined lastFailure = '' + finishAlsoFailed = false pathTakenError = '' poolerUnavailable = undefined if (from) { @@ -424,7 +519,11 @@ * what keeps the restore independent of when the prop it was assigned to reaches this * component. */ - export function open(parked?: WizardResume) { + export async function open(parked?: WizardResume) { + // Awaited before `reset`, which seeds the resource path from `defaultFolder()` and so + // needs the destination's username. Seeding first and correcting later loses whenever + // the folder list resolves first, and never corrects at all if `whoami` fails. + await loadTargetUser() reset(parked ?? resume) opened = true } @@ -472,12 +571,14 @@ // Also retires any check still in flight, so its answer cannot land on the edited value. probeToken++ clearProbe(wiz) - // Read off one attempt against one project; the review step would otherwise warn about - // a limitation that no longer applies while claiming session pooling right above it. + // Read off one attempt against one project, so it does not survive a change of inputs: + // the review step would otherwise warn about a limitation that does not apply to what + // it is describing, while claiming session pooling right above it. poolerUnavailable = undefined // Same for the failure carried back to the review step: it names inputs that have since // been edited, so it would describe a run nobody can still act on. lastFailure = '' + finishAlsoFailed = false if (maxStep > wiz.step) maxStep = wiz.step } @@ -527,7 +628,7 @@ settle({ checking: false, report: undefined, error: undefined }) return } - const report = await probeDatatableConnection($workspaceStore!, database) + const report = await probeDatatableConnection(targetWorkspace, database) settle({ checking: false, report, error: undefined }) } catch (err: any) { settle({ @@ -579,6 +680,13 @@ ) /** Why the last run failed, kept on the review step after the checklist is dropped. */ let lastFailure = $state('') + /** + * The appended `onFinishAlso` step failed while `runSetup` itself succeeded. Tracked apart + * from `run.result`, which stays the setup's own verdict: the data table really was + * created, so a retry must re-run only this last step. Re-running the setup would ask for + * the table name it has just taken, and be refused as a duplicate. + */ + let finishAlsoFailed = $state(false) /** * A refused pre-flight means nothing ran, so the checklist from a previous attempt has to @@ -631,7 +739,7 @@ const name = wiz.review.name.trim() try { if (claimedName !== name) { - const settings = await WorkspaceService.getSettings({ workspace: $workspaceStore! }) + const settings = await WorkspaceService.getSettings({ workspace: targetWorkspace }) if (settings.datatable?.datatables?.[name]) { nameConflictFor = { name, @@ -666,6 +774,7 @@ } run = { steps: planSteps(wiz), running: true } lastFailure = '' + finishAlsoFailed = false // The database is registered by the call whatever it answers, so asking for one is // already leaving something behind. if (wiz.provider === 'instance' && wiz.instance.mode === 'create') { @@ -675,7 +784,7 @@ let result: RunResult | undefined = undefined try { result = await runSetup(wiz, { - workspace: $workspaceStore!, + workspace: targetWorkspace, supabaseToken: supaOauth.token, onInstanceDbsChanged: async () => { await customInstanceDbs.refetch() @@ -684,9 +793,27 @@ onPoolerUnavailable: (reason) => (poolerUnavailable = reason), createdProjects, claims, - username: $userStore?.username ?? '' + username: targetUsername }) } finally { + // The caller's own finishing work, appended to the same checklist. It only runs + // on a clean setup: there is no table for it to act on otherwise. + if (result?.ok && onFinishAlso && finishAlso) { + const title = finishAlso.charAt(0).toUpperCase() + finishAlso.slice(1) + run.steps = [...run.steps, { title, status: 'running' }] + try { + await onFinishAlso() + run.steps = run.steps.map((s, i) => + i === run.steps.length - 1 ? { ...s, status: 'done' as const } : s + ) + } catch (err: any) { + const description = err?.body ?? err?.message ?? String(err) + run.steps = run.steps.map((s, i) => + i === run.steps.length - 1 ? { ...s, status: 'failed' as const, description } : s + ) + finishAlsoFailed = true + } + } // `runSetup` catches per step, but anything escaping it would otherwise leave the // button spinning with a page reload the only way out. // Kept, not replaced: what an earlier attempt wrote is still out there, so a later @@ -713,7 +840,10 @@ /** * Whether closing would throw away work. A failed run counts: its inputs are still editable * and it may have left something behind. A run in flight cannot be closed at all, and one - * that succeeded has nothing left to lose. + * that made its data table has nothing left to lose — including when `onFinishAlso` failed + * afterwards, because the table is real and working and the caller owns what is left. The + * import step, the only caller that passes one, shows that failure on its own row with a + * way to run it again and will not let Finish through while it stands. */ function hasUnfinishedIntent(): boolean { return wiz.provider !== undefined && !run.running && !run.result?.ok @@ -730,19 +860,52 @@ return } dismissing = true - const confirmed = await confirmationModal.ask({ - title: 'Leave without adding a data table?', - // A run that failed and was sent back to be edited leaves whatever it got through - // behind it, so promising otherwise would be a lie exactly when it matters most. - children: leftBehind - ? 'The setup that already ran left what it created behind, and what you have filled in here will be lost.' - : 'Nothing has been created yet, and what you have filled in here will be lost.', - confirmationText: 'Discard' - }) - dismissing = false - // Re-read rather than trust the entry check: a run can start while the dialog is up, and - // answering Discard would otherwise tear the modal down in the middle of it. - if (confirmed && !preventClose) close() + // `finally`, because the flag is what blocks a second attempt: an `ask` that throws + // would otherwise leave the dialog permanently undismissable — the backdrop, Escape + // and the close button all return early here, so the only way out would be a reload. + try { + const confirmed = await confirmationModal.ask({ + title: 'Leave without adding a data table?', + // A run that failed and was sent back to be edited leaves whatever it got through + // behind it, so promising otherwise would be a lie exactly when it matters most. + children: leftBehind + ? 'The setup that already ran left what it created behind, and what you have filled in here will be lost.' + : 'Nothing has been created yet, and what you have filled in here will be lost.', + confirmationText: 'Discard' + }) + // Re-read rather than trust the entry check: a run can start while the dialog is up, and + // answering Discard would otherwise tear the modal down in the middle of it. + if (confirmed && !preventClose) close() + } finally { + dismissing = false + } + } + + /** Re-runs only the appended step, which is the only thing that failed. */ + async function retryFinishAlso() { + if (!onFinishAlso || !finishAlso) return + const title = finishAlso.charAt(0).toUpperCase() + finishAlso.slice(1) + finishAlsoFailed = false + run = { + ...run, + running: true, + steps: [...run.steps.slice(0, -1), { title, status: 'running' as const }] + } + try { + await onFinishAlso() + run.steps = run.steps.map((s, i) => + i === run.steps.length - 1 ? { ...s, status: 'done' as const } : s + ) + } catch (err: any) { + const description = err?.body ?? err?.message ?? String(err) + run.steps = run.steps.map((s, i) => + i === run.steps.length - 1 ? { ...s, status: 'failed' as const, description } : s + ) + finishAlsoFailed = true + } finally { + run = { ...run, running: false } + onDone() + } } function close() { @@ -752,10 +915,18 @@ // The single primary action. Its label says what it is about to do, and doing it is what // moves the wizard on. let primary = $derived.by(() => { + // Ahead of everything: without the destination's membership the resource path would be + // guessed, and a guess here writes database credentials into somebody else's namespace. + if (aimedElsewhere && membershipFailed) + return { label: 'Cannot read your access to this workspace', disabled: true } if (submitting && !run.running) return { label: 'Setting things up', disabled: true, busy: true } if (run.steps.length) { if (run.running) return { label: 'Setting things up', disabled: true, busy: true } + // Before the `ok` check: the setup succeeded and the step after it did not, so + // "Done" would be offered over a failed row. + if (finishAlsoFailed) + return { label: 'Try again', disabled: false, act: retryFinishAlso } if (run.result?.ok) return { label: 'Done', disabled: false, act: close } // A run that died because the Supabase token expired would retry into the same 401 // forever; authorizing again is the only thing that can move it on. @@ -809,11 +980,12 @@ act: enterReview } } + const created = + wiz.provider === 'supabase' && wiz.supabase.mode === 'create' + ? 'Create project and data table' + : 'Create data table' return { - label: - wiz.provider === 'supabase' && wiz.supabase.mode === 'create' - ? 'Create project and data table' - : 'Create data table', + label: finishAlso ? `${created} and ${finishAlso}` : created, disabled: // Guards the way back as well as the way forward: the stepper can return to step 2, // and not every control there invalidates the review it just made stale. @@ -842,7 +1014,7 @@ else opened = v } } - target="#content" + target={modalTarget} formStyling title="Add a data table" contentClasses="flex flex-col" @@ -1034,7 +1206,7 @@ {#if wiz.instance.mode === 'existing'} {@const shared = ( customInstanceDbs.current?.[wiz.instance.dbName ?? '']?.used_by_workspaces ?? [] - ).filter((w) => w !== $workspaceStore)} + ).filter((w) => w !== targetWorkspace)} {#if shared.length} @@ -1047,7 +1219,7 @@
    {#each instanceDbs as { name, db } (name)} {@const selected = wiz.instance.dbName === name} - {@const others = (db.used_by_workspaces ?? []).filter((w) => w !== $workspaceStore)} + {@const others = (db.used_by_workspaces ?? []).filter((w) => w !== targetWorkspace)} +
    +
    +
    + {:else if step === 2} +
    + {#if !choiceIsExisting} +
    +

    Name the new workspace

    +
    + +
    + + +
    + + {#if !automateUsername} + + {/if} + {:else} +
    +

    Pick a workspace

    +

    The project is imported into this one.

    +
    + + {#if workspaceList.loading} +
    + Loading your workspaces… +
    + {:else if workspaceList.error} +

    + Could not list your workspaces. Reload the page, or go back and create a new one. +

    + {:else if workspaces.length === 0} +

    + You are not a member of any workspace yet. Go back and create one, or ask an admin to + invite you. +

    + {:else} + + {#if workspaces.length > 1} +
    +
    + + +
    + {#if hasForks} + + {/if} +
    + {/if} + + + + {/if} + {/if} + +
    + + {#if !choiceIsExisting} + + {/if} +
    +
    + {:else if step === 3} + go({ folder }, 3, { replace: true })} + onFinish={() => + setupNeeded + ? // Replaces rather than pushes: after a reload on step 4 the run is gone, and + // a step-3 entry in history is a browser-Back route to the same fresh import + // the stepper is now blocked from reaching. + go({}, 4, { replace: true }) + : finish()} + onBack={() => go({}, 2)} + onExecution={(e) => (execution = e)} + resume={execution} + /> + {:else} + go({}, 3) : undefined} + /> + {/if} + +{/if} diff --git a/frontend/src/routes/(root)/(logged)/projects/install/+page.svelte b/frontend/src/routes/(root)/(logged)/projects/install/+page.svelte deleted file mode 100644 index 06f2344356..0000000000 --- a/frontend/src/routes/(root)/(logged)/projects/install/+page.svelte +++ /dev/null @@ -1,374 +0,0 @@ - - -
    - {#if !slug} -

    Missing ?hub=<slug>.

    - {:else if loading} -
    - Loading project… -
    - {:else if loadError} -

    Failed to load project: {loadError}

    - {:else if data} -

    Add “{data.project.name}” to workspace

    -

    {data.project.summary}

    - -
    -

    - Folder in {workspace} -

    - -

    - Items import under f/{folderName.trim() || data.project.slug}/. -

    -
    - -
    - {counts?.scripts} scripts - {counts?.flows} flows - {counts?.apps} apps - {counts?.resources} resources - {counts?.triggers} triggers - {#if counts && counts.migrations > 0} - {counts.migrations} data table migrations - {/if} -
    - -
    - Resources are imported as empty stubs — set their values after import; a resource whose path - already exists is reported as failed (existing values are never overwritten). Trigger kinds - are recreated disabled, except GCP and Azure triggers, which manage cloud subscriptions at - creation and must be re-created manually after filling their resource. Kafka, NATS, SQS, GCP - and Azure triggers all require Enterprise. Triggers that reference a resource depend on stubs - imported empty, so fill in the resource value before re-enabling the trigger. -
    - -
    - - {#if done} - - {/if} -
    - - {#if results.length} -
      - {#each results as r} -
    • - {r.ok ? '✓' : '✗'} - {r.path} - {#if !r.ok}— {r.error}{/if} -
    • - {/each} -
    - {/if} - {/if} -
    - - - - - - closeMigrationReview(false)}> - closeMigrationReview(false)}> -
    -

    - This project ships migrations that recreate the data tables it uses. Review and edit the - SQL, then choose which to run. A migration runs against the data table of the same name in - {workspace}; if that data table has migrations enabled it is - recorded, otherwise it runs once as a preview job. -

    - {#each reviewList as m (m.datatable_name)} -
    -
    - {m.datatable_name} - -
    - {#if m.run} - - {/if} -
    - {/each} -
    - {#snippet actions()} - - - {/snippet} -
    -
    diff --git a/frontend/src/routes/(root)/(logged)/projects/install/+page.ts b/frontend/src/routes/(root)/(logged)/projects/install/+page.ts new file mode 100644 index 0000000000..718b624bd4 --- /dev/null +++ b/frontend/src/routes/(root)/(logged)/projects/install/+page.ts @@ -0,0 +1,13 @@ +import { redirect } from '@sveltejs/kit' +import { base } from '$app/paths' +import type { PageLoad } from './$types' + +/** + * `/projects/install?hub=` was where the hub's "Add to workspace" button + * pointed before the import wizard existed. Hubs upgrade on their own schedule — + * a self-hosted one may keep sending people here for a long time — so the old + * entry point forwards to the wizard rather than 404ing, query string intact. + */ +export const load: PageLoad = ({ url }) => { + redirect(307, `${base}/projects/import${url.search}`) +} diff --git a/frontend/src/routes/(root)/(logged)/user/(user)/login/+page.svelte b/frontend/src/routes/(root)/(logged)/user/(user)/login/+page.svelte index d7520bd873..df85c1f1c1 100644 --- a/frontend/src/routes/(root)/(logged)/user/(user)/login/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/user/(user)/login/+page.svelte @@ -1,4 +1,5 @@ diff --git a/frontend/src/routes/(root)/(logged)/user/(user)/workspaces/+page.svelte b/frontend/src/routes/(root)/(logged)/user/(user)/workspaces/+page.svelte index c4d2168d26..ed9bf2b11e 100644 --- a/frontend/src/routes/(root)/(logged)/user/(user)/workspaces/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/user/(user)/workspaces/+page.svelte @@ -26,6 +26,7 @@ import { switchWorkspace } from '$lib/storeUtils' import { GitFork, Settings, User, Search, ChevronsDownUp, ChevronsUpDown } from 'lucide-svelte' import { isCloudHosted } from '$lib/cloud' + import { canCreateWorkspace } from '$lib/workspaceCreation' import AnimatedButton from '$lib/components/common/button/AnimatedButton.svelte' import { emptyString } from '$lib/utils' import { getUserExt } from '$lib/user' @@ -104,9 +105,7 @@ let onlyAdminsWorkspace = $derived(allWorkspaces.length === 1 && allWorkspaces[0].id === 'admins') async function getCreateWorkspaceRequireSuperadmin() { - const r = await fetch(base + '/api/workspaces/create_workspace_require_superadmin') - const t = await r.text() - createWorkspace = t != 'true' + createWorkspace = await canCreateWorkspace(false) } let createWorkspace = $state($superadmin || isCloudHosted()) diff --git a/frontend/src/routes/(root)/+layout.svelte b/frontend/src/routes/(root)/+layout.svelte index 1f7a813eac..0e404afbcc 100644 --- a/frontend/src/routes/(root)/+layout.svelte +++ b/frontend/src/routes/(root)/+layout.svelte @@ -1,4 +1,5 @@ @@ -154,17 +145,15 @@

    {#snippet actions()} - - - + {/snippet} @@ -260,8 +249,8 @@
    Developers

    - Calculated on the MAXIMUM number of users in a given billing - period, see the Customer Portal for more info. + Calculated on the MAXIMUM number of users in a given billing period, see the + Customer Portal for more info.

    @@ -276,8 +265,8 @@
    Operators

    - Calculated on the MAXIMUM number of operators in a given - billing period, see the Customer Portal for more info. + Calculated on the MAXIMUM number of operators in a given billing period, see + the Customer Portal for more info.

    @@ -295,8 +284,9 @@ 1 developer = 1 seat, 2 operators = 1 seat.

    - u = ceil({formatNumber(premiumInfo.developerNb)} + {formatNumber(premiumInfo.operatorNb)}/2) - = {formatNumber(premiumInfo.seatsFromUsers)} + u = ceil({formatNumber(premiumInfo.developerNb)} + {formatNumber( + premiumInfo.operatorNb + )}/2) = {formatNumber(premiumInfo.seatsFromUsers)}

    @@ -311,9 +301,8 @@
    Executions this month

    - One execution equals one job - up to 1 second on a worker with 2GB of memory, with each additional - second counting as an extra execution. + One execution equals one job up to 1 second on a worker with 2GB of memory, + with each additional second counting as an extra execution.

    @@ -365,8 +354,8 @@ Used seats (billed)

    - Highest between seats from 'Developers + Operators' and 'Seats from executions'. - This is the number of seats used for billing this month. + Highest between seats from 'Developers + Operators' and 'Seats from + executions'. This is the number of seats used for billing this month.

    u + c = {formatNumber(premiumInfo.usedSeats)} @@ -398,8 +387,8 @@

    Estimate your monthly cost

    - This is a rough estimate based on your expected team size and workload. Actual billing is based - on the maximum number of users and executions in a given month. + This is a rough estimate based on your expected team size and workload. Actual billing is + based on the maximum number of users and executions in a given month.

    @@ -420,13 +409,13 @@
    Operators
    - {estimatedOps} operator{estimatedOps === 1 ? '' : 's'} + {estimatedOps} operator{estimatedOps === 1 + ? '' + : 's'}
    -

    - 2 operators = 1 seat -

    +

    2 operators = 1 seat

    @@ -434,8 +423,8 @@
    Monthly executions
    - One execution equals one job up to 1 second on a virtual CPU with 2 GB of memory, with - each additional second counting as an extra execution. + One execution equals one job up to 1 second on a virtual CPU with 2 GB of memory, + with each additional second counting as an extra execution.
    @@ -449,9 +438,7 @@ format={(v) => `${v * 10}k`} hideInput /> -

    - Each seat includes 10k executions per month. -

    +

    Each seat includes 10k executions per month.

    @@ -558,8 +545,8 @@
  • Every seat includes 10 000 executions - One execution equals one job up to 1 second on a virtual CPU with 2 GB of memory, with - each additional second counting as an extra execution. + One execution equals one job up to 1 second on a virtual CPU with 2 GB of memory, + with each additional second counting as an extra execution.
  • {:else} @@ -586,9 +573,7 @@ {/if} {:else} -
    - Workspace is on the team plan -
    +
    Workspace is on the team plan
    {/if} {:else if planTitle == 'Enterprise'} {#if plan != 'enterprise'} @@ -601,9 +586,7 @@ See more {:else} -
    - Workspace is on enterprise plan -
    +
    Workspace is on enterprise plan
    {/if} {:else if planTitle === 'Free'} {#if plan} @@ -611,9 +594,7 @@ Cancel your plan in the Customer Portal to downgrade to the free plan {:else} -
    - Workspace is on the free plan -
    +
    Workspace is on the free plan
    {/if} {/if} diff --git a/frontend/src/lib/components/sidebar/SidebarUsage.svelte b/frontend/src/lib/components/sidebar/SidebarUsage.svelte index e62cd02565..1625ed83e2 100644 --- a/frontend/src/lib/components/sidebar/SidebarUsage.svelte +++ b/frontend/src/lib/components/sidebar/SidebarUsage.svelte @@ -2,21 +2,18 @@ import { resource } from 'runed' import { goto } from '$lib/navigation' import { isCloudHosted } from '$lib/cloud' - import { UserService } from '$lib/gen' + import { WorkspaceService } from '$lib/gen' import { isPremiumStore, usageStore, userStore, - userWorkspaces, workspaceMembershipVersion, workspaceStore, - workspaceUsageStore, - type UserWorkspace + workspaceUsageStore } from '$lib/stores' import { refreshExecutions } from '$lib/usage.svelte' import { logFeatureUsage } from '$lib/utils/featureUsage' import { scopedValue, tagged } from '$lib/utils/scopedValue' - import { findWorkspaceAncestors } from '$lib/utils/workspaceHierarchy' import { Button } from '$lib/components/common' import Modal from '$lib/components/common/modal/Modal.svelte' import { Tooltip } from '$lib/components/meltComponents' @@ -30,51 +27,30 @@ let open = $state(false) - // A fork's usage and tier resolve to its billing root while its member list is a - // subset of the root's, so seats must come from the root or the cap is fork-sized - // against root usage. `undefined` when the root isn't visible from here: the cap - // is then unknowable, and the caller hides the meter rather than guessing. - function billingRoot(workspace: string, all: UserWorkspace[]): string | undefined { - const self = all.find((w) => w.id === workspace) - if (!self) return undefined - if (!self.parent_workspace_id) return workspace - const top = findWorkspaceAncestors(workspace, all).at(-1) - return top && !top.parent_workspace_id ? top.id : undefined - } + // Seat count for a paid workspace, the basis of its included executions. The server + // resolves a fork to the workspace its plan is billed on and counts the seats there, + // because neither is answerable from here: a fork's member list is a subset of that + // root's, and a fork member need not be a member of the root at all. + const fetchSeats = tagged( + async (workspace: string) => (await WorkspaceService.getBillableSeats({ workspace })).seats + ) - // Seat count for a paid workspace, the basis of its included executions. Only the - // user list is needed: `premium_info` carries the same usage number as - // `workspaceUsageStore` but requires admin and only exists when Stripe is - // configured, so it would leave regular members with no block at all. - const fetchSeats = tagged(async (root: string) => { - // Throws for a fork member with no seat in the root, which is the same answer as - // an unresolvable root: leave the paid meter hidden. - const users = await UserService.listUsers({ workspace: root }) - // Same basis as the backend's `count_paid_seats`: disabled members and service - // accounts are not billed, so counting them inflates the cap and hides a real - // overage. 1 developer = 1 seat, 2 operators = 1 seat. - const billable = users.filter((u) => !u.disabled && !u.is_service_account) - const developers = billable.filter((u) => !u.operator).length - const operators = billable.length - developers - return Math.ceil(developers + operators / 2) - }) - - const billingRootId = $derived.by(() => { - const workspace = $workspaceStore - if (!isCloudHosted() || !$isPremiumStore || !workspace) return undefined - return billingRoot(workspace, $userWorkspaces ?? []) - }) + const meteredWorkspace = $derived( + isCloudHosted() && $isPremiumStore ? $workspaceStore : undefined + ) // The membership version is in the key so a change re-resolves the cap, but not in // the tag: tagging by it would blank the bar on every change. const seatsResource = resource( () => - billingRootId ? { root: billingRootId, version: $workspaceMembershipVersion } : undefined, - async (key) => (key ? await fetchSeats(key.root) : undefined) + meteredWorkspace + ? { workspace: meteredWorkspace, version: $workspaceMembershipVersion } + : undefined, + async (key) => (key ? await fetchSeats(key.workspace) : undefined) ) const scopedSeats = scopedValue() - const seats = $derived(scopedSeats(billingRootId, seatsResource.current)) + const seats = $derived(scopedSeats(meteredWorkspace, seatsResource.current)) type QuotaKey = 'user' | 'workspace' diff --git a/frontend/src/routes/(root)/(logged)/workspace_settings/+page.svelte b/frontend/src/routes/(root)/(logged)/workspace_settings/+page.svelte index 782da1bb2c..8dd2937273 100644 --- a/frontend/src/routes/(root)/(logged)/workspace_settings/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/workspace_settings/+page.svelte @@ -41,7 +41,7 @@ import { sendUserToast } from '$lib/toast' import { clone, emptyString, encodeState, hasUnsavedChanges } from '$lib/utils' import { downloadViaClient, shouldDownloadViaClient } from '$lib/utils/downloadFile' - import { Slack, Target } from 'lucide-svelte' + import { ExternalLink, Slack, Target } from 'lucide-svelte' import SidebarNavigation from '$lib/components/common/sidebar/SidebarNavigation.svelte' import PremiumInfo from '$lib/components/settings/PremiumInfo.svelte' @@ -1472,9 +1472,30 @@ This workspace is a fork of {currentWorkspace.parent_workspace_id}. It runs on the parent's plan and its executions count toward the parent's usage and bill, - so there is no separate subscription here. Manage billing, seats, and quotas from - the parent workspace's settings. + so it is never invoiced separately. Manage billing, seats, and quotas from the + parent workspace's settings. + {#if plan} +
    + + It is on a paid plan that is billed on its own, so this workspace is paid for + twice. Cancel that subscription in the customer portal to keep only + {currentWorkspace.parent_workspace_id}'s plan. This workspace keeps + running either way, on the parent's plan. + {#if customer_id} +
    + +
    + {/if} +
    +
    + {/if} {:else} {/if} From 7a0c81d7222f3e7bb971c3cb9baeb82f153ba749 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Fri, 28 Aug 2026 17:26:18 +0200 Subject: [PATCH 34/34] chore(main): release 1.799.0 (#10874) * chore(main): release 1.799.0 * Apply automatic changes --------- Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com> --- .release-please-manifest.json | 2 +- CHANGELOG.md | 18 + backend/Cargo.lock | 377 +++++++++++------- backend/Cargo.toml | 4 +- .../parsers/windmill-parser-wasm/Cargo.lock | 48 +-- .../parsers/windmill-parser-wasm/Cargo.toml | 2 +- backend/windmill-api/openapi.yaml | 2 +- benchmarks/lib.ts | 2 +- cli/src/core/constants.ts | 2 +- frontend/package-lock.json | 48 ++- frontend/package.json | 2 +- lsp/Pipfile | 2 +- openflow.openapi.yaml | 2 +- .../WindmillClient/WindmillClient.psd1 | 2 +- python-client/wmill/pyproject.toml | 2 +- typescript-client/jsr.json | 2 +- typescript-client/package.json | 2 +- version.txt | 2 +- windmill-yaml-validator/package-lock.json | 4 +- windmill-yaml-validator/package.json | 2 +- 20 files changed, 333 insertions(+), 194 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 2c5c3efad9..c46e4845e3 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.798.1" + ".": "1.799.0" } diff --git a/CHANGELOG.md b/CHANGELOG.md index c4b042bab5..aa9bd16eac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## [1.799.0](https://github.com/windmill-labs/windmill/compare/v1.798.1...v1.799.0) (2026-08-28) + + +### Features + +* enable Anthropic prompt caching on Vertex AI agent steps ([#10876](https://github.com/windmill-labs/windmill/issues/10876)) ([320f400](https://github.com/windmill-labs/windmill/commit/320f4005124202852e6e9c70b394e7f87231d278)) +* instrument AI fill/fix, evals, agents and the debugger ([#10853](https://github.com/windmill-labs/windmill/issues/10853)) ([0bbd559](https://github.com/windmill-labs/windmill/commit/0bbd559ac8a35dba04ba5e8d6f2fd8d1d1124891)) + + +### Bug Fixes + +* **datatables:** stop a fork's pg_dump restore from failing silently ([#10830](https://github.com/windmill-labs/windmill/issues/10830)) ([3ce9bbc](https://github.com/windmill-labs/windmill/commit/3ce9bbc7168b837cb2111aabd533bb67803502b8)) +* key build artifact caches on a runnable's inline modules ([#10819](https://github.com/windmill-labs/windmill/issues/10819)) ([b72ccc3](https://github.com/windmill-labs/windmill/commit/b72ccc35934165b4bad112b947ca5af064aab26f)) +* nested template literals in step inputs, and unresolvable $args tags ([#10856](https://github.com/windmill-labs/windmill/issues/10856)) ([8f349c0](https://github.com/windmill-labs/windmill/commit/8f349c032a0d75fc3350292075e5050a030f6166)) +* pre-fill the test panel JSON args editor and align its placeholder ([#10871](https://github.com/windmill-labs/windmill/issues/10871)) ([fb82f36](https://github.com/windmill-labs/windmill/commit/fb82f36e6d6492dd0740984d8d78ea4eaa30361e)) +* reject a prefixed error_handler_path on triggers ([#10847](https://github.com/windmill-labs/windmill/issues/10847)) ([d334831](https://github.com/windmill-labs/windmill/commit/d33483173526a3b352d2829ac8a2e1e229cc1127)) +* unify billable seat counting and prevent fork subscriptions ([#10818](https://github.com/windmill-labs/windmill/issues/10818)) ([7dd88c4](https://github.com/windmill-labs/windmill/commit/7dd88c470caee5f095dc240667aa7550c55696bc)) + ## [1.798.1](https://github.com/windmill-labs/windmill/compare/v1.798.0...v1.798.1) (2026-08-27) diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 0701814b4b..b7f278d437 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -23,7 +23,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ - "crypto-common", + "crypto-common 0.1.7", "generic-array", ] @@ -261,13 +261,13 @@ dependencies = [ [[package]] name = "argon2" -version = "0.5.3" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +checksum = "134c52ddac6d63c576bef8168db10c83c49c26444ecbc68060fef078925a901c" dependencies = [ "base64ct", - "blake2", - "cpufeatures 0.2.17", + "blake2 0.11.0", + "cpufeatures 0.3.1", "password-hash", ] @@ -1338,7 +1338,7 @@ dependencies = [ "http 1.5.0", "http-body 0.4.6", "hyper 0.14.32", - "hyper 1.11.0", + "hyper 1.11.1", "hyper-rustls 0.24.2", "hyper-rustls 0.27.9", "hyper-util", @@ -1532,7 +1532,7 @@ dependencies = [ "http 1.5.0", "http-body 1.1.0", "http-body-util", - "hyper 1.11.0", + "hyper 1.11.1", "hyper-util", "itoa", "matchit 0.8.4", @@ -1629,7 +1629,7 @@ dependencies = [ "addr2line", "cfg-if", "libc", - "miniz_oxide", + "miniz_oxide 0.8.9", "object 0.37.3", "rustc-demangle", "windows-link 0.2.1", @@ -1824,6 +1824,15 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "blake2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b5d4d889834ee8ecfc0f8426ad30faf7cdcb10f741a8e6d7224d95325479f6f" +dependencies = [ + "digest 0.11.3", +] + [[package]] name = "blake3" version = "1.8.7" @@ -1834,7 +1843,7 @@ dependencies = [ "cc", "cfg-if", "constant_time_eq 0.4.2", - "cpufeatures 0.3.0", + "cpufeatures 0.3.1", ] [[package]] @@ -1856,6 +1865,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + [[package]] name = "block-modes" version = "0.8.1" @@ -1908,7 +1926,7 @@ dependencies = [ "hex", "http 1.5.0", "http-body-util", - "hyper 1.11.0", + "hyper 1.11.1", "hyper-named-pipe", "hyper-util", "hyperlocal", @@ -2332,12 +2350,12 @@ checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" [[package]] name = "chacha20" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" dependencies = [ "cfg-if", - "cpufeatures 0.3.0", + "cpufeatures 0.3.1", "rand_core 0.10.1", ] @@ -2390,7 +2408,7 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "crypto-common", + "crypto-common 0.1.7", "inout", ] @@ -2454,6 +2472,12 @@ dependencies = [ "cc", ] +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + [[package]] name = "cms" version = "0.2.3" @@ -2676,9 +2700,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" dependencies = [ "libc", ] @@ -2818,6 +2842,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + [[package]] name = "csv" version = "1.4.0" @@ -2848,6 +2881,15 @@ dependencies = [ "cipher 0.4.4", ] +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + [[package]] name = "curl-sys" version = "0.4.90+curl-8.21.0" @@ -3413,7 +3455,7 @@ dependencies = [ "arrow", "arrow-buffer", "base64 0.22.1", - "blake2", + "blake2 0.10.6", "blake3", "chrono", "datafusion-common", @@ -3953,7 +3995,7 @@ dependencies = [ "hickory-resolver", "http 1.5.0", "http-body-util", - "hyper 1.11.0", + "hyper 1.11.1", "hyper-rustls 0.27.9", "hyper-util", "ipnet", @@ -4156,7 +4198,7 @@ dependencies = [ "deno_error 0.6.1", "deno_tls", "http-body-util", - "hyper 1.11.0", + "hyper 1.11.1", "hyper-rustls 0.27.9", "hyper-util", "log", @@ -4455,10 +4497,21 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", "const-oid", - "crypto-common", + "crypto-common 0.1.7", "subtle", ] +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "crypto-common 0.2.2", + "ctutils", +] + [[package]] name = "dirs" version = "4.0.0" @@ -5030,13 +5083,13 @@ dependencies = [ [[package]] name = "flate2" -version = "1.1.9" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" dependencies = [ "crc32fast", "libz-sys", - "miniz_oxide", + "miniz_oxide 0.9.1", "zlib-rs", ] @@ -6109,7 +6162,7 @@ dependencies = [ "futures", "http 1.5.0", "http-body-util", - "hyper 1.11.0", + "hyper 1.11.1", "hyper-rustls 0.26.0", "hyper-tls", "hyper-tungstenite", @@ -6133,6 +6186,15 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15cdd26707701c53297e2fa6afb323d55fbc1d0810c3aec078ae3ef0424c3c15" +[[package]] +name = "hybrid-array" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" version = "0.14.32" @@ -6159,9 +6221,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" +checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" dependencies = [ "atomic-waker", "bytes", @@ -6189,7 +6251,7 @@ dependencies = [ "futures-util", "headers", "http 1.5.0", - "hyper 1.11.0", + "hyper 1.11.1", "hyper-rustls 0.27.9", "hyper-util", "pin-project-lite", @@ -6205,7 +6267,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fab3637d6b04a8037af8a266fdf6cf92ea957e8c53981a2bf6136572531025bf" dependencies = [ "hex", - "hyper 1.11.0", + "hyper 1.11.1", "hyper-util", "pin-project-lite", "tokio", @@ -6236,7 +6298,7 @@ checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c" dependencies = [ "futures-util", "http 1.5.0", - "hyper 1.11.0", + "hyper 1.11.1", "hyper-util", "log", "rustls 0.22.4", @@ -6254,7 +6316,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ "http 1.5.0", - "hyper 1.11.0", + "hyper 1.11.1", "hyper-util", "log", "rustls 0.23.35", @@ -6271,7 +6333,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" dependencies = [ - "hyper 1.11.0", + "hyper 1.11.1", "hyper-util", "pin-project-lite", "tokio", @@ -6286,7 +6348,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", "http-body-util", - "hyper 1.11.0", + "hyper 1.11.1", "hyper-util", "native-tls", "tokio", @@ -6301,7 +6363,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a343d17fe7885302ed7252767dc7bb83609a874b6ff581142241ec4b73957ad" dependencies = [ "http-body-util", - "hyper 1.11.0", + "hyper 1.11.1", "hyper-util", "pin-project-lite", "tokio", @@ -6321,7 +6383,7 @@ dependencies = [ "futures-util", "http 1.5.0", "http-body 1.1.0", - "hyper 1.11.0", + "hyper 1.11.1", "ipnet", "libc", "percent-encoding", @@ -6342,7 +6404,7 @@ checksum = "986c5ce3b994526b3cd75578e62554abd09f0899d6206de48b3e96ab34ccc8c7" dependencies = [ "hex", "http-body-util", - "hyper 1.11.0", + "hyper 1.11.1", "hyper-util", "pin-project-lite", "tokio", @@ -6946,7 +7008,7 @@ dependencies = [ "http 1.5.0", "http-body 1.1.0", "http-body-util", - "hyper 1.11.0", + "hyper 1.11.1", "hyper-http-proxy", "hyper-rustls 0.27.9", "hyper-timeout", @@ -7194,9 +7256,9 @@ dependencies = [ [[package]] name = "libredox" -version = "0.1.20" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d0a00925a9f930d679b6789b721e3a7f9ed110f41b86d2497caa780c3a070a" +checksum = "d7955dfc218a8afb29dfeffd540e3a6e96baeb94fe7138228dd7cc6937fbbf96" dependencies = [ "bitflags 2.13.1", "libc", @@ -7328,9 +7390,9 @@ dependencies = [ [[package]] name = "lru" -version = "0.18.2" +version = "0.18.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d2f2f9b4ba7e6b24d95e7e899329d35be83bcded72c8540cdd5368932d1d90a" +checksum = "0d317b4b9eb398e6acce275758ec6125535505e7a146fb1a9b8bda2451b0ff4c" dependencies = [ "hashbrown 0.17.1", ] @@ -7701,6 +7763,15 @@ name = "miniz_oxide" version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" dependencies = [ "adler2", "simd-adler32", @@ -7814,7 +7885,7 @@ dependencies = [ "futures-sink", "futures-util", "keyed_priority_queue", - "lru 0.18.2", + "lru 0.18.3", "mysql_common", "native-tls", "pem 3.0.6", @@ -8319,7 +8390,7 @@ dependencies = [ "http-body-util", "httparse", "humantime", - "hyper 1.11.0", + "hyper 1.11.1", "itertools 0.14.0", "md-5 0.10.6", "parking_lot", @@ -8778,9 +8849,9 @@ dependencies = [ [[package]] name = "owo-colors" -version = "4.3.0" +version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" +checksum = "13c45bb4a6ae1280ec0803b1ef9d3455eb50f01efbbe1447ab020f1d54fba9d8" [[package]] name = "p12-keystore" @@ -8918,13 +8989,12 @@ dependencies = [ [[package]] name = "password-hash" -version = "0.5.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +checksum = "aab41826031698d6ffcd9cff78ef56ef998e39dc7e5067cdfebe373842d4723b" dependencies = [ - "base64ct", - "rand_core 0.6.4", - "subtle", + "getrandom 0.4.3", + "phc", ] [[package]] @@ -9068,6 +9138,17 @@ dependencies = [ "phf 0.11.3", ] +[[package]] +name = "phc" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44dc769b75f93afdddd8c7fa12d685292ddeff1e66f7f0f3a234cf1818afe892" +dependencies = [ + "base64ct", + "ctutils", + "getrandom 0.4.3", +] + [[package]] name = "phf" version = "0.11.3" @@ -10212,7 +10293,7 @@ dependencies = [ "http 1.5.0", "http-body 1.1.0", "http-body-util", - "hyper 1.11.0", + "hyper 1.11.1", "hyper-rustls 0.27.9", "hyper-tls", "hyper-util", @@ -10260,7 +10341,7 @@ dependencies = [ "http 1.5.0", "http-body 1.1.0", "http-body-util", - "hyper 1.11.0", + "hyper 1.11.1", "hyper-rustls 0.27.9", "hyper-util", "js-sys", @@ -10316,7 +10397,7 @@ dependencies = [ "futures", "getrandom 0.2.17", "http 1.5.0", - "hyper 1.11.0", + "hyper 1.11.1", "reqwest 0.13.4", "reqwest-middleware", "retry-policies", @@ -13524,7 +13605,7 @@ dependencies = [ "http 1.5.0", "http-body 1.1.0", "http-body-util", - "hyper 1.11.0", + "hyper 1.11.1", "hyper-timeout", "hyper-util", "percent-encoding", @@ -13556,7 +13637,7 @@ dependencies = [ "http 1.5.0", "http-body 1.1.0", "http-body-util", - "hyper 1.11.0", + "hyper 1.11.1", "hyper-timeout", "hyper-util", "percent-encoding", @@ -13909,9 +13990,9 @@ dependencies = [ [[package]] name = "twox-hash" -version = "2.1.3" +version = "2.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8464ec13c3691491391d9fce00f6416c9a48e46972f72d7865688be2080192c9" +checksum = "5283634e518fe9e82c7b20520bb4bc209009fd16c82077c802f8111ecbb0117a" [[package]] name = "typed-path" @@ -14148,7 +14229,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" dependencies = [ - "crypto-common", + "crypto-common 0.1.7", "subtle", ] @@ -14280,7 +14361,7 @@ dependencies = [ "fslock", "gzip-header", "home", - "miniz_oxide", + "miniz_oxide 0.8.9", "paste", "which 6.0.3", ] @@ -14671,7 +14752,7 @@ dependencies = [ [[package]] name = "windmill" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "async-nats", @@ -14756,7 +14837,7 @@ dependencies = [ [[package]] name = "windmill-ai" -version = "1.798.1" +version = "1.799.0" dependencies = [ "async-stream", "async-trait", @@ -14789,7 +14870,7 @@ dependencies = [ [[package]] name = "windmill-alerting" -version = "1.798.1" +version = "1.799.0" dependencies = [ "axum 0.8.9", "chrono", @@ -14802,7 +14883,7 @@ dependencies = [ [[package]] name = "windmill-api" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "argon2", @@ -14832,7 +14913,7 @@ dependencies = [ "hex", "hmac", "http 1.5.0", - "hyper 1.11.0", + "hyper 1.11.1", "indexmap 2.14.0", "itertools 0.14.0", "jsonwebtoken 8.3.0", @@ -14942,12 +15023,12 @@ dependencies = [ [[package]] name = "windmill-api-agent-workers" -version = "1.798.1" +version = "1.799.0" dependencies = [ "axum 0.8.9", "chrono", "http 1.5.0", - "hyper 1.11.0", + "hyper 1.11.1", "lazy_static", "quick_cache", "serde", @@ -14965,7 +15046,7 @@ dependencies = [ [[package]] name = "windmill-api-assets" -version = "1.798.1" +version = "1.799.0" dependencies = [ "axum 0.8.9", "chrono", @@ -14982,7 +15063,7 @@ dependencies = [ [[package]] name = "windmill-api-auth" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "axum 0.8.9", @@ -15008,7 +15089,7 @@ dependencies = [ [[package]] name = "windmill-api-client" -version = "1.798.1" +version = "1.799.0" dependencies = [ "reqwest 0.12.28", "serde", @@ -15018,7 +15099,7 @@ dependencies = [ [[package]] name = "windmill-api-configs" -version = "1.798.1" +version = "1.799.0" dependencies = [ "axum 0.8.9", "chrono", @@ -15035,7 +15116,7 @@ dependencies = [ [[package]] name = "windmill-api-debug" -version = "1.798.1" +version = "1.799.0" dependencies = [ "axum 0.8.9", "base64 0.22.1", @@ -15057,7 +15138,7 @@ dependencies = [ [[package]] name = "windmill-api-embeddings" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "axum 0.8.9", @@ -15080,7 +15161,7 @@ dependencies = [ [[package]] name = "windmill-api-flow-conversations" -version = "1.798.1" +version = "1.799.0" dependencies = [ "axum 0.8.9", "chrono", @@ -15096,11 +15177,11 @@ dependencies = [ [[package]] name = "windmill-api-flows" -version = "1.798.1" +version = "1.799.0" dependencies = [ "axum 0.8.9", "chrono", - "hyper 1.11.0", + "hyper 1.11.1", "serde", "serde_json", "sql-builder", @@ -15118,7 +15199,7 @@ dependencies = [ [[package]] name = "windmill-api-groups" -version = "1.798.1" +version = "1.799.0" dependencies = [ "axum 0.8.9", "chrono", @@ -15139,7 +15220,7 @@ dependencies = [ [[package]] name = "windmill-api-inputs" -version = "1.798.1" +version = "1.799.0" dependencies = [ "axum 0.8.9", "chrono", @@ -15153,7 +15234,7 @@ dependencies = [ [[package]] name = "windmill-api-integration-tests" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "async-nats", @@ -15188,14 +15269,14 @@ dependencies = [ [[package]] name = "windmill-api-jobs" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "axum 0.8.9", "base64 0.22.1", "chrono", "http 1.5.0", - "hyper 1.11.0", + "hyper 1.11.1", "lazy_static", "serde", "serde_json", @@ -15213,7 +15294,7 @@ dependencies = [ [[package]] name = "windmill-api-npm-proxy" -version = "1.798.1" +version = "1.799.0" dependencies = [ "axum 0.8.9", "chrono", @@ -15241,7 +15322,7 @@ dependencies = [ [[package]] name = "windmill-api-openapi" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "axum 0.8.9", @@ -15263,7 +15344,7 @@ dependencies = [ [[package]] name = "windmill-api-schedule" -version = "1.798.1" +version = "1.799.0" dependencies = [ "axum 0.8.9", "chrono", @@ -15283,13 +15364,13 @@ dependencies = [ [[package]] name = "windmill-api-scripts" -version = "1.798.1" +version = "1.799.0" dependencies = [ "axum 0.8.9", "chrono", "futures", "http 1.5.0", - "hyper 1.11.0", + "hyper 1.11.1", "itertools 0.14.0", "lazy_static", "prometheus", @@ -15321,7 +15402,7 @@ dependencies = [ [[package]] name = "windmill-api-settings" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "axum 0.8.9", @@ -15349,7 +15430,7 @@ dependencies = [ [[package]] name = "windmill-api-sse" -version = "1.798.1" +version = "1.799.0" dependencies = [ "lazy_static", "serde", @@ -15361,13 +15442,13 @@ dependencies = [ [[package]] name = "windmill-api-users" -version = "1.798.1" +version = "1.799.0" dependencies = [ "argon2", "axum 0.8.9", "chrono", "http 1.5.0", - "hyper 1.11.0", + "hyper 1.11.1", "lazy_static", "serde", "serde_json", @@ -15385,7 +15466,7 @@ dependencies = [ [[package]] name = "windmill-api-workers" -version = "1.798.1" +version = "1.799.0" dependencies = [ "axum 0.8.9", "chrono", @@ -15399,14 +15480,14 @@ dependencies = [ [[package]] name = "windmill-api-workspaces" -version = "1.798.1" +version = "1.799.0" dependencies = [ "axum 0.8.9", "chrono", "futures", "hex", "http 1.5.0", - "hyper 1.11.0", + "hyper 1.11.1", "lazy_static", "magic-crypt", "regex", @@ -15434,7 +15515,7 @@ dependencies = [ [[package]] name = "windmill-audit" -version = "1.798.1" +version = "1.799.0" dependencies = [ "chrono", "lazy_static", @@ -15448,7 +15529,7 @@ dependencies = [ [[package]] name = "windmill-autoscaling" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "axum 0.8.9", @@ -15467,7 +15548,7 @@ dependencies = [ [[package]] name = "windmill-common" -version = "1.798.1" +version = "1.799.0" dependencies = [ "aes-gcm", "aho-corasick", @@ -15504,7 +15585,7 @@ dependencies = [ "globset", "hex", "hmac", - "hyper 1.11.0", + "hyper 1.11.1", "indexmap 2.14.0", "itertools 0.14.0", "jsonwebtoken 8.3.0", @@ -15571,7 +15652,7 @@ dependencies = [ [[package]] name = "windmill-dep-map" -version = "1.798.1" +version = "1.799.0" dependencies = [ "chrono", "itertools 0.14.0", @@ -15590,7 +15671,7 @@ dependencies = [ [[package]] name = "windmill-git-sync" -version = "1.798.1" +version = "1.799.0" dependencies = [ "regex", "serde", @@ -15605,7 +15686,7 @@ dependencies = [ [[package]] name = "windmill-indexer" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "astral-tokio-tar", @@ -15629,7 +15710,7 @@ dependencies = [ [[package]] name = "windmill-jseval" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "futures", @@ -15646,7 +15727,7 @@ dependencies = [ [[package]] name = "windmill-macros" -version = "1.798.1" +version = "1.799.0" dependencies = [ "itertools 0.14.0", "lazy_static", @@ -15662,7 +15743,7 @@ dependencies = [ [[package]] name = "windmill-mcp" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "async-trait", @@ -15683,7 +15764,7 @@ dependencies = [ [[package]] name = "windmill-native-triggers" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "async-trait", @@ -15714,7 +15795,7 @@ dependencies = [ [[package]] name = "windmill-oauth" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "arc-swap", @@ -15739,7 +15820,7 @@ dependencies = [ [[package]] name = "windmill-object-store" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "async-stream", @@ -15773,7 +15854,7 @@ dependencies = [ [[package]] name = "windmill-operator" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "futures", @@ -15791,7 +15872,7 @@ dependencies = [ [[package]] name = "windmill-parser" -version = "1.798.1" +version = "1.799.0" dependencies = [ "convert_case 0.6.0", "serde", @@ -15800,7 +15881,7 @@ dependencies = [ [[package]] name = "windmill-parser-bash" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "lazy_static", @@ -15812,7 +15893,7 @@ dependencies = [ [[package]] name = "windmill-parser-csharp" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "serde_json", @@ -15824,7 +15905,7 @@ dependencies = [ [[package]] name = "windmill-parser-go" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "gosyn", @@ -15836,7 +15917,7 @@ dependencies = [ [[package]] name = "windmill-parser-graphql" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "lazy_static", @@ -15848,7 +15929,7 @@ dependencies = [ [[package]] name = "windmill-parser-java" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "serde_json", @@ -15860,7 +15941,7 @@ dependencies = [ [[package]] name = "windmill-parser-nu" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "nu-parser", @@ -15871,7 +15952,7 @@ dependencies = [ [[package]] name = "windmill-parser-php" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "itertools 0.14.0", @@ -15882,7 +15963,7 @@ dependencies = [ [[package]] name = "windmill-parser-py" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "itertools 0.14.0", @@ -15894,7 +15975,7 @@ dependencies = [ [[package]] name = "windmill-parser-py-asset" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "rustpython-ast", @@ -15905,7 +15986,7 @@ dependencies = [ [[package]] name = "windmill-parser-py-imports" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "async-recursion", @@ -15927,7 +16008,7 @@ dependencies = [ [[package]] name = "windmill-parser-r" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "serde_json", @@ -15939,7 +16020,7 @@ dependencies = [ [[package]] name = "windmill-parser-ruby" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "lazy_static", @@ -15953,7 +16034,7 @@ dependencies = [ [[package]] name = "windmill-parser-rust" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "convert_case 0.6.0", @@ -15970,7 +16051,7 @@ dependencies = [ [[package]] name = "windmill-parser-sql" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "lazy_static", @@ -15983,7 +16064,7 @@ dependencies = [ [[package]] name = "windmill-parser-sql-asset" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "serde", @@ -15995,7 +16076,7 @@ dependencies = [ [[package]] name = "windmill-parser-ts" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "lazy_static", @@ -16013,7 +16094,7 @@ dependencies = [ [[package]] name = "windmill-parser-ts-asset" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "serde-wasm-bindgen", @@ -16029,7 +16110,7 @@ dependencies = [ [[package]] name = "windmill-parser-wac" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "rustpython-ast", @@ -16045,7 +16126,7 @@ dependencies = [ [[package]] name = "windmill-parser-yaml" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "lazy_static", @@ -16059,7 +16140,7 @@ dependencies = [ [[package]] name = "windmill-queue" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "async-recursion", @@ -16098,7 +16179,7 @@ dependencies = [ [[package]] name = "windmill-runtime-nativets" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "const_format", @@ -16138,7 +16219,7 @@ dependencies = [ [[package]] name = "windmill-sql-datatype-parser-wasm" -version = "1.798.1" +version = "1.799.0" dependencies = [ "getrandom 0.3.4", "wasm-bindgen", @@ -16149,7 +16230,7 @@ dependencies = [ [[package]] name = "windmill-store" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "async-recursion", @@ -16160,7 +16241,7 @@ dependencies = [ "futures", "hex", "http 1.5.0", - "hyper 1.11.0", + "hyper 1.11.1", "lazy_static", "magic-crypt", "quick_cache", @@ -16184,7 +16265,7 @@ dependencies = [ [[package]] name = "windmill-test-utils" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "async-trait", @@ -16208,14 +16289,14 @@ dependencies = [ [[package]] name = "windmill-trigger" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "async-trait", "axum 0.8.9", "chrono", "http 1.5.0", - "hyper 1.11.0", + "hyper 1.11.1", "itertools 0.14.0", "lazy_static", "rand 0.9.0", @@ -16241,7 +16322,7 @@ dependencies = [ [[package]] name = "windmill-trigger-amqp" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "async-trait", @@ -16268,7 +16349,7 @@ dependencies = [ [[package]] name = "windmill-trigger-azure" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "async-trait", @@ -16301,7 +16382,7 @@ dependencies = [ [[package]] name = "windmill-trigger-email" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "async-trait", @@ -16321,7 +16402,7 @@ dependencies = [ [[package]] name = "windmill-trigger-gcp" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "async-trait", @@ -16355,7 +16436,7 @@ dependencies = [ [[package]] name = "windmill-trigger-http" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "async-trait", @@ -16367,7 +16448,7 @@ dependencies = [ "hex", "hmac", "http 1.5.0", - "hyper 1.11.0", + "hyper 1.11.1", "itertools 0.14.0", "lazy_static", "matchit 0.7.3", @@ -16391,7 +16472,7 @@ dependencies = [ [[package]] name = "windmill-trigger-kafka" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "async-trait", @@ -16414,7 +16495,7 @@ dependencies = [ [[package]] name = "windmill-trigger-mqtt" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "async-trait", @@ -16438,7 +16519,7 @@ dependencies = [ [[package]] name = "windmill-trigger-nats" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "async-nats", @@ -16462,7 +16543,7 @@ dependencies = [ [[package]] name = "windmill-trigger-postgres" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "async-trait", @@ -16497,7 +16578,7 @@ dependencies = [ [[package]] name = "windmill-trigger-sqs" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "async-trait", @@ -16525,7 +16606,7 @@ dependencies = [ [[package]] name = "windmill-trigger-websocket" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "async-trait", @@ -16550,7 +16631,7 @@ dependencies = [ [[package]] name = "windmill-types" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "bitflags 2.13.1", @@ -16569,7 +16650,7 @@ dependencies = [ [[package]] name = "windmill-worker" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "async-once-cell", @@ -16686,7 +16767,7 @@ dependencies = [ [[package]] name = "windmill-worker-volumes" -version = "1.798.1" +version = "1.799.0" dependencies = [ "bytes", "futures", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 75cc6bc007..a05bd109ae 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "windmill" -version = "1.798.1" +version = "1.799.0" authors.workspace = true edition.workspace = true @@ -88,7 +88,7 @@ members = [ exclude = ["./windmill-duckdb-ffi-internal", "./parsers/windmill-parser-wasm"] [workspace.package] -version = "1.798.1" +version = "1.799.0" authors = ["Ruben Fiszel "] edition = "2021" diff --git a/backend/parsers/windmill-parser-wasm/Cargo.lock b/backend/parsers/windmill-parser-wasm/Cargo.lock index b6d83a9efb..47d807dd0d 100644 --- a/backend/parsers/windmill-parser-wasm/Cargo.lock +++ b/backend/parsers/windmill-parser-wasm/Cargo.lock @@ -6191,7 +6191,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windmill-common" -version = "1.798.1" +version = "1.799.0" dependencies = [ "aho-corasick", "anyhow", @@ -6274,7 +6274,7 @@ dependencies = [ [[package]] name = "windmill-macros" -version = "1.798.1" +version = "1.799.0" dependencies = [ "proc-macro2", "quote", @@ -6286,7 +6286,7 @@ dependencies = [ [[package]] name = "windmill-parser" -version = "1.798.1" +version = "1.799.0" dependencies = [ "convert_case", "serde", @@ -6295,7 +6295,7 @@ dependencies = [ [[package]] name = "windmill-parser-bash" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "lazy_static", @@ -6307,7 +6307,7 @@ dependencies = [ [[package]] name = "windmill-parser-csharp" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "serde_json", @@ -6319,7 +6319,7 @@ dependencies = [ [[package]] name = "windmill-parser-go" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "gosyn", @@ -6331,7 +6331,7 @@ dependencies = [ [[package]] name = "windmill-parser-graphql" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "lazy_static", @@ -6343,7 +6343,7 @@ dependencies = [ [[package]] name = "windmill-parser-java" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "serde_json", @@ -6355,7 +6355,7 @@ dependencies = [ [[package]] name = "windmill-parser-nu" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "nu-parser", @@ -6366,7 +6366,7 @@ dependencies = [ [[package]] name = "windmill-parser-php" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "itertools 0.14.0", @@ -6377,7 +6377,7 @@ dependencies = [ [[package]] name = "windmill-parser-py" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "itertools 0.14.0", @@ -6389,7 +6389,7 @@ dependencies = [ [[package]] name = "windmill-parser-py-asset" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "rustpython-ast", @@ -6400,7 +6400,7 @@ dependencies = [ [[package]] name = "windmill-parser-py-imports" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "async-recursion", @@ -6422,7 +6422,7 @@ dependencies = [ [[package]] name = "windmill-parser-r" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "serde_json", @@ -6434,7 +6434,7 @@ dependencies = [ [[package]] name = "windmill-parser-ruby" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "lazy_static", @@ -6448,7 +6448,7 @@ dependencies = [ [[package]] name = "windmill-parser-rust" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "convert_case", @@ -6465,7 +6465,7 @@ dependencies = [ [[package]] name = "windmill-parser-sql" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "lazy_static", @@ -6478,7 +6478,7 @@ dependencies = [ [[package]] name = "windmill-parser-sql-asset" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "serde", @@ -6490,7 +6490,7 @@ dependencies = [ [[package]] name = "windmill-parser-ts" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "lazy_static", @@ -6508,7 +6508,7 @@ dependencies = [ [[package]] name = "windmill-parser-ts-asset" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "serde-wasm-bindgen", @@ -6524,7 +6524,7 @@ dependencies = [ [[package]] name = "windmill-parser-wac" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "rustpython-ast", @@ -6540,7 +6540,7 @@ dependencies = [ [[package]] name = "windmill-parser-wasm" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "getrandom 0.2.17", @@ -6572,7 +6572,7 @@ dependencies = [ [[package]] name = "windmill-parser-yaml" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "lazy_static", @@ -6586,7 +6586,7 @@ dependencies = [ [[package]] name = "windmill-types" -version = "1.798.1" +version = "1.799.0" dependencies = [ "anyhow", "bitflags", diff --git a/backend/parsers/windmill-parser-wasm/Cargo.toml b/backend/parsers/windmill-parser-wasm/Cargo.toml index 7296524140..c4a544e545 100644 --- a/backend/parsers/windmill-parser-wasm/Cargo.toml +++ b/backend/parsers/windmill-parser-wasm/Cargo.toml @@ -12,7 +12,7 @@ resolver = "2" members = ["."] [workspace.package] -version = "1.798.1" +version = "1.799.0" edition = "2021" authors = ["Ruben Fiszel "] diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 1a14a9fcf6..d2e036ac38 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -1,7 +1,7 @@ openapi: "3.0.3" info: - version: 1.798.1 + version: 1.799.0 title: Windmill API contact: diff --git a/benchmarks/lib.ts b/benchmarks/lib.ts index 96c29cb209..b4f7ce465c 100644 --- a/benchmarks/lib.ts +++ b/benchmarks/lib.ts @@ -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.798.1"; +export const VERSION = "v1.799.0"; export async function login(email: string, password: string): Promise { return await windmill.UserService.login({ diff --git a/cli/src/core/constants.ts b/cli/src/core/constants.ts index 3abd8b1e9a..c6aaf0b173 100644 --- a/cli/src/core/constants.ts +++ b/cli/src/core/constants.ts @@ -10,4 +10,4 @@ export const WM_FORK_PREFIX = "wm-fork"; // (e.g. utils.ts) can read it without importing main.ts and creating a circular // dependency (main → workspace → utils → main) that triggers a TDZ. // Re-exported from main.ts for backwards compatibility. -export const VERSION = "1.798.1"; +export const VERSION = "1.799.0"; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 0eed58d7d8..e6549e22ef 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "@windmill-labs/components", - "version": "1.798.1", + "version": "1.799.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@windmill-labs/components", - "version": "1.798.1", + "version": "1.799.0", "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { @@ -1755,6 +1755,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1771,6 +1772,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1787,6 +1789,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1803,6 +1806,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1819,6 +1823,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1835,6 +1840,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1851,6 +1857,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1867,6 +1874,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1883,6 +1891,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1899,6 +1908,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1915,6 +1925,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1931,6 +1942,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1947,6 +1959,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1963,6 +1976,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7569,7 +7583,7 @@ "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", - "devOptional": true, + "dev": true, "license": "MIT", "bin": { "jiti": "bin/jiti.js" @@ -8265,6 +8279,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -8285,6 +8300,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -8305,6 +8321,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -8325,6 +8342,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -8345,6 +8363,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -8365,6 +8384,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -8385,6 +8405,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -8405,6 +8426,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -8425,6 +8447,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -8445,6 +8468,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -8465,6 +8489,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -13170,6 +13195,21 @@ } } }, + "node_modules/svelte-check/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/svelte-eslint-parser": { "version": "0.43.0", "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.43.0.tgz", @@ -13949,7 +13989,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", diff --git a/frontend/package.json b/frontend/package.json index 895f218ba7..2fe0532761 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "@windmill-labs/components", - "version": "1.798.1", + "version": "1.799.0", "scripts": { "dev": "vite dev", "dev:ui-builder": "mv static/ui_builder static/ui_builder.dev-disabled 2>/dev/null || true ; trap 'mv static/ui_builder.dev-disabled static/ui_builder 2>/dev/null || true' EXIT ; vite dev", diff --git a/lsp/Pipfile b/lsp/Pipfile index d22dabf5c7..22eb7440d2 100644 --- a/lsp/Pipfile +++ b/lsp/Pipfile @@ -4,7 +4,7 @@ verify_ssl = true name = "pypi" [packages] -wmill = ">=1.798.1" +wmill = ">=1.799.0" sendgrid = "*" mysql-connector-python = "*" pymongo = "*" diff --git a/openflow.openapi.yaml b/openflow.openapi.yaml index 338f03070f..38f43d1878 100644 --- a/openflow.openapi.yaml +++ b/openflow.openapi.yaml @@ -1,7 +1,7 @@ openapi: '3.0.3' info: - version: 1.798.1 + version: 1.799.0 title: OpenFlow Spec contact: name: Ruben Fiszel diff --git a/powershell-client/WindmillClient/WindmillClient.psd1 b/powershell-client/WindmillClient/WindmillClient.psd1 index ea290b77d6..3cc5775a77 100644 --- a/powershell-client/WindmillClient/WindmillClient.psd1 +++ b/powershell-client/WindmillClient/WindmillClient.psd1 @@ -12,7 +12,7 @@ RootModule = 'WindmillClient.psm1' # Version number of this module. - ModuleVersion = '1.798.1' + ModuleVersion = '1.799.0' # Supported PSEditions # CompatiblePSEditions = @() diff --git a/python-client/wmill/pyproject.toml b/python-client/wmill/pyproject.toml index fb91df65df..2df87500db 100644 --- a/python-client/wmill/pyproject.toml +++ b/python-client/wmill/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "wmill" -version = "1.798.1" +version = "1.799.0" description = "A client library for accessing Windmill server wrapping the Windmill client API" license = "Apache-2.0" homepage = "https://windmill.dev" diff --git a/typescript-client/jsr.json b/typescript-client/jsr.json index e804a7ae57..1f0f5e2030 100644 --- a/typescript-client/jsr.json +++ b/typescript-client/jsr.json @@ -1,6 +1,6 @@ { "name": "@windmill/windmill", - "version": "1.798.1", + "version": "1.799.0", "exports": "./src/index.ts", "publish": { "exclude": ["!src", "./s3Types.ts", "./sqlUtils.ts", "./client.ts", "./wacError.ts"] diff --git a/typescript-client/package.json b/typescript-client/package.json index d39061ec8d..a73353f780 100644 --- a/typescript-client/package.json +++ b/typescript-client/package.json @@ -1,7 +1,7 @@ { "name": "windmill-client", "description": "Windmill SDK client for browsers and Node.js", - "version": "1.798.1", + "version": "1.799.0", "author": "Ruben Fiszel", "license": "Apache 2.0", "homepage": "https://github.com/windmill-labs/windmill/tree/main/typescript-client#readme", diff --git a/version.txt b/version.txt index 6cbed1aa9f..6e752179bd 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.798.1 +1.799.0 diff --git a/windmill-yaml-validator/package-lock.json b/windmill-yaml-validator/package-lock.json index 64c7e322fb..678259b8ff 100644 --- a/windmill-yaml-validator/package-lock.json +++ b/windmill-yaml-validator/package-lock.json @@ -1,12 +1,12 @@ { "name": "windmill-yaml-validator", - "version": "1.798.1", + "version": "1.799.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "windmill-yaml-validator", - "version": "1.798.1", + "version": "1.799.0", "license": "Apache 2.0", "dependencies": { "@stoplight/yaml": "^4.3.0", diff --git a/windmill-yaml-validator/package.json b/windmill-yaml-validator/package.json index 5edfb07b4e..10a6f9918d 100644 --- a/windmill-yaml-validator/package.json +++ b/windmill-yaml-validator/package.json @@ -1,6 +1,6 @@ { "name": "windmill-yaml-validator", - "version": "1.798.1", + "version": "1.799.0", "description": "YAML validator for Windmill flow, schedule, and trigger files", "main": "dist/index.js", "types": "dist/index.d.ts",