diff --git a/CHANGELOG.md b/CHANGELOG.md index 365084fbfb..bb9eda6725 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,42 @@ # Changelog +## [1.751.0](https://github.com/windmill-labs/windmill/compare/v1.750.0...v1.751.0) (2026-07-06) + + +### Features + +* add cosmetic dev/staging label for dev workspaces ([#9959](https://github.com/windmill-labs/windmill/issues/9959)) ([fd8e64d](https://github.com/windmill-labs/windmill/commit/fd8e64d11fea3ffdb7858100c67cb2e9ca841ed6)) +* **auth:** add runtime NO_AUTH mode for authentication bypass ([#9962](https://github.com/windmill-labs/windmill/issues/9962)) ([91e1b08](https://github.com/windmill-labs/windmill/commit/91e1b087a206efb7189824b4184e1f3f4cda7211)) +* **frontend:** custom skills — detail modal, batch manage, shared validation ([#9847](https://github.com/windmill-labs/windmill/issues/9847)) ([2e14302](https://github.com/windmill-labs/windmill/commit/2e14302e4abbad595584806bff12548d520fcb58)) +* **pipelines:** auto-derive cascade edges from ducklake/s3 reads (+ muted-read badge) ([#9963](https://github.com/windmill-labs/windmill/issues/9963)) ([3dcd394](https://github.com/windmill-labs/windmill/commit/3dcd3949a14199b106506994ea31ca3de7e636b3)) + + +### Bug Fixes + +* **ai:** centralize Anthropic Messages API routing across completion paths ([#9960](https://github.com/windmill-labs/windmill/issues/9960)) ([cc2f638](https://github.com/windmill-labs/windmill/commit/cc2f638de6cebeffb9fee1d4835a0cfd565af86c)) +* **assets:** responsive layout for small screens ([#9961](https://github.com/windmill-labs/windmill/issues/9961)) ([45946d1](https://github.com/windmill-labs/windmill/commit/45946d1185c0bd07948d4d8454880c2801571f9d)) +* **cli:** quote non-identifier property names in resource-type namespace ([#9964](https://github.com/windmill-labs/windmill/issues/9964)) ([dc6b997](https://github.com/windmill-labs/windmill/commit/dc6b99775b550e7433fee8a159c30eaf296500c5)) +* critical alerts modal mute toggles no longer close popover or fail to save ([#9969](https://github.com/windmill-labs/windmill/issues/9969)) ([6587019](https://github.com/windmill-labs/windmill/commit/6587019d263374ee5707d258f5d8eec7e73c690d)) +* **frontend:** theme-aware code block background in prose markdown ([#9968](https://github.com/windmill-labs/windmill/issues/9968)) ([9821596](https://github.com/windmill-labs/windmill/commit/9821596251cff698958ffbfbd11fffa6a7988c6c)) + +## [1.750.0](https://github.com/windmill-labs/windmill/compare/v1.749.0...v1.750.0) (2026-07-06) + + +### Features + +* chat-scoped session changes bar + unified diff drawer ([#9762](https://github.com/windmill-labs/windmill/issues/9762)) ([a6c0b37](https://github.com/windmill-labs/windmill/commit/a6c0b3756be78ca3fadc7bad6bae98c0887fd538)) +* **pipelines:** require data uploads before running a pipeline ([#9953](https://github.com/windmill-labs/windmill/issues/9953)) ([a1c5b7a](https://github.com/windmill-labs/windmill/commit/a1c5b7aa3ed2841f09f4f148ded5c5b5ef10fd3d)) +* **pipelines:** wm_partition macro for grain-agnostic partition filters ([#9950](https://github.com/windmill-labs/windmill/issues/9950)) ([43044c2](https://github.com/windmill-labs/windmill/commit/43044c2e28139b1dbde6844c781a821f8de68f58)) + + +### Bug Fixes + +* **ai:** test key routes Azure Foundry Claude models via Anthropic Messages API ([#9956](https://github.com/windmill-labs/windmill/issues/9956)) ([ea19cc9](https://github.com/windmill-labs/windmill/commit/ea19cc9dc459bd259e27f7fcc29601a010c5f8f0)) +* **cli:** HD-1 test_edges + HD-2 scd2 _current write in --local pipeline graph ([#9947](https://github.com/windmill-labs/windmill/issues/9947)) ([ad6f23d](https://github.com/windmill-labs/windmill/commit/ad6f23d6bfcf1056bcb6d8c6b552114e88177328)) +* **pipelines:** make node & pipeline-level run affordances always visible ([#9948](https://github.com/windmill-labs/windmill/issues/9948)) ([6eabb96](https://github.com/windmill-labs/windmill/commit/6eabb96ae78fb966f9916f907bb693d569b04c0b)) +* read chat drafts via own-draft route so drawer-kind drafts deploy ([#9913](https://github.com/windmill-labs/windmill/issues/9913)) ([056ebdb](https://github.com/windmill-labs/windmill/commit/056ebdb03543a93094c80ca354c117236cd8d6c8)) +* resolve extensionless bun relative imports on windows loader ([#9949](https://github.com/windmill-labs/windmill/issues/9949)) ([bf96621](https://github.com/windmill-labs/windmill/commit/bf9662172ad7e0ff53d39adc338fd7886672c8f9)) + ## [1.749.0](https://github.com/windmill-labs/windmill/compare/v1.748.0...v1.749.0) (2026-07-05) diff --git a/ai_evals/adapters/frontend/mockBackend.ts b/ai_evals/adapters/frontend/mockBackend.ts index 379eb6d447..f9023449a2 100644 --- a/ai_evals/adapters/frontend/mockBackend.ts +++ b/ai_evals/adapters/frontend/mockBackend.ts @@ -11,6 +11,7 @@ import type { DataTableTables, DataTableTableSchema, GetDraftForUserResponse, + GetOwnDraftResponse, ListDraftsResponse, ScriptLang, UpdateDraftResponse, @@ -294,8 +295,8 @@ export function getBenchmarkJobLogs(workspace: string, jobId: string): string { /** * In-memory stand-in for the per-user draft backend (`DraftService`). The global * AI chat now persists and reads drafts through the backend DB instead of an - * in-tab `UserDraft` cell, so the eval mocks the three draft endpoints it - * exercises (`updateDraft` / `getDraftForUser` / `listDrafts`) and keeps the + * in-tab `UserDraft` cell, so the eval mocks the draft endpoints it exercises + * (`updateDraft` / `getOwnDraft` / `getDraftForUser` / `listDrafts`) and keeps the * saved values here, keyed by workspace + draft kind + storage path. Mirrors the * semantics of the production unit test's mock in * `frontend/src/lib/components/copilot/chat/global/core.test.ts`. @@ -379,6 +380,20 @@ export function getBenchmarkDraftForUser(input: { return { value: entry.value, created_at: BENCHMARK_DRAFT_TIMESTAMP } } +/** Mirror `DraftService.getOwnDraft`: `null` (200) when absent — unlike + * `getDraftForUser`, absence is not an error on this route. */ +export function getBenchmarkOwnDraft(input: { + workspace: string + kind: UserDraftItemKind + path: string +}): GetOwnDraftResponse { + const entry = benchmarkDrafts.get(benchmarkDraftKey(input.workspace, input.kind, input.path)) + if (!entry) { + return null + } + return { value: entry.value, created_at: BENCHMARK_DRAFT_TIMESTAMP } +} + /** Mirror `DraftService.listDrafts`: metadata rows (no value) for a workspace. */ export function listBenchmarkDrafts(workspace: string): ListDraftsResponse { return [...benchmarkDrafts.values()] diff --git a/ai_evals/adapters/frontend/mockBackendDrafts.test.ts b/ai_evals/adapters/frontend/mockBackendDrafts.test.ts index a720de5e43..0ab79d216e 100644 --- a/ai_evals/adapters/frontend/mockBackendDrafts.test.ts +++ b/ai_evals/adapters/frontend/mockBackendDrafts.test.ts @@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'bun:test' import { clearBenchmarkDrafts, getBenchmarkDraftForUser, + getBenchmarkOwnDraft, listBenchmarkDrafts, resetBenchmarkMockBackend, seedBenchmarkDraft, @@ -55,6 +56,27 @@ describe('mockBackend drafts', () => { expect(() => getBenchmarkDraftForUser({ workspace: WORKSPACE, kind: 'variable', path: 'f/evals/token' })).toThrow() }) + it('returns null from getOwnDraft when no draft exists', () => { + expect( + getBenchmarkOwnDraft({ workspace: WORKSPACE, kind: 'trigger_schedule', path: 'u/evals/missing' }) + ).toBeNull() + }) + + // The global chat hydrates drawer-kind drafts (schedule/trigger/resource/variable) + // through getOwnDraft — getDraftForUser rejects those kinds as private. + it('hydrates a saved drawer-kind draft through getOwnDraft', () => { + const value = { path: 'u/evals/nightly', schedule: '0 0 9 * * *' } + updateBenchmarkDraft({ + workspace: WORKSPACE, + kind: 'trigger_schedule', + path: 'u/evals/nightly', + requestBody: { value } + }) + expect( + getBenchmarkOwnDraft({ workspace: WORKSPACE, kind: 'trigger_schedule', path: 'u/evals/nightly' })?.value + ).toEqual(value) + }) + it('throws a 404-shaped error when no draft exists', () => { try { getBenchmarkDraftForUser({ workspace: WORKSPACE, kind: 'script', path: 'f/evals/missing' }) diff --git a/ai_evals/adapters/frontend/vitestAdapter.test.ts b/ai_evals/adapters/frontend/vitestAdapter.test.ts index dcaa1d2ca4..92e33414df 100644 --- a/ai_evals/adapters/frontend/vitestAdapter.test.ts +++ b/ai_evals/adapters/frontend/vitestAdapter.test.ts @@ -40,6 +40,7 @@ vi.mock('$lib/gen', async () => { getBenchmarkDraftForUser, getBenchmarkFlowByPath, getBenchmarkJobLogs, + getBenchmarkOwnDraft, getBenchmarkScriptByHash, getBenchmarkScriptByPath, hasBenchmarkWorkspace, @@ -86,6 +87,10 @@ vi.mock('$lib/gen', async () => { hasBenchmarkWorkspace(data.workspace) ? getBenchmarkDraftForUser(data) : actual.DraftService.getDraftForUser(data), + getOwnDraft: async (data: { workspace: string; kind: any; path: string }) => + hasBenchmarkWorkspace(data.workspace) + ? getBenchmarkOwnDraft(data) + : actual.DraftService.getOwnDraft(data), listDrafts: async (data: { workspace: string }) => hasBenchmarkWorkspace(data.workspace) ? listBenchmarkDrafts(data.workspace) diff --git a/backend/.sqlx/query-530a797e67ff352471f1b34f260dd530a653081619e6d3132bf07996520b1e25.json b/backend/.sqlx/query-530a797e67ff352471f1b34f260dd530a653081619e6d3132bf07996520b1e25.json new file mode 100644 index 0000000000..465ee41633 --- /dev/null +++ b/backend/.sqlx/query-530a797e67ff352471f1b34f260dd530a653081619e6d3132bf07996520b1e25.json @@ -0,0 +1,19 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO workspace\n (id, name, owner, parent_workspace_id, is_dev_workspace, dev_workspace_label)\n VALUES ($1, $2, $3, $4, $5, $6)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Varchar", + "Varchar", + "Varchar", + "Bool", + "Varchar" + ] + }, + "nullable": [] + }, + "hash": "530a797e67ff352471f1b34f260dd530a653081619e6d3132bf07996520b1e25" +} diff --git a/backend/.sqlx/query-63d6d968905cf82fb3bb0577d41a29a8f88010891fcf3eaf4761a57461f97703.json b/backend/.sqlx/query-63d6d968905cf82fb3bb0577d41a29a8f88010891fcf3eaf4761a57461f97703.json new file mode 100644 index 0000000000..3959f4e8cc --- /dev/null +++ b/backend/.sqlx/query-63d6d968905cf82fb3bb0577d41a29a8f88010891fcf3eaf4761a57461f97703.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE workspace SET dev_workspace_label = $1 WHERE id = $2 AND is_dev_workspace RETURNING id", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Varchar", + "Text" + ] + }, + "nullable": [ + false + ] + }, + "hash": "63d6d968905cf82fb3bb0577d41a29a8f88010891fcf3eaf4761a57461f97703" +} diff --git a/backend/.sqlx/query-868985685d95197efc534bb2f3e0c956bea94dffb46b3b838c096f04b66d6c52.json b/backend/.sqlx/query-868985685d95197efc534bb2f3e0c956bea94dffb46b3b838c096f04b66d6c52.json new file mode 100644 index 0000000000..34d99a7e61 --- /dev/null +++ b/backend/.sqlx/query-868985685d95197efc534bb2f3e0c956bea94dffb46b3b838c096f04b66d6c52.json @@ -0,0 +1,34 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, name, dev_workspace_label FROM workspace WHERE parent_workspace_id = $1 AND is_dev_workspace AND deleted = false", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "dev_workspace_label", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + false, + true + ] + }, + "hash": "868985685d95197efc534bb2f3e0c956bea94dffb46b3b838c096f04b66d6c52" +} diff --git a/backend/.sqlx/query-8ed229e88dc49b0ba7328d48f991493fcdbab4ae91cae271ea911f5b14ecf0d4.json b/backend/.sqlx/query-8ed229e88dc49b0ba7328d48f991493fcdbab4ae91cae271ea911f5b14ecf0d4.json new file mode 100644 index 0000000000..3f8deb8835 --- /dev/null +++ b/backend/.sqlx/query-8ed229e88dc49b0ba7328d48f991493fcdbab4ae91cae271ea911f5b14ecf0d4.json @@ -0,0 +1,18 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO workspace (id, name, owner, deleted, premium, parent_workspace_id, is_dev_workspace, dev_workspace_label)\n SELECT $1, $2, owner, false, premium,\n CASE WHEN $4 THEN parent_workspace_id ELSE NULL END, $5,\n CASE WHEN $5 THEN dev_workspace_label ELSE NULL END\n FROM workspace WHERE id = $3", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Varchar", + "Text", + "Bool", + "Bool" + ] + }, + "nullable": [] + }, + "hash": "8ed229e88dc49b0ba7328d48f991493fcdbab4ae91cae271ea911f5b14ecf0d4" +} diff --git a/backend/.sqlx/query-9f567f04f67ce3b197eaa641eaf2d0bbe1f5aff27187505778b6abe2c87a5d01.json b/backend/.sqlx/query-9f567f04f67ce3b197eaa641eaf2d0bbe1f5aff27187505778b6abe2c87a5d01.json new file mode 100644 index 0000000000..a29b5f0924 --- /dev/null +++ b/backend/.sqlx/query-9f567f04f67ce3b197eaa641eaf2d0bbe1f5aff27187505778b6abe2c87a5d01.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE workspace SET parent_workspace_id = $1, is_dev_workspace = true, dev_workspace_label = $3 WHERE id = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Text", + "Varchar" + ] + }, + "nullable": [] + }, + "hash": "9f567f04f67ce3b197eaa641eaf2d0bbe1f5aff27187505778b6abe2c87a5d01" +} diff --git a/backend/.sqlx/query-af19b9e3deb4f5c9e6ba77963a5da5c60aa6878b2c77f6028a68d64f797c3322.json b/backend/.sqlx/query-af19b9e3deb4f5c9e6ba77963a5da5c60aa6878b2c77f6028a68d64f797c3322.json new file mode 100644 index 0000000000..c21a21163e --- /dev/null +++ b/backend/.sqlx/query-af19b9e3deb4f5c9e6ba77963a5da5c60aa6878b2c77f6028a68d64f797c3322.json @@ -0,0 +1,70 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT workspace.id, workspace.name, usr.username, workspace_settings.color, workspace.parent_workspace_id,\n workspace.is_dev_workspace, workspace.dev_workspace_label,\n CASE WHEN usr.operator THEN workspace_settings.operator_settings ELSE NULL END as operator_settings,\n usr.disabled\n FROM workspace\n JOIN usr ON usr.workspace_id = workspace.id\n JOIN workspace_settings ON workspace_settings.workspace_id = workspace.id\n WHERE usr.email = $1 AND workspace.deleted = false", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "username", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "color", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "parent_workspace_id", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "is_dev_workspace", + "type_info": "Bool" + }, + { + "ordinal": 6, + "name": "dev_workspace_label", + "type_info": "Varchar" + }, + { + "ordinal": 7, + "name": "operator_settings", + "type_info": "Jsonb" + }, + { + "ordinal": 8, + "name": "disabled", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + false, + false, + true, + true, + false, + true, + null, + false + ] + }, + "hash": "af19b9e3deb4f5c9e6ba77963a5da5c60aa6878b2c77f6028a68d64f797c3322" +} diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 68c0e0be39..54e75bd33f 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -2491,18 +2491,18 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.15" +version = "0.5.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-deque" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -2510,27 +2510,27 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.18" +version = "0.9.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-queue" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +checksum = "803d13fb3b09d88be9f4dbc29062c66b19bf7170867ceb746d2a8689bf6c7a26" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.21" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" [[package]] name = "crossterm_winapi" @@ -6237,11 +6237,11 @@ dependencies = [ [[package]] name = "jobserver" -version = "0.1.34" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" dependencies = [ - "getrandom 0.3.4", + "getrandom 0.4.3", "libc", ] @@ -13746,7 +13746,7 @@ dependencies = [ [[package]] name = "windmill" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "async-nats", @@ -13829,7 +13829,7 @@ dependencies = [ [[package]] name = "windmill-ai" -version = "1.749.0" +version = "1.751.0" dependencies = [ "async-stream", "async-trait", @@ -13862,7 +13862,7 @@ dependencies = [ [[package]] name = "windmill-alerting" -version = "1.749.0" +version = "1.751.0" dependencies = [ "axum 0.8.9", "chrono", @@ -13875,7 +13875,7 @@ dependencies = [ [[package]] name = "windmill-api" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "argon2", @@ -14013,7 +14013,7 @@ dependencies = [ [[package]] name = "windmill-api-agent-workers" -version = "1.749.0" +version = "1.751.0" dependencies = [ "axum 0.8.9", "chrono", @@ -14036,7 +14036,7 @@ dependencies = [ [[package]] name = "windmill-api-assets" -version = "1.749.0" +version = "1.751.0" dependencies = [ "axum 0.8.9", "chrono", @@ -14051,7 +14051,7 @@ dependencies = [ [[package]] name = "windmill-api-auth" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "axum 0.8.9", @@ -14077,7 +14077,7 @@ dependencies = [ [[package]] name = "windmill-api-client" -version = "1.749.0" +version = "1.751.0" dependencies = [ "reqwest 0.12.28", "serde", @@ -14087,7 +14087,7 @@ dependencies = [ [[package]] name = "windmill-api-configs" -version = "1.749.0" +version = "1.751.0" dependencies = [ "axum 0.8.9", "chrono", @@ -14104,7 +14104,7 @@ dependencies = [ [[package]] name = "windmill-api-debug" -version = "1.749.0" +version = "1.751.0" dependencies = [ "axum 0.8.9", "base64 0.22.1", @@ -14126,7 +14126,7 @@ dependencies = [ [[package]] name = "windmill-api-embeddings" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "axum 0.8.9", @@ -14149,7 +14149,7 @@ dependencies = [ [[package]] name = "windmill-api-flow-conversations" -version = "1.749.0" +version = "1.751.0" dependencies = [ "axum 0.8.9", "chrono", @@ -14165,7 +14165,7 @@ dependencies = [ [[package]] name = "windmill-api-flows" -version = "1.749.0" +version = "1.751.0" dependencies = [ "axum 0.8.9", "chrono", @@ -14186,7 +14186,7 @@ dependencies = [ [[package]] name = "windmill-api-groups" -version = "1.749.0" +version = "1.751.0" dependencies = [ "axum 0.8.9", "chrono", @@ -14207,7 +14207,7 @@ dependencies = [ [[package]] name = "windmill-api-inputs" -version = "1.749.0" +version = "1.751.0" dependencies = [ "axum 0.8.9", "chrono", @@ -14221,7 +14221,7 @@ dependencies = [ [[package]] name = "windmill-api-integration-tests" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "async-nats", @@ -14256,7 +14256,7 @@ dependencies = [ [[package]] name = "windmill-api-jobs" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "axum 0.8.9", @@ -14281,7 +14281,7 @@ dependencies = [ [[package]] name = "windmill-api-npm-proxy" -version = "1.749.0" +version = "1.751.0" dependencies = [ "axum 0.8.9", "flate2", @@ -14299,7 +14299,7 @@ dependencies = [ [[package]] name = "windmill-api-openapi" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "axum 0.8.9", @@ -14321,7 +14321,7 @@ dependencies = [ [[package]] name = "windmill-api-schedule" -version = "1.749.0" +version = "1.751.0" dependencies = [ "axum 0.8.9", "chrono", @@ -14341,7 +14341,7 @@ dependencies = [ [[package]] name = "windmill-api-scripts" -version = "1.749.0" +version = "1.751.0" dependencies = [ "axum 0.8.9", "chrono", @@ -14378,7 +14378,7 @@ dependencies = [ [[package]] name = "windmill-api-settings" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "axum 0.8.9", @@ -14406,7 +14406,7 @@ dependencies = [ [[package]] name = "windmill-api-sse" -version = "1.749.0" +version = "1.751.0" dependencies = [ "lazy_static", "serde", @@ -14418,7 +14418,7 @@ dependencies = [ [[package]] name = "windmill-api-users" -version = "1.749.0" +version = "1.751.0" dependencies = [ "argon2", "axum 0.8.9", @@ -14443,7 +14443,7 @@ dependencies = [ [[package]] name = "windmill-api-workers" -version = "1.749.0" +version = "1.751.0" dependencies = [ "axum 0.8.9", "chrono", @@ -14457,7 +14457,7 @@ dependencies = [ [[package]] name = "windmill-api-workspaces" -version = "1.749.0" +version = "1.751.0" dependencies = [ "axum 0.8.9", "chrono", @@ -14491,7 +14491,7 @@ dependencies = [ [[package]] name = "windmill-audit" -version = "1.749.0" +version = "1.751.0" dependencies = [ "chrono", "lazy_static", @@ -14505,7 +14505,7 @@ dependencies = [ [[package]] name = "windmill-autoscaling" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "axum 0.8.9", @@ -14524,7 +14524,7 @@ dependencies = [ [[package]] name = "windmill-common" -version = "1.749.0" +version = "1.751.0" dependencies = [ "aes-gcm", "aho-corasick", @@ -14626,7 +14626,7 @@ dependencies = [ [[package]] name = "windmill-dep-map" -version = "1.749.0" +version = "1.751.0" dependencies = [ "chrono", "itertools 0.14.0", @@ -14645,7 +14645,7 @@ dependencies = [ [[package]] name = "windmill-git-sync" -version = "1.749.0" +version = "1.751.0" dependencies = [ "regex", "serde", @@ -14660,7 +14660,7 @@ dependencies = [ [[package]] name = "windmill-indexer" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "astral-tokio-tar", @@ -14684,7 +14684,7 @@ dependencies = [ [[package]] name = "windmill-jseval" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "futures", @@ -14701,7 +14701,7 @@ dependencies = [ [[package]] name = "windmill-macros" -version = "1.749.0" +version = "1.751.0" dependencies = [ "itertools 0.14.0", "lazy_static", @@ -14717,7 +14717,7 @@ dependencies = [ [[package]] name = "windmill-mcp" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "async-trait", @@ -14738,7 +14738,7 @@ dependencies = [ [[package]] name = "windmill-native-triggers" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "async-trait", @@ -14769,7 +14769,7 @@ dependencies = [ [[package]] name = "windmill-oauth" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "arc-swap", @@ -14794,7 +14794,7 @@ dependencies = [ [[package]] name = "windmill-object-store" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "async-stream", @@ -14828,7 +14828,7 @@ dependencies = [ [[package]] name = "windmill-operator" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "futures", @@ -14846,7 +14846,7 @@ dependencies = [ [[package]] name = "windmill-parser" -version = "1.749.0" +version = "1.751.0" dependencies = [ "convert_case 0.6.0", "serde", @@ -14855,7 +14855,7 @@ dependencies = [ [[package]] name = "windmill-parser-bash" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "lazy_static", @@ -14867,7 +14867,7 @@ dependencies = [ [[package]] name = "windmill-parser-csharp" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "serde_json", @@ -14879,7 +14879,7 @@ dependencies = [ [[package]] name = "windmill-parser-go" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "gosyn", @@ -14891,7 +14891,7 @@ dependencies = [ [[package]] name = "windmill-parser-graphql" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "lazy_static", @@ -14903,7 +14903,7 @@ dependencies = [ [[package]] name = "windmill-parser-java" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "serde_json", @@ -14915,7 +14915,7 @@ dependencies = [ [[package]] name = "windmill-parser-nu" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "nu-parser", @@ -14926,7 +14926,7 @@ dependencies = [ [[package]] name = "windmill-parser-php" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "itertools 0.14.0", @@ -14937,7 +14937,7 @@ dependencies = [ [[package]] name = "windmill-parser-py" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "itertools 0.14.0", @@ -14949,7 +14949,7 @@ dependencies = [ [[package]] name = "windmill-parser-py-asset" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "rustpython-ast", @@ -14960,7 +14960,7 @@ dependencies = [ [[package]] name = "windmill-parser-py-imports" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "async-recursion", @@ -14982,7 +14982,7 @@ dependencies = [ [[package]] name = "windmill-parser-r" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "serde_json", @@ -14994,7 +14994,7 @@ dependencies = [ [[package]] name = "windmill-parser-ruby" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "lazy_static", @@ -15008,7 +15008,7 @@ dependencies = [ [[package]] name = "windmill-parser-rust" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "convert_case 0.6.0", @@ -15025,7 +15025,7 @@ dependencies = [ [[package]] name = "windmill-parser-sql" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "lazy_static", @@ -15038,7 +15038,7 @@ dependencies = [ [[package]] name = "windmill-parser-sql-asset" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "serde", @@ -15050,7 +15050,7 @@ dependencies = [ [[package]] name = "windmill-parser-ts" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "lazy_static", @@ -15068,7 +15068,7 @@ dependencies = [ [[package]] name = "windmill-parser-ts-asset" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "serde-wasm-bindgen", @@ -15084,7 +15084,7 @@ dependencies = [ [[package]] name = "windmill-parser-wac" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "rustpython-ast", @@ -15100,7 +15100,7 @@ dependencies = [ [[package]] name = "windmill-parser-yaml" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "serde", @@ -15111,7 +15111,7 @@ dependencies = [ [[package]] name = "windmill-queue" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "async-recursion", @@ -15150,7 +15150,7 @@ dependencies = [ [[package]] name = "windmill-runtime-nativets" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "const_format", @@ -15189,7 +15189,7 @@ dependencies = [ [[package]] name = "windmill-sql-datatype-parser-wasm" -version = "1.749.0" +version = "1.751.0" dependencies = [ "getrandom 0.3.4", "wasm-bindgen", @@ -15200,7 +15200,7 @@ dependencies = [ [[package]] name = "windmill-store" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "async-recursion", @@ -15234,7 +15234,7 @@ dependencies = [ [[package]] name = "windmill-test-utils" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "async-trait", @@ -15258,7 +15258,7 @@ dependencies = [ [[package]] name = "windmill-trigger" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "async-trait", @@ -15291,7 +15291,7 @@ dependencies = [ [[package]] name = "windmill-trigger-azure" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "async-trait", @@ -15324,7 +15324,7 @@ dependencies = [ [[package]] name = "windmill-trigger-email" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "async-trait", @@ -15344,7 +15344,7 @@ dependencies = [ [[package]] name = "windmill-trigger-gcp" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "async-trait", @@ -15378,7 +15378,7 @@ dependencies = [ [[package]] name = "windmill-trigger-http" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "async-trait", @@ -15414,7 +15414,7 @@ dependencies = [ [[package]] name = "windmill-trigger-kafka" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "async-trait", @@ -15437,7 +15437,7 @@ dependencies = [ [[package]] name = "windmill-trigger-mqtt" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "async-trait", @@ -15461,7 +15461,7 @@ dependencies = [ [[package]] name = "windmill-trigger-nats" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "async-nats", @@ -15485,7 +15485,7 @@ dependencies = [ [[package]] name = "windmill-trigger-postgres" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "async-trait", @@ -15520,7 +15520,7 @@ dependencies = [ [[package]] name = "windmill-trigger-sqs" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "async-trait", @@ -15548,7 +15548,7 @@ dependencies = [ [[package]] name = "windmill-trigger-websocket" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "async-trait", @@ -15573,7 +15573,7 @@ dependencies = [ [[package]] name = "windmill-types" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "bitflags 2.13.0", @@ -15592,7 +15592,7 @@ dependencies = [ [[package]] name = "windmill-worker" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "async-once-cell", @@ -15702,7 +15702,7 @@ dependencies = [ [[package]] name = "windmill-worker-volumes" -version = "1.749.0" +version = "1.751.0" dependencies = [ "bytes", "futures", @@ -16428,18 +16428,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.52" +version = "0.8.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" +checksum = "75726053136156d419e285b9b7eddaaea9e3fea6ce32eed44a89901f0bd98de1" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.52" +version = "0.8.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" +checksum = "4714fd92cf900833d49538023a9b3915155210801d1c1169eba513b2addefd71" dependencies = [ "proc-macro2", "quote", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 532bfda9b2..43fb1cfc55 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "windmill" -version = "1.749.0" +version = "1.751.0" authors.workspace = true edition.workspace = true @@ -87,7 +87,7 @@ members = [ exclude = ["./windmill-duckdb-ffi-internal", "./parsers/windmill-parser-wasm"] [workspace.package] -version = "1.749.0" +version = "1.751.0" authors = ["Ruben Fiszel "] edition = "2021" diff --git a/backend/migrations/20260706094033_add_dev_workspace_label.down.sql b/backend/migrations/20260706094033_add_dev_workspace_label.down.sql new file mode 100644 index 0000000000..0b9b81636a --- /dev/null +++ b/backend/migrations/20260706094033_add_dev_workspace_label.down.sql @@ -0,0 +1 @@ +ALTER TABLE workspace DROP COLUMN dev_workspace_label; diff --git a/backend/migrations/20260706094033_add_dev_workspace_label.up.sql b/backend/migrations/20260706094033_add_dev_workspace_label.up.sql new file mode 100644 index 0000000000..951f1ec3c9 --- /dev/null +++ b/backend/migrations/20260706094033_add_dev_workspace_label.up.sql @@ -0,0 +1,4 @@ +-- Cosmetic display label for a dev workspace: NULL/'dev' render as "dev", 'staging' renders as "stg". +-- Only meaningful when is_dev_workspace = true; changes nothing about behavior (locking, promote and +-- compare all key off is_dev_workspace / parent_workspace_id). The value is validated in the handler. +ALTER TABLE workspace ADD COLUMN dev_workspace_label VARCHAR; diff --git a/backend/parsers/windmill-parser-wasm/Cargo.lock b/backend/parsers/windmill-parser-wasm/Cargo.lock index 6d36c2f8f2..9707e0b6d4 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.749.0" +version = "1.751.0" dependencies = [ "aho-corasick", "anyhow", @@ -6272,7 +6272,7 @@ dependencies = [ [[package]] name = "windmill-macros" -version = "1.749.0" +version = "1.751.0" dependencies = [ "proc-macro2", "quote", @@ -6284,7 +6284,7 @@ dependencies = [ [[package]] name = "windmill-parser" -version = "1.749.0" +version = "1.751.0" dependencies = [ "convert_case", "serde", @@ -6293,7 +6293,7 @@ dependencies = [ [[package]] name = "windmill-parser-bash" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "lazy_static", @@ -6305,7 +6305,7 @@ dependencies = [ [[package]] name = "windmill-parser-csharp" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "serde_json", @@ -6317,7 +6317,7 @@ dependencies = [ [[package]] name = "windmill-parser-go" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "gosyn", @@ -6329,7 +6329,7 @@ dependencies = [ [[package]] name = "windmill-parser-graphql" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "lazy_static", @@ -6341,7 +6341,7 @@ dependencies = [ [[package]] name = "windmill-parser-java" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "serde_json", @@ -6353,7 +6353,7 @@ dependencies = [ [[package]] name = "windmill-parser-nu" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "nu-parser", @@ -6364,7 +6364,7 @@ dependencies = [ [[package]] name = "windmill-parser-php" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "itertools 0.14.0", @@ -6375,7 +6375,7 @@ dependencies = [ [[package]] name = "windmill-parser-py" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "itertools 0.14.0", @@ -6387,7 +6387,7 @@ dependencies = [ [[package]] name = "windmill-parser-py-asset" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "rustpython-ast", @@ -6398,7 +6398,7 @@ dependencies = [ [[package]] name = "windmill-parser-py-imports" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "async-recursion", @@ -6420,7 +6420,7 @@ dependencies = [ [[package]] name = "windmill-parser-r" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "serde_json", @@ -6432,7 +6432,7 @@ dependencies = [ [[package]] name = "windmill-parser-ruby" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "lazy_static", @@ -6446,7 +6446,7 @@ dependencies = [ [[package]] name = "windmill-parser-rust" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "convert_case", @@ -6463,7 +6463,7 @@ dependencies = [ [[package]] name = "windmill-parser-sql" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "lazy_static", @@ -6476,7 +6476,7 @@ dependencies = [ [[package]] name = "windmill-parser-sql-asset" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "serde", @@ -6488,7 +6488,7 @@ dependencies = [ [[package]] name = "windmill-parser-ts" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "lazy_static", @@ -6506,7 +6506,7 @@ dependencies = [ [[package]] name = "windmill-parser-ts-asset" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "serde-wasm-bindgen", @@ -6522,7 +6522,7 @@ dependencies = [ [[package]] name = "windmill-parser-wac" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "rustpython-ast", @@ -6538,7 +6538,7 @@ dependencies = [ [[package]] name = "windmill-parser-wasm" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "getrandom 0.2.17", @@ -6570,7 +6570,7 @@ dependencies = [ [[package]] name = "windmill-parser-yaml" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "serde", @@ -6581,7 +6581,7 @@ dependencies = [ [[package]] name = "windmill-types" -version = "1.749.0" +version = "1.751.0" dependencies = [ "anyhow", "bitflags", diff --git a/backend/parsers/windmill-parser-wasm/Cargo.toml b/backend/parsers/windmill-parser-wasm/Cargo.toml index b8b9263a91..a0b5a09e27 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.749.0" +version = "1.751.0" edition = "2021" authors = ["Ruben Fiszel "] diff --git a/backend/parsers/windmill-parser/src/asset_parser.rs b/backend/parsers/windmill-parser/src/asset_parser.rs index 045cac87e6..bdce9ec764 100644 --- a/backend/parsers/windmill-parser/src/asset_parser.rs +++ b/backend/parsers/windmill-parser/src/asset_parser.rs @@ -528,6 +528,16 @@ pub struct PipelineAnnotations { pub column_lineage: Vec, pub macros: bool, pub use_libs: Vec, + // `// mute ` — suppress the auto-derived cascade edge for a read + // that would otherwise trigger this script (a lookup / slowly-changing + // dimension you read every run but don't want to re-run on). Only Asset + // specs are stored; native trigger kinds are never auto-derived, so + // muting them is meaningless. + pub mute: Vec, + // `// mute all` — opt out of auto-derivation entirely for this script. + // Falls back to explicit-`// on`-only semantics. Explicit `// on` edges + // are unaffected. + pub mute_all: bool, } impl ParseAssetsOutput { @@ -956,6 +966,23 @@ pub fn parse_pipeline_annotations(code: &str) -> PipelineAnnotations { continue; } + // `// mute all` opts out of auto-derived cascade edges entirely; + // `// mute ` suppresses the one edge. Only asset refs are + // muteable — native trigger kinds are never auto-derived. Checked + // before the generic `on`/asset shorthand (a complete word, so + // prose like `// muted for now` never matches). + if let Some(after_kw) = consume_keyword(rest, "mute") { + let arg = after_kw.trim(); + if arg == "all" { + out.mute_all = true; + } else if let Some(spec @ TriggerSpec::Asset { .. }) = parse_trigger_spec(arg) { + if !out.mute.contains(&spec) { + out.mute.push(spec); + } + } + continue; + } + // `data_test` is checked before `on`/asset shorthands and is a complete // word (so it never collides with the `// test:` CI annotation, which // has no whitespace after `test`). Accumulates — every well-formed line diff --git a/backend/parsers/windmill-parser/tests/fixtures/pipeline_annotations.json b/backend/parsers/windmill-parser/tests/fixtures/pipeline_annotations.json index cabfe20ddf..226f32914c 100644 --- a/backend/parsers/windmill-parser/tests/fixtures/pipeline_annotations.json +++ b/backend/parsers/windmill-parser/tests/fixtures/pipeline_annotations.json @@ -769,5 +769,109 @@ "tag": null, "retry": null } + }, + { + "name": "mute suppresses a single ducklake read edge", + "code": "// pipeline\n// mute ducklake://main.orders\nselect 1", + "expected": { + "in_pipeline": true, + "asset_triggers": [], + "native_triggers": [], + "partition": null, + "freshness": null, + "tag": null, + "retry": null, + "mute": [ + "ducklake:main.orders" + ] + } + }, + { + "name": "mute all opts out of all auto-derivation", + "code": "// pipeline\n// mute all\nselect 1", + "expected": { + "in_pipeline": true, + "asset_triggers": [], + "native_triggers": [], + "partition": null, + "freshness": null, + "tag": null, + "retry": null, + "mute_all": true + } + }, + { + "name": "mute accumulates in order and dedups", + "code": "// pipeline\n// mute ducklake://main.a\n// mute s3://raw/b\n// mute ducklake://main.a\nselect 1", + "expected": { + "in_pipeline": true, + "asset_triggers": [], + "native_triggers": [], + "partition": null, + "freshness": null, + "tag": null, + "retry": null, + "mute": [ + "ducklake:main.a", + "s3object:raw/b" + ] + } + }, + { + "name": "mute of a native trigger kind is dropped (only assets are muteable)", + "code": "// pipeline\n// mute kafka\nselect 1", + "expected": { + "in_pipeline": true, + "asset_triggers": [], + "native_triggers": [], + "partition": null, + "freshness": null, + "tag": null, + "retry": null + } + }, + { + "name": "mute prose without an asset ref never false-positives", + "code": "// pipeline\n// muted for now\nselect 1", + "expected": { + "in_pipeline": true, + "asset_triggers": [], + "native_triggers": [], + "partition": null, + "freshness": null, + "tag": null, + "retry": null + } + }, + { + "name": "mute all coexists with explicit on edges", + "code": "// pipeline\n// mute all\n// on ducklake://main.orders\nselect 1", + "expected": { + "in_pipeline": true, + "asset_triggers": [ + "ducklake:main.orders" + ], + "native_triggers": [], + "partition": null, + "freshness": null, + "tag": null, + "retry": null, + "mute_all": true + } + }, + { + "name": "on asset ref strips trailing key=value opts", + "code": "// pipeline\n// on ducklake://main.orders debounce=60s\nselect 1", + "expected": { + "in_pipeline": true, + "asset_triggers": [ + "ducklake:main.orders" + ], + "native_triggers": [], + "partition": null, + "freshness": null, + "tag": null, + "retry": null + } } ] diff --git a/backend/parsers/windmill-parser/tests/pipeline_annotations_parity.rs b/backend/parsers/windmill-parser/tests/pipeline_annotations_parity.rs index 7adb07b55c..fff818edb2 100644 --- a/backend/parsers/windmill-parser/tests/pipeline_annotations_parity.rs +++ b/backend/parsers/windmill-parser/tests/pipeline_annotations_parity.rs @@ -53,6 +53,13 @@ struct Expected { // `// use ` accumulation, declaration order, deduped. Absent === []. #[serde(default)] use_libs: Vec, + // `// mute ` accumulation as `kind:path`, declaration order, deduped. + // Absent === []. + #[serde(default)] + mute: Vec, + // `// mute all` marker. Absent === false. + #[serde(default)] + mute_all: bool, } #[derive(Deserialize)] @@ -251,5 +258,18 @@ fn pipeline_annotation_fixtures_match() { assert_eq!(got.macros, f.expected.macros, "{ctx}: macros"); assert_eq!(got.use_libs, f.expected.use_libs, "{ctx}: use_libs"); + + let mute: Vec = got + .mute + .iter() + .filter_map(|t| match t { + TriggerSpec::Asset { asset_kind, path, .. } => { + Some(format!("{}:{}", kind_str(*asset_kind), path)) + } + _ => None, + }) + .collect(); + assert_eq!(mute, f.expected.mute, "{ctx}: mute"); + assert_eq!(got.mute_all, f.expected.mute_all, "{ctx}: mute_all"); } } diff --git a/backend/src/main.rs b/backend/src/main.rs index 0186f271e6..4115e2a12b 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -578,6 +578,7 @@ fn print_help() { println!(" JSON_FMT = false Output logs in JSON instead of logfmt"); println!(" METRICS_ADDR = None (EE only) Prometheus metrics addr at /metrics; set \"true\" to use :8001"); println!(" SUPERADMIN_SECRET = None Virtual superadmin token (server)"); + println!(" NO_AUTH = false Bypass all auth; every request acts as the admin@windmill.dev superadmin (only behind a trusted gateway; ignored when CLOUD_HOSTED)"); println!(" LICENSE_KEY = None (EE only) Enterprise license key (workers require valid key)"); println!(" RUN_UPDATE_CA_CERTIFICATE_AT_START = false Run system CA update at startup"); println!(" RUN_UPDATE_CA_CERTIFICATE_PATH = /usr/sbin/update-ca-certificates Path to CA update tool"); @@ -641,6 +642,15 @@ async fn windmill_main() -> anyhow::Result<()> { println!("Running in MCP mode"); } + if *windmill_common::worker::NO_AUTH { + println!("############################################################"); + println!("# NO_AUTH mode is ENABLED: authentication is fully #"); + println!("# bypassed and every request is treated as the #"); + println!("# admin@windmill.dev superadmin. Only run this behind a #"); + println!("# trusted authenticating gateway on a private network. #"); + println!("############################################################"); + } + #[cfg(all(not(target_env = "msvc"), feature = "jemalloc"))] println!("jemalloc enabled"); diff --git a/backend/windmill-api-auth/src/auth.rs b/backend/windmill-api-auth/src/auth.rs index dea200e417..59d9cd80b3 100644 --- a/backend/windmill-api-auth/src/auth.rs +++ b/backend/windmill-api-auth/src/auth.rs @@ -131,6 +131,13 @@ impl AuthCache { w_id: Option, token: &str, ) -> Option { + // In no-auth mode there are no real tokens: resolve directly as the + // admin superadmin so direct cache callers (e.g. get_all_runnables, + // which re-validates the request token per workspace) don't reject the + // fabricated token. + if is_no_auth() { + return Some(OptJobAuthed { authed: no_auth_admin_authed(), job_id: None }); + } let key = ( w_id.as_ref().unwrap_or(&"".to_string()).to_string(), token.to_string(), @@ -598,6 +605,12 @@ where let tokened = Self { token }; parts.extensions.insert(tokened.clone()); Ok(tokened) + } else if is_no_auth() { + // In `--no-auth` mode requests carry no token, but handlers that + // also require Tokened (e.g. global_whoami) must still resolve. + let tokened = Self { token: "no_auth".to_string() }; + parts.extensions.insert(tokened.clone()); + Ok(tokened) } else { BRUTE_FORCE_COUNTER.increment().await; Err((StatusCode::UNAUTHORIZED, "Unauthorized".to_owned())) @@ -677,6 +690,30 @@ fn maybe_get_workspace_id_from_path(path_vec: &[&str]) -> Option { workspace_id } +/// `--no-auth` mode: compiled-in `oss` builds, or the `NO_AUTH` runtime flag on +/// any build (the runtime flag is force-disabled on CLOUD_HOSTED). When on, +/// every request resolves as the admin superadmin so a fronting gateway can +/// handle authentication instead. +pub fn is_no_auth() -> bool { + cfg!(feature = "no_auth") || *windmill_common::worker::NO_AUTH +} + +/// The synthetic superadmin identity returned for every request in no-auth mode. +fn no_auth_admin_authed() -> ApiAuthed { + ApiAuthed { + email: "admin@windmill.dev".to_string(), + username: "admin".to_string(), + is_admin: true, + is_operator: false, + groups: Vec::new(), + folders: Vec::new(), + scopes: None, + username_override: None, + token_prefix: None, + read_only: false, + } +} + /// Resolves OptJobAuthed from request parts. /// Takes ownership of Parts and returns them back. #[allow(unreachable_code, unused_mut)] @@ -687,21 +724,11 @@ pub async fn resolve_opt_job_authed( return Ok((OptJobAuthed::default(), parts)); }; - #[cfg(feature = "no_auth")] - { - let authed = ApiAuthed { - email: "admin@windmill.dev".to_string(), - username: "admin".to_string(), - is_admin: true, - is_operator: false, - groups: Vec::new(), - folders: Vec::new(), - scopes: None, - username_override: None, - token_prefix: None, - read_only: false, - }; - return Ok((OptJobAuthed { authed, job_id: None }, parts)); + if is_no_auth() { + return Ok(( + OptJobAuthed { authed: no_auth_admin_authed(), job_id: None }, + parts, + )); } let already_authed = parts.extensions.get::().cloned(); diff --git a/backend/windmill-api-auth/src/lib.rs b/backend/windmill-api-auth/src/lib.rs index d2347aa22e..b1bf2d4273 100644 --- a/backend/windmill-api-auth/src/lib.rs +++ b/backend/windmill-api-auth/src/lib.rs @@ -31,8 +31,8 @@ use scopes::ScopeDefinition; // Re-export key auth types and functions pub use auth::{ - get_end_user_email, invalidate_token_from_cache, AuthCache, ExpiringAuthCache, OptTokened, - Tokened, TruncatedTokenWithEmail, AUTH_CACHE, + get_end_user_email, invalidate_token_from_cache, is_no_auth, AuthCache, ExpiringAuthCache, + OptTokened, Tokened, TruncatedTokenWithEmail, AUTH_CACHE, }; // ------------ ApiAuthed & OptJobAuthed types ------------ diff --git a/backend/windmill-api-scripts/src/scripts.rs b/backend/windmill-api-scripts/src/scripts.rs index d7a461dafa..bfc7dd1ac7 100644 --- a/backend/windmill-api-scripts/src/scripts.rs +++ b/backend/windmill-api-scripts/src/scripts.rs @@ -45,8 +45,9 @@ use windmill_dep_map::scoped_dependency_map::ScopedDependencyMap; use windmill_common::{ assets::{ clear_script_triggers, clear_static_asset_usage, clear_static_asset_usage_by_script_hash, - insert_script_trigger, parse_duration_secs, parse_pipeline_annotations, - replace_static_asset_usage, trigger_spec_to_row, AssetUsageKind, TriggerSpec, + derive_pipeline_asset_trigger_refs, insert_script_trigger, parse_duration_secs, + parse_pipeline_annotations, replace_static_asset_usage, trigger_spec_to_row, + AssetUsageKind, ScriptTriggerKind, TriggerSpec, }, error::{self, to_anyhow}, min_version::{MIN_VERSION_SUPPORTS_DEBOUNCING, MIN_VERSION_SUPPORTS_DEBOUNCING_V2}, @@ -1457,11 +1458,22 @@ async fn create_script_internal<'c>( // fire and the view would be an orphan node in the lineage graph. for (target_kind, path) in m.write_targets() { let kind = windmill_common::assets::asset_kind_from_parser(target_kind); - if !a.iter().any(|x| x.kind == kind && x.path == path) { + use windmill_common::assets::AssetUsageAccessType; + if let Some(existing) = a.iter_mut().find(|x| x.kind == kind && x.path == path) { + // The body reads its own managed target (an incremental/merge + // model `SELECT`ing from the table it materializes). The runtime + // still generates the write, so the effective access is RW — mark + // it so, otherwise it stays a plain read and (a) auto-derives a + // self-cascade edge back to this producer and (b) shows as a muted + // read on the canvas. Both are wrong: it's the script's own output. + if existing.access_type != Some(AssetUsageAccessType::W) { + existing.access_type = Some(AssetUsageAccessType::RW); + } + } else { a.push(windmill_common::assets::AssetWithAltAccessType { path, kind, - access_type: Some(windmill_common::assets::AssetUsageAccessType::W), + access_type: Some(AssetUsageAccessType::W), alt_access_type: None, columns: None, }); @@ -2035,6 +2047,63 @@ async fn create_script_internal<'c>( .await?; } + // Auto-derived cascade edges: within a `// pipeline`, a read of a ducklake + // table or s3 object wires the cascade edge straight from the FROM clause, + // so `// on ` is only needed for edges inference can't see (dynamic + // SQL) or to carry per-edge opts. `// mute ` / `// mute all` opt out. + // Explicit `// on` asset edges (inserted just above) win the dedup — they + // carry the per-edge debounce, so a derived row must not shadow them. + if in_pipeline && !pipeline_annotations.mute_all { + let asset_ref = |spec: &TriggerSpec| { + trigger_spec_to_row(spec) + .filter(|(k, _)| *k == ScriptTriggerKind::Asset) + .map(|(_, r)| r) + }; + let explicit_refs: std::collections::HashSet = + pipeline_triggers.iter().filter_map(asset_ref).collect(); + let muted_refs: std::collections::HashSet = pipeline_annotations + .mute + .iter() + .filter_map(asset_ref) + .collect(); + let derived = derive_pipeline_asset_trigger_refs( + effective_assets.as_deref().unwrap_or(&[]), + &explicit_refs, + &muted_refs, + pipeline_annotations.mute_all, + ); + // Derived edges have no per-`// on` opts, so they take the script-level + // `// debounce` default and `// retry` policy — same as writing a bare + // `// on ` would. + let derived_debounce_s = pipeline_debounce_default + .as_deref() + .and_then(parse_duration_secs); + let derived_retry_count = pipeline_annotations + .retry + .as_ref() + .map(|r| r.count.min(i16::MAX as u32) as i16); + let derived_retry_delay_s = pipeline_annotations + .retry + .as_ref() + .and_then(|r| r.delay.as_deref()) + .and_then(parse_duration_secs); + for trigger_ref in derived { + insert_script_trigger( + &mut *tx, + &w_id, + AssetUsageKind::Script, + &ns.path, + ScriptTriggerKind::Asset, + &trigger_ref, + pipeline_join_all, + derived_debounce_s, + derived_retry_count, + derived_retry_delay_s, + ) + .await?; + } + } + // Schedule annotations (`// on schedule`) are marker-only — the binding // lives on the schedule row's own `script_path` field, which the user // creates separately via the schedule editor. No script-create-time diff --git a/backend/windmill-api-users/src/users.rs b/backend/windmill-api-users/src/users.rs index 3fcebdc083..ff0e8e754e 100644 --- a/backend/windmill-api-users/src/users.rs +++ b/backend/windmill-api-users/src/users.rs @@ -1942,8 +1942,10 @@ async fn login( Extension(argon2): Extension>>, Json(Login { email, password }): Json, ) -> Result { - #[cfg(feature = "no_auth")] - { + // In `--no-auth` mode there is no real login; the frontend never needs a + // session cookie because every request already resolves as the admin + // superadmin (see resolve_opt_job_authed). + if windmill_api_auth::is_no_auth() { return Ok("no_auth".to_string()); } diff --git a/backend/windmill-api-workspaces/src/workspaces.rs b/backend/windmill-api-workspaces/src/workspaces.rs index 46d06c458c..7e1cc1a743 100644 --- a/backend/windmill-api-workspaces/src/workspaces.rs +++ b/backend/windmill-api-workspaces/src/workspaces.rs @@ -156,6 +156,7 @@ pub fn workspaced_service() -> Router { .route("/create_fork", post(create_workspace_fork)) .route("/attach_dev_workspace", post(attach_dev_workspace)) .route("/detach_dev_workspace", post(detach_dev_workspace)) + .route("/set_dev_workspace_label", post(set_dev_workspace_label)) .route("/get_dev_workspace", get(get_dev_workspace)) .route("/change_workspace_name", post(change_workspace_name)) .route("/change_workspace_color", post(change_workspace_color)) @@ -472,6 +473,10 @@ struct CreateWorkspaceFork { /// the team can work in it. Defaults off; the dev-workspace UI defaults it on. #[serde(default)] copy_members: bool, + /// Cosmetic display label for the dev workspace: 'dev' | 'staging'. Purely visual (badge text + + /// wording); ignored for non-dev forks. None defaults to 'dev'. + #[serde(default)] + dev_workspace_label: Option, } #[derive(Deserialize)] @@ -501,6 +506,7 @@ struct UserWorkspace { pub operator_settings: Option>, pub parent_workspace_id: Option, pub is_dev_workspace: bool, + pub dev_workspace_label: Option, pub disabled: bool, } @@ -678,6 +684,20 @@ async fn exists_workspace( struct DevWorkspaceInfo { id: String, name: String, + dev_workspace_label: Option, +} + +/// Normalize/validate the cosmetic dev-workspace display label. None or 'dev' both render as "dev"; +/// 'staging' renders as "stg". Anything else is rejected. Stored explicitly ('dev'/'staging') so it +/// round-trips, but a NULL column is treated as 'dev' on the read side too. +fn normalize_dev_workspace_label(label: Option) -> Result> { + match label.as_deref() { + None | Some("dev") => Ok(Some("dev".to_string())), + Some("staging") => Ok(Some("staging".to_string())), + Some(other) => Err(Error::BadRequest(format!( + "invalid dev workspace label '{other}' (expected 'dev' or 'staging')" + ))), + } } /// This workspace's active canonical dev workspace, if any. The create-fork UI and the dev-workspace @@ -691,7 +711,7 @@ async fn get_dev_workspace( ) -> JsonResult> { let dev = sqlx::query_as!( DevWorkspaceInfo, - "SELECT id, name FROM workspace WHERE parent_workspace_id = $1 AND is_dev_workspace AND deleted = false", + "SELECT id, name, dev_workspace_label FROM workspace WHERE parent_workspace_id = $1 AND is_dev_workspace AND deleted = false", &w_id ) .fetch_optional(&db) @@ -3824,7 +3844,7 @@ async fn user_workspaces( let workspaces = sqlx::query_as!( UserWorkspace, "SELECT workspace.id, workspace.name, usr.username, workspace_settings.color, workspace.parent_workspace_id, - workspace.is_dev_workspace, + workspace.is_dev_workspace, workspace.dev_workspace_label, CASE WHEN usr.operator THEN workspace_settings.operator_settings ELSE NULL END as operator_settings, usr.disabled FROM workspace @@ -5324,6 +5344,8 @@ async fn create_workspace_fork_branch( // that second call. Validating early lets a bad request fail before any branch is created. if nw.is_dev_workspace { validate_dev_workspace_id(&nw.id)?; + // Reject a bad cosmetic label before any git branch is created (acted on in create_workspace_fork). + normalize_dev_workspace_label(nw.dev_workspace_label.clone())?; ensure_dev_parent_is_root(&db, &w_id).await?; // Reject before creating any git branch if the parent already has a dev workspace, // otherwise the deferred branch-creation job leaves a dangling branch on the synced repos. @@ -5555,6 +5577,12 @@ async fn create_workspace_fork( validate_fork_workspace_id(&nw.id)?; } validate_workspace_name(&nw.name)?; + // Cosmetic label only applies to dev workspaces; a non-dev fork stores NULL. + let dev_workspace_label = if nw.is_dev_workspace { + normalize_dev_workspace_label(nw.dev_workspace_label.clone())? + } else { + None + }; // Check the id conflict before the CE workspace-count limit so that // re-using a taken (possibly archived) fork id reports the actual // conflict instead of a misleading "maximum number of workspaces" error. @@ -5632,13 +5660,14 @@ async fn create_workspace_fork( sqlx::query!( "INSERT INTO workspace - (id, name, owner, parent_workspace_id, is_dev_workspace) - VALUES ($1, $2, $3, $4, $5)", + (id, name, owner, parent_workspace_id, is_dev_workspace, dev_workspace_label) + VALUES ($1, $2, $3, $4, $5, $6)", forked_id, nw.name, authed.email, parent_workspace_id, nw.is_dev_workspace, + dev_workspace_label, ) .execute(&mut *tx) .await?; @@ -5771,6 +5800,9 @@ struct AttachDevWorkspace { lock_prod_deploy: bool, #[serde(default)] lock_prod_forking: bool, + /// Cosmetic display label for the attached dev workspace: 'dev' | 'staging'. None defaults to 'dev'. + #[serde(default)] + dev_workspace_label: Option, } #[derive(Deserialize)] @@ -5824,6 +5856,7 @@ async fn attach_dev_workspace( // The id is interpolated into a `wm-fork//` branch name like any fork. validate_dev_workspace_id(&dev_w_id)?; + let dev_workspace_label = normalize_dev_workspace_label(req.dev_workspace_label.clone())?; let dev = sqlx::query!( r#"SELECT parent_workspace_id, deleted FROM workspace WHERE id = $1"#, @@ -5891,9 +5924,10 @@ async fn attach_dev_workspace( let mut tx = db.begin().await?; sqlx::query!( - "UPDATE workspace SET parent_workspace_id = $1, is_dev_workspace = true WHERE id = $2", + "UPDATE workspace SET parent_workspace_id = $1, is_dev_workspace = true, dev_workspace_label = $3 WHERE id = $2", &prod_w_id, - &dev_w_id + &dev_w_id, + dev_workspace_label, ) .execute(&mut *tx) .await?; @@ -5959,6 +5993,51 @@ async fn attach_dev_workspace( )) } +#[derive(Deserialize)] +struct SetDevWorkspaceLabel { + #[serde(default)] + dev_workspace_label: Option, +} + +/// Change the cosmetic display label ('dev' | 'staging') of the current workspace, which must itself +/// be a dev workspace. Purely visual (badge text + wording); requires admin of the dev workspace. +async fn set_dev_workspace_label( + authed: ApiAuthed, + Extension(db): Extension, + Path(w_id): Path, + Json(req): Json, +) -> Result { + require_admin(authed.is_admin, &authed.username)?; + let label = normalize_dev_workspace_label(req.dev_workspace_label)?; + + let mut tx = db.begin().await?; + let updated = sqlx::query_scalar!( + "UPDATE workspace SET dev_workspace_label = $1 WHERE id = $2 AND is_dev_workspace RETURNING id", + label, + &w_id, + ) + .fetch_optional(&mut *tx) + .await?; + if updated.is_none() { + return Err(Error::BadRequest(format!( + "Workspace '{w_id}' is not a dev workspace" + ))); + } + + audit_log( + &mut *tx, + &authed, + "workspaces.set_dev_workspace_label", + ActionKind::Update, + &w_id, + label.as_deref(), + None, + ) + .await?; + tx.commit().await?; + Ok(format!("Updated dev workspace label for {w_id}")) +} + /// Reverse [`attach_dev_workspace`] / clear the dev designation: unset the dev flag and remove the /// prod lock. The workspace keeps its `parent_workspace_id` (it remains an ordinary fork). async fn detach_dev_workspace( diff --git a/backend/windmill-api-workspaces/src/workspaces_extra.rs b/backend/windmill-api-workspaces/src/workspaces_extra.rs index 9fbaad2bf3..6b4ace5f0a 100644 --- a/backend/windmill-api-workspaces/src/workspaces_extra.rs +++ b/backend/windmill-api-workspaces/src/workspaces_extra.rs @@ -91,9 +91,10 @@ pub(crate) async fn change_workspace_id( .await?; } sqlx::query!( - "INSERT INTO workspace (id, name, owner, deleted, premium, parent_workspace_id, is_dev_workspace) + "INSERT INTO workspace (id, name, owner, deleted, premium, parent_workspace_id, is_dev_workspace, dev_workspace_label) SELECT $1, $2, owner, false, premium, - CASE WHEN $4 THEN parent_workspace_id ELSE NULL END, $5 + CASE WHEN $4 THEN parent_workspace_id ELSE NULL END, $5, + CASE WHEN $5 THEN dev_workspace_label ELSE NULL END FROM workspace WHERE id = $3", &rw.new_id, &rw.new_name, @@ -1095,7 +1096,10 @@ pub(crate) async fn delete_workspace( // effort: failures are logged — the workspace row is already gone, and broken storage // credentials must not have made it undeletable. for e in cleanup_fork_ducklake_namespaces(&db, &w_id, fork_ducklake_cleanups).await { - tracing::warn!("deleted workspace {w_id}: ducklake namespace cleanup: {}", e.msg); + tracing::warn!( + "deleted workspace {w_id}: ducklake namespace cleanup: {}", + e.msg + ); } if let Some(parent) = dev_lock_parent { diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 33d42c0c74..86bb4bd565 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -1,7 +1,7 @@ openapi: "3.0.3" info: - version: 1.749.0 + version: 1.751.0 title: Windmill API contact: @@ -1205,6 +1205,9 @@ paths: type: boolean lock_prod_forking: type: boolean + dev_workspace_label: + type: string + enum: [dev, staging] required: - dev_workspace_id responses: @@ -1263,10 +1266,40 @@ paths: type: string name: type: string + dev_workspace_label: + type: string + nullable: true + description: "Cosmetic display label ('dev' | 'staging'); null defaults to 'dev'" required: - id - name + /w/{workspace}/workspaces/set_dev_workspace_label: + post: + summary: set the cosmetic display label (dev/staging) of this dev workspace + operationId: setDevWorkspaceLabel + tags: + - workspace + parameters: + - $ref: "#/components/parameters/WorkspaceId" + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + dev_workspace_label: + type: string + enum: [dev, staging] + responses: + "200": + description: dev workspace label updated + content: + text/plain: + schema: + type: string + /workspaces/exists: post: summary: exists workspace @@ -28180,6 +28213,10 @@ components: nullable: true is_dev_workspace: type: boolean + dev_workspace_label: + type: string + nullable: true + description: "Cosmetic display label of the dev workspace ('dev' | 'staging'); null defaults to 'dev'" created_by: type: string nullable: true @@ -28249,6 +28286,10 @@ components: copy_members: type: boolean description: "Copy the parent's members (users + group memberships) into the fork so the team can work in it" + dev_workspace_label: + type: string + enum: [dev, staging] + description: "Cosmetic display label for the dev workspace (badge text + wording only); ignored for non-dev forks" required: - id - name diff --git a/backend/windmill-api/src/db.rs b/backend/windmill-api/src/db.rs index b81a9dbb84..8b9489263b 100644 --- a/backend/windmill-api/src/db.rs +++ b/backend/windmill-api/src/db.rs @@ -96,6 +96,14 @@ lazy_static::lazy_static! { pub struct CustomMigrator { inner: PoolConnection, } +impl CustomMigrator { + /// The connection the migrator already holds (with the migration advisory lock). + /// Migration housekeeping runs on it instead of re-acquiring: a second connection + /// while this one is held deadlocks a single-connection backend (e.g. embedded pglite). + pub fn connection(&mut self) -> &mut PgConnection { + &mut *self.inner + } +} impl Migrate for CustomMigrator { fn ensure_migrations_table( &mut self, @@ -272,7 +280,7 @@ pub async fn migrate( version=20250131115248 OR version=20250902085503 OR version=20250201145630 OR version=20250201145631 OR version=20250201145632 OR version=20251006143821" ) - .execute(db) + .execute(custom_migrator.connection()) .await { tracing::info!("Could not remove sqlx migrations: {err:#}"); @@ -310,7 +318,7 @@ pub async fn migrate( sqlx::query("DELETE FROM _sqlx_migrations WHERE version = $1 AND checksum != $2") .bind(m.version) .bind(&*m.checksum) - .execute(db) + .execute(custom_migrator.connection()) .await { tracing::info!("Could not clean up stale migration {}: {err:#}", m.version); @@ -340,7 +348,7 @@ pub async fn migrate( } } - crate::live_migrations::custom_migrations(&mut custom_migrator, db).await?; + crate::live_migrations::custom_migrations(&mut custom_migrator).await?; Ok(None) } diff --git a/backend/windmill-api/src/live_migrations.rs b/backend/windmill-api/src/live_migrations.rs index e7fa3eb13f..d5c759fc18 100644 --- a/backend/windmill-api/src/live_migrations.rs +++ b/backend/windmill-api/src/live_migrations.rs @@ -9,26 +9,26 @@ use sqlx::Postgres; use windmill_common::error::Error; -use crate::db::{CustomMigrator, DB}; +use crate::db::CustomMigrator; use sqlx::migrate::Migrate; +use sqlx::Acquire; use sqlx::Executor; -pub async fn custom_migrations(migrator: &mut CustomMigrator, db: &DB) -> Result<(), Error> { - if let Err(err) = fix_flow_versioning_migration(migrator, db).await { +pub async fn custom_migrations(migrator: &mut CustomMigrator) -> Result<(), Error> { + if let Err(err) = fix_flow_versioning_migration(migrator).await { tracing::error!("Could not apply flow versioning fix migration: {err:#}"); } Ok(()) } -async fn fix_flow_versioning_migration( - migrator: &mut CustomMigrator, - db: &DB, -) -> Result<(), Error> { +// Runs on the migrator's held connection (see CustomMigrator::connection): re-acquiring +// from the pool here would deadlock a single-connection backend. +async fn fix_flow_versioning_migration(migrator: &mut CustomMigrator) -> Result<(), Error> { let has_done_migration = sqlx::query_scalar!( "SELECT EXISTS(SELECT name FROM windmill_migrations WHERE name = 'fix_flow_versioning_2')", ) - .fetch_one(db) + .fetch_one(migrator.connection()) .await? .unwrap_or(false); @@ -44,14 +44,14 @@ async fn fix_flow_versioning_migration( let has_done_migration = sqlx::query_scalar!( "SELECT EXISTS(SELECT name FROM windmill_migrations WHERE name = 'fix_flow_versioning_2')", ) - .fetch_one(db) + .fetch_one(migrator.connection()) .await? .unwrap_or(false); if !has_done_migration { let query = include_str!("../../custom_migrations/fix_flow_versioning_2.sql"); tracing::info!("Applying fix_flow_versioning_2.sql"); - let mut tx: sqlx::Transaction<'_, Postgres> = db.begin().await?; + let mut tx: sqlx::Transaction<'_, Postgres> = migrator.connection().begin().await?; tx.execute(query).await?; tracing::info!("Applied fix_flow_versioning_2.sql"); sqlx::query!( diff --git a/backend/windmill-common/src/assets.rs b/backend/windmill-common/src/assets.rs index 862bd645c3..62f6d32070 100644 --- a/backend/windmill-common/src/assets.rs +++ b/backend/windmill-common/src/assets.rs @@ -169,6 +169,68 @@ fn is_write_access(access: Option) -> bool { ) } +/// Kinds whose *read* usage auto-derives a cascade trigger edge inside a +/// `// pipeline`. Scoped to the two intra-pipeline data kinds — a ducklake +/// table read (the core case) and an s3 object read (file-ingestion +/// producers). Resource / datatable / volume reads stay explicit-`// on`: +/// a config/lookup read cascading is more often surprising than wanted. +fn is_auto_trigger_kind(kind: AssetKind) -> bool { + matches!(kind, AssetKind::Ducklake | AssetKind::S3Object) +} + +/// Trigger refs auto-derived from a pipeline script's inferred reads, so the +/// FROM clause alone wires the cascade edge (no redundant `// on `). +/// +/// Included: an input read read-*only* (`R`) of a supported kind +/// ([`is_auto_trigger_kind`]). The effective access type is +/// `access_type.or(alt_access_type)` — same precedence as the persisted +/// `asset.usage_access_type` and the frontend mirror's `access_type ?? +/// alt_access_type`, so a manual read override on an ambiguous parse still +/// derives an edge (and the live canvas and the deployed graph agree). +/// Excluded, each for a reason: +/// - `RW` / `W` — the script also writes the asset; an edge would be a +/// self-triggering loop. +/// - `None` access — usage is ambiguous (poisoned merge) with no override; +/// can't confirm a read, so fail safe and don't cascade. +/// - already in `explicit_refs` — the author wrote `// on `, which +/// wins (it carries the per-edge debounce/opts). +/// - in `muted_refs` — a `// mute ` opt-out (lookup / SCD input). +/// +/// `mute_all` (from `// mute all`) short-circuits to no derivation, leaving +/// only the explicit `// on` edges. Returns canonical refs (e.g. +/// `ducklake://main.orders`), deduped, in input order. +pub fn derive_pipeline_asset_trigger_refs( + assets: &[AssetWithAltAccessType], + explicit_refs: &HashSet, + muted_refs: &HashSet, + mute_all: bool, +) -> Vec { + if mute_all { + return vec![]; + } + let mut out = vec![]; + let mut seen = HashSet::new(); + for a in assets { + // Effective access mirrors the persisted `usage_access_type` and the + // frontend derivation: an explicit parse wins, else the manual override. + let access = a.access_type.or(a.alt_access_type); + if access != Some(AssetUsageAccessType::R) || !is_auto_trigger_kind(a.kind) { + continue; + } + let Some(prefix) = a.kind.canonical_prefix() else { + continue; + }; + let r = format!("{}{}", prefix, a.path); + if explicit_refs.contains(&r) || muted_refs.contains(&r) { + continue; + } + if seen.insert(r.clone()) { + out.push(r); + } + } + out +} + /// Clear and reinsert the full static-asset usage set of a script in one tx, /// invalidating the producer-writes cache at most once and only on a real /// change. The cache (asset_dispatch::ASSET_PRODUCER_WRITES_CACHE) keys a @@ -369,6 +431,134 @@ mod debounce_duration_tests { } } +#[cfg(test)] +mod derive_trigger_tests { + use super::{derive_pipeline_asset_trigger_refs, AssetKind, AssetUsageAccessType}; + use std::collections::HashSet; + use windmill_types::assets::AssetWithAltAccessType; + + fn asset( + kind: AssetKind, + path: &str, + at: Option, + ) -> AssetWithAltAccessType { + AssetWithAltAccessType { + path: path.to_string(), + kind, + access_type: at, + alt_access_type: None, + columns: None, + } + } + + fn derive(assets: &[AssetWithAltAccessType]) -> Vec { + derive_pipeline_asset_trigger_refs(assets, &HashSet::new(), &HashSet::new(), false) + } + + #[test] + fn read_only_ducklake_and_s3_derive_an_edge() { + use AssetUsageAccessType::R; + let a = [ + asset(AssetKind::Ducklake, "main.orders", Some(R)), + asset(AssetKind::S3Object, "raw/events", Some(R)), + ]; + assert_eq!( + derive(&a), + vec![ + "ducklake://main.orders".to_string(), + "s3://raw/events".to_string() + ] + ); + } + + #[test] + fn writes_and_rw_are_skipped_to_avoid_self_edges() { + use AssetUsageAccessType::*; + // W (pure producer) and RW (reads *and* writes the same table — a + // self-cascade if edged) both derive nothing. + let a = [ + asset(AssetKind::Ducklake, "main.out", Some(W)), + asset(AssetKind::Ducklake, "main.self", Some(RW)), + ]; + assert!(derive(&a).is_empty()); + } + + #[test] + fn ambiguous_access_and_unsupported_kinds_are_skipped() { + use AssetUsageAccessType::R; + let a = [ + asset(AssetKind::Ducklake, "main.ambiguous", None), // poisoned merge + asset(AssetKind::Resource, "f/db", Some(R)), // out of scope + asset(AssetKind::DataTable, "main.dt", Some(R)), // out of scope + ]; + assert!(derive(&a).is_empty()); + } + + #[test] + fn manual_read_override_on_ambiguous_parse_derives_an_edge() { + use AssetUsageAccessType::{R, W}; + // Parser can't confirm access (`access_type: None`) but the user manually + // overrode it. Effective access = `access_type.or(alt_access_type)`, the + // same value persisted to `asset.usage_access_type` and used by the + // frontend canvas — so a read override derives an edge (parity, no + // silently-vanishing edge on deploy) and a write override does not. + let read_override = AssetWithAltAccessType { + path: "main.override_r".to_string(), + kind: AssetKind::Ducklake, + access_type: None, + alt_access_type: Some(R), + columns: None, + }; + let write_override = AssetWithAltAccessType { + path: "main.override_w".to_string(), + kind: AssetKind::Ducklake, + access_type: None, + alt_access_type: Some(W), + columns: None, + }; + assert_eq!( + derive(&[read_override, write_override]), + vec!["ducklake://main.override_r".to_string()] + ); + } + + #[test] + fn explicit_and_muted_refs_are_excluded() { + use AssetUsageAccessType::R; + let a = [ + asset(AssetKind::Ducklake, "main.explicit", Some(R)), + asset(AssetKind::Ducklake, "main.muted", Some(R)), + asset(AssetKind::Ducklake, "main.keep", Some(R)), + ]; + let explicit: HashSet = ["ducklake://main.explicit".to_string()].into(); + let muted: HashSet = ["ducklake://main.muted".to_string()].into(); + assert_eq!( + derive_pipeline_asset_trigger_refs(&a, &explicit, &muted, false), + vec!["ducklake://main.keep".to_string()] + ); + } + + #[test] + fn mute_all_derives_nothing() { + use AssetUsageAccessType::R; + let a = [asset(AssetKind::Ducklake, "main.orders", Some(R))]; + assert!( + derive_pipeline_asset_trigger_refs(&a, &HashSet::new(), &HashSet::new(), true) + .is_empty() + ); + } + + #[test] + fn duplicate_reads_dedup() { + use AssetUsageAccessType::R; + let a = [ + asset(AssetKind::Ducklake, "main.orders", Some(R)), + asset(AssetKind::Ducklake, "main.orders", Some(R)), + ]; + assert_eq!(derive(&a), vec!["ducklake://main.orders".to_string()]); + } +} + #[cfg(test)] mod trigger_ref_roundtrip_tests { use super::{parse_asset_trigger_ref, trigger_spec_to_row, AssetKind, ScriptTriggerKind}; diff --git a/backend/windmill-common/src/worker.rs b/backend/windmill-common/src/worker.rs index 3a1469d5e3..ea2b5455f3 100644 --- a/backend/windmill-common/src/worker.rs +++ b/backend/windmill-common/src/worker.rs @@ -273,6 +273,14 @@ lazy_static::lazy_static! { /// production `app.windmill.dev` cluster, not on staging or self-hosted. pub static ref CLOUD_PRODUCTION_HOST: &'static str = "app.windmill.dev"; + /// `--no-auth` mode: when set, every API request is treated as + /// authenticated as the `admin@windmill.dev` superadmin and no login is + /// ever required. Meant for self-hosted deployments that front Windmill + /// with their own authenticating gateway. Never honored on the managed + /// cloud (`CLOUD_HOSTED`), which must always enforce real authentication. + pub static ref NO_AUTH: bool = !*CLOUD_HOSTED + && std::env::var("NO_AUTH").ok().is_some_and(|x| x == "1" || x == "true"); + pub static ref CUSTOM_TAGS: Vec = std::env::var("CUSTOM_TAGS") .ok() .map(|x| x.split(',').map(|x| x.to_string()).collect::>()).unwrap_or_default(); diff --git a/benchmarks/lib.ts b/benchmarks/lib.ts index 71ec4f546e..acd34be646 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.749.0"; +export const VERSION = "v1.751.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 f016b94c24..10048f2d18 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.749.0"; +export const VERSION = "1.751.0"; diff --git a/cli/src/utils/resource_types.ts b/cli/src/utils/resource_types.ts index 741cb9fb71..5bd56c9a8e 100644 --- a/cli/src/utils/resource_types.ts +++ b/cli/src/utils/resource_types.ts @@ -1,5 +1,9 @@ import { Schema, SchemaProperty } from "../../bootstrap/common.ts"; +function quotePropName(name: string): string { + return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) ? name : JSON.stringify(name); +} + export function compileResourceTypeToTsType(schema: Schema) { function rec(x: { [name: string]: SchemaProperty }, root = false) { let res = "{\n"; @@ -10,15 +14,15 @@ export function compileResourceTypeToTsType(schema: Schema) { let i = 0; for (let [name, prop] of entries) { if (prop.type == "object") { - res += ` ${name}: ${rec(prop.properties ?? {})}`; + res += ` ${quotePropName(name)}: ${rec(prop.properties ?? {})}`; } else if (prop.type == "array") { - res += ` ${name}: ${prop?.items?.type ?? "any"}[]`; + res += ` ${quotePropName(name)}: ${prop?.items?.type ?? "any"}[]`; } else { let typ = prop?.type ?? "any"; if (typ == "integer") { typ = "number"; } - res += ` ${name}: ${typ}`; + res += ` ${quotePropName(name)}: ${typ}`; } i++; if (i < entries.length) { diff --git a/cli/test/resource_types_unit.test.ts b/cli/test/resource_types_unit.test.ts new file mode 100644 index 0000000000..0966ae25bf --- /dev/null +++ b/cli/test/resource_types_unit.test.ts @@ -0,0 +1,68 @@ +import { expect, test } from "bun:test"; + +import { compileResourceTypeToTsType } from "../src/utils/resource_types.ts"; +import type { Schema } from "../bootstrap/common.ts"; + +// ============================================================================= +// Resource-type namespace generation (WIN-2132) +// +// `compileResourceTypeToTsType` renders a JSON Schema into the body of a +// TypeScript type used in the generated `rt.d.ts` (RT namespace). JSON Schema +// property names are unconstrained, so a name with a colon, hyphen, or space +// is legal in the schema but not a valid bare TS identifier. Emitting it raw +// produced syntactically invalid output that broke `tsc`. These tests pin that +// such names are quoted while plain identifiers stay bare. +// ============================================================================= + +function schema(properties: Schema["properties"]): Schema { + return { + $schema: undefined, + type: "object", + properties, + required: [], + }; +} + +test("plain identifiers are emitted without quotes", () => { + const out = compileResourceTypeToTsType( + schema({ + host: { type: "string" }, + _port: { type: "integer" }, + $ref: { type: "boolean" }, + }) + ); + expect(out).toContain(" host: string"); + expect(out).toContain(" _port: number"); + expect(out).toContain(" $ref: boolean"); + expect(out).not.toContain('"host"'); +}); + +test("non-identifier property names are double-quoted", () => { + const out = compileResourceTypeToTsType( + schema({ + "content-type": { type: "string" }, + "x:api:key": { type: "string" }, + "with space": { type: "integer" }, + "3leading": { type: "boolean" }, + }) + ); + expect(out).toContain(' "content-type": string'); + expect(out).toContain(' "x:api:key": string'); + expect(out).toContain(' "with space": number'); + expect(out).toContain(' "3leading": boolean'); +}); + +test("nested object and array property names are quoted too", () => { + const out = compileResourceTypeToTsType( + schema({ + "nested-obj": { + type: "object", + properties: { "inner-key": { type: "string" } }, + }, + "arr-field": { type: "array", items: { type: "string" } }, + }) + ); + expect(out).toContain('"nested-obj": {'); + expect(out).toContain('"inner-key": string'); + expect(out).toContain('"arr-field": string[]'); +}); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 4489e6160e..3023846ce6 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "@windmill-labs/components", - "version": "1.749.0", + "version": "1.751.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@windmill-labs/components", - "version": "1.749.0", + "version": "1.751.0", "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { diff --git a/frontend/package.json b/frontend/package.json index 42ab8ba684..70d24aba68 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "@windmill-labs/components", - "version": "1.749.0", + "version": "1.751.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/frontend/src/lib/components/DevWorkspaceSetting.svelte b/frontend/src/lib/components/DevWorkspaceSetting.svelte index bdba23c57f..e6295d24cd 100644 --- a/frontend/src/lib/components/DevWorkspaceSetting.svelte +++ b/frontend/src/lib/components/DevWorkspaceSetting.svelte @@ -1,7 +1,7 @@ @@ -54,7 +56,9 @@ {tooltip} {/if} - {#if actionButton} + {#if headerAction} + {@render headerAction()} + {:else if actionButton} {/each} diff --git a/frontend/src/lib/components/sidebar/CriticalAlertModal.svelte b/frontend/src/lib/components/sidebar/CriticalAlertModal.svelte index f8418b048a..4a41c65a31 100644 --- a/frontend/src/lib/components/sidebar/CriticalAlertModal.svelte +++ b/frontend/src/lib/components/sidebar/CriticalAlertModal.svelte @@ -1,6 +1,4 @@ @@ -197,10 +186,14 @@ {#snippet headerRight()} {#if $superadmin || $userStore?.is_admin} + {#snippet trigger()}
@@ -246,8 +239,8 @@ {#if $superadmin} {#snippet trigger()}
@@ -304,6 +297,7 @@ 0 } + // Load the channel state on mount so the "no channels" warning doesn't depend on + // there being unacknowledged alerts to trigger a refresh (muting auto-acks them). + onMount(() => { + if ($superadmin) checkCriticalAlertChannels() + }) + async function acknowledgeAlert(id: number) { await acknowledgeCriticalAlert({ id }) getAlerts(false) @@ -133,7 +143,7 @@ - {#if !hasCriticalAlertChannels && $superadmin} + {#if $superadmin && isMuted && !hasCriticalAlertChannels}
Go to the diff --git a/frontend/src/lib/components/sidebar/WorkspaceMenu.svelte b/frontend/src/lib/components/sidebar/WorkspaceMenu.svelte index 53ac410cc8..baf8f95a42 100644 --- a/frontend/src/lib/components/sidebar/WorkspaceMenu.svelte +++ b/frontend/src/lib/components/sidebar/WorkspaceMenu.svelte @@ -29,6 +29,7 @@ import type { MenubarBuilders } from '@melt-ui/svelte' import { buildWorkspaceHierarchy } from '$lib/utils/workspaceHierarchy' import { getContrastTextColor } from '$lib/utils' + import { devBadgeText, devLabelWord } from '$lib/utils/devWorkspaceLabel' interface Props { isCollapsed?: boolean @@ -133,7 +134,7 @@ iconProps={iconColor ? { style: `color: ${iconColor}` } : undefined} label={removePrefix($workspaceStore ?? '', 'wm-fork-')} sublabel={parentWorkspace?.name - ? `${forkedWorkspace.is_dev_workspace ? 'Dev workspace of' : 'Fork of'} ${parentWorkspace.name}` + ? `${forkedWorkspace.is_dev_workspace ? `${devLabelWord(forkedWorkspace.dev_workspace_label)} workspace of` : 'Fork of'} ${parentWorkspace.name}` : undefined} {isCollapsed} color={$workspaceColor} @@ -177,6 +178,7 @@ workspaceColor={workspace.color} {isForked} isDevWorkspace={workspace.is_dev_workspace} + devWorkspaceLabel={workspace.dev_workspace_label} {parentName} />
@@ -191,7 +193,7 @@ {workspace.name}{workspace.disabled ? ' (user disabled)' : ''}
{#if workspace.is_dev_workspace} - dev + {devBadgeText(workspace.dev_workspace_label)} {/if}
@@ -135,7 +137,7 @@ {/if} {#if workspace.is_dev_workspace} - dev + {devBadgeText(workspace.dev_workspace_label)} {/if} - {#if workspace.id === 'admins'} diff --git a/frontend/src/lib/components/workspace/WorkspaceIcon.svelte b/frontend/src/lib/components/workspace/WorkspaceIcon.svelte index b43b558335..86e15d5d0b 100644 --- a/frontend/src/lib/components/workspace/WorkspaceIcon.svelte +++ b/frontend/src/lib/components/workspace/WorkspaceIcon.svelte @@ -2,11 +2,13 @@ import { Building, GitFork } from 'lucide-svelte' import { Tooltip } from '$lib/components/meltComponents' import { getContrastTextColor } from '$lib/utils' + import { devLabelWord } from '$lib/utils/devWorkspaceLabel' interface Props { workspaceColor?: string isForked?: boolean isDevWorkspace?: boolean + devWorkspaceLabel?: string | null parentName?: string size?: number } @@ -15,6 +17,7 @@ workspaceColor, isForked = false, isDevWorkspace = false, + devWorkspaceLabel, parentName, size = 14 }: Props = $props() @@ -27,7 +30,7 @@ {#snippet text()} {#if isForked && parentName} - {isDevWorkspace ? 'Dev workspace of' : 'Fork of'} + {isDevWorkspace ? `${devLabelWord(devWorkspaceLabel)} workspace of` : 'Fork of'} {parentName} {/if} {/snippet} diff --git a/frontend/src/lib/components/workspaceSettings/AiSkillsSettings.svelte b/frontend/src/lib/components/workspaceSettings/AiSkillsSettings.svelte index c6a5bf843b..199da77c4b 100644 --- a/frontend/src/lib/components/workspaceSettings/AiSkillsSettings.svelte +++ b/frontend/src/lib/components/workspaceSettings/AiSkillsSettings.svelte @@ -1,18 +1,35 @@ + + +{#snippet pasteZone()} + + {#if editingOriginalName || pasteContent.trim()} +
+ {isDirty && pasteError ? pasteError : ''} + +
+ {/if} +{/snippet} + -
-