mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-15 08:02:27 +00:00
Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e4cf139b1 | ||
|
|
cfb9f1dbc2 | ||
|
|
cbf54d4eb4 | ||
|
|
9793d01575 | ||
|
|
24446e8009 | ||
|
|
e90b2be8fa | ||
|
|
29c67ced97 | ||
|
|
6dfccd9d88 | ||
|
|
fc797a35fe | ||
|
|
11d0e65f3a | ||
|
|
984ea728d9 |
@@ -1,5 +1,29 @@
|
||||
# Changelog
|
||||
|
||||
## [1.738.0](https://github.com/windmill-labs/windmill/compare/v1.737.0...v1.738.0) (2026-06-23)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add resource and infrastructure telemetry ([#9737](https://github.com/windmill-labs/windmill/issues/9737)) ([9793d01](https://github.com/windmill-labs/windmill/commit/9793d01575415963a89609a1baf2cd64f0d050cc))
|
||||
* render mermaid diagrams in chat code blocks ([#9738](https://github.com/windmill-labs/windmill/issues/9738)) ([cfb9f1d](https://github.com/windmill-labs/windmill/commit/cfb9f1dbc23110ecf8f91bb3c8c81fc6e35dc09b))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **ai-chat:** Fix incorrect editor edits from ai chat [#1](https://github.com/windmill-labs/windmill/issues/1) ([#9741](https://github.com/windmill-labs/windmill/issues/9741)) ([fc797a3](https://github.com/windmill-labs/windmill/commit/fc797a35fe7885630c81453df0fc94769e73873a))
|
||||
* allow object storage test for non-super-admins, harden on cloud ([#9739](https://github.com/windmill-labs/windmill/issues/9739)) ([24446e8](https://github.com/windmill-labs/windmill/commit/24446e80093ade349f7fbf65063d2d1cb5551c1e))
|
||||
* **frontend:** debounce external code→Monaco sync in Editor ([#9743](https://github.com/windmill-labs/windmill/issues/9743)) ([29c67ce](https://github.com/windmill-labs/windmill/commit/29c67ced97bf2919584986f9d9eceb4337c34ad9))
|
||||
* **frontend:** preserve editor content when closing instance settings drawer ([#9740](https://github.com/windmill-labs/windmill/issues/9740)) ([11d0e65](https://github.com/windmill-labs/windmill/commit/11d0e65f3af9a048bc1921bbdd3d676a07483a57))
|
||||
* pipeline annotation false-positives from body comments ([#9736](https://github.com/windmill-labs/windmill/issues/9736)) ([984ea72](https://github.com/windmill-labs/windmill/commit/984ea728d98649b66b1cae899bdab9af3176caa7))
|
||||
* preserve fork parent linkage on workspace id change ([#9716](https://github.com/windmill-labs/windmill/issues/9716)) ([cbf54d4](https://github.com/windmill-labs/windmill/commit/cbf54d4eb432638e27f67c4c8b879cbcc0291da3))
|
||||
* prevent variable push from corrupting is_secret variables ([#9705](https://github.com/windmill-labs/windmill/issues/9705)) ([ba4b368](https://github.com/windmill-labs/windmill/commit/ba4b368706e95e22f346a10e5fe145b0795ac3f6))
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **monitor:** skip protected prefix in retention delete via cross-batch watermark (WIN-2088) ([#9744](https://github.com/windmill-labs/windmill/issues/9744)) ([e90b2be](https://github.com/windmill-labs/windmill/commit/e90b2be8fade1eb78cd685890291f5a4553a6a10))
|
||||
|
||||
## [1.737.0](https://github.com/windmill-labs/windmill/compare/v1.736.0...v1.737.0) (2026-06-23)
|
||||
|
||||
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT SUM(pg_database_size(datname))::BIGINT AS \"v!\" FROM pg_database",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "v!",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "074dd26f6427f4ff97e92c35163ad042144e656adcca56a4936a2eb196d3f48c"
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT language AS \"language!: _\", COUNT(*)::BIGINT AS \"count!\"\n FROM script\n WHERE archived = false AND deleted = false AND kind = 'script'\n AND (auto_kind IS NULL OR auto_kind <> 'wac')\n GROUP BY language\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "language!: _",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "script_lang",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"python3",
|
||||
"deno",
|
||||
"go",
|
||||
"bash",
|
||||
"postgresql",
|
||||
"nativets",
|
||||
"bun",
|
||||
"mysql",
|
||||
"bigquery",
|
||||
"snowflake",
|
||||
"graphql",
|
||||
"powershell",
|
||||
"mssql",
|
||||
"php",
|
||||
"bunnative",
|
||||
"rust",
|
||||
"ansible",
|
||||
"csharp",
|
||||
"oracledb",
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb",
|
||||
"ruby",
|
||||
"rlang"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "count!",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "11813108dbf6b104eba968c3609c74ac5a589542d3e76b1e756a82fb19d49ee8"
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT COUNT(*)::INT AS \"v!\" FROM pg_stat_activity",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "v!",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "3b439ae7af0fcbb9df8e19faf84abf590e5e94898954711d57a602e6fd8a2f84"
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT COUNT(*)::BIGINT AS \"count!\" FROM script WHERE archived = false AND deleted = false AND auto_kind = 'wac'",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "count!",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "3db1c61295c284725eef9e74a8aa2bc7822d263605a445f1f4a76e58e76a3e79"
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE workspace SET parent_workspace_id = $1 WHERE parent_workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "40a8cf5e87bb489fd172689e9a6f0f1075b878f9916145929b3cd3b1a53b777e"
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT p.id AS \"id!\", p.deleted AS \"deleted!\"\n FROM workspace f\n JOIN workspace p ON p.id = f.parent_workspace_id\n WHERE f.id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id!",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "deleted!",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "42322020ff9cc7dd7ebafc1cb4122ba3d670cc36bdbc6451f29b8f22f8cff688"
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT pg_database_size(current_database())::BIGINT AS \"v!\"",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "v!",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "48a5df355a2bca557a3a541cf66c8e75790b7c3dd7845359019ff645a1f7c8bf"
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT current_setting('max_connections')::INT AS \"v!\"",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "v!",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "49e1f5663eed128ed956c9a50bc73a85256c0a3e5a701cc13c944e66f6402617"
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT COUNT(*)::BIGINT AS \"count!\" FROM flow WHERE archived = false",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "count!",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "50490ff42fb1f2d78864d7b374d299bf8290c3b969b576cb185c8b5b0abb0265"
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT current_setting('server_version_num')::INT AS \"v!\"",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "v!",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "56a98a07a2f6af4d694db05d57acbe0b55cc39d64f73a3c90c250ea670f9cdee"
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n COUNT(*) FILTER (WHERE av.raw_app = false)::BIGINT AS \"low_code!\",\n COUNT(*) FILTER (WHERE av.raw_app = true)::BIGINT AS \"raw!\"\n FROM app a\n JOIN app_version av ON av.id = a.versions[array_upper(a.versions, 1)]\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "low_code!",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "raw!",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
null,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "60dc0f1fa17bd2946ba7ddf0c41fe58b8a53d071cdc83eca3092194b4a9c9174"
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT COUNT(*)::BIGINT AS \"count!\" FROM workspace WHERE deleted = false AND id NOT LIKE 'wm-fork%'",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "count!",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "9170a350e1da0b33a421a119d4a5b86575c1be761de158ad664670e981524cbf"
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM v2_job_completed\n WHERE id IN (\n SELECT id FROM v2_job_completed\n WHERE completed_at <= now() - ($1::bigint::text || ' s')::interval\n AND ($3::timestamptz IS NULL OR completed_at >= $3)\n ORDER BY completed_at ASC\n LIMIT $2\n FOR UPDATE SKIP LOCKED\n )\n RETURNING id, completed_at",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "completed_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8",
|
||||
"Timestamptz"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "a2d4a8aedb15e9faf0a2512fa4241a9e9f2d5a56e12a9d6805ca58ff40f61614"
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO workspace (id, name, owner, deleted, premium, parent_workspace_id)\n SELECT $1, $2, owner, false, premium,\n CASE WHEN $4 THEN parent_workspace_id ELSE NULL END\n FROM workspace WHERE id = $3",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "a54efa4a7466e61fd54d8fe293cb775225dcb430026cebe15ba4994ac636514d"
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n EXISTS(SELECT 1 FROM pg_proc WHERE proname = 'aurora_version') AS \"aurora!\",\n EXISTS(SELECT 1 FROM pg_roles WHERE rolname = 'rds_superuser') AS \"rds!\",\n EXISTS(SELECT 1 FROM pg_roles WHERE rolname = 'cloudsqlsuperuser') AS \"cloudsql!\",\n EXISTS(SELECT 1 FROM pg_roles WHERE rolname IN ('azure_pg_admin', 'azuresu')) AS \"azure!\"\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "aurora!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "rds!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "cloudsql!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "azure!",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "a90e3a1d7c7c0dfb422f44b0ed599f681f5630c024ae9a437a301f149636b0db"
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM v2_job_completed\n WHERE id IN (\n SELECT jc.id FROM v2_job_completed jc\n LEFT JOIN v2_job j ON j.id = jc.id\n WHERE jc.completed_at <= now() - ($1::bigint::text || ' s')::interval\n AND ($4::timestamptz IS NULL OR jc.completed_at >= $4)\n AND COALESCE(j.root_job, j.flow_innermost_root_job, jc.id) NOT IN (\n SELECT u FROM unnest($3::uuid[]) AS u WHERE u IS NOT NULL\n )\n ORDER BY jc.completed_at ASC\n LIMIT $2\n FOR UPDATE OF jc SKIP LOCKED\n )\n RETURNING id, completed_at",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "completed_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8",
|
||||
"UuidArray",
|
||||
"Timestamptz"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "c033a690fde04da79745e850b72fa7cfd861f1dcad88c7ea75a0a8b014ec1f75"
|
||||
}
|
||||
-24
@@ -1,24 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM v2_job_completed\n WHERE id IN (\n SELECT jc.id FROM v2_job_completed jc\n LEFT JOIN v2_job j ON j.id = jc.id\n WHERE jc.completed_at <= now() - ($1::bigint::text || ' s')::interval\n AND COALESCE(j.root_job, j.flow_innermost_root_job, jc.id) NOT IN (\n SELECT u FROM unnest($3::uuid[]) AS u WHERE u IS NOT NULL\n )\n ORDER BY jc.completed_at ASC\n LIMIT $2\n FOR UPDATE OF jc SKIP LOCKED\n )\n RETURNING id",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8",
|
||||
"UuidArray"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "fbe3a876efd1253d2ef086b03366b2bd117ceb6bc152d2abcd45850ff6aecff9"
|
||||
}
|
||||
Generated
+78
-78
@@ -13735,7 +13735,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-nats",
|
||||
@@ -13817,7 +13817,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-ai"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"async-stream",
|
||||
"async-trait",
|
||||
@@ -13850,7 +13850,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-alerting"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"chrono",
|
||||
@@ -13863,7 +13863,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
@@ -14001,7 +14001,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-agent-workers"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"chrono",
|
||||
@@ -14024,7 +14024,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-assets"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"chrono",
|
||||
@@ -14037,7 +14037,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-auth"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.8.9",
|
||||
@@ -14063,7 +14063,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-client"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"reqwest 0.12.28",
|
||||
"serde",
|
||||
@@ -14073,7 +14073,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-configs"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"chrono",
|
||||
@@ -14090,7 +14090,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-debug"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"base64 0.22.1",
|
||||
@@ -14112,7 +14112,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-embeddings"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.8.9",
|
||||
@@ -14135,7 +14135,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-flow-conversations"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"chrono",
|
||||
@@ -14151,7 +14151,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-flows"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"chrono",
|
||||
@@ -14172,7 +14172,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-groups"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"chrono",
|
||||
@@ -14193,7 +14193,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-inputs"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"chrono",
|
||||
@@ -14207,7 +14207,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-integration-tests"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-nats",
|
||||
@@ -14242,7 +14242,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-jobs"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.8.9",
|
||||
@@ -14267,7 +14267,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-npm-proxy"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"flate2",
|
||||
@@ -14285,7 +14285,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-openapi"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.8.9",
|
||||
@@ -14307,7 +14307,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-schedule"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"chrono",
|
||||
@@ -14327,7 +14327,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-scripts"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"chrono",
|
||||
@@ -14364,7 +14364,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-settings"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.8.9",
|
||||
@@ -14392,7 +14392,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-sse"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"serde",
|
||||
@@ -14404,7 +14404,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-users"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"argon2",
|
||||
"axum 0.8.9",
|
||||
@@ -14429,7 +14429,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-workers"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"chrono",
|
||||
@@ -14443,7 +14443,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-workspaces"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"axum 0.8.9",
|
||||
"chrono",
|
||||
@@ -14476,7 +14476,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-audit"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"lazy_static",
|
||||
@@ -14490,7 +14490,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-autoscaling"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.8.9",
|
||||
@@ -14509,7 +14509,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-common"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"aho-corasick",
|
||||
@@ -14611,7 +14611,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-dep-map"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"itertools 0.14.0",
|
||||
@@ -14630,7 +14630,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-git-sync"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"regex",
|
||||
"serde",
|
||||
@@ -14645,7 +14645,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-indexer"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"astral-tokio-tar",
|
||||
@@ -14669,7 +14669,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-jseval"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"futures",
|
||||
@@ -14686,7 +14686,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-macros"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"itertools 0.14.0",
|
||||
"lazy_static",
|
||||
@@ -14702,7 +14702,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-mcp"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -14723,7 +14723,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-native-triggers"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -14754,7 +14754,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-oauth"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
@@ -14779,7 +14779,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-object-store"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-stream",
|
||||
@@ -14813,7 +14813,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-operator"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"futures",
|
||||
@@ -14831,7 +14831,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"convert_case 0.6.0",
|
||||
"serde",
|
||||
@@ -14840,7 +14840,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-bash"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -14852,7 +14852,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-csharp"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -14864,7 +14864,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-go"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"gosyn",
|
||||
@@ -14876,7 +14876,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-graphql"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -14888,7 +14888,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-java"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -14900,7 +14900,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-nu"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"nu-parser",
|
||||
@@ -14911,7 +14911,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-php"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -14922,7 +14922,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -14934,7 +14934,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py-asset"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"rustpython-ast",
|
||||
@@ -14945,7 +14945,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py-imports"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -14967,7 +14967,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-r"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -14979,7 +14979,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ruby"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -14993,7 +14993,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-rust"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"convert_case 0.6.0",
|
||||
@@ -15010,7 +15010,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-sql"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -15023,7 +15023,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-sql-asset"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
@@ -15035,7 +15035,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ts"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -15053,7 +15053,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ts-asset"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde-wasm-bindgen",
|
||||
@@ -15069,7 +15069,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-wac"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"rustpython-ast",
|
||||
@@ -15085,7 +15085,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-yaml"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
@@ -15096,7 +15096,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-queue"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -15134,7 +15134,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-runtime-nativets"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"const_format",
|
||||
@@ -15173,7 +15173,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-sql-datatype-parser-wasm"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
"wasm-bindgen",
|
||||
@@ -15184,7 +15184,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-store"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -15218,7 +15218,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-test-utils"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -15242,7 +15242,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -15275,7 +15275,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-azure"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -15308,7 +15308,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-email"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -15328,7 +15328,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-gcp"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -15362,7 +15362,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-http"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -15398,7 +15398,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-kafka"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -15421,7 +15421,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-mqtt"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -15445,7 +15445,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-nats"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-nats",
|
||||
@@ -15469,7 +15469,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-postgres"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -15504,7 +15504,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-sqs"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -15532,7 +15532,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-websocket"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -15557,7 +15557,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-types"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.13.0",
|
||||
@@ -15576,7 +15576,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-worker"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-once-cell",
|
||||
@@ -15686,7 +15686,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-worker-volumes"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures",
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "windmill"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
@@ -87,7 +87,7 @@ members = [
|
||||
exclude = ["./windmill-duckdb-ffi-internal", "./parsers/windmill-parser-wasm"]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
|
||||
edition = "2021"
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
ac1f6f666f36141cb6ba6f8eaa614821a90464ad
|
||||
6d3301507db50818f1683dac3941d3e0cf1152a7
|
||||
|
||||
+24
-24
@@ -6191,7 +6191,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windmill-common"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"anyhow",
|
||||
@@ -6272,7 +6272,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-macros"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -6284,7 +6284,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"convert_case",
|
||||
"serde",
|
||||
@@ -6293,7 +6293,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-bash"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -6305,7 +6305,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-csharp"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -6317,7 +6317,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-go"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"gosyn",
|
||||
@@ -6329,7 +6329,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-graphql"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -6341,7 +6341,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-java"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -6353,7 +6353,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-nu"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"nu-parser",
|
||||
@@ -6364,7 +6364,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-php"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -6375,7 +6375,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -6387,7 +6387,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py-asset"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"rustpython-ast",
|
||||
@@ -6398,7 +6398,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py-imports"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -6420,7 +6420,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-r"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -6432,7 +6432,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ruby"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -6446,7 +6446,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-rust"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"convert_case",
|
||||
@@ -6463,7 +6463,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-sql"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -6476,7 +6476,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-sql-asset"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
@@ -6488,7 +6488,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ts"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -6506,7 +6506,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ts-asset"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde-wasm-bindgen",
|
||||
@@ -6522,7 +6522,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-wac"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"rustpython-ast",
|
||||
@@ -6538,7 +6538,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-wasm"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"getrandom 0.2.17",
|
||||
@@ -6570,7 +6570,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-yaml"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
@@ -6581,7 +6581,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-types"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags",
|
||||
|
||||
@@ -12,7 +12,7 @@ resolver = "2"
|
||||
members = ["."]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
edition = "2021"
|
||||
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
|
||||
|
||||
|
||||
@@ -487,9 +487,13 @@ fn parse_kv_opts(s: &str) -> BTreeMap<String, String> {
|
||||
out
|
||||
}
|
||||
|
||||
// Scan raw source for pipeline annotations. Language-agnostic: any line
|
||||
// whose first non-whitespace tokens are a comment prefix (`//`, `#`, or
|
||||
// `--`) followed by one of the recognized keywords:
|
||||
// Scan the leading comment header for pipeline annotations. Only the
|
||||
// contiguous block of comment lines at the top of the file is considered
|
||||
// (blank lines tolerated, scan stops at the first line of actual code) so
|
||||
// that ordinary comments in the body can't false-positive as annotations.
|
||||
// Language-agnostic: any header line whose first non-whitespace tokens are
|
||||
// a comment prefix (`//`, `#`, or `--`) followed by one of the recognized
|
||||
// keywords:
|
||||
// - `pipeline` → opt-in marker (must be alone on the line)
|
||||
// - `on <trigger-spec>` → asset / native trigger edge (including
|
||||
// the marker-only `on schedule` form)
|
||||
@@ -527,6 +531,9 @@ pub fn parse_pipeline_annotations(code: &str) -> PipelineAnnotations {
|
||||
|
||||
for raw_line in code.lines() {
|
||||
let line = raw_line.trim_start();
|
||||
if line.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let rest = if let Some(r) = line.strip_prefix("//") {
|
||||
r
|
||||
} else if let Some(r) = line.strip_prefix("--") {
|
||||
@@ -534,7 +541,11 @@ pub fn parse_pipeline_annotations(code: &str) -> PipelineAnnotations {
|
||||
} else if let Some(r) = line.strip_prefix('#') {
|
||||
r
|
||||
} else {
|
||||
continue;
|
||||
// Annotations live in the leading comment header. Stop at the first
|
||||
// line of actual code so comments inside the body (e.g. a regular
|
||||
// `# tag ...` prose comment) can't false-positive as annotations.
|
||||
// Mirrors BashAnnotations::sandbox_image / ssh_target.
|
||||
break;
|
||||
};
|
||||
let rest = rest.trim_start();
|
||||
|
||||
@@ -584,7 +595,14 @@ pub fn parse_pipeline_annotations(code: &str) -> PipelineAnnotations {
|
||||
|
||||
if let Some(after_kw) = consume_keyword(rest, "tag") {
|
||||
let name = after_kw.trim();
|
||||
if !name.is_empty() && out.tag.is_none() {
|
||||
// Worker tags are single-word identifiers (e.g. `heavy`, `gpu`).
|
||||
// A value with whitespace or beyond the `script.tag` column width
|
||||
// is almost certainly a regular comment starting with "# tag ...".
|
||||
if !name.is_empty()
|
||||
&& !name.contains(char::is_whitespace)
|
||||
&& name.len() <= 50
|
||||
&& out.tag.is_none()
|
||||
{
|
||||
out.tag = Some(name.to_string());
|
||||
}
|
||||
continue;
|
||||
@@ -1074,6 +1092,56 @@ mod pipeline_annotation_tests {
|
||||
assert!(out.tag.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tag_with_whitespace_is_skipped() {
|
||||
// A regular English comment starting with "# tag " must not be
|
||||
// mistaken for a worker-tag annotation (worker tags are single words).
|
||||
let out =
|
||||
parse_pipeline_annotations("# tag this function so we remember to refactor it later");
|
||||
assert!(out.tag.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tag_too_long_is_skipped() {
|
||||
let long = "x".repeat(51);
|
||||
let out = parse_pipeline_annotations(&format!("// tag {long}"));
|
||||
assert!(out.tag.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn annotations_in_body_are_ignored() {
|
||||
// Only the leading comment header is scanned. A regular `# tag ...`
|
||||
// prose comment buried in the body — the WIN-2090 false-positive that
|
||||
// crashed the `script.tag` INSERT — must not be treated as an
|
||||
// annotation once real code has started.
|
||||
let code = concat!(
|
||||
"import pandas as pd\n",
|
||||
"\n",
|
||||
"def main():\n",
|
||||
" # tag each row with its source so downstream steps can filter\n",
|
||||
" # on s3://should/not/parse\n",
|
||||
" return pd.DataFrame()\n",
|
||||
);
|
||||
let out = parse_pipeline_annotations(code);
|
||||
assert!(out.tag.is_none());
|
||||
assert!(out.triggers.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_allows_blank_lines_before_code() {
|
||||
// Blank lines (e.g. after a shebang) don't end the header; the first
|
||||
// line of real code does.
|
||||
let code = concat!(
|
||||
"#!/usr/bin/env python\n",
|
||||
"\n",
|
||||
"# tag heavy\n",
|
||||
"import os\n",
|
||||
"# tag light\n",
|
||||
);
|
||||
let out = parse_pipeline_annotations(code);
|
||||
assert_eq!(out.tag.as_deref(), Some("heavy"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retry_count_only() {
|
||||
let out = parse_pipeline_annotations("// retry 3");
|
||||
|
||||
+83
-33
@@ -1324,6 +1324,9 @@ pub async fn delete_expired_items(db: &DB) -> () {
|
||||
let cleanup_start = Instant::now();
|
||||
let mut total_deleted = 0u64;
|
||||
let mut batch_num = 0i32;
|
||||
// Watermark carried across batches so each one resumes after the rows the previous batch
|
||||
// already processed instead of re-scanning the (potentially undeletable) oldest prefix.
|
||||
let mut completed_at_floor: Option<DateTime<Utc>> = None;
|
||||
|
||||
// Process batches until no more expired jobs or max batches reached
|
||||
loop {
|
||||
@@ -1336,14 +1339,17 @@ pub async fn delete_expired_items(db: &DB) -> () {
|
||||
}
|
||||
|
||||
// Each batch runs in its own transaction to avoid long-running locks
|
||||
let batch_result = delete_expired_jobs_batch(db, job_retention_secs, batch_size).await;
|
||||
let batch_result =
|
||||
delete_expired_jobs_batch(db, job_retention_secs, batch_size, completed_at_floor)
|
||||
.await;
|
||||
|
||||
match batch_result {
|
||||
Ok(deleted_count) => {
|
||||
Ok((deleted_count, max_completed_at)) => {
|
||||
if deleted_count == 0 {
|
||||
// No more expired jobs to delete
|
||||
break;
|
||||
}
|
||||
completed_at_floor = max_completed_at.or(completed_at_floor);
|
||||
total_deleted += deleted_count as u64;
|
||||
batch_num += 1;
|
||||
}
|
||||
@@ -1510,12 +1516,20 @@ pub async fn check_expiring_tokens(db: &DB) {
|
||||
|
||||
/// Delete a batch of expired jobs with LIMIT and SKIP LOCKED for high-scale environments.
|
||||
/// Uses a single transaction per batch to minimize lock duration.
|
||||
/// Returns the number of jobs deleted in this batch.
|
||||
///
|
||||
/// `completed_at_floor` is the watermark from the previous batch in the same cleanup run (the
|
||||
/// max `completed_at` it deleted); pass `None` for the first batch. It is re-applied as
|
||||
/// `completed_at >= floor` so the scan resumes past the rows already processed instead of
|
||||
/// re-walking them (see the inline comment on the DELETE for why this matters).
|
||||
///
|
||||
/// Returns `(jobs deleted in this batch, max completed_at deleted)`. The caller feeds the
|
||||
/// returned watermark back in as `completed_at_floor` for the next batch.
|
||||
async fn delete_expired_jobs_batch(
|
||||
db: &DB,
|
||||
job_retention_secs: i64,
|
||||
batch_size: i64,
|
||||
) -> error::Result<usize> {
|
||||
completed_at_floor: Option<DateTime<Utc>>,
|
||||
) -> error::Result<(usize, Option<DateTime<Utc>>)> {
|
||||
let mut tx = db.begin().await?;
|
||||
|
||||
// Fetch active ROOT job IDs that started before the retention period. We only care about
|
||||
@@ -1531,34 +1545,70 @@ async fn delete_expired_jobs_batch(
|
||||
.fetch_all(&mut *tx)
|
||||
.await?;
|
||||
|
||||
// Use FOR UPDATE SKIP LOCKED to avoid contention between replicas
|
||||
// ORDER BY completed_at ensures we delete oldest jobs first.
|
||||
// Active-root exclusion uses `NOT IN (SELECT ... unnest($3))` rather than
|
||||
// `!= ALL($3)`: the subquery form lets the planner build a one-time hashed
|
||||
// SubPlan and apply it as a filter on the ordered index scan, giving O(1)
|
||||
// membership per candidate instead of a per-row linear array scan (which
|
||||
// degrades sharply when many root jobs are active). The `u IS NOT NULL` guard
|
||||
// sidesteps NOT IN's null-trap semantics ($3 holds non-null PK ids).
|
||||
let deleted_jobs: Vec<Uuid> = sqlx::query_scalar!(
|
||||
"DELETE FROM v2_job_completed
|
||||
WHERE id IN (
|
||||
SELECT jc.id FROM v2_job_completed jc
|
||||
LEFT JOIN v2_job j ON j.id = jc.id
|
||||
WHERE jc.completed_at <= now() - ($1::bigint::text || ' s')::interval
|
||||
AND COALESCE(j.root_job, j.flow_innermost_root_job, jc.id) NOT IN (
|
||||
SELECT u FROM unnest($3::uuid[]) AS u WHERE u IS NOT NULL
|
||||
)
|
||||
ORDER BY jc.completed_at ASC
|
||||
LIMIT $2
|
||||
FOR UPDATE OF jc SKIP LOCKED
|
||||
)
|
||||
RETURNING id",
|
||||
job_retention_secs,
|
||||
batch_size,
|
||||
&active_root_job_ids
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
.await?;
|
||||
// `completed_at_floor` is a watermark carried across batches within a cleanup run: it is the
|
||||
// max(completed_at) deleted by the previous batch. Re-applying it as `completed_at >= floor`
|
||||
// lets each batch resume after the rows the previous batch already processed instead of
|
||||
// re-scanning them. This matters when the oldest rows are undeletable (children of a
|
||||
// still-active root flow): without the floor the `ORDER BY completed_at ASC` scan walks that
|
||||
// same protected prefix on every batch, turning a cleanup run quadratic in prefix size.
|
||||
// Floor only ever skips rows the current run already deleted, was protecting, or skip-locked —
|
||||
// all correctly deferred to the next run, identical to the unbounded scan's semantics.
|
||||
//
|
||||
// Use FOR UPDATE SKIP LOCKED to avoid contention between replicas; ORDER BY completed_at
|
||||
// deletes oldest jobs first.
|
||||
let (deleted_jobs, max_completed_at) = if active_root_job_ids.is_empty() {
|
||||
// Common case: no old root flow is still running, so nothing is protected and the
|
||||
// v2_job join (a PK lookup per candidate) is pure overhead — skip it entirely.
|
||||
let rows = sqlx::query!(
|
||||
"DELETE FROM v2_job_completed
|
||||
WHERE id IN (
|
||||
SELECT id FROM v2_job_completed
|
||||
WHERE completed_at <= now() - ($1::bigint::text || ' s')::interval
|
||||
AND ($3::timestamptz IS NULL OR completed_at >= $3)
|
||||
ORDER BY completed_at ASC
|
||||
LIMIT $2
|
||||
FOR UPDATE SKIP LOCKED
|
||||
)
|
||||
RETURNING id, completed_at",
|
||||
job_retention_secs,
|
||||
batch_size,
|
||||
completed_at_floor,
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
.await?;
|
||||
let max = rows.iter().map(|r| r.completed_at).max();
|
||||
(rows.into_iter().map(|r| r.id).collect::<Vec<Uuid>>(), max)
|
||||
} else {
|
||||
// Active-root exclusion uses `NOT IN (SELECT ... unnest($3))` rather than `!= ALL($3)`:
|
||||
// the subquery form lets the planner build a one-time hashed SubPlan and apply it as a
|
||||
// filter on the ordered index scan, giving O(1) membership per candidate instead of a
|
||||
// per-row linear array scan (which degrades sharply when many root jobs are active). The
|
||||
// `u IS NOT NULL` guard sidesteps NOT IN's null-trap semantics ($3 holds non-null PK ids).
|
||||
let rows = sqlx::query!(
|
||||
"DELETE FROM v2_job_completed
|
||||
WHERE id IN (
|
||||
SELECT jc.id FROM v2_job_completed jc
|
||||
LEFT JOIN v2_job j ON j.id = jc.id
|
||||
WHERE jc.completed_at <= now() - ($1::bigint::text || ' s')::interval
|
||||
AND ($4::timestamptz IS NULL OR jc.completed_at >= $4)
|
||||
AND COALESCE(j.root_job, j.flow_innermost_root_job, jc.id) NOT IN (
|
||||
SELECT u FROM unnest($3::uuid[]) AS u WHERE u IS NOT NULL
|
||||
)
|
||||
ORDER BY jc.completed_at ASC
|
||||
LIMIT $2
|
||||
FOR UPDATE OF jc SKIP LOCKED
|
||||
)
|
||||
RETURNING id, completed_at",
|
||||
job_retention_secs,
|
||||
batch_size,
|
||||
&active_root_job_ids,
|
||||
completed_at_floor,
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
.await?;
|
||||
let max = rows.iter().map(|r| r.completed_at).max();
|
||||
(rows.into_iter().map(|r| r.id).collect::<Vec<Uuid>>(), max)
|
||||
};
|
||||
|
||||
let deleted_count = deleted_jobs.len();
|
||||
|
||||
@@ -1618,7 +1668,7 @@ async fn delete_expired_jobs_batch(
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(deleted_count)
|
||||
Ok((deleted_count, max_completed_at))
|
||||
}
|
||||
|
||||
async fn delete_log_files_from_disk_and_store(
|
||||
|
||||
@@ -646,6 +646,20 @@ async fn test_workspace_endpoints(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
.unwrap();
|
||||
assert_eq!(resp.json::<bool>().await?, true);
|
||||
|
||||
// Regression: changing a fork's workspace id must preserve its parent
|
||||
// linkage. Dropping it leaves a wm-fork- workspace with no parent — a
|
||||
// "fork of nothing" that can no longer be compared or merged.
|
||||
let parent: Option<String> =
|
||||
sqlx::query_scalar("SELECT parent_workspace_id FROM workspace WHERE id = $1")
|
||||
.bind("wm-fork-renamed")
|
||||
.fetch_one(&db)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
parent.as_deref(),
|
||||
Some("new-test-ws"),
|
||||
"renamed fork must keep its parent_workspace_id"
|
||||
);
|
||||
|
||||
// --- create_fork over an existing (active) workspace id: clear 400, not a raw SQL 500 ---
|
||||
let resp = authed(client().post(format!("{new_ws_base}/create_fork")))
|
||||
.json(&json!({
|
||||
|
||||
@@ -256,52 +256,300 @@ pub async fn test_s3_bucket(
|
||||
use bytes::Bytes;
|
||||
use futures::StreamExt;
|
||||
|
||||
require_super_admin(&db, &authed.email).await?;
|
||||
// The probe executes on the API server itself. On multi-tenant Cloud that is a shared control
|
||||
// plane, so we constrain untrusted callers to remove the SSRF / credential-exfiltration /
|
||||
// local-filesystem surface (see validate_object_storage_test). On self-hosted instances the
|
||||
// object store usually lives on the local/private network and all authenticated users are
|
||||
// trusted, so testing there stays unrestricted. Super admins keep the unrestricted path too.
|
||||
let is_super_admin = is_super_admin_email(&db, &authed.email).await?;
|
||||
let restrict = !is_super_admin && *CLOUD_HOSTED;
|
||||
if restrict {
|
||||
validate_object_storage_test(&test_s3_bucket).await?;
|
||||
}
|
||||
|
||||
let client = build_object_store_from_settings(test_s3_bucket, Some(&db))
|
||||
.await?
|
||||
.store;
|
||||
|
||||
let mut list = client.list(Some(
|
||||
&windmill_object_store::object_store_reexports::Path::from("".to_string()),
|
||||
));
|
||||
let first_file = list.next().await;
|
||||
if first_file.is_some() {
|
||||
if let Err(e) = first_file.as_ref().unwrap() {
|
||||
tracing::error!("error listing bucket: {e:#}");
|
||||
error::Error::internal_err(format!("Failed to list files in blob storage: {e:#}"));
|
||||
let run = async {
|
||||
let mut list = client.list(Some(
|
||||
&windmill_object_store::object_store_reexports::Path::from("".to_string()),
|
||||
));
|
||||
let first_file = list.next().await;
|
||||
if first_file.is_some() {
|
||||
if let Err(e) = first_file.as_ref().unwrap() {
|
||||
tracing::error!("error listing bucket: {e:#}");
|
||||
error::Error::internal_err(format!("Failed to list files in blob storage: {e:#}"));
|
||||
}
|
||||
tracing::info!("Listed files: {:?}", first_file.unwrap());
|
||||
} else {
|
||||
tracing::info!("No files in blob storage");
|
||||
}
|
||||
tracing::info!("Listed files: {:?}", first_file.unwrap());
|
||||
|
||||
let path = windmill_object_store::object_store_reexports::Path::from(format!(
|
||||
"/test-s3-bucket-{uuid}",
|
||||
uuid = uuid::Uuid::new_v4()
|
||||
));
|
||||
tracing::info!("Testing blob storage at path: {path}");
|
||||
client
|
||||
.put(
|
||||
&path,
|
||||
windmill_object_store::object_store_reexports::PutPayload::from_static(b"hello"),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("error writing file to {path}: {e:#}"))?;
|
||||
let content = client
|
||||
.get(&path)
|
||||
.await
|
||||
.map_err(to_anyhow)?
|
||||
.bytes()
|
||||
.await
|
||||
.map_err(to_anyhow)?;
|
||||
if content != Bytes::from_static(b"hello") {
|
||||
return Err(error::Error::internal_err(
|
||||
"Failed to read back from blob storage".to_string(),
|
||||
));
|
||||
}
|
||||
client.delete(&path).await.map_err(to_anyhow)?;
|
||||
Ok::<String, error::Error>("Tested blob storage successfully".to_string())
|
||||
};
|
||||
|
||||
if restrict {
|
||||
// The object-store client is built with timeouts disabled, so a malicious endpoint could
|
||||
// otherwise hold the API server connection open indefinitely.
|
||||
tokio::time::timeout(Duration::from_secs(15), run)
|
||||
.await
|
||||
.map_err(|_| {
|
||||
error::Error::internal_err("Object storage connectivity test timed out".to_string())
|
||||
})?
|
||||
} else {
|
||||
tracing::info!("No files in blob storage");
|
||||
run.await
|
||||
}
|
||||
}
|
||||
|
||||
// Hardening for the object-storage connectivity test by an untrusted (non-super-admin) caller on
|
||||
// Cloud. The probe runs on the shared API server, so without these constraints an authenticated
|
||||
// user could coerce the server into connecting to arbitrary internal endpoints (SSRF), signing
|
||||
// requests with the instance role (credential exfiltration), or reading/writing the server's local
|
||||
// disk (filesystem object store).
|
||||
#[cfg(feature = "parquet")]
|
||||
async fn validate_object_storage_test(settings: &ObjectSettings) -> error::Result<()> {
|
||||
fn non_empty(opt: &Option<String>) -> bool {
|
||||
opt.as_ref().is_some_and(|s| !s.is_empty())
|
||||
}
|
||||
|
||||
let path = windmill_object_store::object_store_reexports::Path::from(format!(
|
||||
"/test-s3-bucket-{uuid}",
|
||||
uuid = uuid::Uuid::new_v4()
|
||||
));
|
||||
tracing::info!("Testing blob storage at path: {path}");
|
||||
client
|
||||
.put(
|
||||
&path,
|
||||
windmill_object_store::object_store_reexports::PutPayload::from_static(b"hello"),
|
||||
)
|
||||
// Reject backends that rely on the server's identity or local filesystem, require explicit
|
||||
// credentials for the rest (so the server never falls back to its own ambient credentials), and
|
||||
// resolve the host the client will actually connect to. We derive the *effective* endpoint here
|
||||
// — mirroring build_*_from_settings: the region/account-derived default and the virtual-hosted
|
||||
// bucket prefix — rather than only validating a caller-supplied `endpoint`, so caller-controlled
|
||||
// `region`/`account_name`/`bucket` cannot smuggle an internal host past the check (e.g. an empty
|
||||
// endpoint with region = "@169.254.169.254/" otherwise resolves to the cloud metadata service).
|
||||
let effective_endpoint: Option<String> = match settings {
|
||||
ObjectSettings::Filesystem(_) => {
|
||||
return Err(error::Error::NotAuthorized(
|
||||
"Testing a local filesystem object store requires a super admin".to_string(),
|
||||
));
|
||||
}
|
||||
ObjectSettings::AwsOidc(_) => {
|
||||
return Err(error::Error::NotAuthorized(
|
||||
"Testing OIDC-based object storage requires a super admin".to_string(),
|
||||
));
|
||||
}
|
||||
ObjectSettings::S3(s3) => {
|
||||
if !(non_empty(&s3.access_key) && non_empty(&s3.secret_key)) {
|
||||
return Err(error::Error::NotAuthorized(
|
||||
"Testing S3 storage without explicit credentials requires a super admin"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
let region = s3
|
||||
.region
|
||||
.clone()
|
||||
.filter(|r| !r.is_empty())
|
||||
.or_else(|| std::env::var("AWS_REGION").ok().filter(|r| !r.is_empty()))
|
||||
.unwrap_or_else(|| "us-east-1".to_string());
|
||||
let raw_endpoint = s3
|
||||
.endpoint
|
||||
.clone()
|
||||
.filter(|e| !e.is_empty())
|
||||
.or_else(|| std::env::var("S3_ENDPOINT").ok().filter(|e| !e.is_empty()))
|
||||
.unwrap_or_else(|| format!("s3.{region}.amazonaws.com"));
|
||||
Some(windmill_object_store::render_endpoint(
|
||||
raw_endpoint,
|
||||
!s3.allow_http.unwrap_or(true),
|
||||
s3.port,
|
||||
s3.path_style,
|
||||
s3.bucket.clone().unwrap_or_default(),
|
||||
))
|
||||
}
|
||||
ObjectSettings::Azure(azure) => {
|
||||
if !non_empty(&azure.access_key) {
|
||||
return Err(error::Error::NotAuthorized(
|
||||
"Testing Azure storage without an explicit access key requires a super admin"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
Some(
|
||||
azure
|
||||
.endpoint
|
||||
.clone()
|
||||
.filter(|e| !e.is_empty())
|
||||
.unwrap_or_else(|| format!("{}.blob.core.windows.net", azure.account_name)),
|
||||
)
|
||||
}
|
||||
ObjectSettings::Gcs(gcs) => {
|
||||
if gcs.service_account_key.is_empty() {
|
||||
return Err(error::Error::NotAuthorized(
|
||||
"Testing GCS storage without a service account key requires a super admin"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
// The service-account-key JSON can override the data-plane URL (`gcs_base_url`) and the
|
||||
// OAuth token endpoint (`token_uri`); the GCS client connects to whatever they point at.
|
||||
// Validate every http(s) URL embedded in the key. When none override it, the host stays
|
||||
// the public storage.googleapis.com, so no further check is needed.
|
||||
if let Ok(serde_json::Value::Object(map)) =
|
||||
serde_json::from_str::<serde_json::Value>(&gcs.service_account_key)
|
||||
{
|
||||
for value in map.values() {
|
||||
if let Some(url) = value.as_str() {
|
||||
// Match how the URL parser reads the value: leading whitespace/control is
|
||||
// ignored and the scheme is case-insensitive.
|
||||
let url =
|
||||
url.trim_start_matches(|c: char| c.is_whitespace() || c.is_control());
|
||||
if strip_http_scheme(url).is_some() {
|
||||
validate_public_endpoint(url).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
// Block non-public network targets (internal services, cloud metadata, loopback, ...).
|
||||
if let Some(endpoint) = effective_endpoint {
|
||||
validate_public_endpoint(&endpoint).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "parquet")]
|
||||
async fn validate_public_endpoint(endpoint: &str) -> error::Result<()> {
|
||||
let host = extract_host(endpoint).ok_or_else(|| {
|
||||
error::Error::BadRequest(format!("Invalid object storage endpoint: {endpoint}"))
|
||||
})?;
|
||||
|
||||
let addrs: Vec<std::net::SocketAddr> = tokio::net::lookup_host((host.as_str(), 443u16))
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("error writing file to {path}: {e:#}"))?;
|
||||
let content = client
|
||||
.get(&path)
|
||||
.await
|
||||
.map_err(to_anyhow)?
|
||||
.bytes()
|
||||
.await
|
||||
.map_err(to_anyhow)?;
|
||||
if content != Bytes::from_static(b"hello") {
|
||||
return Err(error::Error::internal_err(
|
||||
"Failed to read back from blob storage".to_string(),
|
||||
));
|
||||
.map_err(|e| {
|
||||
error::Error::BadRequest(format!(
|
||||
"Could not resolve object storage endpoint '{host}': {e}"
|
||||
))
|
||||
})?
|
||||
.collect();
|
||||
|
||||
if addrs.is_empty() {
|
||||
return Err(error::Error::BadRequest(format!(
|
||||
"Could not resolve object storage endpoint '{host}'"
|
||||
)));
|
||||
}
|
||||
|
||||
// Reject if any resolved address is non-public, which also defeats the simplest DNS-rebinding
|
||||
// attempts (a name resolving to both a public and a private address).
|
||||
for addr in addrs {
|
||||
if is_forbidden_ip(addr.ip()) {
|
||||
return Err(error::Error::NotAuthorized(
|
||||
"Testing object storage at a private, loopback, or link-local endpoint requires a super admin"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Strip a leading `http://`/`https://` scheme case-insensitively (URL schemes are
|
||||
// case-insensitive), returning the remainder when one was present.
|
||||
#[cfg(feature = "parquet")]
|
||||
fn strip_http_scheme(s: &str) -> Option<&str> {
|
||||
for scheme in ["https://", "http://"] {
|
||||
let b = scheme.as_bytes();
|
||||
if s.len() >= b.len() && s.as_bytes()[..b.len()].eq_ignore_ascii_case(b) {
|
||||
return Some(&s[b.len()..]);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(feature = "parquet")]
|
||||
fn extract_host(endpoint: &str) -> Option<String> {
|
||||
let mut s = endpoint.trim();
|
||||
if let Some(rest) = strip_http_scheme(s) {
|
||||
s = rest;
|
||||
}
|
||||
s = s.split(['/', '?', '#', '\\']).next().unwrap_or(s);
|
||||
if let Some((_, rest)) = s.rsplit_once('@') {
|
||||
s = rest;
|
||||
}
|
||||
let host = if let Some(rest) = s.strip_prefix('[') {
|
||||
// IPv6 literal, e.g. [::1]:9000
|
||||
rest.split(']').next().unwrap_or(rest)
|
||||
} else {
|
||||
// host or host:port
|
||||
s.split(':').next().unwrap_or(s)
|
||||
}
|
||||
.trim();
|
||||
if host.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(host.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "parquet")]
|
||||
fn is_forbidden_ip(ip: std::net::IpAddr) -> bool {
|
||||
use std::net::{IpAddr, Ipv4Addr};
|
||||
match ip {
|
||||
IpAddr::V4(v4) => {
|
||||
v4.is_loopback()
|
||||
|| v4.is_private()
|
||||
|| v4.is_link_local() // 169.254.0.0/16, incl. the cloud metadata endpoint
|
||||
|| v4.is_unspecified()
|
||||
|| v4.is_broadcast()
|
||||
|| v4.is_documentation()
|
||||
|| v4.is_multicast()
|
||||
|| v4.octets()[0] == 0 // 0.0.0.0/8
|
||||
|| (v4.octets()[0] == 100 && (v4.octets()[1] & 0xc0) == 64) // 100.64.0.0/10 CGNAT
|
||||
}
|
||||
IpAddr::V6(v6) => {
|
||||
// Any IPv4 embedded in an IPv6 address (IPv4-mapped ::ffff:0:0/96, IPv4-compatible
|
||||
// ::/96, or NAT64 64:ff9b::/96) is re-checked against the IPv4 rules, so e.g.
|
||||
// 64:ff9b::169.254.169.254 cannot route to the metadata endpoint in a NAT64 network.
|
||||
let seg = v6.segments();
|
||||
let is_v4_compatible = seg[0..6] == [0, 0, 0, 0, 0, 0];
|
||||
let is_nat64 = seg[0] == 0x0064 && seg[1] == 0xff9b && seg[2..6] == [0, 0, 0, 0];
|
||||
if let Some(v4) = v6.to_ipv4_mapped() {
|
||||
return is_forbidden_ip(IpAddr::V4(v4));
|
||||
}
|
||||
if is_v4_compatible || is_nat64 {
|
||||
let embedded = Ipv4Addr::new(
|
||||
(seg[6] >> 8) as u8,
|
||||
(seg[6] & 0xff) as u8,
|
||||
(seg[7] >> 8) as u8,
|
||||
(seg[7] & 0xff) as u8,
|
||||
);
|
||||
if is_forbidden_ip(IpAddr::V4(embedded)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
v6.is_loopback()
|
||||
|| v6.is_unspecified()
|
||||
|| v6.is_multicast()
|
||||
|| (seg[0] & 0xfe00) == 0xfc00 // fc00::/7 unique local
|
||||
|| (seg[0] & 0xffc0) == 0xfe80 // fe80::/10 link-local
|
||||
}
|
||||
}
|
||||
client.delete(&path).await.map_err(to_anyhow)?;
|
||||
Ok("Tested blob storage successfully".to_string())
|
||||
}
|
||||
|
||||
#[cfg(feature = "parquet")]
|
||||
@@ -1861,3 +2109,111 @@ mod tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, feature = "parquet"))]
|
||||
mod object_storage_test_hardening {
|
||||
use super::{extract_host, is_forbidden_ip, validate_object_storage_test};
|
||||
use std::net::IpAddr;
|
||||
use windmill_object_store::ObjectSettings;
|
||||
|
||||
// IP literals (not hostnames) keep validate_public_endpoint deterministic — `lookup_host`
|
||||
// parses them without any network round-trip.
|
||||
fn gcs_settings(gcs_base_url: &str) -> ObjectSettings {
|
||||
serde_json::from_value(serde_json::json!({
|
||||
"type": "Gcs",
|
||||
"bucket": "b",
|
||||
"serviceAccountKey": { "gcs_base_url": gcs_base_url, "client_email": "x@y.z" }
|
||||
}))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn rejects_gcs_internal_base_url() {
|
||||
// gcs_base_url in the service-account key must not smuggle an internal host past the check,
|
||||
// including via a mixed-case scheme (URL schemes are case-insensitive).
|
||||
for url in [
|
||||
"http://169.254.169.254",
|
||||
"HTTP://169.254.169.254",
|
||||
"Https://10.0.0.5",
|
||||
] {
|
||||
assert!(
|
||||
validate_object_storage_test(&gcs_settings(url))
|
||||
.await
|
||||
.is_err(),
|
||||
"{url} should be rejected"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn allows_gcs_public_base_url() {
|
||||
assert!(
|
||||
validate_object_storage_test(&gcs_settings("https://8.8.8.8"))
|
||||
.await
|
||||
.is_ok()
|
||||
);
|
||||
}
|
||||
|
||||
fn ip(s: &str) -> IpAddr {
|
||||
s.parse().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn forbids_internal_ips() {
|
||||
for s in [
|
||||
"127.0.0.1", // loopback
|
||||
"169.254.169.254", // cloud metadata (link-local)
|
||||
"10.0.0.5", // private
|
||||
"172.16.3.4", // private
|
||||
"192.168.1.10", // private
|
||||
"0.0.0.0", // unspecified
|
||||
"100.64.0.1", // CGNAT
|
||||
"::1", // IPv6 loopback
|
||||
"fe80::1", // IPv6 link-local
|
||||
"fc00::1", // IPv6 unique local
|
||||
"::ffff:127.0.0.1", // IPv4-mapped loopback
|
||||
"::ffff:169.254.169.254", // IPv4-mapped metadata
|
||||
"::169.254.169.254", // IPv4-compatible metadata
|
||||
"64:ff9b::169.254.169.254", // NAT64-embedded metadata
|
||||
"64:ff9b::a9fe:a9fe", // NAT64-embedded metadata (hex form)
|
||||
] {
|
||||
assert!(is_forbidden_ip(ip(s)), "{s} should be forbidden");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allows_public_ips() {
|
||||
for s in ["8.8.8.8", "1.1.1.1", "52.95.110.1", "2606:4700:4700::1111"] {
|
||||
assert!(!is_forbidden_ip(ip(s)), "{s} should be allowed");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extracts_host_from_endpoint() {
|
||||
let cases = [
|
||||
("s3.amazonaws.com", Some("s3.amazonaws.com")),
|
||||
("https://minio.internal:9000", Some("minio.internal")),
|
||||
("http://10.0.0.5:9000/bucket", Some("10.0.0.5")),
|
||||
("user:pass@host.example:443", Some("host.example")),
|
||||
("[::1]:9000", Some("::1")),
|
||||
("https://[fe80::1]/x", Some("fe80::1")),
|
||||
("", None),
|
||||
// Injection via region/bucket interpolation into the default endpoint string: the
|
||||
// userinfo `@` and the path `/` must not hide the real authority from the host check.
|
||||
(
|
||||
"https://s3.@169.254.169.254/.amazonaws.com",
|
||||
Some("169.254.169.254"),
|
||||
),
|
||||
(
|
||||
"https://@169.254.169.254/mybucket.s3.amazonaws.com",
|
||||
Some("169.254.169.254"),
|
||||
),
|
||||
("s3.#@169.254.169.254/x.amazonaws.com", Some("s3.")),
|
||||
// Scheme is case-insensitive.
|
||||
("HTTP://169.254.169.254", Some("169.254.169.254")),
|
||||
];
|
||||
for (input, expected) in cases {
|
||||
assert_eq!(extract_host(input).as_deref(), expected, "input: {input}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -339,13 +339,15 @@ async fn cleanup_job_logs(
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut completed_at_floor: Option<DateTime<Utc>> = None;
|
||||
loop {
|
||||
let (deleted_count, rel_paths) =
|
||||
delete_expired_jobs_batch(db, retention_secs, JOB_BATCH).await?;
|
||||
let (deleted_count, rel_paths, max_completed_at) =
|
||||
delete_expired_jobs_batch(db, retention_secs, JOB_BATCH, completed_at_floor).await?;
|
||||
|
||||
if deleted_count == 0 {
|
||||
break;
|
||||
}
|
||||
completed_at_floor = max_completed_at.or(completed_at_floor);
|
||||
|
||||
let s3_paths: Vec<ObjectPath> = rel_paths
|
||||
.iter()
|
||||
@@ -382,7 +384,8 @@ async fn delete_expired_jobs_batch(
|
||||
db: &DB,
|
||||
job_retention_secs: i64,
|
||||
batch_size: i64,
|
||||
) -> error::Result<(usize, Vec<String>)> {
|
||||
completed_at_floor: Option<DateTime<Utc>>,
|
||||
) -> error::Result<(usize, Vec<String>, Option<DateTime<Utc>>)> {
|
||||
let mut tx = db.begin().await?;
|
||||
|
||||
let active_root_job_ids: Vec<Uuid> = sqlx::query_scalar!(
|
||||
@@ -395,33 +398,61 @@ async fn delete_expired_jobs_batch(
|
||||
.fetch_all(&mut *tx)
|
||||
.await?;
|
||||
|
||||
// Active-root exclusion via NOT IN (hashed SubPlan) instead of `!= ALL($3)`;
|
||||
// see backend/src/monitor.rs::delete_expired_jobs_batch for the rationale.
|
||||
let deleted_jobs: Vec<Uuid> = sqlx::query_scalar!(
|
||||
"DELETE FROM v2_job_completed
|
||||
WHERE id IN (
|
||||
SELECT jc.id FROM v2_job_completed jc
|
||||
LEFT JOIN v2_job j ON j.id = jc.id
|
||||
WHERE jc.completed_at <= now() - ($1::bigint::text || ' s')::interval
|
||||
AND COALESCE(j.root_job, j.flow_innermost_root_job, jc.id) NOT IN (
|
||||
SELECT u FROM unnest($3::uuid[]) AS u WHERE u IS NOT NULL
|
||||
)
|
||||
ORDER BY jc.completed_at ASC
|
||||
LIMIT $2
|
||||
FOR UPDATE OF jc SKIP LOCKED
|
||||
)
|
||||
RETURNING id",
|
||||
job_retention_secs,
|
||||
batch_size,
|
||||
&active_root_job_ids
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
.await?;
|
||||
// `completed_at_floor` carries a watermark across batches so each one resumes after the rows
|
||||
// the previous batch processed instead of re-scanning the (potentially undeletable) oldest
|
||||
// prefix; the empty-active-roots branch skips the v2_job join entirely. See
|
||||
// backend/src/monitor.rs::delete_expired_jobs_batch for the full rationale.
|
||||
let (deleted_jobs, max_completed_at) = if active_root_job_ids.is_empty() {
|
||||
let rows = sqlx::query!(
|
||||
"DELETE FROM v2_job_completed
|
||||
WHERE id IN (
|
||||
SELECT id FROM v2_job_completed
|
||||
WHERE completed_at <= now() - ($1::bigint::text || ' s')::interval
|
||||
AND ($3::timestamptz IS NULL OR completed_at >= $3)
|
||||
ORDER BY completed_at ASC
|
||||
LIMIT $2
|
||||
FOR UPDATE SKIP LOCKED
|
||||
)
|
||||
RETURNING id, completed_at",
|
||||
job_retention_secs,
|
||||
batch_size,
|
||||
completed_at_floor,
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
.await?;
|
||||
let max = rows.iter().map(|r| r.completed_at).max();
|
||||
(rows.into_iter().map(|r| r.id).collect::<Vec<Uuid>>(), max)
|
||||
} else {
|
||||
let rows = sqlx::query!(
|
||||
"DELETE FROM v2_job_completed
|
||||
WHERE id IN (
|
||||
SELECT jc.id FROM v2_job_completed jc
|
||||
LEFT JOIN v2_job j ON j.id = jc.id
|
||||
WHERE jc.completed_at <= now() - ($1::bigint::text || ' s')::interval
|
||||
AND ($4::timestamptz IS NULL OR jc.completed_at >= $4)
|
||||
AND COALESCE(j.root_job, j.flow_innermost_root_job, jc.id) NOT IN (
|
||||
SELECT u FROM unnest($3::uuid[]) AS u WHERE u IS NOT NULL
|
||||
)
|
||||
ORDER BY jc.completed_at ASC
|
||||
LIMIT $2
|
||||
FOR UPDATE OF jc SKIP LOCKED
|
||||
)
|
||||
RETURNING id, completed_at",
|
||||
job_retention_secs,
|
||||
batch_size,
|
||||
&active_root_job_ids,
|
||||
completed_at_floor,
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
.await?;
|
||||
let max = rows.iter().map(|r| r.completed_at).max();
|
||||
(rows.into_iter().map(|r| r.id).collect::<Vec<Uuid>>(), max)
|
||||
};
|
||||
|
||||
let deleted_count = deleted_jobs.len();
|
||||
if deleted_count == 0 {
|
||||
tx.commit().await?;
|
||||
return Ok((0, Vec::new()));
|
||||
return Ok((0, Vec::new(), max_completed_at));
|
||||
}
|
||||
|
||||
if let Err(e) = sqlx::query!(
|
||||
@@ -471,7 +502,7 @@ async fn delete_expired_jobs_batch(
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
Ok((deleted_count, log_paths))
|
||||
Ok((deleted_count, log_paths, max_completed_at))
|
||||
}
|
||||
|
||||
/// Scan S3 under the `logs/` prefix for orphan log files and delete them.
|
||||
|
||||
@@ -717,16 +717,27 @@ async fn create_deployment_request_comment(
|
||||
// ---- helpers ------------------------------------------------------------
|
||||
|
||||
async fn parent_of_fork(db: &DB, w_id: &str) -> Result<String> {
|
||||
sqlx::query_scalar!(
|
||||
"SELECT parent_workspace_id FROM workspace WHERE id = $1",
|
||||
// Resolve the fork's parent and require it to still exist and be active. A
|
||||
// parent that is archived (soft-deleted) can no longer be accessed, so a
|
||||
// diff or deployment request against it targets an unreachable workspace.
|
||||
let parent = sqlx::query!(
|
||||
"SELECT p.id AS \"id!\", p.deleted AS \"deleted!\"
|
||||
FROM workspace f
|
||||
JOIN workspace p ON p.id = f.parent_workspace_id
|
||||
WHERE f.id = $1",
|
||||
w_id,
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await?
|
||||
.flatten()
|
||||
.ok_or_else(|| {
|
||||
Error::BadRequest(format!(
|
||||
.await?;
|
||||
|
||||
match parent {
|
||||
None => Err(Error::BadRequest(format!(
|
||||
"workspace {w_id} is not a fork (no parent_workspace_id)"
|
||||
))
|
||||
})
|
||||
))),
|
||||
Some(p) if p.deleted => Err(Error::BadRequest(format!(
|
||||
"parent workspace {} of fork {w_id} is archived",
|
||||
p.id
|
||||
))),
|
||||
Some(p) => Ok(p.id),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,13 +65,22 @@ pub(crate) async fn change_workspace_id(
|
||||
old_id, rw.new_id
|
||||
);
|
||||
|
||||
// Create new workspace with new id and name
|
||||
// Create new workspace with new id and name. A fork that keeps a wm-fork-
|
||||
// id must carry its parent_workspace_id over, otherwise it becomes a
|
||||
// parentless "fork of nothing" with no source to compare or merge against.
|
||||
// A non-fork target id means the workspace is being promoted out of a fork,
|
||||
// so the parent pointer is intentionally cleared.
|
||||
info!("Creating new workspace row");
|
||||
let new_is_fork = rw.new_id.starts_with(WM_FORK_PREFIX);
|
||||
sqlx::query!(
|
||||
"INSERT INTO workspace SELECT $1, $2, owner, false, premium FROM workspace WHERE id = $3",
|
||||
"INSERT INTO workspace (id, name, owner, deleted, premium, parent_workspace_id)
|
||||
SELECT $1, $2, owner, false, premium,
|
||||
CASE WHEN $4 THEN parent_workspace_id ELSE NULL END
|
||||
FROM workspace WHERE id = $3",
|
||||
&rw.new_id,
|
||||
&rw.new_name,
|
||||
&old_id
|
||||
&old_id,
|
||||
new_is_fork
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
@@ -347,6 +356,18 @@ pub(crate) async fn change_workspace_id(
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
// Re-parent child forks: any fork whose parent_workspace_id was the old id
|
||||
// must follow the renamed parent to the new id, otherwise it is left
|
||||
// pointing at the soft-deleted old shell (whose data has moved here).
|
||||
info!("Re-parenting child forks to the new workspace id");
|
||||
sqlx::query!(
|
||||
"UPDATE workspace SET parent_workspace_id = $1 WHERE parent_workspace_id = $2",
|
||||
&rw.new_id,
|
||||
&old_id
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
info!("Updating workspace_protection_rule table");
|
||||
sqlx::query!(
|
||||
"UPDATE workspace_protection_rule SET workspace_id = $1 WHERE workspace_id = $2",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: "3.0.3"
|
||||
|
||||
info:
|
||||
version: 1.737.0
|
||||
version: 1.738.0
|
||||
title: Windmill API
|
||||
|
||||
contact:
|
||||
|
||||
+1
-1
@@ -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.737.0";
|
||||
export const VERSION = "v1.738.0";
|
||||
|
||||
export async function login(email: string, password: string): Promise<string> {
|
||||
return await windmill.UserService.login({
|
||||
|
||||
@@ -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.737.0";
|
||||
export const VERSION = "1.738.0";
|
||||
|
||||
Generated
+689
-30
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@windmill-labs/components",
|
||||
"version": "1.737.0",
|
||||
"version": "1.738.0",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"dev:ui-builder": "mv static/ui_builder static/ui_builder.dev-disabled 2>/dev/null || true ; trap 'mv static/ui_builder.dev-disabled static/ui_builder 2>/dev/null || true' EXIT ; vite dev",
|
||||
@@ -127,6 +127,7 @@
|
||||
"lru-cache": "^11.1.0",
|
||||
"lucide-svelte": "^0.540.0",
|
||||
"mdast-util-find-and-replace": "^3.0.2",
|
||||
"mermaid": "^11.15.0",
|
||||
"minimatch": "^10.0.1",
|
||||
"monaco-editor": "npm:@codingame/monaco-vscode-editor-api@=25.0.0",
|
||||
"monaco-languageclient": "10.6.0",
|
||||
|
||||
@@ -158,11 +158,6 @@
|
||||
preparedAssetsSqlQueries?: InferAssetsSqlQueryDetails[] | undefined
|
||||
// To execute preview scripts with the right worker group
|
||||
customTag?: string
|
||||
// Opt-in: reflect external `code` prop mutations back into Monaco (see
|
||||
// the effect below). One-way `code={...}` callers that need live
|
||||
// external updates — e.g. the inline flow rawscript — set this. Off by
|
||||
// default so every other caller's behavior is unchanged.
|
||||
syncExternalCode?: boolean
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -195,8 +190,7 @@
|
||||
enablePreprocessorSnippet = false,
|
||||
rawAppRunnableKey = undefined,
|
||||
preparedAssetsSqlQueries,
|
||||
customTag,
|
||||
syncExternalCode = false
|
||||
customTag
|
||||
}: Props = $props()
|
||||
|
||||
$effect.pre(() => {
|
||||
@@ -375,23 +369,12 @@
|
||||
code = ncode
|
||||
}
|
||||
|
||||
if (noHistory) {
|
||||
editor?.setValue(ncode)
|
||||
} else {
|
||||
if (editor?.getModel()) {
|
||||
// editor.setValue(ncode)
|
||||
editor.pushUndoStop()
|
||||
|
||||
editor.executeEdits('set', [
|
||||
{
|
||||
range: editor.getModel()!.getFullModelRange(), // full range
|
||||
text: ncode
|
||||
}
|
||||
])
|
||||
|
||||
editor.pushUndoStop()
|
||||
}
|
||||
}
|
||||
// setCode is an authoritative overwrite (reset, AI apply, module switch).
|
||||
// Cancel any in-flight keystroke debounce first: otherwise alignCodeWithEditor
|
||||
// skips on the `timeoutModel` guard (leaving Monaco stale), and the pending
|
||||
// updateCode later reads the old buffer and writes it back over `ncode`.
|
||||
cancelPendingChanges()
|
||||
alignCodeWithEditor(!noHistory)
|
||||
// Dispatch change immediately when code actually changed. This ensures
|
||||
// callers like the Reset button and copilot trigger on:change handlers.
|
||||
// The debounced onDidChangeModelContent handler will no-op since code
|
||||
@@ -425,6 +408,7 @@
|
||||
return
|
||||
}
|
||||
code = ncode
|
||||
lastEditorCode = ncode
|
||||
dispatch('change', ncode)
|
||||
}
|
||||
|
||||
@@ -436,12 +420,19 @@
|
||||
* see it. Clears the chain state so the next keystroke after this
|
||||
* flush is a fresh leading fire. */
|
||||
export function flushPendingChanges(): void {
|
||||
cancelPendingChanges()
|
||||
updateCode()
|
||||
}
|
||||
|
||||
/** Discard any in-flight keystroke debounce without materializing it, so a
|
||||
* deferred updateCode can't fire later. Resets chain state to a fresh leading
|
||||
* fire on the next keystroke. */
|
||||
function cancelPendingChanges(): void {
|
||||
if (timeoutModel !== undefined) {
|
||||
clearTimeout(timeoutModel)
|
||||
timeoutModel = undefined
|
||||
}
|
||||
changeChainStart = undefined
|
||||
updateCode()
|
||||
}
|
||||
|
||||
export function append(code: string): void {
|
||||
@@ -1901,29 +1892,6 @@
|
||||
lang = scriptLangToEditorLang(scriptLang)
|
||||
})
|
||||
|
||||
// Opt-in (syncExternalCode): reflect external `code` prop mutations into
|
||||
// Monaco's model. Parents that pass `code={...}` one-way (no bind) — e.g.
|
||||
// the inline rawscript in the flow editor — otherwise mutate the prop
|
||||
// without Monaco ever showing the change (the AI chat editing a flow
|
||||
// module's content in a session is the motivating case). Gated off by
|
||||
// default: Editor is sensitive and most callers either bind:code (and
|
||||
// carry their own external-sync) or treat code as init-only, so a blanket
|
||||
// setValue would risk clobbering them. The `getValue() !== code` guard
|
||||
// keeps the caret intact when the change originated from typing inside
|
||||
// Monaco (which round-trips code back via `$bindable`, re-firing this
|
||||
// effect with `code === getValue()`).
|
||||
let lastExternalCodeSync = code
|
||||
$effect(() => {
|
||||
if (!syncExternalCode) return
|
||||
if (code === lastExternalCodeSync) return
|
||||
lastExternalCodeSync = code
|
||||
if (!editor) return
|
||||
untrack(() => {
|
||||
if (editor!.getValue() !== code) {
|
||||
editor!.setValue(code ?? '')
|
||||
}
|
||||
})
|
||||
})
|
||||
$effect(() => {
|
||||
filePath = computePath(path)
|
||||
})
|
||||
@@ -2011,25 +1979,50 @@
|
||||
})
|
||||
})
|
||||
|
||||
// External `code` prop changes should flow into the Monaco editor. The
|
||||
// `untrack` block reads/writes Monaco without subscribing — only the
|
||||
// prop read above is tracked — so the editor's own change handler
|
||||
// (`updateCode`) re-running with the same value short-circuits and we
|
||||
// don't loop.
|
||||
$effect(() => {
|
||||
const next = code ?? ''
|
||||
let applyExternalCode = useDebounce(() => alignCodeWithEditor(true), 800)
|
||||
|
||||
// Last `code` value the editor itself produced or aligned to. Used to tell an
|
||||
// echo (the bindable changed because the user typed — Monaco is already
|
||||
// ahead) from a genuine external write. Without this, a typing burst longer
|
||||
// than the debounce window would sync the lagging `code` back over newer
|
||||
// keystrokes. Must be kept in step with every editor↔`code` sync point.
|
||||
let lastEditorCode = code
|
||||
|
||||
function alignCodeWithEditor(history: boolean) {
|
||||
const ed = editor
|
||||
if (!ed) return
|
||||
untrack(() => {
|
||||
if (ed.getValue() === next) return
|
||||
const model = ed.getModel()
|
||||
if (!model) return
|
||||
const next = code ?? ''
|
||||
const value = ed.getValue()
|
||||
const model = ed.getModel()
|
||||
// Some keystrokes are still being debounced, don't overwrite them.
|
||||
// When the debounce is done, updateCode will be called and the code will be aligned with the editor.
|
||||
if (timeoutModel !== undefined) return
|
||||
if (!model) return
|
||||
lastEditorCode = next
|
||||
if (value === next) return
|
||||
if (history) {
|
||||
ed.pushUndoStop()
|
||||
ed.executeEdits('external', [{ range: model.getFullModelRange(), text: next }])
|
||||
ed.pushUndoStop()
|
||||
} else {
|
||||
ed.setValue(next)
|
||||
}
|
||||
}
|
||||
|
||||
// External `code` prop changes should flow into the Monaco editor. Skip
|
||||
// echoes: when `code` matches what the editor last produced (`updateCode`)
|
||||
// or aligned to, the change came from the editor itself, so syncing back
|
||||
// would clobber input typed since. Only genuine external writes — where
|
||||
// `code` diverges from `lastEditorCode` — schedule a sync. The `untrack`
|
||||
// block reads/writes Monaco without subscribing, so we don't loop.
|
||||
$effect(() => {
|
||||
;[code, editor]
|
||||
if (!editor) return
|
||||
untrack(() => {
|
||||
if (code === lastEditorCode) return
|
||||
applyExternalCode()
|
||||
})
|
||||
})
|
||||
|
||||
let isTsWorkerInitialized = resource([() => lang, () => initialized], async () => {
|
||||
if (lang !== 'typescript' || !initialized) return false
|
||||
// Use the stable model URI (computed once at mount), not filePath which changes on rename
|
||||
|
||||
@@ -12,10 +12,14 @@
|
||||
let comparison: WorkspaceComparison | undefined = $state(undefined)
|
||||
let error: string | undefined = $state(undefined)
|
||||
|
||||
let isFork = $derived($workspaceStore?.startsWith('wm-fork-') ?? false)
|
||||
let currentWorkspaceData = $derived($userWorkspaces.find((w) => w.id === $workspaceStore))
|
||||
let parentWorkspaceId = $derived(currentWorkspaceData?.parent_workspace_id)
|
||||
let parentWorkspaceData = $derived($userWorkspaces.find((w) => w.id === parentWorkspaceId))
|
||||
// A fork must have a parent to compare/merge against. Treating the wm-fork-
|
||||
// prefix alone as "is a fork" renders a parentless "Fork of ()" banner when
|
||||
// the parent linkage was dropped (e.g. by a workspace id change), so require
|
||||
// both, matching the forks/compare page.
|
||||
let isFork = $derived(($workspaceStore?.startsWith('wm-fork-') ?? false) && !!parentWorkspaceId)
|
||||
|
||||
// Drafts in this fork. When the fork is otherwise in sync with its parent, a
|
||||
// user with only pending drafts should still get the draft CTA (mirrors the
|
||||
|
||||
@@ -1063,6 +1063,14 @@
|
||||
<li
|
||||
>AI chat usage (provider, model, mode, session count, message count — last 30 days)</li
|
||||
>
|
||||
<li
|
||||
>resource counts (workspaces, scripts per language, flows, workflows as code, low-code
|
||||
apps, raw apps)</li
|
||||
>
|
||||
<li
|
||||
>infrastructure info (container runtime, managed database provider, database version,
|
||||
size and cluster size, max and active connections, object storage backend)</li
|
||||
>
|
||||
</ul>
|
||||
<br />For air-gapped instances, you can download the telemetry data and send it manually.
|
||||
</div>
|
||||
@@ -1101,6 +1109,10 @@
|
||||
<li
|
||||
>AI chat usage (provider, model, mode, session count, message count — last 30 days)</li
|
||||
>
|
||||
<li
|
||||
>resource counts (workspaces, scripts per language, flows, workflows as code, low-code
|
||||
apps, raw apps)</li
|
||||
>
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -952,9 +952,7 @@
|
||||
}
|
||||
})
|
||||
})
|
||||
$effect(() => {
|
||||
readFieldsRecursively(script)
|
||||
})
|
||||
|
||||
// Mirror the draft triggers (held in a separate `triggersState` $state)
|
||||
// back into `script.draft_triggers` so the UserDraft autosave — which
|
||||
// deep-tracks `script` — picks them up. Pre-PR ScriptBuilder ran its own
|
||||
|
||||
@@ -268,6 +268,7 @@
|
||||
if (activeModuleTab === null && code !== lastSyncedCode) {
|
||||
editorCode = code
|
||||
lastSyncedCode = code
|
||||
editor?.setCode(editorCode) // immediate sync, don't wait for the 800ms debounce
|
||||
untrack(() => inferSchema(code))
|
||||
}
|
||||
})
|
||||
@@ -1591,18 +1592,29 @@
|
||||
let error = $derived(getError(testJob))
|
||||
|
||||
$effect(() => {
|
||||
const options: ScriptOptions = {
|
||||
code,
|
||||
lang: lang as ScriptLang,
|
||||
error,
|
||||
args: args ?? {},
|
||||
path,
|
||||
;[
|
||||
editor,
|
||||
lastSavedCode,
|
||||
lastDeployedCode,
|
||||
diffMode,
|
||||
workflowAsCode: workflowAsCodeAiContext
|
||||
}
|
||||
workflowAsCodeAiContext,
|
||||
args,
|
||||
error,
|
||||
lang,
|
||||
path
|
||||
]
|
||||
untrack(() => {
|
||||
const options: ScriptOptions = {
|
||||
getCode: () => code,
|
||||
lang: lang as ScriptLang,
|
||||
error,
|
||||
args: args ?? {},
|
||||
path,
|
||||
lastSavedCode,
|
||||
lastDeployedCode,
|
||||
diffMode,
|
||||
workflowAsCode: workflowAsCodeAiContext
|
||||
}
|
||||
aiChatManager.scriptEditorOptions = options
|
||||
aiChatManager.scriptEditorApplyCode = async (code: string, opts?: ReviewChangesOpts) => {
|
||||
hideDiffMode()
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
import { base } from '$lib/base'
|
||||
import SearchItems from './SearchItems.svelte'
|
||||
import { page } from '$app/state'
|
||||
import { goto as gotoUrl } from '$app/navigation'
|
||||
import { replaceState } from '$app/navigation'
|
||||
import Version from './Version.svelte'
|
||||
import Uptodate from './Uptodate.svelte'
|
||||
import InstanceSettings from './InstanceSettings.svelte'
|
||||
@@ -70,7 +70,16 @@
|
||||
const index = page.url.href.lastIndexOf('#')
|
||||
if (index === -1) return
|
||||
const hashRemoved = page.url.href.slice(0, index)
|
||||
gotoUrl(hashRemoved)
|
||||
// Strip the drawer's URL hash without a SvelteKit navigation: a `goto`
|
||||
// here re-fires path-reactive effects on the underlying page (e.g. the
|
||||
// script editor's load effect), wiping unsaved editor content.
|
||||
try {
|
||||
replaceState(hashRemoved, page.state)
|
||||
} catch (e) {
|
||||
// replaceState throws if the router isn't initialized yet — possible
|
||||
// when onDestroy runs during router teardown.
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
|
||||
@@ -25,6 +25,38 @@ describe('parsePipelineAnnotations: tag', () => {
|
||||
const out = parsePipelineAnnotations('// tagged heavy')
|
||||
expect(out.tag).toBeUndefined()
|
||||
})
|
||||
|
||||
it('skips a tag value containing whitespace (regular comment false-positive)', () => {
|
||||
const out = parsePipelineAnnotations('# tag this function so we remember to refactor it later')
|
||||
expect(out.tag).toBeUndefined()
|
||||
})
|
||||
|
||||
it('skips a tag value longer than 50 chars', () => {
|
||||
const out = parsePipelineAnnotations('// tag ' + 'x'.repeat(51))
|
||||
expect(out.tag).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('parsePipelineAnnotations: header scan', () => {
|
||||
it('ignores annotations in the body once code has started', () => {
|
||||
const code = [
|
||||
'import pandas as pd',
|
||||
'',
|
||||
'def main():',
|
||||
' # tag each row with its source so downstream steps can filter',
|
||||
' # on s3://should/not/parse',
|
||||
' return pd.DataFrame()'
|
||||
].join('\n')
|
||||
const out = parsePipelineAnnotations(code)
|
||||
expect(out.tag).toBeUndefined()
|
||||
expect(out.triggerAssets).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('tolerates blank lines before code but stops at the first code line', () => {
|
||||
const code = ['#!/usr/bin/env python', '', '# tag heavy', 'import os', '# tag light'].join('\n')
|
||||
const out = parsePipelineAnnotations(code)
|
||||
expect(out.tag).toBe('heavy')
|
||||
})
|
||||
})
|
||||
|
||||
describe('parsePipelineAnnotations: retry', () => {
|
||||
|
||||
@@ -291,8 +291,13 @@ export function parsePipelineAnnotations(code: string): PipelineAnnotations {
|
||||
}
|
||||
|
||||
for (const rawLine of code.split('\n')) {
|
||||
// Annotations live in the leading comment header: skip blank lines but
|
||||
// stop at the first line of actual code, so comments inside the body
|
||||
// (e.g. a regular `# tag ...` prose comment) can't false-positive.
|
||||
// Mirrors the Rust parse_pipeline_annotations header scan.
|
||||
if (rawLine.trim() === '') continue
|
||||
const rest = stripCommentPrefix(rawLine)
|
||||
if (rest === undefined) continue
|
||||
if (rest === undefined) break
|
||||
const inner = rest.trimStart()
|
||||
|
||||
const afterPipeline = consumeKeyword(inner, 'pipeline')
|
||||
@@ -323,7 +328,10 @@ export function parsePipelineAnnotations(code: string): PipelineAnnotations {
|
||||
const afterTag = consumeKeyword(inner, 'tag')
|
||||
if (afterTag !== undefined) {
|
||||
const name = afterTag.trim()
|
||||
if (name && !out.tag) {
|
||||
// Worker tags are single-word identifiers; a value with whitespace
|
||||
// or beyond the script.tag column width is almost certainly a
|
||||
// regular comment starting with "# tag ...".
|
||||
if (name && !out.tag && !/\s/.test(name) && name.length <= 50) {
|
||||
out.tag = name
|
||||
}
|
||||
continue
|
||||
|
||||
@@ -163,7 +163,8 @@
|
||||
{headerLeft}
|
||||
hasDiff={aiChatManager.scriptEditorOptions &&
|
||||
!!aiChatManager.scriptEditorOptions.lastDeployedCode &&
|
||||
aiChatManager.scriptEditorOptions.lastDeployedCode !== aiChatManager.scriptEditorOptions.code}
|
||||
aiChatManager.scriptEditorOptions.lastDeployedCode !==
|
||||
aiChatManager.scriptEditorOptions.getCode()}
|
||||
diffMode={aiChatManager.scriptEditorOptions?.diffMode ?? false}
|
||||
{disabled}
|
||||
{disabledMessage}
|
||||
|
||||
@@ -772,7 +772,7 @@ export class AIChatManager {
|
||||
this.helpers = {
|
||||
getScriptOptions: () => {
|
||||
return {
|
||||
code: this.scriptEditorOptions?.code ?? '',
|
||||
code: this.scriptEditorOptions?.getCode() ?? '',
|
||||
lang: lang,
|
||||
path: this.scriptEditorOptions?.path ?? '',
|
||||
args: this.scriptEditorOptions?.args ?? {}
|
||||
@@ -1922,14 +1922,13 @@ export class AIChatManager {
|
||||
lastDeployedCode: undefined,
|
||||
lastSavedCode: undefined
|
||||
}
|
||||
|
||||
return {
|
||||
args: moduleState?.previewArgs ?? {},
|
||||
error:
|
||||
moduleState && !moduleState.previewSuccess
|
||||
? getStringError(moduleState.previewResult)
|
||||
: undefined,
|
||||
code: module.value.content,
|
||||
getCode: () => module.value.type === 'rawscript' ? module.value.content : '',
|
||||
lang: module.value.language,
|
||||
path: module.id,
|
||||
...editorRelated
|
||||
|
||||
@@ -11,7 +11,7 @@ import type { ExtendedOpenFlow } from '$lib/components/flows/types'
|
||||
|
||||
export interface ScriptOptions {
|
||||
lang: ScriptLang | 'bunnative'
|
||||
code: string
|
||||
getCode: () => string
|
||||
error: string | undefined
|
||||
args: Record<string, any>
|
||||
path: string | undefined
|
||||
@@ -192,7 +192,7 @@ export default class ContextManager {
|
||||
{
|
||||
type: 'code',
|
||||
title: this.getContextCodePath(scriptOptions) ?? '',
|
||||
content: scriptOptions.code,
|
||||
content: scriptOptions.getCode(),
|
||||
lang: scriptOptions.lang
|
||||
}
|
||||
]
|
||||
@@ -209,22 +209,22 @@ export default class ContextManager {
|
||||
}
|
||||
}
|
||||
|
||||
if (scriptOptions.lastSavedCode && scriptOptions.lastSavedCode !== scriptOptions.code) {
|
||||
if (scriptOptions.lastSavedCode && scriptOptions.lastSavedCode !== scriptOptions.getCode()) {
|
||||
newAvailableContext.push({
|
||||
type: 'diff',
|
||||
title: 'diff_with_last_saved_draft', // can't use spaces in the title, because it will break the word match in the context text area hightlighting logic
|
||||
content: scriptOptions.lastSavedCode ?? '',
|
||||
diff: diffLines(scriptOptions.lastSavedCode ?? '', scriptOptions.code),
|
||||
diff: diffLines(scriptOptions.lastSavedCode ?? '', scriptOptions.getCode()),
|
||||
lang: scriptOptions.lang
|
||||
})
|
||||
}
|
||||
|
||||
if (scriptOptions.lastDeployedCode && scriptOptions.lastDeployedCode !== scriptOptions.code) {
|
||||
if (scriptOptions.lastDeployedCode && scriptOptions.lastDeployedCode !== scriptOptions.getCode()) {
|
||||
newAvailableContext.push({
|
||||
type: 'diff',
|
||||
title: 'diff_with_last_deployed_version',
|
||||
content: scriptOptions.lastDeployedCode ?? '',
|
||||
diff: diffLines(scriptOptions.lastDeployedCode ?? '', scriptOptions.code),
|
||||
diff: diffLines(scriptOptions.lastDeployedCode ?? '', scriptOptions.getCode()),
|
||||
lang: scriptOptions.lang
|
||||
})
|
||||
}
|
||||
@@ -251,7 +251,7 @@ export default class ContextManager {
|
||||
{
|
||||
type: 'code',
|
||||
title: this.getContextCodePath(scriptOptions) ?? '',
|
||||
content: scriptOptions.code,
|
||||
content: scriptOptions.getCode(),
|
||||
lang: scriptOptions.lang,
|
||||
deletable: false
|
||||
},
|
||||
@@ -277,7 +277,7 @@ export default class ContextManager {
|
||||
newSelectedContext = newSelectedContext
|
||||
.filter(
|
||||
(c) =>
|
||||
(c.type === 'code_piece' && scriptOptions.code.includes(c.content)) ||
|
||||
(c.type === 'code_piece' && scriptOptions.getCode().includes(c.content)) ||
|
||||
c.type === 'code' ||
|
||||
// Workspace references are user-picked via @-mention and not in
|
||||
// availableContext; preserve so badges survive editor refreshes.
|
||||
@@ -289,7 +289,7 @@ export default class ContextManager {
|
||||
if (c.type === 'code') {
|
||||
return {
|
||||
...c,
|
||||
content: scriptOptions.code,
|
||||
content: scriptOptions.getCode(),
|
||||
title: this.getContextCodePath(scriptOptions)
|
||||
}
|
||||
}
|
||||
@@ -403,7 +403,7 @@ export default class ContextManager {
|
||||
type: 'diff' as const,
|
||||
title: 'diff_with_last_deployed_version',
|
||||
content: this.scriptOptions.lastDeployedCode ?? '',
|
||||
diff: diffLines(this.scriptOptions.lastDeployedCode ?? '', this.scriptOptions.code),
|
||||
diff: diffLines(this.scriptOptions.lastDeployedCode ?? '', this.scriptOptions.getCode()),
|
||||
lang: this.scriptOptions.lang
|
||||
}
|
||||
]
|
||||
|
||||
@@ -129,9 +129,17 @@ export class AIChatEditorHandler {
|
||||
const deletedChange = group.changes[0]
|
||||
const addedChange = group.changes[1]
|
||||
if (deletedChange.type === 'deleted' && addedChange.type === 'added_block') {
|
||||
applyChange(this.editor, deletedChange)
|
||||
addedChange.position.afterLineNumber = deletedChange.range.startLine - 1
|
||||
applyChange(this.editor, addedChange)
|
||||
this.editor.executeEdits('chat', [
|
||||
{
|
||||
range: {
|
||||
startLineNumber: deletedChange.range.startLine,
|
||||
startColumn: 1,
|
||||
endLineNumber: deletedChange.range.endLine + 1,
|
||||
endColumn: 0
|
||||
},
|
||||
text: addedChange.value + '\n'
|
||||
}
|
||||
])
|
||||
} else {
|
||||
throw new Error('Invalid group')
|
||||
}
|
||||
@@ -284,7 +292,7 @@ export class AIChatEditorHandler {
|
||||
})
|
||||
|
||||
if (!opts?.applyAll) {
|
||||
;({ collection, ids } = await displayVisualChanges(
|
||||
; ({ collection, ids } = await displayVisualChanges(
|
||||
'editor-windmill-chat-style',
|
||||
this.editor,
|
||||
changes,
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
import { AIMode } from '../AIChatManager.svelte'
|
||||
import { getAiChatManager } from '../aiChatManagerContext'
|
||||
import { Check, Play } from 'lucide-svelte'
|
||||
import MermaidDisplay from './MermaidDisplay.svelte'
|
||||
|
||||
const aiChatManager = getAiChatManager()
|
||||
|
||||
@@ -90,7 +91,7 @@
|
||||
if (
|
||||
aiChatManager.mode !== AIMode.SCRIPT ||
|
||||
!aiChatManager.scriptEditorApplyCode ||
|
||||
code === aiChatManager.scriptEditorOptions?.code
|
||||
code === aiChatManager.scriptEditorOptions?.getCode()
|
||||
) {
|
||||
return false
|
||||
}
|
||||
@@ -108,14 +109,18 @@
|
||||
<div
|
||||
class="relative w-full border border-gray-300 dark:border-gray-600 rounded-lg overflow-hidden"
|
||||
>
|
||||
<HighlightCode
|
||||
className="p-1"
|
||||
code={code ?? ''}
|
||||
highlightLanguage={SMART_LANG_TO_HIGHLIGHT_LANG[getSmartLang(language as string)]}
|
||||
language={undefined}
|
||||
onApplyCode={handleApplyCode}
|
||||
{showApplyButton}
|
||||
applyButtonIcon={aiChatManager.pendingNewCode ? Check : Play}
|
||||
/>
|
||||
{#if language === 'mermaid'}
|
||||
<MermaidDisplay code={code ?? ''} />
|
||||
{:else}
|
||||
<HighlightCode
|
||||
className="p-1"
|
||||
code={code ?? ''}
|
||||
highlightLanguage={SMART_LANG_TO_HIGHLIGHT_LANG[getSmartLang(language as string)]}
|
||||
language={undefined}
|
||||
onApplyCode={handleApplyCode}
|
||||
{showApplyButton}
|
||||
applyButtonIcon={aiChatManager.pendingNewCode ? Check : Play}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
<script lang="ts">
|
||||
import { randomUUID } from '$lib/utils/uuid'
|
||||
import { useIsDarkMode } from '$lib/components/DarkModeObserver.svelte'
|
||||
|
||||
let { code }: { code: string } = $props()
|
||||
|
||||
const isDarkMode = useIsDarkMode()
|
||||
|
||||
let svg = $state<string | undefined>(undefined)
|
||||
// The exact source that produced `svg`. The diagram is only shown while this
|
||||
// still matches the current `code`, so a later edit that fails to parse falls
|
||||
// back to the raw source instead of leaving a stale, mismatched diagram.
|
||||
let renderedCode = $state<string | undefined>(undefined)
|
||||
// Monotonic token so an earlier-started render that resolves late can't
|
||||
// overwrite the result of a newer one (out-of-order async on rapid code/theme changes).
|
||||
let renderSeq = 0
|
||||
|
||||
async function render(source: string, dark: boolean) {
|
||||
const seq = ++renderSeq
|
||||
if (!source?.trim()) {
|
||||
svg = undefined
|
||||
renderedCode = undefined
|
||||
return
|
||||
}
|
||||
try {
|
||||
const mermaid = (await import('mermaid')).default
|
||||
mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
theme: dark ? 'dark' : 'default',
|
||||
securityLevel: 'strict',
|
||||
// Throw on parse errors instead of injecting an orphan error diagram into the DOM.
|
||||
suppressErrorRendering: true
|
||||
})
|
||||
// mermaid.render needs a fresh element id per attempt to avoid id collisions.
|
||||
const result = await mermaid.render(`mermaid-${randomUUID()}`, source)
|
||||
if (seq !== renderSeq) return
|
||||
svg = result.svg
|
||||
renderedCode = source
|
||||
} catch {
|
||||
// Parse failure (often a partial block still streaming in): fall back to the
|
||||
// raw source. `showSvg` already hides any previous diagram since `renderedCode`
|
||||
// no longer matches the current `code`.
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
void render(code, isDarkMode.val)
|
||||
})
|
||||
|
||||
// Only show the diagram while it corresponds to the current source.
|
||||
let showSvg = $derived(svg !== undefined && renderedCode === code)
|
||||
</script>
|
||||
|
||||
{#if showSvg}
|
||||
<div class="p-2 flex justify-center overflow-x-auto">
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||
{@html svg}
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Fallback while loading or when rendering fails: show the raw source -->
|
||||
<pre class="overflow-auto max-h-screen text-xs p-2">{code}</pre>
|
||||
{/if}
|
||||
@@ -867,7 +867,6 @@
|
||||
bind:this={editor}
|
||||
class="h-full relative"
|
||||
code={flowModule.value.content}
|
||||
syncExternalCode
|
||||
scriptLang={flowModule?.value?.language}
|
||||
automaticLayout={true}
|
||||
cmdEnterAction={async () => {
|
||||
@@ -931,7 +930,6 @@
|
||||
bind:this={editor}
|
||||
class="h-full relative"
|
||||
code={flowModule.value.content}
|
||||
syncExternalCode
|
||||
scriptLang={flowModule?.value?.language}
|
||||
automaticLayout={true}
|
||||
cmdEnterAction={async () => {
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
wmill = ">=1.737.0"
|
||||
wmill = ">=1.738.0"
|
||||
sendgrid = "*"
|
||||
mysql-connector-python = "*"
|
||||
pymongo = "*"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: '3.0.3'
|
||||
|
||||
info:
|
||||
version: 1.737.0
|
||||
version: 1.738.0
|
||||
title: OpenFlow Spec
|
||||
contact:
|
||||
name: Ruben Fiszel
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
RootModule = 'WindmillClient.psm1'
|
||||
|
||||
# Version number of this module.
|
||||
ModuleVersion = '1.737.0'
|
||||
ModuleVersion = '1.738.0'
|
||||
|
||||
# Supported PSEditions
|
||||
# CompatiblePSEditions = @()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "wmill"
|
||||
version = "1.737.0"
|
||||
version = "1.738.0"
|
||||
description = "A client library for accessing Windmill server wrapping the Windmill client API"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://windmill.dev"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@windmill/windmill",
|
||||
"version": "1.737.0",
|
||||
"version": "1.738.0",
|
||||
"exports": "./src/index.ts",
|
||||
"publish": {
|
||||
"exclude": ["!src", "./s3Types.ts", "./sqlUtils.ts", "./client.ts"]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "windmill-client",
|
||||
"description": "Windmill SDK client for browsers and Node.js",
|
||||
"version": "1.737.0",
|
||||
"version": "1.738.0",
|
||||
"author": "Ruben Fiszel",
|
||||
"license": "Apache 2.0",
|
||||
"homepage": "https://github.com/windmill-labs/windmill/tree/main/typescript-client#readme",
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
1.737.0
|
||||
1.738.0
|
||||
|
||||
Reference in New Issue
Block a user