diff --git a/CHANGELOG.md b/CHANGELOG.md index 119e051c3b..11f5d8e97c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,50 @@ # Changelog +## [1.728.1](https://github.com/windmill-labs/windmill/compare/v1.728.0...v1.728.1) (2026-06-17) + + +### Bug Fixes + +* **backend:** purge workspace_diff cache on workspace delete ([#9627](https://github.com/windmill-labs/windmill/issues/9627)) ([8a3f69d](https://github.com/windmill-labs/windmill/commit/8a3f69dda8f2088fb859ed8ed6e54458940423d0)) +* **cli:** fall back to esbuild-wasm on native host/binary mismatch ([#9629](https://github.com/windmill-labs/windmill/issues/9629)) ([86d1d16](https://github.com/windmill-labs/windmill/commit/86d1d160f0d3bd9faabdafada07e2956dd98445d)) +* **frontend:** persist session-editor draft path/summary edits + per-line diff tooltips ([#9622](https://github.com/windmill-labs/windmill/issues/9622)) ([e4bfeb2](https://github.com/windmill-labs/windmill/commit/e4bfeb29bc4e89669863b5f6396904a331167658)) + +## [1.728.0](https://github.com/windmill-labs/windmill/compare/v1.727.0...v1.728.0) (2026-06-16) + + +### Features + +* **frontend:** adapt AI-chat/sessions drafts to DB-backed model ([#9601](https://github.com/windmill-labs/windmill/issues/9601)) ([611c70a](https://github.com/windmill-labs/windmill/commit/611c70acd211cf4b8f8308da4a264c670a2f5f43)) +* **frontend:** consolidate draft-migration errors into a single toast + modal ([#9612](https://github.com/windmill-labs/windmill/issues/9612)) ([bc0d5bf](https://github.com/windmill-labs/windmill/commit/bc0d5bf241df3633921bd9d43d171e91034fbfcf)) +* **frontend:** dedup user drafts against the deployed baseline ([#9618](https://github.com/windmill-labs/windmill/issues/9618)) ([a2ce446](https://github.com/windmill-labs/windmill/commit/a2ce44645fdbfa98bf250fac2d15d2b5b26c4b47)) + + +### Bug Fixes + +* **frontend:** reset deleteWorkspaceForkModal on confirm in SidebarContent ([#9619](https://github.com/windmill-labs/windmill/issues/9619)) ([7cb5c6e](https://github.com/windmill-labs/windmill/commit/7cb5c6e749b2020dee5ee1499f0dc69c5109a6d8)) +* **frontend:** session Drafts drawer uses raw_app kind for the raw-app diff ([#9617](https://github.com/windmill-labs/windmill/issues/9617)) ([46288b6](https://github.com/windmill-labs/windmill/commit/46288b6143efae4dfdf6fe068b97a1e8831fce6a)) +* **nativets:** respect custom CA certs in in-process fetch runtime ([#9615](https://github.com/windmill-labs/windmill/issues/9615)) ([41562c7](https://github.com/windmill-labs/windmill/commit/41562c7d7c708d7d056d9b3d0c39b994a6f4a016)) +* **ResourceForm:** initialize JSON editor when resource type schema is unavailable ([#9611](https://github.com/windmill-labs/windmill/issues/9611)) ([5a24057](https://github.com/windmill-labs/windmill/commit/5a2405743b4622fc1021109114d007057abd5dfd)) +* show folder labels in the folder list table ([#9620](https://github.com/windmill-labs/windmill/issues/9620)) ([651fa13](https://github.com/windmill-labs/windmill/commit/651fa13ee80ff76e5a53ef1ed545b03ce6792294)) +* show last updated date per user in other-users-drafts modal ([#9614](https://github.com/windmill-labs/windmill/issues/9614)) ([f6104ce](https://github.com/windmill-labs/windmill/commit/f6104ce05c4005ffb9fe8112782d1ef6d3065300)) + +## [1.727.0](https://github.com/windmill-labs/windmill/compare/v1.726.1...v1.727.0) (2026-06-16) + + +### Features + +* support temp_script_refs in wmill dev for local relative imports ([#9554](https://github.com/windmill-labs/windmill/issues/9554)) ([33ac287](https://github.com/windmill-labs/windmill/commit/33ac287065742df53f363a5fe09f54f5584a85a6)) + + +### Bug Fixes + +* **cli:** harden legacy flow lock migration ordering and collision guard ([#9557](https://github.com/windmill-labs/windmill/issues/9557)) ([cd09870](https://github.com/windmill-labs/windmill/commit/cd098700c2cd8d7e9150f760938f4eaf34d188ec)) +* **cli:** include __mod/ folder in gitSyncIncludePattern for scripts ([#9606](https://github.com/windmill-labs/windmill/issues/9606)) ([252c1b3](https://github.com/windmill-labs/windmill/commit/252c1b35fc716c3486109d89615127c588bbe90a)) +* **frontend:** allow same-origin redirects in isValidLogoutRedirect ([#9568](https://github.com/windmill-labs/windmill/issues/9568)) ([8500435](https://github.com/windmill-labs/windmill/commit/8500435e82231e13a1b8a874fd0545f0a0a73fee)) +* **frontend:** make UserDraft read-after-write work without live entry ([#9609](https://github.com/windmill-labs/windmill/issues/9609)) ([51e82d7](https://github.com/windmill-labs/windmill/commit/51e82d7c6d30c66c84236feb743c09929934e564)) +* **frontend:** seed detached user-draft handles so new-item drawers render ([#9608](https://github.com/windmill-labs/windmill/issues/9608)) ([9e3c0de](https://github.com/windmill-labs/windmill/commit/9e3c0decf95378c66055d82215c15cd3bf4a69cb)) +* **frontend:** strip server-managed fields from value diffs ([#9599](https://github.com/windmill-labs/windmill/issues/9599)) ([c213801](https://github.com/windmill-labs/windmill/commit/c213801b5aee54d801c14b9eb31422f2a312ef7e)) + ## [1.726.1](https://github.com/windmill-labs/windmill/compare/v1.726.0...v1.726.1) (2026-06-15) diff --git a/backend/.sqlx/query-03c8a797ae734ff76e227259ae011ef6d35f892fe95448c58ec13dea58eee3fa.json b/backend/.sqlx/query-03c8a797ae734ff76e227259ae011ef6d35f892fe95448c58ec13dea58eee3fa.json new file mode 100644 index 0000000000..f3bc714a3e --- /dev/null +++ b/backend/.sqlx/query-03c8a797ae734ff76e227259ae011ef6d35f892fe95448c58ec13dea58eee3fa.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM skip_workspace_diff_tally WHERE workspace_id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [] + }, + "hash": "03c8a797ae734ff76e227259ae011ef6d35f892fe95448c58ec13dea58eee3fa" +} diff --git a/backend/.sqlx/query-126cf6d54f8d2c916cd799f6663892119a94d66637062d1bf5a5fe97d89f8096.json b/backend/.sqlx/query-126cf6d54f8d2c916cd799f6663892119a94d66637062d1bf5a5fe97d89f8096.json new file mode 100644 index 0000000000..d59b38203d --- /dev/null +++ b/backend/.sqlx/query-126cf6d54f8d2c916cd799f6663892119a94d66637062d1bf5a5fe97d89f8096.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT COUNT(*) FROM workspace_diff\n WHERE source_workspace_id = 'wm-fork-test-workspace'\n OR fork_workspace_id = 'wm-fork-test-workspace'", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "count", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + null + ] + }, + "hash": "126cf6d54f8d2c916cd799f6663892119a94d66637062d1bf5a5fe97d89f8096" +} diff --git a/backend/.sqlx/query-353c6a648720e118c0d82ef055c60033f1969fb39155e7e07bb6730505e254b7.json b/backend/.sqlx/query-353c6a648720e118c0d82ef055c60033f1969fb39155e7e07bb6730505e254b7.json new file mode 100644 index 0000000000..46fd1d2ae2 --- /dev/null +++ b/backend/.sqlx/query-353c6a648720e118c0d82ef055c60033f1969fb39155e7e07bb6730505e254b7.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT COUNT(*) FROM skip_workspace_diff_tally WHERE workspace_id = 'wm-fork-test-workspace'", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "count", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + null + ] + }, + "hash": "353c6a648720e118c0d82ef055c60033f1969fb39155e7e07bb6730505e254b7" +} diff --git a/backend/.sqlx/query-5c2d1ae706e997bbca27dadb99cc0d55ffba53bc23ae69181a0d671c640b9ba7.json b/backend/.sqlx/query-5c2d1ae706e997bbca27dadb99cc0d55ffba53bc23ae69181a0d671c640b9ba7.json new file mode 100644 index 0000000000..c306ae1c9e --- /dev/null +++ b/backend/.sqlx/query-5c2d1ae706e997bbca27dadb99cc0d55ffba53bc23ae69181a0d671c640b9ba7.json @@ -0,0 +1,12 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO skip_workspace_diff_tally (workspace_id) VALUES ('wm-fork-test-workspace')", + "describe": { + "columns": [], + "parameters": { + "Left": [] + }, + "nullable": [] + }, + "hash": "5c2d1ae706e997bbca27dadb99cc0d55ffba53bc23ae69181a0d671c640b9ba7" +} diff --git a/backend/.sqlx/query-5eaf2e0bbede9dd80a70f2b37538239273443c7d56a1f7732988208fe3c9c58f.json b/backend/.sqlx/query-5eaf2e0bbede9dd80a70f2b37538239273443c7d56a1f7732988208fe3c9c58f.json new file mode 100644 index 0000000000..3c5b4d3d15 --- /dev/null +++ b/backend/.sqlx/query-5eaf2e0bbede9dd80a70f2b37538239273443c7d56a1f7732988208fe3c9c58f.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM workspace_diff WHERE source_workspace_id = $1 OR fork_workspace_id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [] + }, + "hash": "5eaf2e0bbede9dd80a70f2b37538239273443c7d56a1f7732988208fe3c9c58f" +} diff --git a/backend/.sqlx/query-590c4429347925395794b061943bfda0fb4434407b5e1ba4ffb8a770718d9c5d.json b/backend/.sqlx/query-6fbd5826ae1fafd7bbb218324e3e2d85231b889743de637eb35e756da29ad47b.json similarity index 66% rename from backend/.sqlx/query-590c4429347925395794b061943bfda0fb4434407b5e1ba4ffb8a770718d9c5d.json rename to backend/.sqlx/query-6fbd5826ae1fafd7bbb218324e3e2d85231b889743de637eb35e756da29ad47b.json index ca75762cd6..4bb89660ec 100644 --- a/backend/.sqlx/query-590c4429347925395794b061943bfda0fb4434407b5e1ba4ffb8a770718d9c5d.json +++ b/backend/.sqlx/query-6fbd5826ae1fafd7bbb218324e3e2d85231b889743de637eb35e756da29ad47b.json @@ -1,12 +1,17 @@ { "db_name": "PostgreSQL", - "query": "SELECT COALESCE(u.username, CASE WHEN d.workspace_id = 'admins' THEN d.email END) as \"username?\"\n FROM draft d\n LEFT JOIN usr u\n ON u.workspace_id = d.workspace_id\n AND u.email = d.email\n WHERE d.workspace_id = $1\n AND d.path = $2\n AND d.typ = $3\n AND (d.email IS NULL OR d.email <> $4)\n ORDER BY d.email NULLS LAST", + "query": "SELECT COALESCE(u.username, CASE WHEN d.workspace_id = 'admins' THEN d.email END) as \"username?\",\n d.created_at as \"draft_saved_at!\"\n FROM draft d\n LEFT JOIN usr u\n ON u.workspace_id = d.workspace_id\n AND u.email = d.email\n WHERE d.workspace_id = $1\n AND d.path = $2\n AND d.typ = $3\n AND (d.email IS NULL OR d.email <> $4)\n ORDER BY d.email NULLS LAST", "describe": { "columns": [ { "ordinal": 0, "name": "username?", "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "draft_saved_at!", + "type_info": "Timestamptz" } ], "parameters": { @@ -50,8 +55,9 @@ ] }, "nullable": [ - null + null, + false ] }, - "hash": "590c4429347925395794b061943bfda0fb4434407b5e1ba4ffb8a770718d9c5d" + "hash": "6fbd5826ae1fafd7bbb218324e3e2d85231b889743de637eb35e756da29ad47b" } diff --git a/backend/.sqlx/query-ba7d01509662be936c000aab4ff60f57be839ae365e5cbbb4838a7b622d1771a.json b/backend/.sqlx/query-ba7d01509662be936c000aab4ff60f57be839ae365e5cbbb4838a7b622d1771a.json new file mode 100644 index 0000000000..b7752cc7cf --- /dev/null +++ b/backend/.sqlx/query-ba7d01509662be936c000aab4ff60f57be839ae365e5cbbb4838a7b622d1771a.json @@ -0,0 +1,12 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO workspace_diff\n (source_workspace_id, fork_workspace_id, path, kind, ahead, behind, has_changes)\n VALUES ('wm-fork-test-workspace', 'test-workspace', 'f/shared/other', 'script', 0, 1, true)", + "describe": { + "columns": [], + "parameters": { + "Left": [] + }, + "nullable": [] + }, + "hash": "ba7d01509662be936c000aab4ff60f57be839ae365e5cbbb4838a7b622d1771a" +} diff --git a/backend/.sqlx/query-ca1a39a56d36802418048ddad1301f16394c719ef55494b5f286bc402292c420.json b/backend/.sqlx/query-ca1a39a56d36802418048ddad1301f16394c719ef55494b5f286bc402292c420.json new file mode 100644 index 0000000000..5afd4065ae --- /dev/null +++ b/backend/.sqlx/query-ca1a39a56d36802418048ddad1301f16394c719ef55494b5f286bc402292c420.json @@ -0,0 +1,12 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO workspace_diff\n (source_workspace_id, fork_workspace_id, path, kind, ahead, behind, has_changes, exists_in_source, exists_in_fork)\n VALUES ('test-workspace', 'wm-fork-test-workspace', 'f/shared/leaky', 'script', 1, 0, true, true, true)", + "describe": { + "columns": [], + "parameters": { + "Left": [] + }, + "nullable": [] + }, + "hash": "ca1a39a56d36802418048ddad1301f16394c719ef55494b5f286bc402292c420" +} diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 95efa7d40e..169fe61e0a 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -481,7 +481,7 @@ checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "synstructure", ] @@ -493,7 +493,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -504,7 +504,7 @@ checksum = "0a184645bcc6f52d69d8e7639720699c6a99efb711f886e251ed1d16db8dd90e" dependencies = [ "quote", "swc_macros_common", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -645,7 +645,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -667,7 +667,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -678,7 +678,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1403,7 +1403,7 @@ checksum = "7aa268c23bfbbd2c4363b9cd302a4f504fb2a9dfe7e3451d66f35dd392e20aca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1532,7 +1532,7 @@ dependencies = [ "regex", "rustc-hash 2.1.2", "shlex 1.3.0", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1552,7 +1552,7 @@ dependencies = [ "regex", "rustc-hash 2.1.2", "shlex 1.3.0", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1736,7 +1736,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1760,7 +1760,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1770,7 +1770,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17d4f95e880cfd28c4ca5a006cf7f6af52b4bcb7b5866f573b2faa126fb7affb" dependencies = [ "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1855,9 +1855,9 @@ dependencies = [ [[package]] name = "byte-unit" -version = "5.2.0" +version = "5.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c6d47a4e2961fb8721bcfc54feae6455f2f64e7054f9bc67e875f0e77f4c58d" +checksum = "37bcaa4a0975bed4a760af3efe4368825098ce5f9d37a30c5a021d635dc63d8f" dependencies = [ "rust_decimal", "schemars 1.2.1", @@ -1904,7 +1904,7 @@ checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2042,7 +2042,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b4a6cae9efc04cc6cbb8faf338d2c497c165c83e74509cf4dbedea948bbf6e5" dependencies = [ "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2205,7 +2205,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2638,7 +2638,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2720,7 +2720,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2733,7 +2733,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2766,7 +2766,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core 0.20.11", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2777,7 +2777,7 @@ checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core 0.23.0", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3256,7 +3256,7 @@ checksum = "df6f88d7ee27daf8b108ba910f9015176b36fbc72902b1ca5c2a5f1d1717e1a1" dependencies = [ "datafusion-expr", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3553,7 +3553,7 @@ checksum = "8380a4224d5d2c3f84da4d764c4326cac62e9a1e3d4960442d29136fc07be863" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3564,7 +3564,7 @@ checksum = "9b565e60a9685cdf312c888665b5f8647ac692a7da7e058a5e2268a466da8eaf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3733,7 +3733,7 @@ dependencies = [ "stringcase", "strum", "strum_macros", - "syn 2.0.117", + "syn 2.0.118", "thiserror 2.0.18", ] @@ -3972,7 +3972,7 @@ dependencies = [ "darling 0.20.11", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3992,7 +3992,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core 0.20.2", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -4021,7 +4021,7 @@ checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "unicode-xid", ] @@ -4034,7 +4034,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version 0.4.1", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -4160,7 +4160,7 @@ checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -4183,7 +4183,7 @@ checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -4309,7 +4309,7 @@ dependencies = [ "enum-ordinalize", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -4366,7 +4366,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -4386,7 +4386,7 @@ checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -4406,7 +4406,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -4679,7 +4679,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "308530a56b099da144ebc5d8e179f343ad928fa2b3558d1eb3db9af18d6eff43" dependencies = [ "swc_macros_common", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -4805,7 +4805,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -5111,7 +5111,7 @@ checksum = "53010ccb100b96a67bc32c0175f0ed1426b31b655d562898e57325f81c023ac0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -5381,7 +5381,7 @@ dependencies = [ "indexmap 2.14.0", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -6140,7 +6140,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -6238,7 +6238,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" dependencies = [ "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -6461,7 +6461,7 @@ dependencies = [ "quote", "serde", "serde_json", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -6969,7 +6969,7 @@ dependencies = [ "manyhow-macros", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -7102,7 +7102,7 @@ checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -7197,7 +7197,7 @@ checksum = "e4db6d5580af57bf992f59068d4ea26fd518574ff48d7639b255a36f9de6e7e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -7236,7 +7236,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "termcolor", "thiserror 2.0.18", ] @@ -7432,7 +7432,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -7699,7 +7699,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -7897,7 +7897,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -8403,7 +8403,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -8494,7 +8494,7 @@ dependencies = [ "phf_shared 0.11.3", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -8553,7 +8553,7 @@ checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -8712,7 +8712,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -8764,7 +8764,7 @@ checksum = "07c277e4e643ef00c1233393c673f655e3672cf7eb3ba08a00bdd0ea59139b5f" dependencies = [ "proc-macro-rules-macros", "proc-macro2", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -8776,7 +8776,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -8872,7 +8872,7 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -9309,7 +9309,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76009fbe0614077fc1a2ce255e3a1881a2e3a3527097d5dc6d8212c585e7e38b" dependencies = [ "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -9369,7 +9369,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -9689,7 +9689,7 @@ dependencies = [ "proc-macro2", "quote", "serde_json", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -9728,7 +9728,7 @@ dependencies = [ "proc-macro2", "quote", "rquickjs-core", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -9801,7 +9801,7 @@ dependencies = [ "quote", "rust-embed-utils", "shellexpand", - "syn 2.0.117", + "syn 2.0.118", "walkdir", ] @@ -10288,7 +10288,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -10300,7 +10300,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -10477,7 +10477,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -10488,7 +10488,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -10542,7 +10542,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -10609,7 +10609,7 @@ dependencies = [ "darling 0.23.0", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -10662,7 +10662,7 @@ checksum = "94e153fc76e1c6a068703d6d29c508a0b15c061c4b7e43da59cc097bc342673c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -10987,7 +10987,7 @@ checksum = "da5fc6819faabb412da764b99d3b713bb55083c11e7e0c00144d386cd6a1939c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -11052,7 +11052,7 @@ dependencies = [ "quote", "sqlx-core", "sqlx-macros-core", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -11075,7 +11075,7 @@ dependencies = [ "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", - "syn 2.0.117", + "syn 2.0.118", "tokio", "url", ] @@ -11238,7 +11238,7 @@ checksum = "ae36a4951ca7bd1cfd991c241584a9824a70f6aff1e7d4f693fb3f2465e4030e" dependencies = [ "quote", "swc_macros_common", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -11297,7 +11297,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -11400,7 +11400,7 @@ dependencies = [ "proc-macro2", "quote", "swc_macros_common", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -11455,7 +11455,7 @@ checksum = "e276dc62c0a2625a560397827989c82a93fd545fcf6f7faec0935a82cc4ddbb8" dependencies = [ "proc-macro2", "swc_macros_common", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -11556,7 +11556,7 @@ dependencies = [ "proc-macro2", "quote", "swc_macros_common", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -11661,7 +11661,7 @@ checksum = "c16ce73424a6316e95e09065ba6a207eba7765496fed113702278b7711d4b632" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -11672,7 +11672,7 @@ checksum = "aae1efbaa74943dc5ad2a2fb16cbd78b77d7e4d63188f3c5b4df2b4dcd2faaae" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -11723,9 +11723,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.117" +version = "2.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" dependencies = [ "proc-macro2", "quote", @@ -11749,7 +11749,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -11778,7 +11778,7 @@ checksum = "181f22127402abcf8ee5c83ccd5b408933fec36a6095cf82cda545634692657e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -12095,7 +12095,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -12106,7 +12106,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -12355,7 +12355,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -12794,7 +12794,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -13047,7 +13047,7 @@ checksum = "cf808357c6ed7e13ba0f3277ec8d8f21b2d501274895104263985330c726c1c5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -13502,7 +13502,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "wasm-bindgen-shared", ] @@ -13537,7 +13537,7 @@ checksum = "ffc003a991398a8ee604a401e194b6b3a39677b3173d6e74495eb51b82e99a32" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -13572,7 +13572,7 @@ checksum = "a369369e4360c2884c3168d22bded735c43cccae97bbc147586d4b480edd138d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -13792,7 +13792,7 @@ dependencies = [ [[package]] name = "windmill" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "async-nats", @@ -13874,7 +13874,7 @@ dependencies = [ [[package]] name = "windmill-ai" -version = "1.726.1" +version = "1.728.1" dependencies = [ "async-stream", "async-trait", @@ -13907,7 +13907,7 @@ dependencies = [ [[package]] name = "windmill-alerting" -version = "1.726.1" +version = "1.728.1" dependencies = [ "axum 0.8.9", "chrono", @@ -13920,7 +13920,7 @@ dependencies = [ [[package]] name = "windmill-api" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "argon2", @@ -14058,7 +14058,7 @@ dependencies = [ [[package]] name = "windmill-api-agent-workers" -version = "1.726.1" +version = "1.728.1" dependencies = [ "axum 0.8.9", "chrono", @@ -14081,7 +14081,7 @@ dependencies = [ [[package]] name = "windmill-api-assets" -version = "1.726.1" +version = "1.728.1" dependencies = [ "axum 0.8.9", "chrono", @@ -14094,7 +14094,7 @@ dependencies = [ [[package]] name = "windmill-api-auth" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "axum 0.8.9", @@ -14120,7 +14120,7 @@ dependencies = [ [[package]] name = "windmill-api-client" -version = "1.726.1" +version = "1.728.1" dependencies = [ "reqwest 0.12.28", "serde", @@ -14130,7 +14130,7 @@ dependencies = [ [[package]] name = "windmill-api-configs" -version = "1.726.1" +version = "1.728.1" dependencies = [ "axum 0.8.9", "chrono", @@ -14147,7 +14147,7 @@ dependencies = [ [[package]] name = "windmill-api-debug" -version = "1.726.1" +version = "1.728.1" dependencies = [ "axum 0.8.9", "base64 0.22.1", @@ -14169,7 +14169,7 @@ dependencies = [ [[package]] name = "windmill-api-embeddings" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "axum 0.8.9", @@ -14192,7 +14192,7 @@ dependencies = [ [[package]] name = "windmill-api-flow-conversations" -version = "1.726.1" +version = "1.728.1" dependencies = [ "axum 0.8.9", "chrono", @@ -14208,7 +14208,7 @@ dependencies = [ [[package]] name = "windmill-api-flows" -version = "1.726.1" +version = "1.728.1" dependencies = [ "axum 0.8.9", "chrono", @@ -14229,7 +14229,7 @@ dependencies = [ [[package]] name = "windmill-api-groups" -version = "1.726.1" +version = "1.728.1" dependencies = [ "axum 0.8.9", "chrono", @@ -14250,7 +14250,7 @@ dependencies = [ [[package]] name = "windmill-api-inputs" -version = "1.726.1" +version = "1.728.1" dependencies = [ "axum 0.8.9", "chrono", @@ -14264,7 +14264,7 @@ dependencies = [ [[package]] name = "windmill-api-integration-tests" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "async-nats", @@ -14299,7 +14299,7 @@ dependencies = [ [[package]] name = "windmill-api-jobs" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "axum 0.8.9", @@ -14324,7 +14324,7 @@ dependencies = [ [[package]] name = "windmill-api-npm-proxy" -version = "1.726.1" +version = "1.728.1" dependencies = [ "axum 0.8.9", "flate2", @@ -14342,7 +14342,7 @@ dependencies = [ [[package]] name = "windmill-api-openapi" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "axum 0.8.9", @@ -14364,7 +14364,7 @@ dependencies = [ [[package]] name = "windmill-api-schedule" -version = "1.726.1" +version = "1.728.1" dependencies = [ "axum 0.8.9", "chrono", @@ -14384,7 +14384,7 @@ dependencies = [ [[package]] name = "windmill-api-scripts" -version = "1.726.1" +version = "1.728.1" dependencies = [ "axum 0.8.9", "chrono", @@ -14420,7 +14420,7 @@ dependencies = [ [[package]] name = "windmill-api-settings" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "axum 0.8.9", @@ -14448,7 +14448,7 @@ dependencies = [ [[package]] name = "windmill-api-sse" -version = "1.726.1" +version = "1.728.1" dependencies = [ "lazy_static", "serde", @@ -14460,7 +14460,7 @@ dependencies = [ [[package]] name = "windmill-api-users" -version = "1.726.1" +version = "1.728.1" dependencies = [ "argon2", "axum 0.8.9", @@ -14485,7 +14485,7 @@ dependencies = [ [[package]] name = "windmill-api-workers" -version = "1.726.1" +version = "1.728.1" dependencies = [ "axum 0.8.9", "chrono", @@ -14499,7 +14499,7 @@ dependencies = [ [[package]] name = "windmill-api-workspaces" -version = "1.726.1" +version = "1.728.1" dependencies = [ "axum 0.8.9", "chrono", @@ -14532,7 +14532,7 @@ dependencies = [ [[package]] name = "windmill-audit" -version = "1.726.1" +version = "1.728.1" dependencies = [ "chrono", "lazy_static", @@ -14546,7 +14546,7 @@ dependencies = [ [[package]] name = "windmill-autoscaling" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "axum 0.8.9", @@ -14565,7 +14565,7 @@ dependencies = [ [[package]] name = "windmill-common" -version = "1.726.1" +version = "1.728.1" dependencies = [ "aes-gcm", "aho-corasick", @@ -14667,7 +14667,7 @@ dependencies = [ [[package]] name = "windmill-dep-map" -version = "1.726.1" +version = "1.728.1" dependencies = [ "chrono", "itertools 0.14.0", @@ -14686,7 +14686,7 @@ dependencies = [ [[package]] name = "windmill-git-sync" -version = "1.726.1" +version = "1.728.1" dependencies = [ "regex", "serde", @@ -14701,7 +14701,7 @@ dependencies = [ [[package]] name = "windmill-indexer" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "astral-tokio-tar", @@ -14725,7 +14725,7 @@ dependencies = [ [[package]] name = "windmill-jseval" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "futures", @@ -14742,7 +14742,7 @@ dependencies = [ [[package]] name = "windmill-macros" -version = "1.726.1" +version = "1.728.1" dependencies = [ "itertools 0.14.0", "lazy_static", @@ -14753,12 +14753,12 @@ dependencies = [ "serde", "serde_derive", "serde_yml", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "windmill-mcp" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "async-trait", @@ -14779,7 +14779,7 @@ dependencies = [ [[package]] name = "windmill-native-triggers" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "async-trait", @@ -14810,7 +14810,7 @@ dependencies = [ [[package]] name = "windmill-oauth" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "arc-swap", @@ -14835,7 +14835,7 @@ dependencies = [ [[package]] name = "windmill-object-store" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "async-stream", @@ -14869,7 +14869,7 @@ dependencies = [ [[package]] name = "windmill-operator" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "futures", @@ -14887,7 +14887,7 @@ dependencies = [ [[package]] name = "windmill-parser" -version = "1.726.1" +version = "1.728.1" dependencies = [ "convert_case 0.6.0", "serde", @@ -14896,7 +14896,7 @@ dependencies = [ [[package]] name = "windmill-parser-bash" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "lazy_static", @@ -14908,7 +14908,7 @@ dependencies = [ [[package]] name = "windmill-parser-csharp" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "serde_json", @@ -14920,7 +14920,7 @@ dependencies = [ [[package]] name = "windmill-parser-go" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "gosyn", @@ -14932,7 +14932,7 @@ dependencies = [ [[package]] name = "windmill-parser-graphql" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "lazy_static", @@ -14944,7 +14944,7 @@ dependencies = [ [[package]] name = "windmill-parser-java" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "serde_json", @@ -14956,7 +14956,7 @@ dependencies = [ [[package]] name = "windmill-parser-nu" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "nu-parser", @@ -14967,7 +14967,7 @@ dependencies = [ [[package]] name = "windmill-parser-php" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "itertools 0.14.0", @@ -14978,7 +14978,7 @@ dependencies = [ [[package]] name = "windmill-parser-py" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "itertools 0.14.0", @@ -14990,7 +14990,7 @@ dependencies = [ [[package]] name = "windmill-parser-py-asset" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "rustpython-ast", @@ -15001,7 +15001,7 @@ dependencies = [ [[package]] name = "windmill-parser-py-imports" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "async-recursion", @@ -15023,7 +15023,7 @@ dependencies = [ [[package]] name = "windmill-parser-r" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "serde_json", @@ -15035,7 +15035,7 @@ dependencies = [ [[package]] name = "windmill-parser-ruby" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "lazy_static", @@ -15049,7 +15049,7 @@ dependencies = [ [[package]] name = "windmill-parser-rust" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "convert_case 0.6.0", @@ -15059,14 +15059,14 @@ dependencies = [ "quote", "regex", "serde_json", - "syn 2.0.117", + "syn 2.0.118", "toml", "windmill-parser", ] [[package]] name = "windmill-parser-sql" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "lazy_static", @@ -15079,7 +15079,7 @@ dependencies = [ [[package]] name = "windmill-parser-sql-asset" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "serde", @@ -15091,7 +15091,7 @@ dependencies = [ [[package]] name = "windmill-parser-ts" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "lazy_static", @@ -15109,7 +15109,7 @@ dependencies = [ [[package]] name = "windmill-parser-ts-asset" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "serde-wasm-bindgen", @@ -15125,7 +15125,7 @@ dependencies = [ [[package]] name = "windmill-parser-wac" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "rustpython-ast", @@ -15141,7 +15141,7 @@ dependencies = [ [[package]] name = "windmill-parser-yaml" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "serde", @@ -15152,7 +15152,7 @@ dependencies = [ [[package]] name = "windmill-queue" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "async-recursion", @@ -15190,7 +15190,7 @@ dependencies = [ [[package]] name = "windmill-runtime-nativets" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "const_format", @@ -15211,6 +15211,7 @@ dependencies = [ "futures", "itertools 0.14.0", "lazy_static", + "rcgen", "regex", "reqwest 0.13.1", "rustls 0.23.35", @@ -15228,7 +15229,7 @@ dependencies = [ [[package]] name = "windmill-sql-datatype-parser-wasm" -version = "1.726.1" +version = "1.728.1" dependencies = [ "getrandom 0.3.4", "wasm-bindgen", @@ -15239,7 +15240,7 @@ dependencies = [ [[package]] name = "windmill-store" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "async-recursion", @@ -15271,7 +15272,7 @@ dependencies = [ [[package]] name = "windmill-test-utils" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "async-trait", @@ -15295,7 +15296,7 @@ dependencies = [ [[package]] name = "windmill-trigger" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "async-trait", @@ -15328,7 +15329,7 @@ dependencies = [ [[package]] name = "windmill-trigger-azure" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "async-trait", @@ -15361,7 +15362,7 @@ dependencies = [ [[package]] name = "windmill-trigger-email" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "async-trait", @@ -15381,7 +15382,7 @@ dependencies = [ [[package]] name = "windmill-trigger-gcp" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "async-trait", @@ -15415,7 +15416,7 @@ dependencies = [ [[package]] name = "windmill-trigger-http" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "async-trait", @@ -15451,7 +15452,7 @@ dependencies = [ [[package]] name = "windmill-trigger-kafka" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "async-trait", @@ -15474,7 +15475,7 @@ dependencies = [ [[package]] name = "windmill-trigger-mqtt" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "async-trait", @@ -15498,7 +15499,7 @@ dependencies = [ [[package]] name = "windmill-trigger-nats" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "async-nats", @@ -15522,7 +15523,7 @@ dependencies = [ [[package]] name = "windmill-trigger-postgres" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "async-trait", @@ -15557,7 +15558,7 @@ dependencies = [ [[package]] name = "windmill-trigger-sqs" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "async-trait", @@ -15585,7 +15586,7 @@ dependencies = [ [[package]] name = "windmill-trigger-websocket" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "async-trait", @@ -15610,7 +15611,7 @@ dependencies = [ [[package]] name = "windmill-types" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "bitflags 2.13.0", @@ -15629,7 +15630,7 @@ dependencies = [ [[package]] name = "windmill-worker" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "async-once-cell", @@ -15739,7 +15740,7 @@ dependencies = [ [[package]] name = "windmill-worker-volumes" -version = "1.726.1" +version = "1.728.1" dependencies = [ "bytes", "futures", @@ -15866,7 +15867,7 @@ checksum = "f6fc35f58ecd95a9b71c4f2329b911016e6bec66b3f2e6a4aad86bd2e99e2f9b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -15877,7 +15878,7 @@ checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -15888,7 +15889,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -15899,7 +15900,7 @@ checksum = "08990546bf4edef8f431fa6326e032865f27138718c587dc21bc0265bbcb57cc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -15910,7 +15911,7 @@ checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -15921,7 +15922,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -16388,7 +16389,7 @@ dependencies = [ "heck", "indexmap 2.14.0", "prettyplease", - "syn 2.0.117", + "syn 2.0.118", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -16404,7 +16405,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -16547,7 +16548,7 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "synstructure", ] @@ -16568,7 +16569,7 @@ checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -16588,7 +16589,7 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "synstructure", ] @@ -16628,7 +16629,7 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 44bdedcd11..5d2c556965 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "windmill" -version = "1.726.1" +version = "1.728.1" authors.workspace = true edition.workspace = true @@ -87,7 +87,7 @@ members = [ exclude = ["./windmill-duckdb-ffi-internal", "./parsers/windmill-parser-wasm"] [workspace.package] -version = "1.726.1" +version = "1.728.1" authors = ["Ruben Fiszel "] edition = "2021" diff --git a/backend/migrations/20260616120048_backfill_legacy_draft_emails.down.sql b/backend/migrations/20260616120048_backfill_legacy_draft_emails.down.sql new file mode 100644 index 0000000000..a0ef6d0cfe --- /dev/null +++ b/backend/migrations/20260616120048_backfill_legacy_draft_emails.down.sql @@ -0,0 +1,3 @@ +-- Irreversible data backfill: once an email is attached, the row is +-- indistinguishable from a draft that was always per-user owned, so the +-- original NULL state cannot be reconstructed. No-op on revert. diff --git a/backend/migrations/20260616120048_backfill_legacy_draft_emails.up.sql b/backend/migrations/20260616120048_backfill_legacy_draft_emails.up.sql new file mode 100644 index 0000000000..cdf4b19c79 --- /dev/null +++ b/backend/migrations/20260616120048_backfill_legacy_draft_emails.up.sql @@ -0,0 +1,28 @@ +-- Backfill the owner `email` on legacy drafts (rows persisted before per-user +-- sync, hence `email IS NULL`). A user-owned draft path is `u//...`, +-- so resolve `` against `usr` for the same workspace and adopt that +-- user's email. +-- +-- Guards: +-- - the resolved email must exist in `password` (the `draft_password_fkey` +-- target), or the UPDATE would violate the FK; +-- - skip rows that would collide with an existing per-user draft at the same +-- (workspace_id, path, typ, email) under the `draft_pkey_with_user` partial +-- unique index — the per-user row is authoritative, so the legacy row is +-- left untouched. +UPDATE draft d +SET email = u.email +FROM usr u +WHERE d.email IS NULL + AND split_part(d.path, '/', 1) = 'u' + AND split_part(d.path, '/', 2) <> '' + AND u.workspace_id = d.workspace_id + AND u.username = split_part(d.path, '/', 2) + AND EXISTS (SELECT 1 FROM password p WHERE p.email = u.email) + AND NOT EXISTS ( + SELECT 1 FROM draft d2 + WHERE d2.workspace_id = d.workspace_id + AND d2.path = d.path + AND d2.typ = d.typ + AND d2.email = u.email + ); diff --git a/backend/migrations/20260616210529_cleanup_orphaned_workspace_diff.down.sql b/backend/migrations/20260616210529_cleanup_orphaned_workspace_diff.down.sql new file mode 100644 index 0000000000..6e1600b72a --- /dev/null +++ b/backend/migrations/20260616210529_cleanup_orphaned_workspace_diff.down.sql @@ -0,0 +1,3 @@ +-- Irreversible data cleanup: deleted orphaned rows and reset cached verdicts +-- cannot be reconstructed. No-op on rollback. +SELECT 1; diff --git a/backend/migrations/20260616210529_cleanup_orphaned_workspace_diff.up.sql b/backend/migrations/20260616210529_cleanup_orphaned_workspace_diff.up.sql new file mode 100644 index 0000000000..2d4f82ab83 --- /dev/null +++ b/backend/migrations/20260616210529_cleanup_orphaned_workspace_diff.up.sql @@ -0,0 +1,31 @@ +-- workspace_diff / skip_workspace_diff_tally are keyed by workspace id with no FK +-- cascade, so until the matching delete_workspace cleanup landed, deleting a fork +-- left its cached diff rows behind. Workspace ids are reused (recreating a fork +-- under the same name), so those orphaned rows leaked onto the new fork and +-- produced a spurious "changes not visible" warning that hid the deploy button. + +-- Drop rows referencing workspaces that no longer exist (leftovers from past deletes). +DELETE FROM workspace_diff +WHERE source_workspace_id NOT IN (SELECT id FROM workspace) + OR fork_workspace_id NOT IN (SELECT id FROM workspace); + +DELETE FROM skip_workspace_diff_tally +WHERE workspace_id NOT IN (SELECT id FROM workspace); + +-- Reused-id victims keep a skip-tally row pointing at a LIVE workspace, so the +-- orphan cleanup above can't reach them. compare_workspaces consults +-- skip_workspace_diff_tally first and short-circuits to an empty comparison, so a +-- stale skip row would also defeat the has_changes reset below. A genuinely +-- skipped workspace is excluded from tallying and therefore never has +-- workspace_diff rows — so a skip row that coexists with workspace_diff rows for +-- that id is provably leaked from a previous occupant of a reused id. Drop those. +DELETE FROM skip_workspace_diff_tally s +WHERE EXISTS ( + SELECT 1 FROM workspace_diff d WHERE d.fork_workspace_id = s.workspace_id +); + +-- Remaining reused-id victims keep workspace_diff rows pointing at live +-- workspaces, so they can't be told apart from valid cache. Reset the cached +-- verdict to force a recompute on the next compare; compare_workspaces +-- re-evaluates NULL rows and corrects or deletes them. +UPDATE workspace_diff SET has_changes = NULL; diff --git a/backend/parsers/windmill-parser-wasm/Cargo.lock b/backend/parsers/windmill-parser-wasm/Cargo.lock index 31fc72031f..3acc018c14 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.726.1" +version = "1.728.1" dependencies = [ "aho-corasick", "anyhow", @@ -6272,7 +6272,7 @@ dependencies = [ [[package]] name = "windmill-macros" -version = "1.726.1" +version = "1.728.1" dependencies = [ "proc-macro2", "quote", @@ -6284,7 +6284,7 @@ dependencies = [ [[package]] name = "windmill-parser" -version = "1.726.1" +version = "1.728.1" dependencies = [ "convert_case", "serde", @@ -6293,7 +6293,7 @@ dependencies = [ [[package]] name = "windmill-parser-bash" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "lazy_static", @@ -6305,7 +6305,7 @@ dependencies = [ [[package]] name = "windmill-parser-csharp" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "serde_json", @@ -6317,7 +6317,7 @@ dependencies = [ [[package]] name = "windmill-parser-go" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "gosyn", @@ -6329,7 +6329,7 @@ dependencies = [ [[package]] name = "windmill-parser-graphql" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "lazy_static", @@ -6341,7 +6341,7 @@ dependencies = [ [[package]] name = "windmill-parser-java" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "serde_json", @@ -6353,7 +6353,7 @@ dependencies = [ [[package]] name = "windmill-parser-nu" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "nu-parser", @@ -6364,7 +6364,7 @@ dependencies = [ [[package]] name = "windmill-parser-php" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "itertools 0.14.0", @@ -6375,7 +6375,7 @@ dependencies = [ [[package]] name = "windmill-parser-py" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "itertools 0.14.0", @@ -6387,7 +6387,7 @@ dependencies = [ [[package]] name = "windmill-parser-py-asset" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "rustpython-ast", @@ -6398,7 +6398,7 @@ dependencies = [ [[package]] name = "windmill-parser-py-imports" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "async-recursion", @@ -6420,7 +6420,7 @@ dependencies = [ [[package]] name = "windmill-parser-r" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "serde_json", @@ -6432,7 +6432,7 @@ dependencies = [ [[package]] name = "windmill-parser-ruby" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "lazy_static", @@ -6446,7 +6446,7 @@ dependencies = [ [[package]] name = "windmill-parser-rust" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "convert_case", @@ -6463,7 +6463,7 @@ dependencies = [ [[package]] name = "windmill-parser-sql" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "lazy_static", @@ -6476,7 +6476,7 @@ dependencies = [ [[package]] name = "windmill-parser-sql-asset" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "serde", @@ -6488,7 +6488,7 @@ dependencies = [ [[package]] name = "windmill-parser-ts" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "lazy_static", @@ -6506,7 +6506,7 @@ dependencies = [ [[package]] name = "windmill-parser-ts-asset" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "serde-wasm-bindgen", @@ -6522,7 +6522,7 @@ dependencies = [ [[package]] name = "windmill-parser-wac" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "rustpython-ast", @@ -6538,7 +6538,7 @@ dependencies = [ [[package]] name = "windmill-parser-wasm" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "getrandom 0.2.17", @@ -6570,7 +6570,7 @@ dependencies = [ [[package]] name = "windmill-parser-yaml" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "serde", @@ -6581,7 +6581,7 @@ dependencies = [ [[package]] name = "windmill-types" -version = "1.726.1" +version = "1.728.1" dependencies = [ "anyhow", "bitflags", diff --git a/backend/parsers/windmill-parser-wasm/Cargo.toml b/backend/parsers/windmill-parser-wasm/Cargo.toml index 5425aa9b35..686be7d91a 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.726.1" +version = "1.728.1" edition = "2021" authors = ["Ruben Fiszel "] diff --git a/backend/windmill-api-integration-tests/tests/workspace_comparison.rs b/backend/windmill-api-integration-tests/tests/workspace_comparison.rs index a0f7f7d460..88ffe6bb26 100644 --- a/backend/windmill-api-integration-tests/tests/workspace_comparison.rs +++ b/backend/windmill-api-integration-tests/tests/workspace_comparison.rs @@ -1336,3 +1336,188 @@ async fn test_compare_workspaces_fork_only_folder_visibility( Ok(()) } + +/// Regression test: deleting a fork must purge its `workspace_diff` and +/// `skip_workspace_diff_tally` rows. These tables are keyed by workspace id with +/// no FK cascade, and a fork id is reused when a fork is deleted and recreated +/// under the same name. If the cached diff rows survive the delete, they leak +/// onto the next fork sharing that id and produce a spurious "changes not +/// visible" warning that hides the deploy button (WIN-2066). +#[sqlx::test(migrations = "../migrations", fixtures("base"))] +async fn test_delete_fork_purges_workspace_diff(db: Pool) -> anyhow::Result<()> { + initialize_tracing().await; + + let server = ApiServer::start(db.clone()).await?; + let port = server.addr.port(); + let client = windmill_api_client::create_client( + &format!("http://localhost:{port}"), + "SECRET_TOKEN".to_string(), + ); + let base_url = format!("http://localhost:{port}/api"); + + // Create the fork so the caller owns it (delete is authorized for fork owners). + let fork_response = client + .client() + .post(&format!( + "{base_url}/w/test-workspace/workspaces/create_fork" + )) + .json(&json!({ + "id": "wm-fork-test-workspace", + "name": "Test Fork", + "color": "#0000ff" + })) + .send() + .await?; + assert!( + fork_response.status().is_success(), + "Fork creation should succeed: {}", + fork_response.status() + ); + + // Seed cached diff state for the fork: as the fork side of a pair, as the + // source side of a pair, and a skip-tally row. + sqlx::query!( + "INSERT INTO workspace_diff + (source_workspace_id, fork_workspace_id, path, kind, ahead, behind, has_changes, exists_in_source, exists_in_fork) + VALUES ('test-workspace', 'wm-fork-test-workspace', 'f/shared/leaky', 'script', 1, 0, true, true, true)" + ) + .execute(&db) + .await?; + sqlx::query!( + "INSERT INTO workspace_diff + (source_workspace_id, fork_workspace_id, path, kind, ahead, behind, has_changes) + VALUES ('wm-fork-test-workspace', 'test-workspace', 'f/shared/other', 'script', 0, 1, true)" + ) + .execute(&db) + .await?; + sqlx::query!( + "INSERT INTO skip_workspace_diff_tally (workspace_id) VALUES ('wm-fork-test-workspace')" + ) + .execute(&db) + .await?; + + // Delete the fork through the real handler. + let delete_response = client + .client() + .delete(&format!("{base_url}/workspaces/delete/wm-fork-test-workspace")) + .send() + .await?; + assert!( + delete_response.status().is_success(), + "Fork deletion should succeed: {}", + delete_response.status() + ); + + let leftover_diffs = sqlx::query_scalar!( + "SELECT COUNT(*) FROM workspace_diff + WHERE source_workspace_id = 'wm-fork-test-workspace' + OR fork_workspace_id = 'wm-fork-test-workspace'" + ) + .fetch_one(&db) + .await?; + assert_eq!( + leftover_diffs, + Some(0), + "workspace_diff rows referencing the deleted fork must be purged" + ); + + let leftover_skip = sqlx::query_scalar!( + "SELECT COUNT(*) FROM skip_workspace_diff_tally WHERE workspace_id = 'wm-fork-test-workspace'" + ) + .fetch_one(&db) + .await?; + assert_eq!( + leftover_skip, + Some(0), + "skip_workspace_diff_tally row for the deleted fork must be purged" + ); + + Ok(()) +} + +/// Regression test: creating a fork must start with clean diff state even when +/// the (reusable) fork id was previously occupied by a deleted fork. Stale +/// `workspace_diff` / `skip_workspace_diff_tally` rows left behind by an earlier +/// occupant would otherwise leak onto the new fork — a stale skip row suppresses +/// comparison entirely, and stale diff rows produce a spurious "changes not +/// visible" warning that hides the deploy button (WIN-2066). +#[sqlx::test(migrations = "../migrations", fixtures("base"))] +async fn test_create_fork_purges_stale_diff_state(db: Pool) -> anyhow::Result<()> { + initialize_tracing().await; + + let server = ApiServer::start(db.clone()).await?; + let port = server.addr.port(); + let client = windmill_api_client::create_client( + &format!("http://localhost:{port}"), + "SECRET_TOKEN".to_string(), + ); + let base_url = format!("http://localhost:{port}/api"); + + // Simulate leftovers from a previously deleted fork that reused this id: + // diff rows on both sides plus a skip-tally row, with no workspace yet. + sqlx::query!( + "INSERT INTO workspace_diff + (source_workspace_id, fork_workspace_id, path, kind, ahead, behind, has_changes, exists_in_source, exists_in_fork) + VALUES ('test-workspace', 'wm-fork-test-workspace', 'f/shared/leaky', 'script', 1, 0, true, true, true)" + ) + .execute(&db) + .await?; + sqlx::query!( + "INSERT INTO workspace_diff + (source_workspace_id, fork_workspace_id, path, kind, ahead, behind, has_changes) + VALUES ('wm-fork-test-workspace', 'test-workspace', 'f/shared/other', 'script', 0, 1, true)" + ) + .execute(&db) + .await?; + sqlx::query!( + "INSERT INTO skip_workspace_diff_tally (workspace_id) VALUES ('wm-fork-test-workspace')" + ) + .execute(&db) + .await?; + + // Create the fork reusing that id; the conflict check passes because no + // workspace row exists for it. + let fork_response = client + .client() + .post(&format!( + "{base_url}/w/test-workspace/workspaces/create_fork" + )) + .json(&json!({ + "id": "wm-fork-test-workspace", + "name": "Test Fork", + "color": "#0000ff" + })) + .send() + .await?; + assert!( + fork_response.status().is_success(), + "Fork creation should succeed: {}", + fork_response.status() + ); + + let leftover_diffs = sqlx::query_scalar!( + "SELECT COUNT(*) FROM workspace_diff + WHERE source_workspace_id = 'wm-fork-test-workspace' + OR fork_workspace_id = 'wm-fork-test-workspace'" + ) + .fetch_one(&db) + .await?; + assert_eq!( + leftover_diffs, + Some(0), + "stale workspace_diff rows must be purged on fork creation" + ); + + let leftover_skip = sqlx::query_scalar!( + "SELECT COUNT(*) FROM skip_workspace_diff_tally WHERE workspace_id = 'wm-fork-test-workspace'" + ) + .fetch_one(&db) + .await?; + assert_eq!( + leftover_skip, + Some(0), + "stale skip_workspace_diff_tally row must be purged on fork creation" + ); + + Ok(()) +} diff --git a/backend/windmill-api-workspaces/src/workspaces.rs b/backend/windmill-api-workspaces/src/workspaces.rs index 265a74d1ed..788c090aa2 100644 --- a/backend/windmill-api-workspaces/src/workspaces.rs +++ b/backend/windmill-api-workspaces/src/workspaces.rs @@ -3661,6 +3661,30 @@ async fn check_fork_w_id_conflict(db: &DB, w_id: &str) -> Result<()> { } } +/// A fork id is reusable: it is freed when a fork is deleted and can be claimed +/// again under the same name. `workspace_diff` and `skip_workspace_diff_tally` +/// are keyed by workspace id with no FK cascade, so a freshly created fork could +/// inherit cached diff state from a previous occupant of its id — a stale skip +/// row suppresses comparison entirely, and stale workspace_diff rows produce a +/// spurious "changes not visible" warning that hides the deploy button. Clear +/// both so a new fork always starts with clean diff state, regardless of how the +/// id was freed. +async fn purge_stale_fork_diff_state(db: &DB, fork_id: &str) -> Result<()> { + sqlx::query!( + "DELETE FROM workspace_diff WHERE source_workspace_id = $1 OR fork_workspace_id = $1", + fork_id + ) + .execute(db) + .await?; + sqlx::query!( + "DELETE FROM skip_workspace_diff_tally WHERE workspace_id = $1", + fork_id + ) + .execute(db) + .await?; + Ok(()) +} + lazy_static::lazy_static! { pub static ref CREATE_WORKSPACE_REQUIRE_SUPERADMIN: bool = { @@ -4864,6 +4888,7 @@ async fn create_workspace_fork_branch( // Fail before creating any git branch so a name conflict doesn't leave a // dangling branch on the synced repos. check_fork_w_id_conflict(&db, &nw.id).await?; + purge_stale_fork_diff_state(&db, &nw.id).await?; Ok(Json( handle_fork_branch_creation(&authed.email, &authed.username, &db, &w_id, &nw.id).await?, @@ -5008,6 +5033,7 @@ async fn create_workspace_fork( // re-using a taken (possibly archived) fork id reports the actual // conflict instead of a misleading "maximum number of workspaces" error. check_fork_w_id_conflict(&db, &nw.id).await?; + purge_stale_fork_diff_state(&db, &nw.id).await?; #[cfg(not(feature = "enterprise"))] _check_nb_of_workspaces(&db).await?; diff --git a/backend/windmill-api-workspaces/src/workspaces_extra.rs b/backend/windmill-api-workspaces/src/workspaces_extra.rs index 4c6b57172f..aa64514b4c 100644 --- a/backend/windmill-api-workspaces/src/workspaces_extra.rs +++ b/backend/windmill-api-workspaces/src/workspaces_extra.rs @@ -868,6 +868,25 @@ pub(crate) async fn delete_workspace( .execute(&mut *tx) .await?; + // workspace_diff and skip_workspace_diff_tally are keyed by workspace id with no + // FK cascade. A fork id is reused when a fork is deleted and recreated under the + // same name, so leaving these rows behind leaks the previous fork's cached diff + // verdicts onto the new fork — causing a spurious "changes not visible" warning + // that hides the deploy button. + sqlx::query!( + "DELETE FROM workspace_diff WHERE source_workspace_id = $1 OR fork_workspace_id = $1", + &w_id + ) + .execute(&mut *tx) + .await?; + + sqlx::query!( + "DELETE FROM skip_workspace_diff_tally WHERE workspace_id = $1", + &w_id + ) + .execute(&mut *tx) + .await?; + sqlx::query!("DELETE FROM workspace WHERE id = $1", &w_id) .execute(&mut *tx) .await?; diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index e99a1917dc..1f59692b52 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -1,7 +1,7 @@ openapi: "3.0.3" info: - version: 1.726.1 + version: 1.728.1 title: Windmill API contact: @@ -21533,6 +21533,13 @@ components: Workspace username of the draft owner. `null` represents the legacy workspace-level (NULL-email) row. Emails never leave the server. + draft_saved_at: + type: string + format: date-time + description: | + When this user's draft was last saved (`draft.created_at`), + surfaced in the fork modal as "Last updated". + required: [draft_saved_at] required: [is_draft] UserDraftItemKind: type: string diff --git a/backend/windmill-common/src/user_drafts.rs b/backend/windmill-common/src/user_drafts.rs index 6ae4a2ec8d..620cbe2a5f 100644 --- a/backend/windmill-common/src/user_drafts.rs +++ b/backend/windmill-common/src/user_drafts.rs @@ -188,6 +188,9 @@ pub struct WithDraftQuery { pub struct OtherDraftUser { /// `None` represents a legacy workspace-level draft (no owner). pub username: Option, + /// When this user's draft was last saved (the `draft.created_at` upsert + /// timestamp), surfaced in the fork modal as "Last updated". + pub draft_saved_at: DateTime, } /// Response wrapper: the deployed entity untouched plus the authed user's @@ -240,7 +243,8 @@ async fn fetch_other_drafts_users( // row keeps `username = None` (its `d.email` is NULL, so the CASE yields NULL). let rows = sqlx::query_as!( OtherDraftUser, - r#"SELECT COALESCE(u.username, CASE WHEN d.workspace_id = 'admins' THEN d.email END) as "username?" + r#"SELECT COALESCE(u.username, CASE WHEN d.workspace_id = 'admins' THEN d.email END) as "username?", + d.created_at as "draft_saved_at!" FROM draft d LEFT JOIN usr u ON u.workspace_id = d.workspace_id diff --git a/backend/windmill-runtime-nativets/Cargo.toml b/backend/windmill-runtime-nativets/Cargo.toml index edad615ae5..d3686d2264 100644 --- a/backend/windmill-runtime-nativets/Cargo.toml +++ b/backend/windmill-runtime-nativets/Cargo.toml @@ -48,6 +48,9 @@ futures.workspace = true sqlx.workspace = true rustls.workspace = true +[dev-dependencies] +rcgen = "0.13.2" + [build-dependencies] deno_fetch.workspace = true deno_webidl.workspace = true diff --git a/backend/windmill-runtime-nativets/src/cert_tests.rs b/backend/windmill-runtime-nativets/src/cert_tests.rs new file mode 100644 index 0000000000..677dc1be9b --- /dev/null +++ b/backend/windmill-runtime-nativets/src/cert_tests.rs @@ -0,0 +1,184 @@ +//! Regression tests for custom CA support in the in-process nativets fetch +//! runtime (WIN-2055). +//! +//! `deno_fetch` with `root_cert_store_provider: None` trusts only the Mozilla +//! webpki roots, so scripts calling internal APIs fronted by a corporate CA +//! failed with `invalid peer certificate: UnknownIssuer`. The provider built by +//! `build_native_root_cert_store_provider` merges CAs from `DENO_CERT` / +//! `SSL_CERT_FILE` / `NODE_EXTRA_CA_CERTS` / `DENO_TLS_CA_STORE=system` into the +//! default store. These tests pin that behaviour. + +use crate::{build_native_root_cert_store_provider, load_pem_certs_from_path}; + +/// The CA-related env vars the provider inspects. Cleared around each test so a +/// CI runner that happens to set one of them can't perturb the result. +const CA_ENV_VARS: &[&str] = &[ + "DENO_CERT", + "SSL_CERT_FILE", + "NODE_EXTRA_CA_CERTS", + "DENO_TLS_CA_STORE", +]; + +fn write_test_ca(suffix: &str) -> std::path::PathBuf { + let cert = rcgen::generate_simple_self_signed(vec!["windmill-test-ca".to_string()]) + .expect("generate self-signed cert"); + let pem = cert.cert.pem(); + let path = std::env::temp_dir().join(format!( + "windmill-nativets-ca-{}-{}.pem", + std::process::id(), + suffix + )); + std::fs::write(&path, pem).expect("write cert"); + path +} + +/// Serializes the env-mutating tests against each other — env is process-global, +/// so concurrent `set_var`/`remove_var` would otherwise interleave. +static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(()); + +fn with_cleared_ca_env(f: impl FnOnce() -> T) -> T { + let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner()); + let saved: Vec<(&str, Option)> = CA_ENV_VARS + .iter() + .map(|k| (*k, std::env::var(k).ok())) + .collect(); + for k in CA_ENV_VARS { + std::env::remove_var(k); + } + let out = f(); + for (k, v) in saved { + match v { + Some(v) => std::env::set_var(k, v), + None => std::env::remove_var(k), + } + } + out +} + +#[test] +fn load_pem_certs_parses_self_signed_cert() { + let path = write_test_ca("load"); + let certs = load_pem_certs_from_path(path.to_str().unwrap()).expect("load certs"); + assert_eq!(certs.len(), 1, "expected exactly one cert in the bundle"); + let _ = std::fs::remove_file(&path); +} + +#[test] +fn load_pem_certs_errors_on_missing_file() { + let missing = std::env::temp_dir().join("windmill-nativets-does-not-exist.pem"); + assert!(load_pem_certs_from_path(missing.to_str().unwrap()).is_err()); +} + +#[test] +fn no_ca_env_yields_no_provider() { + // serialize against the env-mutating tests via the shared guard + with_cleared_ca_env(|| { + assert!( + build_native_root_cert_store_provider().is_none(), + "without any CA env var the provider must stay None (default-only behaviour)" + ); + }); +} + +#[test] +fn ssl_cert_file_adds_custom_root() { + let path = write_test_ca("ssl"); + with_cleared_ca_env(|| { + std::env::set_var("SSL_CERT_FILE", &path); + let provider = build_native_root_cert_store_provider() + .expect("a custom CA was configured, provider must be Some"); + let store = provider.get_or_try_init().expect("store init"); + let default_len = deno_tls::create_default_root_cert_store().len(); + assert_eq!( + store.len(), + default_len + 1, + "custom CA should be added on top of the Mozilla defaults" + ); + }); + let _ = std::fs::remove_file(&path); +} + +#[test] +fn node_extra_ca_certs_adds_custom_root() { + let path = write_test_ca("node"); + with_cleared_ca_env(|| { + std::env::set_var("NODE_EXTRA_CA_CERTS", &path); + let provider = build_native_root_cert_store_provider() + .expect("provider must be Some for NODE_EXTRA_CA_CERTS"); + let store = provider.get_or_try_init().expect("store init"); + assert_eq!( + store.len(), + deno_tls::create_default_root_cert_store().len() + 1 + ); + }); + let _ = std::fs::remove_file(&path); +} + +#[test] +fn multiple_ca_env_vars_pointing_at_same_file_dedupe_to_one_root() { + // The tracing proxy points SSL_CERT_FILE, NODE_EXTRA_CA_CERTS and DENO_CERT at + // the same bundle; the path dedupe in build_native_root_cert_store_provider + // loads it once, so the store grows by exactly one (rustls' add does not dedupe). + let path = write_test_ca("dupe"); + with_cleared_ca_env(|| { + std::env::set_var("SSL_CERT_FILE", &path); + std::env::set_var("NODE_EXTRA_CA_CERTS", &path); + std::env::set_var("DENO_CERT", &path); + let provider = build_native_root_cert_store_provider().expect("provider must be Some"); + let store = provider.get_or_try_init().expect("store init"); + assert_eq!( + store.len(), + deno_tls::create_default_root_cert_store().len() + 1 + ); + }); + let _ = std::fs::remove_file(&path); +} + +#[test] +fn worker_config_env_var_adds_custom_root() { + // A CA configured only through the worker-group config (DB `env_vars_static` + // / allowlisted forwarded vars) lands in `WORKER_CONFIG.env_vars`, not the + // worker's own process env. Child Deno/Bun jobs receive it via `.envs(...)`; + // nativets must pick it up from the same place. Regression for the in-process + // path missing that source. + use windmill_common::worker::WORKER_CONFIG; + + let path = write_test_ca("workercfg"); + with_cleared_ca_env(|| { + let prev = WORKER_CONFIG.load_full(); + let mut cfg = (*prev).clone(); + cfg.env_vars.insert( + "SSL_CERT_FILE".to_string(), + path.to_string_lossy().into_owned(), + ); + WORKER_CONFIG.store(std::sync::Arc::new(cfg)); + + let provider = build_native_root_cert_store_provider(); + // restore before asserting so a failure can't leak the mutated global + WORKER_CONFIG.store(prev); + + let provider = provider.expect("worker-config CA must produce a provider"); + let store = provider.get_or_try_init().expect("store init"); + assert_eq!( + store.len(), + deno_tls::create_default_root_cert_store().len() + 1 + ); + }); + let _ = std::fs::remove_file(&path); +} + +#[test] +fn deno_cert_adds_custom_root() { + let path = write_test_ca("deno"); + with_cleared_ca_env(|| { + std::env::set_var("DENO_CERT", &path); + let provider = + build_native_root_cert_store_provider().expect("provider must be Some for DENO_CERT"); + let store = provider.get_or_try_init().expect("store init"); + assert_eq!( + store.len(), + deno_tls::create_default_root_cert_store().len() + 1 + ); + }); + let _ = std::fs::remove_file(&path); +} diff --git a/backend/windmill-runtime-nativets/src/lib.rs b/backend/windmill-runtime-nativets/src/lib.rs index 52e23f11da..2058268256 100644 --- a/backend/windmill-runtime-nativets/src/lib.rs +++ b/backend/windmill-runtime-nativets/src/lib.rs @@ -18,6 +18,9 @@ pub use dedicated::{ExecutingIsolate, PrewarmedIsolate, PrewarmedResult}; #[cfg(test)] mod smoke_tests; +#[cfg(test)] +mod cert_tests; + use std::{ borrow::Cow, cell::RefCell, @@ -35,8 +38,10 @@ use deno_core::{ v8::{self, IsolateHandle}, Extension, JsRuntime, OpState, PollEventLoopOptions, RuntimeOptions, }; +use deno_error::JsErrorBox; use deno_fetch::FetchPermissions; use deno_net::NetPermissions; +use deno_tls::{rustls::pki_types::CertificateDer, rustls::RootCertStore, RootCertStoreProvider}; use deno_web::{BlobStore, TimersPermission}; use itertools::Itertools; use lazy_static::lazy_static; @@ -214,6 +219,123 @@ lazy_static! { Regex::new(r"^(https?)://(([^:@\s]+):([^:@\s]+)@)?([^:@\s]+)(:(\d+))?$").unwrap(); } +lazy_static! { + /// Root cert store for the in-process nativets fetch runtime. + /// + /// Unlike the Deno/Bun executors, nativets never spawns a child process, so + /// the CA env vars those executors forward (`DENO_CERT`, `DENO_TLS_CA_STORE`, + /// `SSL_CERT_FILE`/`NODE_EXTRA_CA_CERTS`) are never consumed by deno's CLI + /// layer. `deno_fetch` with `root_cert_store_provider: None` falls back to + /// the Mozilla webpki roots only, so corporate CAs fail with `UnknownIssuer`. + /// We read those env vars here and merge the certs into the default store. + /// + /// Snapshotted once for the process lifetime, like the Deno executor's + /// `DENO_CERT`/`DENO_TLS_CA_STORE` lazy statics (`deno_executor.rs`): the + /// fetch root store is shared across all (potentially prewarmed) isolates, so + /// per-job CA reconfiguration is out of scope. A later `WORKER_CONFIG` reload + /// is not picked up until the process restarts. + static ref NATIVE_ROOT_CERT_STORE_PROVIDER: Option> = + build_native_root_cert_store_provider(); +} + +struct NativeRootCertStoreProvider { + store: RootCertStore, +} + +impl RootCertStoreProvider for NativeRootCertStoreProvider { + fn get_or_try_init(&self) -> Result<&RootCertStore, JsErrorBox> { + Ok(&self.store) + } +} + +/// Resolve a CA-related env var the same way the child executors see it: the +/// worker's own process env, then the worker-group config (`env_vars_allowlist` +/// forwarded values + DB `env_vars_static` literals, resolved into +/// `WORKER_CONFIG.env_vars`). Child Deno/Bun jobs receive that config map via +/// `.envs(...)`, so nativets must consult it too or a CA set only through worker +/// config would silently not apply in-process. +fn resolve_ca_env_var(name: &str) -> Option { + if let Ok(v) = std::env::var(name) { + if !v.is_empty() { + return Some(v); + } + } + windmill_common::worker::WORKER_CONFIG + .load() + .env_vars + .get(name) + .filter(|v| !v.is_empty()) + .cloned() +} + +/// Build a root cert store seeded with the Mozilla webpki roots plus any custom +/// CAs configured via env. Returns `None` when no custom CA is configured, which +/// preserves the previous default-only behaviour. +fn build_native_root_cert_store_provider() -> Option> { + let mut store = deno_tls::create_default_root_cert_store(); + let mut added = 0usize; + + // File-path env vars, each pointing at a PEM bundle of one or more certs. + // `DENO_CERT` mirrors the Deno CLI; `SSL_CERT_FILE` is the OpenSSL standard + // also honoured by Bun/Node (via NODE_EXTRA_CA_CERTS). Dedupe by path because + // the tracing proxy points several of these at the same bundle, and rustls' + // RootCertStore::add does not dedupe — we'd otherwise trust the same root N times. + let mut seen_paths = std::collections::HashSet::new(); + for var in ["DENO_CERT", "SSL_CERT_FILE", "NODE_EXTRA_CA_CERTS"] { + let Some(path) = resolve_ca_env_var(var).filter(|p| !p.is_empty()) else { + continue; + }; + if !seen_paths.insert(path.clone()) { + continue; + } + match load_pem_certs_from_path(&path) { + Ok(certs) => { + for cert in certs { + if let Err(e) = store.add(cert) { + tracing::warn!("nativets: failed to add cert from {var}={path}: {e}"); + } else { + added += 1; + } + } + } + Err(e) => tracing::warn!("nativets: failed to read CA file {var}={path}: {e}"), + } + } + + // `DENO_TLS_CA_STORE=system` (comma-separated, may also contain `mozilla`) + // pulls in the OS trust store. Unlike the Deno CLI — where the list selects + // and orders the stores — this is purely additive: the Mozilla defaults are + // always seeded above, and `system` augments them. That is a strict superset + // of the public roots, which is what the corporate-CA use case needs. + if resolve_ca_env_var("DENO_TLS_CA_STORE") + .map(|v| v.split(',').any(|s| s.trim() == "system")) + .unwrap_or(false) + { + match deno_tls::deno_native_certs::load_native_certs() { + Ok(certs) => { + for cert in certs { + if store.add(CertificateDer::from(cert.0)).is_ok() { + added += 1; + } + } + } + Err(e) => tracing::warn!("nativets: failed to load system CA store: {e}"), + } + } + + if added == 0 { + return None; + } + tracing::info!("nativets: loaded {added} custom CA cert(s) into fetch root store"); + Some(Arc::new(NativeRootCertStoreProvider { store })) +} + +fn load_pem_certs_from_path(path: &str) -> anyhow::Result>> { + let file = std::fs::File::open(path)?; + let mut reader = std::io::BufReader::new(file); + deno_tls::load_certs(&mut reader).map_err(|e| anyhow::anyhow!(e)) +} + // ── Public interface ───────────────────────────────────────────────── /// Set up the deno_core/V8 runtime. Idempotent — safe to call multiple times. @@ -433,7 +555,7 @@ pub(crate) fn create_nativets_runtime( let ext = Extension { name: "windmill", ops: ops.into(), ..Default::default() }; let fetch_options = deno_fetch::Options { - root_cert_store_provider: None, + root_cert_store_provider: NATIVE_ROOT_CERT_STORE_PROVIDER.clone(), user_agent: ann.useragent.unwrap_or_else(|| "windmill/beta".to_string()), proxy: ann.proxy.map(|x| deno_tls::Proxy::Http { url: x.0, diff --git a/benchmarks/lib.ts b/benchmarks/lib.ts index a82663c132..742f715e5a 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.726.1"; +export const VERSION = "v1.728.1"; export async function login(email: string, password: string): Promise { return await windmill.UserService.login({ diff --git a/cli/build-npm.ts b/cli/build-npm.ts index 665021ffdc..5c9353dd21 100644 --- a/cli/build-npm.ts +++ b/cli/build-npm.ts @@ -28,6 +28,9 @@ rmSync(outDir, { recursive: true, force: true }); // Build with bun — bundle everything except esbuild (platform-specific binary), // svelte (optional, only needed for `wmill app bundle/dev`), and parser packages // (loaded at runtime via init() with readFileSync for the .wasm binary). +// esbuild-wasm is not a dependency at all: the host/binary-mismatch fallback in +// esbuild_loader.ts downloads and caches the whole esbuild-wasm package at +// runtime, so it stays out of the bundle and the published dependencies. console.log("Bundling with bun build..."); const buildResult = Bun.spawnSync([ "bun", "build", "src/main.ts", diff --git a/cli/src/commands/app/bundle.ts b/cli/src/commands/app/bundle.ts index e2f8553e22..07ee266145 100644 --- a/cli/src/commands/app/bundle.ts +++ b/cli/src/commands/app/bundle.ts @@ -6,6 +6,7 @@ import * as log from "../../core/log.ts"; import { colors } from "@cliffy/ansi/colors"; import * as windmillUtils from "@windmill-labs/shared-utils"; import { readTextFile, readTextFileSync } from "../../utils/utils.ts"; +import { getEsbuild, stopEsbuild } from "../../utils/esbuild_loader.ts"; export interface BundleOptions { entryPoint?: string; outDir?: string; @@ -170,8 +171,9 @@ export async function ensureNodeModules(appDir?: string): Promise { export async function createBundle( options: BundleOptions = {} ): Promise { - // Dynamically import esbuild - const esbuild = await import("esbuild"); + // Native esbuild with a transparent esbuild-wasm fallback on host/binary + // version mismatch (see esbuild_loader.ts). + const esbuild = await getEsbuild(); // Detect frameworks to determine default entry point. // Use the entryPoint's directory if provided, otherwise fall back to cwd. @@ -286,6 +288,10 @@ export async function createBundle( outfile, sourcemap, minify, + // Keep outputs in memory: esbuild-wasm cannot write to the filesystem + // ("write" option unavailable), and the dist files were discarded after the + // read anyway. Native esbuild supports write:false + outputFiles too. + write: false as const, define: { "process.env.NODE_ENV": production ? '"production"' : '"development"', }, @@ -307,29 +313,24 @@ export async function createBundle( log.info(colors.green("✅ Bundle created successfully")); - // Read the generated files - const jsPath = path.join(process.cwd(), outfile); - const cssPath = path.join(process.cwd(), outDir, "bundle.css"); + const outputFiles = result.outputFiles ?? []; + const jsFile = outputFiles.find((f) => f.path.endsWith(".js")); + const cssFile = outputFiles.find((f) => f.path.endsWith(".css")); - if (!fs.existsSync(jsPath)) { - throw new Error(`Expected JS bundle at ${jsPath} but file not found`); + if (!jsFile) { + throw new Error("Expected a JS bundle in esbuild output but none found"); } - const jsContent = readTextFileSync(jsPath); - const cssContent = fs.existsSync(cssPath) - ? readTextFileSync(cssPath) - : ""; - try { fs.rmSync(distDir, { recursive: true }); } catch { //ignore } - return { js: jsContent, css: cssContent }; - + return { js: jsFile.text, css: cssFile?.text ?? "" }; + } finally { - // Stop esbuild - await esbuild.stop(); + // Stop the native esbuild service so the process can exit (no-op for wasm). + await stopEsbuild(); } } diff --git a/cli/src/commands/app/dev.ts b/cli/src/commands/app/dev.ts index c73a8685c8..1a226ec5ef 100644 --- a/cli/src/commands/app/dev.ts +++ b/cli/src/commands/app/dev.ts @@ -437,7 +437,11 @@ async function dev(opts: DevOptions, appFolder?: string) { const rawApp = (await yamlParseFile(rawAppPath)) as any; const appPath = rawApp?.custom_path ?? "u/unknown/newapp"; - // Dynamically import esbuild only when the dev command is called + // Dynamically import esbuild only when the dev command is called. + // Native-only here (no esbuild-wasm fallback via getEsbuild): dev is a local + // interactive command that relies on context()/watch, whose semantics under + // wasm are untested. The host/binary-mismatch fallback covers the bundling + // paths that run on workers/CI via `wmill sync push`. const esbuild = await import("esbuild"); const host = opts.host ?? DEFAULT_HOST; diff --git a/cli/src/commands/script/script.ts b/cli/src/commands/script/script.ts index 717e8acf46..ed57b1747d 100644 --- a/cli/src/commands/script/script.ts +++ b/cli/src/commands/script/script.ts @@ -58,6 +58,7 @@ import { SyncCodebase, listSyncCodebases } from "../../utils/codebase.ts"; import { pollJobWithQueueLogging } from "../../utils/job_polling.ts"; import fs from "node:fs"; import { createTarBlob, type TarEntry } from "../../utils/tar.ts"; +import { getEsbuild } from "../../utils/esbuild_loader.ts"; import { execSync } from "node:child_process"; import { NewScript, Script, ScriptModule } from "../../../gen/types.gen.ts"; @@ -328,7 +329,7 @@ export async function handleFile( }).toString(); log.info("Custom bundler executed for " + path); } else { - const esbuild = await import("esbuild"); + const esbuild = await getEsbuild(); log.info(`Started bundling ${path} ...`); const startTime = performance.now(); @@ -1565,7 +1566,7 @@ async function preview( maxBuffer: 1024 * 1024 * 50, }).toString(); } else { - const esbuild = await import("esbuild"); + const esbuild = await getEsbuild(); if (!opts.silent) { log.info(`Bundling ${filePath} for preview...`); diff --git a/cli/src/core/constants.ts b/cli/src/core/constants.ts index b30d6f2f92..0807ef50e0 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.726.1"; +export const VERSION = "1.728.1"; diff --git a/cli/src/utils/esbuild_loader.ts b/cli/src/utils/esbuild_loader.ts new file mode 100644 index 0000000000..4801c51a46 --- /dev/null +++ b/cli/src/utils/esbuild_loader.ts @@ -0,0 +1,228 @@ +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; +import process from "node:process"; +import { createGunzip } from "node:zlib"; +import { Readable } from "node:stream"; +import { pathToFileURL } from "node:url"; +import * as tar from "tar-stream"; +import * as log from "../core/log.ts"; + +// esbuild splits into a JS host package and a per-platform native binary +// (@esbuild/). They must be the same version. A broken or incremental +// install can leave the on-disk binary at a different version than the pinned +// host, which crashes service start with +// Cannot start service: Host version "X" does not match binary version "Y" +// The running code can't fix what npm/bun put on disk, so when that happens we +// fall back to esbuild-wasm, whose binary is a single version-pinned .wasm. To +// keep that 14MB out of every CLI install, the esbuild-wasm package is not a +// dependency: it is downloaded once and cached on disk, on the fallback path +// only. We download the whole package (not just the .wasm) because esbuild-wasm +// reads the app's files from disk by spawning `node bin/esbuild`, which needs +// bin/esbuild + esbuild.wasm + wasm_exec*.js co-located on disk. + +type Esbuild = typeof import("esbuild"); + +// Version to fall back to if the native host's version can't be read. Keep in +// sync with the "esbuild" pin in cli/package.json. +const FALLBACK_VERSION = "0.28.0"; + +let cached: Esbuild | undefined; +let inFlight: Promise | undefined; +// Distinguishes concurrent extraction temp dirs within a process. +let extractCounter = 0; + +/** + * Returns a working esbuild module, preferring the native binary and falling + * back to esbuild-wasm only when the native host/binary versions don't match. + * Memoized for the process: concurrent first callers (e.g. a parallel + * `wmill sync push`) share one probe/download instead of each running their own. + */ +export function getEsbuild(): Promise { + if (cached) return Promise.resolve(cached); + if (inFlight) return inFlight; + inFlight = acquireEsbuild() + .then((esbuild) => { + cached = esbuild; + return esbuild; + }) + .finally(() => { + inFlight = undefined; + }); + return inFlight; +} + +async function acquireEsbuild(): Promise { + // Escape hatch: skip native entirely (e.g. a host known to have a broken + // install, or to exercise the fallback path). + if (process.env.WINDMILL_FORCE_ESBUILD_WASM) { + return loadWasmEsbuild(await nativeHostVersion()); + } + + try { + const esbuild = await import("esbuild"); + // The native service only starts on the first call; force it with the most + // trivial op so any breakage (host/binary version mismatch, a dead service) + // surfaces now rather than mid-build. The mismatch detail is printed to the + // child's stderr while the thrown error is generic ("service was stopped"), + // so we fall back on ANY smoke-test failure rather than matching a string. + await esbuild.transform(""); + return esbuild; + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + log.warn( + `native esbuild is not usable; falling back to esbuild-wasm (${msg.trim()})` + ); + } + + return loadWasmEsbuild(await nativeHostVersion()); +} + +/** + * Stops the esbuild service (native or wasm — both spawn a child process) so the + * process can exit. Safe to call repeatedly; the service restarts lazily on the + * next build. + */ +export async function stopEsbuild(): Promise { + await cached?.stop(); +} + +async function nativeHostVersion(): Promise { + try { + return (await import("esbuild")).version ?? FALLBACK_VERSION; + } catch { + return FALLBACK_VERSION; + } +} + +async function loadWasmEsbuild(version: string): Promise { + const pkgDir = await ensureWasmPackage(version); + const mainJs = path.join(pkgDir, "lib", "main.js"); + // The Node build (lib/main.js) reads app files from disk by spawning + // `node bin/esbuild`, so it works with on-disk entry points and node_modules, + // unlike the browser build. + return (await import(pathToFileURL(mainJs).href)) as unknown as Esbuild; +} + +/** + * Returns a directory containing an extracted esbuild-wasm package (with + * lib/main.js). Uses an explicit override, then an on-disk cache, then downloads + * and extracts the npm tarball. + */ +async function ensureWasmPackage(version: string): Promise { + // Explicit local override wins (air-gapped / self-hosted workers): a path to + // an already-extracted esbuild-wasm package directory. + const override = process.env.WINDMILL_ESBUILD_WASM_PATH; + if (override) return override; + + const destDir = path.join(cacheDir(), `esbuild-wasm-${version}`); + if (fs.existsSync(path.join(destDir, "lib", "main.js"))) { + return destDir; + } + + const url = process.env.WINDMILL_ESBUILD_WASM_URL ?? + `https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-${version}.tgz`; + log.info(`Downloading esbuild-wasm@${version} from ${url} ...`); + const res = await fetch(url); + if (!res.ok || !res.body) { + throw new Error( + `Failed to download esbuild-wasm@${version} (${res.status} ${res.statusText}). ` + + `Set WINDMILL_ESBUILD_WASM_PATH to an extracted esbuild-wasm package dir, ` + + `point WINDMILL_ESBUILD_WASM_URL at a reachable tarball, or repair the native esbuild install.` + ); + } + + // Extract to a unique temp dir and rename into place so a crash or a + // concurrent writer can't leave a half-extracted package behind, and so two + // extractions never share an in-progress directory. + const tmpDir = `${destDir}.${process.pid}.${extractCounter++}.tmp`; + fs.rmSync(tmpDir, { recursive: true, force: true }); + await extractTarball(res.body, tmpDir); + if (!fs.existsSync(path.join(tmpDir, "lib", "main.js"))) { + fs.rmSync(tmpDir, { recursive: true, force: true }); + throw new Error(`esbuild-wasm@${version} tarball did not contain lib/main.js`); + } + try { + fs.renameSync(tmpDir, destDir); + } catch { + // Another process won the race, or rename across devices failed; clean up + // and let the existsSync check below decide whether the cache is usable. + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + if (!fs.existsSync(path.join(destDir, "lib", "main.js"))) { + throw new Error(`Failed to cache esbuild-wasm@${version} at ${destDir}`); + } + return destDir; +} + +/** + * Resolves a tar entry to an absolute path inside destDir, stripping the leading + * "package/" component that npm tarballs use. Returns null if the entry would + * escape destDir (tar-slip), since WINDMILL_ESBUILD_WASM_URL allows untrusted + * tarball sources. + */ +export function resolveTarEntryPath( + destDir: string, + entryName: string +): string | null { + const rel = entryName.replace(/^[^/]+\//, ""); + const root = path.resolve(destDir); + const outPath = path.resolve(root, rel); + if (outPath !== root && !outPath.startsWith(root + path.sep)) { + return null; + } + return outPath; +} + +// Extracts an npm tarball (gzipped tar) into destDir, stripping the leading +// "package/" path component that npm tarballs use. +async function extractTarball( + body: ReadableStream, + destDir: string +): Promise { + const extract = tar.extract(); + extract.on("entry", (header, stream, next) => { + if (header.type !== "file") { + stream.resume(); + stream.on("end", next); + return; + } + const outPath = resolveTarEntryPath(destDir, header.name); + if (!outPath) { + // Reject tar-slip entries that would write outside the cache dir. + stream.resume(); + stream.on("end", () => + next(new Error(`unsafe path in esbuild-wasm tarball: ${header.name}`)) + ); + return; + } + fs.mkdirSync(path.dirname(outPath), { recursive: true }); + const ws = fs.createWriteStream(outPath, { mode: header.mode ?? 0o644 }); + stream.pipe(ws); + ws.on("finish", next); + ws.on("error", next); + stream.on("error", next); + }); + + await new Promise((resolve, reject) => { + extract.on("finish", resolve); + extract.on("error", reject); + Readable.fromWeb(body as unknown as Parameters[0]) + .pipe(createGunzip()) + .on("error", reject) + .pipe(extract) + .on("error", reject); + }); +} + +function cacheDir(): string { + const explicit = process.env.WINDMILL_CACHE_DIR; + if (explicit) return explicit; + const xdg = process.env.XDG_CACHE_HOME; + if (xdg) return path.join(xdg, "windmill"); + try { + return path.join(os.homedir(), ".cache", "windmill"); + } catch { + return path.join(os.tmpdir(), "windmill"); + } +} diff --git a/cli/src/utils/local_path_scripts.ts b/cli/src/utils/local_path_scripts.ts index c9d321dd55..9e03db8a5f 100644 --- a/cli/src/utils/local_path_scripts.ts +++ b/cli/src/utils/local_path_scripts.ts @@ -1,6 +1,7 @@ import { execFileSync } from "node:child_process"; import { stat } from "node:fs/promises"; import { readTextFile } from "./utils.ts"; +import { getEsbuild } from "./esbuild_loader.ts"; import type { SyncCodebase } from "./codebase.ts"; import { parseMetadataFileIfExists } from "./metadata.ts"; import { inferContentTypeFromFilePath } from "./script_common.ts"; @@ -43,7 +44,7 @@ async function bundleSingleFileCodebaseScript( ).toString(); } - const esbuild = await import("esbuild"); + const esbuild = await getEsbuild(); const out = await esbuild.build({ entryPoints: [filePath], // Inline rawscripts are executed through the standard module wrapper, diff --git a/cli/test/esbuild_loader_unit.test.ts b/cli/test/esbuild_loader_unit.test.ts new file mode 100644 index 0000000000..9e75607dbf --- /dev/null +++ b/cli/test/esbuild_loader_unit.test.ts @@ -0,0 +1,31 @@ +/** + * Unit tests for esbuild_loader pure logic (no backend, no network). + */ + +import { expect, test, describe } from "bun:test"; +import { resolveTarEntryPath } from "../src/utils/esbuild_loader.ts"; +import { sep, resolve } from "node:path"; + +describe("resolveTarEntryPath", () => { + const dest = resolve("/tmp/cache/esbuild-wasm-0.28.0"); + + test("strips the leading package/ component and resolves inside dest", () => { + expect(resolveTarEntryPath(dest, "package/lib/main.js")).toBe( + dest + sep + "lib" + sep + "main.js" + ); + expect(resolveTarEntryPath(dest, "package/esbuild.wasm")).toBe( + dest + sep + "esbuild.wasm" + ); + }); + + test("rejects tar-slip entries that escape the dest dir", () => { + expect(resolveTarEntryPath(dest, "package/../../etc/passwd")).toBeNull(); + expect(resolveTarEntryPath(dest, "package/../../../outside")).toBeNull(); + }); + + test("rejects entries that only share a prefix with dest", () => { + // ".../esbuild-wasm-0.28.0-evil" must not be treated as inside dest + expect(resolveTarEntryPath(dest, "evil/../../esbuild-wasm-0.28.0-evil/x")) + .toBeNull(); + }); +}); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 552b593ba2..5931bf0f9d 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "@windmill-labs/components", - "version": "1.726.1", + "version": "1.728.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@windmill-labs/components", - "version": "1.726.1", + "version": "1.728.1", "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { diff --git a/frontend/package.json b/frontend/package.json index 1772425f02..a0fe73116f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "@windmill-labs/components", - "version": "1.726.1", + "version": "1.728.1", "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/AutosaveIndicator.svelte b/frontend/src/lib/components/AutosaveIndicator.svelte index 168f68ebd7..21ad7a4ac0 100644 --- a/frontend/src/lib/components/AutosaveIndicator.svelte +++ b/frontend/src/lib/components/AutosaveIndicator.svelte @@ -216,7 +216,7 @@
{#if flashActive} diff --git a/frontend/src/lib/components/DraftMigrationErrorModal.svelte b/frontend/src/lib/components/DraftMigrationErrorModal.svelte new file mode 100644 index 0000000000..ccbc5eb501 --- /dev/null +++ b/frontend/src/lib/components/DraftMigrationErrorModal.svelte @@ -0,0 +1,124 @@ + + + +
+

+ Drafts are now user-scoped and synced to the database. + {#if draftMigrationErrors.list.length} + These local storage drafts could not be + migrated to the server — view their contents before deciding, then delete the ones you no + longer need. + {/if} + Learn more. +

+ + {#if draftMigrationErrors.list.length === 0} +

All issues resolved.

+ {:else} +
+ +
+
    + {#each draftMigrationErrors.list as error (error.key)} +
  • +
    +
    {error.path}
    +
    + {error.itemKind} · {error.workspace} +
    +
    + + +
  • + {/each} +
+ {/if} + +
+ +
+
+
+ + + {#snippet headerRight()} + + {/snippet} +
+
{JSON.stringify(jsonView?.value ?? {}, null, 2)}
+
+
diff --git a/frontend/src/lib/components/EditorHeader.svelte b/frontend/src/lib/components/EditorHeader.svelte index 0e107593ea..87a0c6e1da 100644 --- a/frontend/src/lib/components/EditorHeader.svelte +++ b/frontend/src/lib/components/EditorHeader.svelte @@ -237,6 +237,7 @@ value={summary ?? ''} placeholder="Add a summary..." editable={summaryEditable} + commitOnInput size="sm" onSave={handleSummarySave} textClass="text-xs font-semibold text-emphasis leading-tight" diff --git a/frontend/src/lib/components/FlowBuilder.svelte b/frontend/src/lib/components/FlowBuilder.svelte index 595b52c82d..4ba68d6586 100644 --- a/frontend/src/lib/components/FlowBuilder.svelte +++ b/frontend/src/lib/components/FlowBuilder.svelte @@ -119,6 +119,8 @@ loadedFromHistoryFromUrl, noInitial = false, liveEditorDraftStoragePath = undefined, + autosaveWorkspace = undefined, + autosavePath = undefined, onDeploy, onDeployError, onDetails, @@ -131,6 +133,12 @@ onTestJob }: FlowBuilderProps = $props() + // Key the AutosaveIndicator watches. Falls back to this component's own + // draft key, so the full-page editor is unchanged; the sessions preview + // overrides both to the (forked) workspace + path its autosave saves under. + const indicatorWorkspace = $derived(autosaveWorkspace ?? $workspaceStore) + const indicatorPath = $derived(autosavePath ?? liveEditorDraftStoragePath) + let initialPathStore = writable(initialPath) // For preserve_on_behalf_of feature @@ -1084,11 +1092,11 @@ onBehalfOfEmail={$savedOnBehalfOfEmail} onNavigate={(item) => onNavigate?.(item)} /> - {#if $workspaceStore && liveEditorDraftStoragePath !== undefined} + {#if indicatorWorkspace && indicatorPath !== undefined} {#if customUi?.topBar?.diff != false} - + +
+ +
{/if} {#if !compactTopbar} {@render previewButtons()} diff --git a/frontend/src/lib/components/ResourceForm.svelte b/frontend/src/lib/components/ResourceForm.svelte index 90ba39275b..ced60bf4dc 100644 --- a/frontend/src/lib/components/ResourceForm.svelte +++ b/frontend/src/lib/components/ResourceForm.svelte @@ -101,6 +101,14 @@ } }) + // Seed the JSON editor when the resource type schema is missing + // (restores the old ResourceEditor's catch-block behavior) + $effect(() => { + if (resource_type && !loadingSchema && !resourceSchema && rawCode === undefined) { + rawCode = JSON.stringify(args, null, 2) + } + }) + $effect(() => { if (textFileContent) parseTextFileContent() }) diff --git a/frontend/src/lib/components/ScriptBuilder.svelte b/frontend/src/lib/components/ScriptBuilder.svelte index d77d0bf366..063a17f9d1 100644 --- a/frontend/src/lib/components/ScriptBuilder.svelte +++ b/frontend/src/lib/components/ScriptBuilder.svelte @@ -109,6 +109,8 @@ fullyLoaded = true, initialPath = $bindable(''), userDraftPath = '', + autosaveWorkspace = undefined, + autosavePath = undefined, template = $bindable('script'), initialArgs = {}, lockedLanguage = false, @@ -159,9 +161,19 @@ let deployedBy: string | undefined = $state(undefined) // Author let confirmCallback: () => void = $state(() => {}) // What happens when user clicks `override` in warning - // Top-bar responsive collapse — container width, not viewport. + // Top-bar responsive collapse — container width, not viewport. Collapse the + // right group (hide the ~200px tag select, icon-only Diff/Settings) before the + // full group crowds the path into heavy truncation; ~900 is where the path + // keeps a usable width given the right group's natural ~440px. let topbarWidth = $state(0) - const compactTopbar = $derived(topbarWidth > 0 && topbarWidth < 720) + const compactTopbar = $derived(topbarWidth > 0 && topbarWidth < 900) + + // AutosaveIndicator watch key. Falls back to the full-page editor's + // global store + URL draft path; the sessions preview overrides both so the + // icon tracks the session's (forked) workspace + target path where autosave + // actually happens. + const indicatorWorkspace = $derived(autosaveWorkspace ?? $workspaceStore) + const indicatorPath = $derived(autosavePath ?? userDraftPath) function getCompactMenuItems(): Item[] { const hasTags = ($workerTags?.length ?? 0) > 0 @@ -728,7 +740,10 @@ }) } - function computeDropdownItems(initialPath: string, savedScript: Script | NewScript | undefined) { + function computeDropdownItems( + initialPath: string, + savedScript: ((Script | NewScript) & { no_deployed?: boolean }) | undefined + ) { let dropdownItems: { label: string; onClick: () => void }[] = initialPath != '' && customUi?.topBar?.extraDeployOptions != false ? [ @@ -759,7 +774,7 @@ ] : []), ...(!inSessionPane && - (savedScript as any)?.no_deployed !== true && + savedScript?.no_deployed !== true && script.kind === 'script' && !script.auto_kind ? [ @@ -1831,7 +1846,7 @@ {hasPreprocessor} canHavePreprocessor={canHavePreprocessor(script.language)} args={hasPreprocessor && selectedInputTab !== 'preprocessor' ? {} : args} - isDeployed={savedScript && (savedScript as any)?.no_deployed !== true} + isDeployed={savedScript && savedScript?.no_deployed !== true} schema={script.schema} runnableVersion={script.parent_hash} onDeployTrigger={handleDeployTrigger} @@ -1849,7 +1864,7 @@
-
+
{#if customUi?.topBar?.path != false} - onNavigate?.(item)} - /> +
+ onNavigate?.(item)} + /> +
{/if} - {#if $workspaceStore} + {#if indicatorWorkspace} openDiffDrawer()} - disabled={!savedScript || !diffDrawer || isDraftOnly} - iconOnly={compactTopbar} - title={isDraftOnly - ? 'Deploy this script once to compare against the deployed version' - : 'Diff'} - startIcon={{ icon: DiffIcon }} - > - Diff - + {@const isDraftOnly = savedScript?.no_deployed === true} + {@const diffDisabled = !savedScript || !diffDrawer || isDraftOnly} + {@const diffTitle = isDraftOnly + ? 'Deploy this script once to compare against the deployed version' + : 'Diff'} + +
+ +
{/if} {/snippet} {#if compactTopbar} diff --git a/frontend/src/lib/components/WorkspaceItemDrillPicker.svelte b/frontend/src/lib/components/WorkspaceItemDrillPicker.svelte index 5f2039e353..f9c538b50a 100644 --- a/frontend/src/lib/components/WorkspaceItemDrillPicker.svelte +++ b/frontend/src/lib/components/WorkspaceItemDrillPicker.svelte @@ -23,6 +23,7 @@ would be surprising. import { buildWorkspaceTree, legacyScopeToPath, relativizeWorkspacePath } from './workspaceTree' import { listGlobalDrafts } from '$lib/components/copilot/chat/global/userDraftAdapter' import { isGlobalAiEnabled } from '$lib/components/copilot/chat/global/gate' + import { resource } from 'runed' type Kind = WorkspaceItemKind type ScopeKind = Kind | 'all' @@ -82,11 +83,15 @@ would be surprising. // be surprising (they'd appear as navigable items that 404 on the backend // draft fetch). const KIND_TO_DRAFT_TYPE = { flow: 'flow', script: 'script', app: 'app' } as const + // `listGlobalDrafts` is backend-backed (async); fetch once and derive the + // per-kind lists synchronously from the resolved snapshot. + const globalDraftsResource = resource( + () => ({ ws: $workspaceStore, enabled: isGlobalAiEnabled() }), + async ({ ws, enabled }) => (enabled && ws ? await listGlobalDrafts(ws) : []) + ) function aiDraftsForKind(k: Kind): WorkspaceItem[] { - if (!isGlobalAiEnabled()) return [] - if (!$workspaceStore) return [] const targetType = KIND_TO_DRAFT_TYPE[k] - return listGlobalDrafts($workspaceStore) + return (globalDraftsResource.current ?? []) .filter((d) => d.type === targetType) .map((d) => ({ path: d.path, diff --git a/frontend/src/lib/components/WorkspaceItemRow.svelte b/frontend/src/lib/components/WorkspaceItemRow.svelte index 8ddd3fa317..fe923b12ca 100644 --- a/frontend/src/lib/components/WorkspaceItemRow.svelte +++ b/frontend/src/lib/components/WorkspaceItemRow.svelte @@ -44,10 +44,15 @@ doesn't steal focus from a sibling search input (matches the picker). navKey?: string /** Per-row vertical padding class (e.g. `py-1` / `py-1.5`). */ baseClass?: string + /** Reserve two lines of height and vertically center the content so + * summary and summary-less rows are the same height (diff viewer). */ + uniformHeight?: boolean /** Extra left padding (px) for tree-view indentation. Adds to the * default `px-3` horizontal padding. */ indent?: number - /** Title tooltip shown on hover; defaults to the secondary text. */ + /** Full path tooltip for the secondary line; defaults to the secondary + * text. The summary line gets its own tooltip (the full summary) so each + * truncated line reveals its own content on hover. */ title?: string /** When set, the row renders as an `` link * instead of a ` + +