Compare commits

..

2 Commits

Author SHA1 Message Date
wendrul
ba775edef2 Merge remote-tracking branch 'origin/main' into folder-deploy
# Conflicts:
#	frontend/src/lib/utils_workspace_deploy.ts
2026-04-02 15:39:36 +02:00
wendrul
91f0a564b9 fix: deployment UIs for folders 2026-04-02 15:33:12 +02:00
77 changed files with 361 additions and 1462 deletions

View File

@@ -1,32 +1,5 @@
# Changelog
## [1.673.0](https://github.com/windmill-labs/windmill/compare/v1.672.0...v1.673.0) (2026-04-02)
### Features
* add endpoint to restart workers in a worker group ([#8659](https://github.com/windmill-labs/windmill/issues/8659)) ([f0437eb](https://github.com/windmill-labs/windmill/commit/f0437eba1925a9aa4c430008027d637a0c89ee39))
* add Entra ID (Azure Workload Identity) database auth ([#8526](https://github.com/windmill-labs/windmill/issues/8526)) ([6a5cfbc](https://github.com/windmill-labs/windmill/commit/6a5cfbc159a0ad7925fd7ce5eefc8eaa21bbb70b))
* add LIMIT_WINDOWS_TO_1CU env var for Windows worker memory limits ([#8681](https://github.com/windmill-labs/windmill/issues/8681)) ([d2d6810](https://github.com/windmill-labs/windmill/commit/d2d6810db954114f3333853bd3476cb8fc735f92))
* restore bun for dedicated workers, fix dispatch & serialization, cross-workspace deps ([#8645](https://github.com/windmill-labs/windmill/issues/8645)) ([619ebb6](https://github.com/windmill-labs/windmill/commit/619ebb65ce8dce8264add31c3147919802a8286a))
### Bug Fixes
* add HMAC signature verification to Slack interactive callback endpoint ([#8611](https://github.com/windmill-labs/windmill/issues/8611)) ([55e8a5c](https://github.com/windmill-labs/windmill/commit/55e8a5cff1f185b1dbd332d37b877972efa1ed7d))
* correct raw app flow inputs ([#8667](https://github.com/windmill-labs/windmill/issues/8667)) ([28c0730](https://github.com/windmill-labs/windmill/commit/28c073056c65d4ed1600e39679497e5af964347f))
* pass selected language to AI agent when generating flow scripts ([#8680](https://github.com/windmill-labs/windmill/issues/8680)) ([381011a](https://github.com/windmill-labs/windmill/commit/381011a4a8e48454e9c146c64db502293e646b99))
* poll for preview results to avoid undici headers timeout ([#8682](https://github.com/windmill-labs/windmill/issues/8682)) ([ff5fa9f](https://github.com/windmill-labs/windmill/commit/ff5fa9f64fe4aaf33e06b20f02373894b5df0f95))
* pre-fix trigger edited_by for superadmins not in workspace ([#8669](https://github.com/windmill-labs/windmill/issues/8669)) ([350ffdc](https://github.com/windmill-labs/windmill/commit/350ffdce297ba5b84f9dd247eede6da0c6b0956c))
* resolve race condition where flow sync push reverts to stale version ([#8673](https://github.com/windmill-labs/windmill/issues/8673)) ([d569e9e](https://github.com/windmill-labs/windmill/commit/d569e9e29c588243a90b1cd25f866efb0d178640))
* respect disabled fields in JSON input mode ([#8663](https://github.com/windmill-labs/windmill/issues/8663)) ([7fd0bf9](https://github.com/windmill-labs/windmill/commit/7fd0bf974d2ba2644bb01dd5e9ddc84749e166f5))
* Run typed pg queries in a single protocol conversation ([#8679](https://github.com/windmill-labs/windmill/issues/8679)) ([8581a33](https://github.com/windmill-labs/windmill/commit/8581a3300d056040b7e3ab77d629c74f034c9c97))
* sanitize MCP tool schemas for JSON Schema draft 2020-12 compliance ([#8666](https://github.com/windmill-labs/windmill/issues/8666)) ([8c3c97f](https://github.com/windmill-labs/windmill/commit/8c3c97f7a670d47019cc666219f8187f48499672))
* skip generate-metadata confirmation prompt in non-interactive CI ([#8678](https://github.com/windmill-labs/windmill/issues/8678)) ([39af1b7](https://github.com/windmill-labs/windmill/commit/39af1b75afc8458f85dec4fe51dfaed3d0cb000d))
* strip f/ prefix from folder paths when deploying from workspace forks ([#8662](https://github.com/windmill-labs/windmill/issues/8662)) ([7ab0ea5](https://github.com/windmill-labs/windmill/commit/7ab0ea581d349fbfdb56d22cf9903a90efa045bb))
* support branch-specific folder.meta.yaml in missing-meta check ([#8661](https://github.com/windmill-labs/windmill/issues/8661)) ([c87a6a0](https://github.com/windmill-labs/windmill/commit/c87a6a0f2c1346bf5e21f128d32d89bdca039243))
* validate rd redirect on login with same rules as logout ([#8655](https://github.com/windmill-labs/windmill/issues/8655)) ([bcce627](https://github.com/windmill-labs/windmill/commit/bcce62738791a4e9b9f4dbc64731eef163230172))
## [1.672.0](https://github.com/windmill-labs/windmill/compare/v1.671.0...v1.672.0) (2026-04-01)

View File

@@ -34,8 +34,7 @@
"nu",
"java",
"duckdb",
"ruby",
"rlang"
"ruby"
]
}
}

View File

@@ -76,8 +76,7 @@
"nu",
"java",
"duckdb",
"ruby",
"rlang"
"ruby"
]
}
}

View File

@@ -33,8 +33,7 @@
"nu",
"java",
"duckdb",
"ruby",
"rlang"
"ruby"
]
}
}

View File

@@ -43,8 +43,7 @@
"nu",
"java",
"duckdb",
"ruby",
"rlang"
"ruby"
]
}
}

View File

@@ -1,16 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE flow SET versions = array_append(versions, $1) WHERE path = $2 AND workspace_id = $3 AND (versions[array_upper(versions, 1)] IS DISTINCT FROM $1)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "32f26d69b15a74fc96dd585f9eb3c998447ca8b97894bb1a2b238f984c1dc588"
}

View File

@@ -33,8 +33,7 @@
"nu",
"java",
"duckdb",
"ruby",
"rlang"
"ruby"
]
}
}

View File

@@ -44,8 +44,7 @@
"nu",
"java",
"duckdb",
"ruby",
"rlang"
"ruby"
]
}
}

View File

@@ -70,8 +70,7 @@
"nu",
"java",
"duckdb",
"ruby",
"rlang"
"ruby"
]
}
}

View File

@@ -34,8 +34,7 @@
"nu",
"java",
"duckdb",
"ruby",
"rlang"
"ruby"
]
}
}

View File

@@ -40,8 +40,7 @@
"nu",
"java",
"duckdb",
"ruby",
"rlang"
"ruby"
]
}
}

View File

@@ -15,7 +15,7 @@
]
},
"nullable": [
true
null
]
},
"hash": "5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55"

View File

@@ -148,8 +148,7 @@
"nu",
"java",
"duckdb",
"ruby",
"rlang"
"ruby"
]
}
}

View File

@@ -43,8 +43,7 @@
"nu",
"java",
"duckdb",
"ruby",
"rlang"
"ruby"
]
}
}

View File

@@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO notify_event (channel, payload) VALUES ('restart_worker_group', $1)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text"
]
},
"nullable": []
},
"hash": "78f7c4efd7be2aad03f0ec67ab882e1f9aaa7141ab75046a71ac789ab659bdc6"
}

View File

@@ -76,8 +76,7 @@
"nu",
"java",
"duckdb",
"ruby",
"rlang"
"ruby"
]
}
}

View File

@@ -41,8 +41,7 @@
"nu",
"java",
"duckdb",
"ruby",
"rlang"
"ruby"
]
}
}

View File

@@ -153,8 +153,7 @@
"nu",
"java",
"duckdb",
"ruby",
"rlang"
"ruby"
]
}
}

View File

@@ -118,8 +118,7 @@
"nu",
"java",
"duckdb",
"ruby",
"rlang"
"ruby"
]
}
}

View File

@@ -83,8 +83,7 @@
"nu",
"java",
"duckdb",
"ruby",
"rlang"
"ruby"
]
}
}

View File

@@ -133,8 +133,7 @@
"nu",
"java",
"duckdb",
"ruby",
"rlang"
"ruby"
]
}
}

View File

@@ -38,8 +38,7 @@
"nu",
"java",
"duckdb",
"ruby",
"rlang"
"ruby"
]
}
}

View File

@@ -153,8 +153,7 @@
"nu",
"java",
"duckdb",
"ruby",
"rlang"
"ruby"
]
}
}

View File

@@ -123,8 +123,7 @@
"nu",
"java",
"duckdb",
"ruby",
"rlang"
"ruby"
]
}
}

View File

@@ -43,8 +43,7 @@
"nu",
"java",
"duckdb",
"ruby",
"rlang"
"ruby"
]
}
}

View File

@@ -40,8 +40,7 @@
"nu",
"java",
"duckdb",
"ruby",
"rlang"
"ruby"
]
}
}

View File

@@ -77,8 +77,7 @@
"nu",
"java",
"duckdb",
"ruby",
"rlang"
"ruby"
]
}
}

View File

@@ -1,23 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT value AS \"value!: Json<Box<RawValue>>\" FROM flow WHERE path = $1 AND workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "value!: Json<Box<RawValue>>",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false
]
},
"hash": "c569535d4aded82ffef5e5066e24948c8fcdb8d36913775449b982782c8583e9"
}

View File

@@ -68,8 +68,7 @@
"nu",
"java",
"duckdb",
"ruby",
"rlang"
"ruby"
]
}
}

View File

@@ -77,8 +77,7 @@
"nu",
"java",
"duckdb",
"ruby",
"rlang"
"ruby"
]
}
}

View File

@@ -133,8 +133,7 @@
"nu",
"java",
"duckdb",
"ruby",
"rlang"
"ruby"
]
}
}

View File

@@ -40,8 +40,7 @@
"nu",
"java",
"duckdb",
"ruby",
"rlang"
"ruby"
]
}
}

View File

@@ -38,8 +38,7 @@
"nu",
"java",
"duckdb",
"ruby",
"rlang"
"ruby"
]
}
}
@@ -85,8 +84,7 @@
"nu",
"java",
"duckdb",
"ruby",
"rlang"
"ruby"
]
}
}

View File

@@ -33,8 +33,7 @@
"nu",
"java",
"duckdb",
"ruby",
"rlang"
"ruby"
]
}
}

296
backend/Cargo.lock generated
View File

@@ -2108,7 +2108,7 @@ dependencies = [
"rayon",
"safetensors",
"thiserror 2.0.18",
"yoke 0.8.2",
"yoke 0.8.1",
"zip",
]
@@ -2373,9 +2373,9 @@ dependencies = [
[[package]]
name = "cmov"
version = "0.5.3"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f88a43d011fc4a6876cb7344703e297c71dda42494fee094d5f7c76bf13f746"
checksum = "de0758edba32d61d1fd9f4d69491b47604b91ee2f7e6b33de7e54ca4ebe55dc3"
[[package]]
name = "codespan-reporting"
@@ -2805,9 +2805,9 @@ dependencies = [
[[package]]
name = "ctutils"
version = "0.4.2"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e"
checksum = "1005a6d4446f5120ef475ad3d2af2b30c49c2c9c6904258e3bb30219bebed5e4"
dependencies = [
"cmov",
]
@@ -7224,23 +7224,22 @@ dependencies = [
[[package]]
name = "icu_collections"
version = "2.2.0"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43"
dependencies = [
"displaydoc",
"potential_utf",
"utf8_iter",
"yoke 0.8.2",
"yoke 0.8.1",
"zerofrom",
"zerovec",
]
[[package]]
name = "icu_locale_core"
version = "2.2.0"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6"
dependencies = [
"displaydoc",
"litemap",
@@ -7251,9 +7250,9 @@ dependencies = [
[[package]]
name = "icu_normalizer"
version = "2.2.0"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599"
dependencies = [
"icu_collections",
"icu_normalizer_data",
@@ -7265,15 +7264,15 @@ dependencies = [
[[package]]
name = "icu_normalizer_data"
version = "2.2.0"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a"
[[package]]
name = "icu_properties"
version = "2.2.0"
version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec"
dependencies = [
"icu_collections",
"icu_locale_core",
@@ -7285,20 +7284,20 @@ dependencies = [
[[package]]
name = "icu_properties_data"
version = "2.2.0"
version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af"
[[package]]
name = "icu_provider"
version = "2.2.0"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614"
dependencies = [
"displaydoc",
"icu_locale_core",
"writeable",
"yoke 0.8.2",
"yoke 0.8.1",
"zerofrom",
"zerotrie",
"zerovec",
@@ -8047,9 +8046,9 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.184"
version = "0.2.183"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af"
checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
[[package]]
name = "libffi"
@@ -8205,9 +8204,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
name = "litemap"
version = "0.8.2"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
[[package]]
name = "litrs"
@@ -10428,9 +10427,9 @@ dependencies = [
[[package]]
name = "potential_utf"
version = "0.1.5"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77"
dependencies = [
"zerovec",
]
@@ -12118,15 +12117,6 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ece8e78b2f38ec51c51f5d475df0a7187ba5111b2a28bdc761ee05b075d40a71"
[[package]]
name = "scc"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46e6f046b7fef48e2660c57ed794263155d713de679057f2d0c169bfc6e756cc"
dependencies = [
"sdd",
]
[[package]]
name = "schannel"
version = "0.1.29"
@@ -12232,12 +12222,6 @@ dependencies = [
"untrusted 0.9.0",
]
[[package]]
name = "sdd"
version = "3.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca"
[[package]]
name = "seahash"
version = "4.1.0"
@@ -12574,32 +12558,6 @@ dependencies = [
"serde",
]
[[package]]
name = "serial_test"
version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "911bd979bf1070a3f3aa7b691a3b3e9968f339ceeec89e08c280a8a22207a32f"
dependencies = [
"futures-executor",
"futures-util",
"log",
"once_cell",
"parking_lot",
"scc",
"serial_test_derive",
]
[[package]]
name = "serial_test_derive"
version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a7d91949b85b0d2fb687445e448b40d322b6b3e4af6b44a29b21d9a5f33e6d9"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "sha1"
version = "0.10.6"
@@ -14277,9 +14235,9 @@ dependencies = [
[[package]]
name = "tinystr"
version = "0.8.3"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869"
dependencies = [
"displaydoc",
"zerovec",
@@ -14667,9 +14625,9 @@ dependencies = [
[[package]]
name = "toml_parser"
version = "1.1.2+spec-1.1.0"
version = "1.1.1+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
checksum = "39ca317ebc49f06bd748bfba29533eac9485569dc9bf80b849024b025e814fb9"
dependencies = [
"winnow 1.0.1",
]
@@ -15983,7 +15941,7 @@ dependencies = [
[[package]]
name = "windmill"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"anyhow",
"async-nats",
@@ -16012,7 +15970,6 @@ dependencies = [
"serde_derive",
"serde_json",
"serde_yml",
"serial_test",
"sha1",
"sha2 0.10.9",
"sql-builder",
@@ -16062,7 +16019,7 @@ dependencies = [
[[package]]
name = "windmill-alerting"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"axum 0.8.4",
"chrono",
@@ -16075,7 +16032,7 @@ dependencies = [
[[package]]
name = "windmill-api"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"anyhow",
"argon2",
@@ -16216,7 +16173,7 @@ dependencies = [
[[package]]
name = "windmill-api-agent-workers"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"axum 0.8.4",
"chrono",
@@ -16239,7 +16196,7 @@ dependencies = [
[[package]]
name = "windmill-api-assets"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"axum 0.8.4",
"chrono",
@@ -16252,7 +16209,7 @@ dependencies = [
[[package]]
name = "windmill-api-auth"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"anyhow",
"axum 0.8.4",
@@ -16278,7 +16235,7 @@ dependencies = [
[[package]]
name = "windmill-api-client"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"reqwest 0.12.28",
"serde",
@@ -16288,7 +16245,7 @@ dependencies = [
[[package]]
name = "windmill-api-configs"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"axum 0.8.4",
"chrono",
@@ -16305,7 +16262,7 @@ dependencies = [
[[package]]
name = "windmill-api-debug"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"axum 0.8.4",
"base64 0.22.1",
@@ -16328,7 +16285,7 @@ dependencies = [
[[package]]
name = "windmill-api-embeddings"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"anyhow",
"axum 0.8.4",
@@ -16351,7 +16308,7 @@ dependencies = [
[[package]]
name = "windmill-api-flow-conversations"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"axum 0.8.4",
"chrono",
@@ -16367,7 +16324,7 @@ dependencies = [
[[package]]
name = "windmill-api-flows"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"axum 0.8.4",
"chrono",
@@ -16387,7 +16344,7 @@ dependencies = [
[[package]]
name = "windmill-api-groups"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"axum 0.8.4",
"chrono",
@@ -16407,7 +16364,7 @@ dependencies = [
[[package]]
name = "windmill-api-inputs"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"axum 0.8.4",
"chrono",
@@ -16421,7 +16378,7 @@ dependencies = [
[[package]]
name = "windmill-api-integration-tests"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"anyhow",
"async-nats",
@@ -16452,7 +16409,7 @@ dependencies = [
[[package]]
name = "windmill-api-jobs"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"anyhow",
"axum 0.8.4",
@@ -16477,7 +16434,7 @@ dependencies = [
[[package]]
name = "windmill-api-npm-proxy"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"axum 0.8.4",
"flate2",
@@ -16495,7 +16452,7 @@ dependencies = [
[[package]]
name = "windmill-api-openapi"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"anyhow",
"axum 0.8.4",
@@ -16517,7 +16474,7 @@ dependencies = [
[[package]]
name = "windmill-api-schedule"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"axum 0.8.4",
"chrono",
@@ -16537,7 +16494,7 @@ dependencies = [
[[package]]
name = "windmill-api-scripts"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"axum 0.8.4",
"chrono",
@@ -16567,7 +16524,7 @@ dependencies = [
[[package]]
name = "windmill-api-settings"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"anyhow",
"axum 0.8.4",
@@ -16594,7 +16551,7 @@ dependencies = [
[[package]]
name = "windmill-api-sse"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"lazy_static",
"serde",
@@ -16606,7 +16563,7 @@ dependencies = [
[[package]]
name = "windmill-api-users"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"argon2",
"axum 0.8.4",
@@ -16631,7 +16588,7 @@ dependencies = [
[[package]]
name = "windmill-api-workers"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"axum 0.8.4",
"chrono",
@@ -16645,7 +16602,7 @@ dependencies = [
[[package]]
name = "windmill-api-workspaces"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"axum 0.8.4",
"chrono",
@@ -16677,7 +16634,7 @@ dependencies = [
[[package]]
name = "windmill-audit"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"chrono",
"lazy_static",
@@ -16691,7 +16648,7 @@ dependencies = [
[[package]]
name = "windmill-autoscaling"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"anyhow",
"axum 0.8.4",
@@ -16710,7 +16667,7 @@ dependencies = [
[[package]]
name = "windmill-common"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"aes-gcm",
"aho-corasick",
@@ -16812,7 +16769,7 @@ dependencies = [
[[package]]
name = "windmill-dep-map"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"chrono",
"itertools 0.14.0",
@@ -16831,7 +16788,7 @@ dependencies = [
[[package]]
name = "windmill-git-sync"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"regex",
"serde",
@@ -16846,7 +16803,7 @@ dependencies = [
[[package]]
name = "windmill-indexer"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"anyhow",
"astral-tokio-tar",
@@ -16870,7 +16827,7 @@ dependencies = [
[[package]]
name = "windmill-jseval"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"anyhow",
"futures",
@@ -16887,7 +16844,7 @@ dependencies = [
[[package]]
name = "windmill-macros"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"itertools 0.14.0",
"lazy_static",
@@ -16903,7 +16860,7 @@ dependencies = [
[[package]]
name = "windmill-mcp"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"anyhow",
"async-trait",
@@ -16924,7 +16881,7 @@ dependencies = [
[[package]]
name = "windmill-native-triggers"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"anyhow",
"async-trait",
@@ -16955,7 +16912,7 @@ dependencies = [
[[package]]
name = "windmill-oauth"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"anyhow",
"async-oauth2",
@@ -16979,7 +16936,7 @@ dependencies = [
[[package]]
name = "windmill-object-store"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"anyhow",
"async-stream",
@@ -17013,7 +16970,7 @@ dependencies = [
[[package]]
name = "windmill-operator"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"anyhow",
"futures",
@@ -17031,7 +16988,7 @@ dependencies = [
[[package]]
name = "windmill-parser"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"convert_case 0.6.0",
"serde",
@@ -17040,7 +16997,7 @@ dependencies = [
[[package]]
name = "windmill-parser-bash"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -17052,7 +17009,7 @@ dependencies = [
[[package]]
name = "windmill-parser-csharp"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"anyhow",
"serde_json",
@@ -17064,7 +17021,7 @@ dependencies = [
[[package]]
name = "windmill-parser-go"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"anyhow",
"gosyn",
@@ -17076,7 +17033,7 @@ dependencies = [
[[package]]
name = "windmill-parser-graphql"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -17088,7 +17045,7 @@ dependencies = [
[[package]]
name = "windmill-parser-java"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"anyhow",
"serde_json",
@@ -17100,7 +17057,7 @@ dependencies = [
[[package]]
name = "windmill-parser-nu"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"anyhow",
"nu-parser",
@@ -17111,7 +17068,7 @@ dependencies = [
[[package]]
name = "windmill-parser-php"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -17122,7 +17079,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -17134,7 +17091,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py-asset"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"anyhow",
"rustpython-ast",
@@ -17145,7 +17102,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py-imports"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -17167,7 +17124,7 @@ dependencies = [
[[package]]
name = "windmill-parser-r"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"anyhow",
"serde_json",
@@ -17179,7 +17136,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ruby"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -17193,7 +17150,7 @@ dependencies = [
[[package]]
name = "windmill-parser-rust"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"anyhow",
"convert_case 0.6.0",
@@ -17210,7 +17167,7 @@ dependencies = [
[[package]]
name = "windmill-parser-sql"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -17223,7 +17180,7 @@ dependencies = [
[[package]]
name = "windmill-parser-sql-asset"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"anyhow",
"serde",
@@ -17235,7 +17192,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -17253,7 +17210,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts-asset"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"anyhow",
"serde-wasm-bindgen",
@@ -17269,7 +17226,7 @@ dependencies = [
[[package]]
name = "windmill-parser-wac"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"anyhow",
"rustpython-ast",
@@ -17285,7 +17242,7 @@ dependencies = [
[[package]]
name = "windmill-parser-yaml"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"anyhow",
"serde",
@@ -17296,7 +17253,7 @@ dependencies = [
[[package]]
name = "windmill-queue"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -17333,7 +17290,7 @@ dependencies = [
[[package]]
name = "windmill-runtime-nativets"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"anyhow",
"const_format",
@@ -17371,7 +17328,7 @@ dependencies = [
[[package]]
name = "windmill-sql-datatype-parser-wasm"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"getrandom 0.3.4",
"wasm-bindgen",
@@ -17382,7 +17339,7 @@ dependencies = [
[[package]]
name = "windmill-store"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -17411,7 +17368,7 @@ dependencies = [
[[package]]
name = "windmill-test-utils"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17435,7 +17392,7 @@ dependencies = [
[[package]]
name = "windmill-trigger"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17468,7 +17425,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-email"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17488,7 +17445,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-gcp"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17522,7 +17479,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-http"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17558,7 +17515,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-kafka"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17581,7 +17538,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-mqtt"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17605,7 +17562,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-nats"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"anyhow",
"async-nats",
@@ -17629,7 +17586,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-postgres"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17664,7 +17621,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-sqs"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17692,7 +17649,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-websocket"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17715,7 +17672,7 @@ dependencies = [
[[package]]
name = "windmill-types"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"anyhow",
"bitflags 2.9.4",
@@ -17734,7 +17691,7 @@ dependencies = [
[[package]]
name = "windmill-worker"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"anyhow",
"async-once-cell",
@@ -17838,14 +17795,13 @@ dependencies = [
"windmill-runtime-nativets",
"windmill-types",
"windmill-worker-volumes",
"windows 0.61.3",
"x509-parser 0.16.0",
"yaml-rust",
]
[[package]]
name = "windmill-worker-volumes"
version = "1.673.0"
version = "1.672.0"
dependencies = [
"bytes",
"futures",
@@ -18545,9 +18501,9 @@ dependencies = [
[[package]]
name = "writeable"
version = "0.6.3"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
[[package]]
name = "wtf8"
@@ -18680,12 +18636,12 @@ dependencies = [
[[package]]
name = "yoke"
version = "0.8.2"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca"
checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954"
dependencies = [
"stable_deref_trait",
"yoke-derive 0.8.2",
"yoke-derive 0.8.1",
"zerofrom",
]
@@ -18703,9 +18659,9 @@ dependencies = [
[[package]]
name = "yoke-derive"
version = "0.8.2"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d"
dependencies = [
"proc-macro2",
"quote",
@@ -18735,18 +18691,18 @@ dependencies = [
[[package]]
name = "zerofrom"
version = "0.1.7"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df"
checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
dependencies = [
"zerofrom-derive",
]
[[package]]
name = "zerofrom-derive"
version = "0.1.7"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
dependencies = [
"proc-macro2",
"quote",
@@ -18776,31 +18732,31 @@ dependencies = [
[[package]]
name = "zerotrie"
version = "0.2.4"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"
checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851"
dependencies = [
"displaydoc",
"yoke 0.8.2",
"yoke 0.8.1",
"zerofrom",
]
[[package]]
name = "zerovec"
version = "0.11.6"
version = "0.11.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002"
dependencies = [
"yoke 0.8.2",
"yoke 0.8.1",
"zerofrom",
"zerovec-derive",
]
[[package]]
name = "zerovec-derive"
version = "0.11.3"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3"
dependencies = [
"proc-macro2",
"quote",

View File

@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.673.0"
version = "1.672.0"
authors.workspace = true
edition.workspace = true
@@ -85,7 +85,7 @@ members = [
exclude = ["./windmill-duckdb-ffi-internal", "./parsers/windmill-parser-wasm"]
[workspace.package]
version = "1.673.0"
version = "1.672.0"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"

View File

@@ -1 +1 @@
57dd88faa3b0b354f813385cf3f6a34eca54a4a1
e08a87450627bef9013498e40ee93a47bedda7ee

View File

@@ -41,69 +41,50 @@ pub async fn connect_db(
};
let pool = connect(database_url.clone(), max_connections, worker_mode).await?;
#[cfg(all(feature = "enterprise", feature = "private"))]
{
let needs_token_refresh = matches!(
database_url,
DatabaseUrl::IamRds(_) | DatabaseUrl::EntraId(_)
);
let label = match &database_url {
DatabaseUrl::IamRds(_) => "IAM RDS",
DatabaseUrl::EntraId(_) => "Entra ID",
DatabaseUrl::Static(_) => "",
};
if needs_token_refresh {
let pool2 = pool.clone();
let database_url2 = database_url.clone();
tokio::spawn(async move {
loop {
tokio::select! {
_ = killpill_rx.recv() => {
break;
}
_ = tokio::time::sleep(std::time::Duration::from_secs(10)) => {
if !database_url2.needs_refresh().await {
continue;
}
let new_url = tokio::time::timeout(
std::time::Duration::from_secs(10),
get_database_url(),
)
.await;
let pool2 = pool.clone();
#[cfg(all(feature = "enterprise", feature = "private"))]
if let DatabaseUrl::IamRds(database_url) = database_url {
tokio::spawn(async move {
loop {
tokio::select! {
_ = killpill_rx.recv() => {
break;
}
_ = tokio::time::sleep(std::time::Duration::from_secs(10)) => {
let needs_refresh = {
let read_guard = database_url.read().await;
read_guard.needs_refresh()
};
if needs_refresh {
let new_url = tokio::time::timeout(std::time::Duration::from_secs(10), get_database_url()).await;
match new_url {
Ok(Ok(new_url)) => {
match new_url.connect_options().await {
Ok(connect_options) => {
pool2.set_connect_options(connect_options);
tracing::info!("Refreshed {label} URL successfully");
tracing::info!("Refreshed IAM RDS URL successfully");
}
Err(e) => {
tracing::error!(
"Error getting {label} connect options, retrying in 10s: {e}"
);
tracing::error!("Error getting IAM RDS connect options, retrying in 10s: {}", e);
continue;
}
}
}
Ok(Err(e)) => {
tracing::error!(
"Error refreshing {label} URL, trying again in 10s: {e}"
);
tracing::error!("Error refreshing IAM RDS URL, trying again in 10s: {}", e);
continue;
}
Err(e) => {
tracing::error!(
"Timeout after 10s refreshing {label} URL, trying again in 10s: {e}"
);
tracing::error!("Timeout after 10s refreshing IAM RDS URL, trying again in 10 seconds: {}", e);
continue;
}
}
}
}
}
});
}
}
});
}
Ok(pool)

View File

@@ -1597,12 +1597,6 @@ async fn process_notify_event(
tracing::debug!("config changed but did not target this server/worker");
}
}
"restart_worker_group" => {
if worker_mode && payload == *WORKER_GROUP {
tracing::info!("Restart requested for worker group '{payload}'");
spawn_graceful_killpill(tx, db, 10, "worker group restart requested").await;
}
}
"notify_webhook_change" => {
tracing::info!(
"Webhook change detected, invalidating webhook cache: {}",

View File

@@ -429,59 +429,6 @@ export function main() {
Ok(())
}
#[sqlx::test(fixtures("base"))]
async fn test_bun_job_syntax_error_unclosed_bracket(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
let port = server.addr.port();
// Reproduces the "Unexpected end of file at main.ts:0" error reported
// when a TS file has a missing closing bracket — Bun's bundler gives no
// useful location info.
let content = r#"
export async function main() {
if (true) {
return "hello";
// missing closing bracket for the function
"#
.to_owned();
let job = JobPayload::Code(RawCode {
hash: None,
content,
path: None,
language: ScriptLang::Bun,
lock: None,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
modules: None,
});
let completed = run_job_in_new_worker_until_complete(&db, false, job, port).await;
assert!(!completed.success);
let result = completed
.result
.as_ref()
.and_then(|v| v.get("error"))
.and_then(|v| v.get("message"))
.and_then(|v| v.as_str())
.unwrap_or("");
assert!(
result.contains("Unexpected end of file"),
"should contain bun parser error, got: {result}"
);
assert!(
result.contains("syntax error"),
"should contain helpful hint about syntax errors, got: {result}"
);
Ok(())
}
// ============================================================================
// Annotation Mode Tests
// ============================================================================

View File

@@ -1107,7 +1107,6 @@ main <- function(msg) {
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
modules: None,
}))
.arg("msg", json!("world"))
.run_until_complete(&db, false, port)
@@ -1145,7 +1144,6 @@ main <- function() {
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
modules: None,
}))
.run_until_complete(&db, false, port)
.await
@@ -1182,7 +1180,6 @@ main <- function() {
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
modules: None,
}))
.run_until_complete(&db, false, port)
.await

View File

@@ -12,7 +12,9 @@ use std::{collections::HashMap, time::Duration};
mod ee;
pub mod ee_oss;
use windmill_api_auth::{require_devops_role, require_super_admin, ApiAuthed};
#[cfg(feature = "enterprise")]
use windmill_api_auth::require_devops_role;
use windmill_api_auth::{require_super_admin, ApiAuthed};
use windmill_common::utils::HTTP_CLIENT_PERMISSIVE as HTTP_CLIENT;
use windmill_common::DB;
@@ -100,10 +102,6 @@ pub fn global_service() -> Router {
.route(
"/sync_cached_resource_types",
post(sync_cached_resource_types),
)
.route(
"/restart_worker_group/{worker_group}",
post(restart_worker_group),
);
// Vault integration routes (EE only - requires both private and enterprise features)
@@ -664,25 +662,6 @@ pub async fn send_stats(Extension(db): Extension<DB>, authed: ApiAuthed) -> Resu
Ok("Sent stats".to_string())
}
async fn restart_worker_group(
Extension(db): Extension<DB>,
authed: ApiAuthed,
Path(worker_group): Path<String>,
) -> error::Result<String> {
require_devops_role(&db, &authed.email).await?;
sqlx::query!(
"INSERT INTO notify_event (channel, payload) VALUES ('restart_worker_group', $1)",
worker_group
)
.execute(&db)
.await?;
Ok(format!(
"Restart signal sent to worker group '{worker_group}'"
))
}
#[derive(serde::Serialize)]
pub struct StatsDownload {
pub signature: String,

View File

@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.673.0
version: 1.672.0
title: Windmill API
contact:
@@ -1370,28 +1370,6 @@ paths:
schema:
type: string
/settings/restart_worker_group/{worker_group}:
post:
summary: restart worker group
description: Send a restart signal to all workers in the specified worker group. Workers will gracefully shut down and are expected to be restarted by their supervisor. Requires devops role.
operationId: restartWorkerGroup
tags:
- setting
parameters:
- name: worker_group
in: path
required: true
schema:
type: string
description: the name of the worker group to restart
responses:
"200":
description: restart signal sent
content:
text/plain:
schema:
type: string
/settings/get_stats:
get:
summary: get telemetry stats with HMAC signature (EE only)

View File

@@ -159,24 +159,13 @@ pub async fn check_nb_of_user(db: &DB) -> error::Result<()> {
#[derive(Clone, Debug)]
#[cfg(not(feature = "private"))]
pub struct SlackVerifier {
mac: HmacSha256,
_mac: HmacSha256,
}
#[cfg(not(feature = "private"))]
impl SlackVerifier {
pub fn new<S: AsRef<[u8]>>(secret: S) -> anyhow::Result<SlackVerifier> {
HmacSha256::new_from_slice(secret.as_ref())
.map(|mac| SlackVerifier { mac })
.map(|mac| SlackVerifier { _mac: mac })
.map_err(|_| anyhow::anyhow!("invalid secret"))
}
pub fn verify(&self, ts: &str, body: &str, exp_sig: &str) -> anyhow::Result<()> {
let basestring = format!("v0:{}:{}", ts, body);
let mut mac = self.mac.clone();
mac.update(basestring.as_bytes());
let sig = format!("v0={}", hex::encode(mac.finalize().into_bytes()));
if sig != exp_sig {
Err(anyhow::anyhow!("signature mismatch"))?;
}
Ok(())
}
}

View File

@@ -1,9 +1,7 @@
use axum::{
extract::{Path, Query},
extract::{Form, Path, Query},
Extension,
};
use bytes::Bytes;
use http::HeaderMap;
use hyper::StatusCode;
use reqwest::Client;
use serde::{Deserialize, Serialize};
@@ -121,41 +119,12 @@ struct PrivateMetadata {
hide_cancel: Option<bool>,
}
#[cfg(feature = "oauth2")]
fn verify_slack_callback_signature(headers: &HeaderMap, body: &str) -> Result<(), Error> {
if let Some(sv) = crate::SLACK_SIGNING_SECRET.as_ref() {
let sig = headers
.get("X-Slack-Signature")
.and_then(|v| v.to_str().ok())
.unwrap_or("");
let ts = headers
.get("X-Slack-Request-Timestamp")
.and_then(|v| v.to_str().ok())
.unwrap_or("");
sv.verify(ts, body, sig)
.map_err(|_| Error::BadRequest("Slack signature verification failed".to_string()))?;
}
Ok(())
}
#[cfg(not(feature = "oauth2"))]
fn verify_slack_callback_signature(_headers: &HeaderMap, _body: &str) -> Result<(), Error> {
Ok(())
}
pub async fn slack_app_callback_handler(
authed: Option<ApiAuthed>,
opt_tokened: OptTokened,
Extension(db): Extension<DB>,
headers: HeaderMap,
body: Bytes,
Form(form_data): Form<SlackFormData>,
) -> Result<StatusCode, Error> {
let body_str = String::from_utf8_lossy(&body);
verify_slack_callback_signature(&headers, &body_str)?;
let form_data: SlackFormData = serde_urlencoded::from_bytes(&body)
.map_err(|e| Error::BadRequest(format!("invalid form data: {}", e)))?;
tracing::debug!("Form data: {:#?}", form_data);
let payload: Payload = serde_json::from_str(&form_data.payload)?;
tracing::debug!("Payload: {:#?}", payload);

View File

@@ -1,45 +0,0 @@
use anyhow::Result;
/// Parsed database connection parameters, shared across DB auth providers (IAM RDS, Entra ID, etc.)
#[derive(Debug, Clone)]
pub struct DatabaseParams {
pub hostname: String,
pub port: u64,
pub username: String,
pub database: String,
}
/// Extract database connection parameters from a PostgreSQL URL
pub fn extract_database_params(database_url: &str) -> Result<DatabaseParams> {
let url = url::Url::parse(database_url)
.map_err(|e| anyhow::anyhow!("Failed to parse database URL: {}", e))?;
let hostname = url
.host_str()
.ok_or_else(|| anyhow::anyhow!("Database URL missing hostname"))?
.to_string();
let port = url.port().unwrap_or(5432) as u64;
let username = if url.username().is_empty() {
return Err(anyhow::anyhow!("Database URL missing username"));
} else {
urlencoding::decode(url.username())?.to_string()
};
let database = url
.path()
.trim_start_matches('/')
.split('/')
.next()
.filter(|s| !s.is_empty())
.ok_or_else(|| anyhow::anyhow!("Database URL missing database name"))?
.to_string();
Ok(DatabaseParams {
hostname,
port,
username,
database: urlencoding::decode(&database)?.to_string(),
})
}

View File

@@ -43,10 +43,7 @@ pub mod cache;
pub mod client;
pub mod db;
#[cfg(all(feature = "enterprise", feature = "private"))]
mod db_entra_ee;
#[cfg(all(feature = "enterprise", feature = "private"))]
mod db_iam_ee;
pub mod db_params;
#[cfg(feature = "private")]
pub mod ee;
pub mod ee_oss;
@@ -636,14 +633,12 @@ impl PgDatabase {
pub enum DatabaseUrl {
#[cfg(all(feature = "enterprise", feature = "private"))]
IamRds(std::sync::Arc<tokio::sync::RwLock<db_iam_ee::IamRdsUrl>>),
#[cfg(all(feature = "enterprise", feature = "private"))]
EntraId(std::sync::Arc<tokio::sync::RwLock<db_entra_ee::EntraIdUrl>>),
Static(String),
}
impl DatabaseUrl {
/// Get the database URL as a string.
/// For token-based auth, this returns the original URL (for metadata extraction).
/// Note: For IAM RDS, this returns the original URL (for metadata extraction).
/// For actual database connections, use connect_options() instead.
pub async fn as_str(&self) -> String {
match self {
@@ -652,18 +647,13 @@ impl DatabaseUrl {
let guard = rds_url.read().await;
guard.as_str().to_string()
}
#[cfg(all(feature = "enterprise", feature = "private"))]
DatabaseUrl::EntraId(entra_url) => {
let guard = entra_url.read().await;
guard.as_str().to_string()
}
DatabaseUrl::Static(url) => url.clone(),
}
}
/// Get PgConnectOptions for this database URL.
/// For token-based auth (IAM RDS, Entra ID), this returns options built directly from the
/// token to avoid double-encoding issues with temporary credentials.
/// For IAM RDS, this returns options built directly from the token to avoid double-encoding
/// issues with temporary credentials (IRSA/Pod Identity).
/// For static URLs, this parses the URL string.
pub async fn connect_options(&self) -> Result<sqlx::postgres::PgConnectOptions, Error> {
match self {
@@ -672,11 +662,6 @@ impl DatabaseUrl {
let guard = rds_url.read().await;
Ok(guard.connect_options())
}
#[cfg(all(feature = "enterprise", feature = "private"))]
DatabaseUrl::EntraId(entra_url) => {
let guard = entra_url.read().await;
Ok(guard.connect_options())
}
DatabaseUrl::Static(url) => sqlx::postgres::PgConnectOptions::from_str(url)
.map_err(|e| Error::InternalErr(format!("Failed to parse database URL: {}", e))),
}
@@ -686,31 +671,9 @@ impl DatabaseUrl {
match self {
#[cfg(all(feature = "enterprise", feature = "private"))]
DatabaseUrl::IamRds(rds_url) => rds_url.write().await.refresh().await,
#[cfg(all(feature = "enterprise", feature = "private"))]
DatabaseUrl::EntraId(entra_url) => entra_url.write().await.refresh().await,
DatabaseUrl::Static(_) => Ok(()),
}
}
pub async fn needs_refresh(&self) -> bool {
match self {
#[cfg(all(feature = "enterprise", feature = "private"))]
DatabaseUrl::IamRds(rds_url) => rds_url.read().await.needs_refresh(),
#[cfg(all(feature = "enterprise", feature = "private"))]
DatabaseUrl::EntraId(entra_url) => entra_url.read().await.needs_refresh(),
DatabaseUrl::Static(_) => false,
}
}
/// Double-checked refresh: read-lock to check, then write-lock to refresh if still needed.
pub async fn refresh_if_needed(&self) -> Result<(), Error> {
if self.needs_refresh().await {
self.refresh().await.map_err(|e| {
Error::InternalErr(format!("Failed to refresh database token: {}", e))
})?;
}
Ok(())
}
}
static DATABASE_URL_CACHE: tokio::sync::OnceCell<DatabaseUrl> = tokio::sync::OnceCell::const_new();
@@ -738,9 +701,7 @@ pub async fn get_database_url() -> Result<DatabaseUrl, Error> {
let parsed_url = url::Url::parse(&url)?;
let password = parsed_url.password().unwrap_or_default();
if password == "iamrds" {
if parsed_url.password().is_some_and(|x| x == "iamrds") {
let region = var("AWS_REGION").map_err(|_| {
Error::BadConfig(
"AWS_REGION env var is required for IAM RDS authentication".to_string(),
@@ -770,68 +731,34 @@ pub async fn get_database_url() -> Result<DatabaseUrl, Error> {
"IAM RDS authentication is not enabled in OSS mode".to_string(),
));
}
} else if password == "entraid" {
let tenant_id = var("AZURE_TENANT_ID").map_err(|_| {
Error::BadConfig(
"AZURE_TENANT_ID env var is required for Entra ID authentication"
.to_string(),
)
})?;
tracing::info!(
"entraid mode detected, generating Entra ID URL for tenant: {tenant_id}"
);
#[cfg(all(feature = "enterprise", feature = "private"))]
{
let client_id = var("AZURE_CLIENT_ID").map_err(|_| {
Error::BadConfig(
"AZURE_CLIENT_ID env var is required for Entra ID authentication"
.to_string(),
)
})?;
let federated_token_file =
var("AZURE_FEDERATED_TOKEN_FILE").map_err(|_| {
Error::BadConfig(
"AZURE_FEDERATED_TOKEN_FILE env var is required for Entra ID authentication".to_string(),
)
})?;
let authority_host = var("AZURE_AUTHORITY_HOST")
.unwrap_or_else(|_| "login.microsoftonline.com".to_string());
let entra_url = db_entra_ee::generate_database_url(
&url,
&tenant_id,
&client_id,
&federated_token_file,
&authority_host,
)
.await
.map_err(|e| {
Error::InternalErr(format!(
"Failed to generate Entra ID database URL: {}",
e
))
})?;
tracing::info!("Entra ID URL generated successfully");
Ok::<DatabaseUrl, Error>(DatabaseUrl::EntraId(std::sync::Arc::new(
tokio::sync::RwLock::new(entra_url),
)))
}
#[cfg(not(all(feature = "enterprise", feature = "private")))]
{
return Err(Error::BadConfig(
"Entra ID authentication is not enabled in OSS mode".to_string(),
));
}
} else {
Ok::<DatabaseUrl, Error>(DatabaseUrl::Static(url.to_string()))
}
})
.await?;
database_url.refresh_if_needed().await?;
// Check if we need to refresh and do so if necessary
#[cfg(all(feature = "enterprise", feature = "private"))]
if let DatabaseUrl::IamRds(ref rds_url_lock) = database_url {
// Check if refresh is needed
let needs_refresh = {
let read_guard = rds_url_lock.read().await;
read_guard.needs_refresh()
};
// If refresh is needed, acquire write lock and refresh
if needs_refresh {
let mut write_guard = rds_url_lock.write().await;
// Double-check after acquiring write lock (another task might have refreshed)
if write_guard.needs_refresh() {
write_guard.refresh().await.map_err(|e| {
Error::InternalErr(format!("Failed to refresh IAM token: {}", e))
})?;
}
}
}
// Return the URL string
Ok(database_url.clone())
}

View File

@@ -401,7 +401,7 @@ pub fn build_parameters(columns: &[SimpleColumn], db_type: DbType) -> String {
.map(|(i, col)| {
let base_type = col.datatype.split('(').next().unwrap_or(&col.datatype);
match db_type {
DbType::Postgresql => format!("-- ${} {} ({})", i + 1, col.field, base_type),
DbType::Postgresql => format!("-- ${} {}", i + 1, col.field),
DbType::Mysql => format!("-- :{} ({})", col.field, base_type),
DbType::MsSqlServer => {
format!("-- @p{} {} ({})", i + 1, col.field, base_type)
@@ -765,7 +765,7 @@ pub fn make_select_query(
quicksearch
));
query.push_str(&format!(" ORDER BY {}\n", order_by));
query.push_str(" LIMIT $1 OFFSET $2");
query.push_str(" LIMIT $1::INT OFFSET $2::INT");
Ok(query)
}
DbType::MsSqlServer => {
@@ -1158,11 +1158,13 @@ pub fn make_delete_query(table: &str, columns: &[ColumnDef], db_type: DbType) ->
.map(|(i, c)| {
let qf = qi(&c.field, db_type);
format!(
"(${} IS NULL AND {} IS NULL OR {} = ${})",
"(${}::text::{} IS NULL AND {} IS NULL OR {} = ${}::text::{})",
i + 1,
c.datatype,
qf,
qf,
i + 1,
c.datatype
)
})
.collect::<Vec<_>>()
@@ -1278,7 +1280,7 @@ fn format_insert_values(columns: &[ColumnDef], db_type: DbType, start_index: usi
.enumerate()
.map(|(i, c)| match db_type {
DbType::Mysql => format!(":{}", c.field),
DbType::Postgresql => format!("${}", start_index + i),
DbType::Postgresql => format!("${}::{}", start_index + i, c.datatype),
DbType::MsSqlServer => format!("@p{}", start_index + i),
DbType::Snowflake => "?".to_string(),
DbType::Bigquery => format!("@{}", c.field),
@@ -1456,19 +1458,21 @@ pub fn make_update_query(
.map(|(i, c)| {
let qf = qi(&c.field, db_type);
format!(
"(${} IS NULL AND {} IS NULL OR {} = ${})",
"(${}::text::{} IS NULL AND {} IS NULL OR {} = ${}::text::{})",
i + 2,
c.datatype,
qf,
qf,
i + 2,
c.datatype
)
})
.collect::<Vec<_>>()
.join("\n AND ");
query.push_str(&format!(
"\nUPDATE {} SET {} = $1 \nWHERE {}\tRETURNING 1",
qt, qcol, conditions
"\nUPDATE {} SET {} = $1::text::{} \nWHERE {}\tRETURNING 1",
qt, qcol, column.datatype, conditions
));
}
DbType::Mysql => {
@@ -2702,7 +2706,7 @@ mod tests {
fn test_build_parameters_postgresql() {
let cols = vec![simple_col("limit", "int"), simple_col("offset", "int")];
let result = build_parameters(&cols, DbType::Postgresql);
assert_eq!(result, "-- $1 limit (int)\n-- $2 offset (int)");
assert_eq!(result, "-- $1 limit\n-- $2 offset");
}
#[test]
@@ -2811,11 +2815,11 @@ mod tests {
make_select_query("my_table", &cols, None, DbType::Postgresql, None, None).unwrap();
assert!(result.starts_with(
"-- $1 limit (int)\n-- $2 offset (int)\n-- $3 quicksearch (text)\n-- $4 order_by (text)\n-- $5 is_desc (boolean)\n"
"-- $1 limit\n-- $2 offset\n-- $3 quicksearch\n-- $4 order_by\n-- $5 is_desc\n"
));
assert!(result.contains("SELECT \"id\"::text, \"name\"::text FROM \"my_table\"\n"));
assert!(result.contains("($3 = '' OR CONCAT(\"id\", \"name\") ILIKE '%' || $3 || '%')"));
assert!(result.contains("LIMIT $1 OFFSET $2"));
assert!(result.contains("LIMIT $1::INT OFFSET $2::INT"));
assert!(result.contains("$4 = 'id' AND $5 IS false THEN \"id\"::text"));
assert!(result.contains("$4 = 'name' AND $5 IS true THEN \"name\"::text END) DESC"));
}
@@ -3007,7 +3011,7 @@ mod tests {
let cols = vec![col("id", "int4"), col("name", "text")];
let result = make_count_query(DbType::Postgresql, "my_table", None, &cols).unwrap();
assert!(result.contains("-- $1 quicksearch (text)"));
assert!(result.contains("-- $1 quicksearch"));
assert!(result.contains("SELECT COUNT(*) as count FROM \"my_table\""));
assert!(result.contains("($1 = '' OR CONCAT(\"id\", \"name\") ILIKE '%' || $1 || '%')"));
// Should use WHERE not AND
@@ -3144,10 +3148,13 @@ mod tests {
let cols = vec![col("id", "int4"), col("name", "text")];
let result = make_delete_query("my_table", &cols, DbType::Postgresql);
assert!(result.contains("-- $1 id (int4)\n-- $2 name (text)"));
assert!(result.contains("-- $1 id\n-- $2 name"));
assert!(result.contains("DELETE FROM \"my_table\""));
assert!(result.contains("($1 IS NULL AND \"id\" IS NULL OR \"id\" = $1)"));
assert!(result.contains("($2 IS NULL AND \"name\" IS NULL OR \"name\" = $2)"));
assert!(result
.contains("($1::text::int4 IS NULL AND \"id\" IS NULL OR \"id\" = $1::text::int4)"));
assert!(result.contains(
"($2::text::text IS NULL AND \"name\" IS NULL OR \"name\" = $2::text::text)"
));
assert!(result.contains("RETURNING 1;"));
}
@@ -3214,8 +3221,9 @@ mod tests {
let cols = vec![col("id", "int4"), col("name", "text")];
let result = make_insert_query("my_table", &cols, DbType::Postgresql).unwrap();
assert!(result.contains("-- $1 id (int4)\n-- $2 name (text)"));
assert!(result.contains("INSERT INTO \"my_table\" (\"id\", \"name\") VALUES ($1, $2)"));
assert!(result.contains("-- $1 id\n-- $2 name"));
assert!(result
.contains("INSERT INTO \"my_table\" (\"id\", \"name\") VALUES ($1::int4, $2::text)"));
}
#[test]
@@ -3271,7 +3279,7 @@ mod tests {
make_insert_query("my_table", &[id_col, name_col], DbType::Postgresql).unwrap();
// id should be skipped from insert columns (has nextval default in pg)
assert!(result.contains("INSERT INTO \"my_table\" (\"name\") VALUES ($1)"));
assert!(result.contains("INSERT INTO \"my_table\" (\"name\") VALUES ($1::text)"));
}
#[test]
@@ -3285,7 +3293,9 @@ mod tests {
make_insert_query("my_table", &[id_col, name_col], DbType::Postgresql).unwrap();
// name should be in insert params, id should be in defaults
assert!(result.contains("INSERT INTO \"my_table\" (\"name\", \"id\") VALUES ($1, '42')"));
assert!(
result.contains("INSERT INTO \"my_table\" (\"name\", \"id\") VALUES ($1::text, '42')")
);
}
#[test]
@@ -3298,7 +3308,7 @@ mod tests {
let result =
make_insert_query("my_table", &[id_col, name_col], DbType::Postgresql).unwrap();
assert!(result.contains("VALUES ($1, NULL)"));
assert!(result.contains("VALUES ($1::text, NULL)"));
}
#[test]
@@ -3313,7 +3323,7 @@ mod tests {
make_insert_query("my_table", &[id_col, name_col], DbType::Postgresql).unwrap();
// Column is hidden, not nullable, has db default, no user default -> omit (use db default)
assert!(result.contains("INSERT INTO \"my_table\" (\"name\") VALUES ($1)"));
assert!(result.contains("INSERT INTO \"my_table\" (\"name\") VALUES ($1::text)"));
}
#[test]
@@ -3327,7 +3337,7 @@ mod tests {
make_insert_query("my_table", &[id_col, name_col], DbType::Postgresql).unwrap();
// Always identity should be omitted
assert!(result.contains("INSERT INTO \"my_table\" (\"name\") VALUES ($1)"));
assert!(result.contains("INSERT INTO \"my_table\" (\"name\") VALUES ($1::text)"));
}
#[test]
@@ -3365,12 +3375,13 @@ mod tests {
let where_cols = vec![simple_col("id", "int4"), simple_col("email", "text")];
let result = make_update_query("my_table", &update_col, &where_cols, DbType::Postgresql);
assert!(
result.contains("-- $1 value_to_update (text)\n-- $2 id (int4)\n-- $3 email (text)")
);
assert!(result.contains("UPDATE \"my_table\" SET \"name\" = $1"));
assert!(result.contains("($2 IS NULL AND \"id\" IS NULL OR \"id\" = $2)"));
assert!(result.contains("($3 IS NULL AND \"email\" IS NULL OR \"email\" = $3)"));
assert!(result.contains("-- $1 value_to_update\n-- $2 id\n-- $3 email"));
assert!(result.contains("UPDATE \"my_table\" SET \"name\" = $1::text::text"));
assert!(result
.contains("($2::text::int4 IS NULL AND \"id\" IS NULL OR \"id\" = $2::text::int4)"));
assert!(result.contains(
"($3::text::text IS NULL AND \"email\" IS NULL OR \"email\" = $3::text::text)"
));
assert!(result.contains("RETURNING 1"));
}
@@ -3452,8 +3463,10 @@ mod tests {
let cols = vec![col("a", "int4"), col("b", "text"), col("c", "bool")];
let result = make_delete_query("t", &cols, DbType::Postgresql);
// Check all three conditions are present and joined
assert!(result.contains("AND ($2 IS NULL AND \"b\" IS NULL OR \"b\" = $2)"));
assert!(result.contains("AND ($3 IS NULL AND \"c\" IS NULL OR \"c\" = $3)"));
assert!(result
.contains("AND ($2::text::text IS NULL AND \"b\" IS NULL OR \"b\" = $2::text::text)"));
assert!(result
.contains("AND ($3::text::bool IS NULL AND \"c\" IS NULL OR \"c\" = $3::text::bool)"));
}
#[test]
@@ -3505,7 +3518,7 @@ mod tests {
let result = make_insert_query("my_table", &[name_col, col1], DbType::Postgresql).unwrap();
// Numeric value should not be quoted
assert!(result.contains("VALUES ($1, 5)"));
assert!(result.contains("VALUES ($1::text, 5)"));
}
// -----------------------------------------------------------------------
@@ -3527,7 +3540,7 @@ mod tests {
assert!(result.is_some());
let sql = result.unwrap().unwrap().code;
assert!(sql.contains("SELECT \"id\"::text, \"name\"::text FROM \"my_table\""));
assert!(sql.contains("LIMIT $1 OFFSET $2"));
assert!(sql.contains("LIMIT $1::INT OFFSET $2::INT"));
}
#[test]
@@ -3558,14 +3571,16 @@ mod tests {
fn test_expand_insert_marker() {
let marker = r#"-- WM_INTERNAL_DB_INSERT {"table":"my_table","columns":[{"field":"id","datatype":"int4"},{"field":"name","datatype":"text"}]}"#;
let sql = expand_code(marker, &ScriptLang::Postgresql);
assert!(sql.contains("INSERT INTO \"my_table\" (\"id\", \"name\") VALUES ($1, $2)"));
assert!(
sql.contains("INSERT INTO \"my_table\" (\"id\", \"name\") VALUES ($1::int4, $2::text)")
);
}
#[test]
fn test_expand_update_marker() {
let marker = r#"-- WM_INTERNAL_DB_UPDATE {"table":"my_table","column":{"field":"name","datatype":"text"},"columns":[{"field":"id","datatype":"int4"}]}"#;
let sql = expand_code(marker, &ScriptLang::Postgresql);
assert!(sql.contains("UPDATE \"my_table\" SET \"name\" = $1"));
assert!(sql.contains("UPDATE \"my_table\" SET \"name\" = $1::text::text"));
assert!(sql.contains("RETURNING 1"));
}

View File

@@ -11,7 +11,6 @@ pub struct Smtp {
pub from: String,
pub tls_implicit: Option<bool>,
pub disable_tls: Option<bool>,
pub clicktracking_off: Option<bool>,
}
#[derive(Serialize, Deserialize, PartialEq)]
@@ -23,7 +22,6 @@ pub struct SmtpConfigOpt {
pub smtp_from: Option<String>,
pub smtp_tls_implicit: Option<bool>,
pub smtp_disable_tls: Option<bool>,
pub smtp_clicktracking_off: Option<bool>,
}
pub async fn load_smtp_config(db: &DB) -> error::Result<Option<Smtp>> {
@@ -48,7 +46,6 @@ pub async fn load_smtp_config(db: &DB) -> error::Result<Option<Smtp>> {
from: config
.smtp_from
.unwrap_or_else(|| "noreply@getwindmill.com".to_string()),
clicktracking_off: config.smtp_clicktracking_off,
})
} else {
None
@@ -75,9 +72,6 @@ pub async fn load_smtp_config(db: &DB) -> error::Result<Option<Smtp>> {
.unwrap_or(587),
from: std::env::var("SMTP_FROM")
.unwrap_or_else(|_| "noreply@getwindmill.com".to_string()),
clicktracking_off: std::env::var("SMTP_CLICKTRACKING_OFF")
.ok()
.and_then(|p| p.parse().ok()),
})
} else {
None
@@ -100,7 +94,6 @@ impl Default for SmtpConfigOpt {
smtp_tls_implicit: None,
smtp_username: None,
smtp_disable_tls: None,
smtp_clicktracking_off: None,
}
}
}

View File

@@ -157,8 +157,6 @@ lazy_static::lazy_static! {
pub static ref NATIVE_MODE: bool = std::env::var("NATIVE_MODE").ok().is_some_and(|x| x == "1" || x == "true");
pub static ref LIMIT_WINDOWS_TO_1CU: bool = std::env::var("LIMIT_WINDOWS_TO_1CU").ok().is_some_and(|x| x == "1" || x == "true");
pub static ref CGROUP_V2_PATH_RE: Regex = Regex::new(r#"(?m)^0::(/.*)$"#).unwrap();
pub static ref CGROUP_V2_CPU_RE: Regex = Regex::new(r#"(?m)^(\d+) \S+$"#).unwrap();
pub static ref CGROUP_V1_INACTIVE_FILE_RE: Regex = Regex::new(r#"(?m)^total_inactive_file (\d+)$"#).unwrap();
@@ -1054,9 +1052,6 @@ pub fn get_vcpus() -> Option<i64> {
#[cfg(windows)]
pub fn get_vcpus() -> Option<i64> {
if *LIMIT_WINDOWS_TO_1CU {
return Some(100000); // 1 vCPU
}
let mut sys = System::new();
sys.refresh_cpu_all();
(sys.cpus().len() * 100000).try_into().ok()
@@ -1106,9 +1101,6 @@ pub fn get_memory() -> Option<i64> {
#[cfg(windows)]
pub fn get_memory() -> Option<i64> {
if *LIMIT_WINDOWS_TO_1CU {
return Some(2 * 1024 * 1024 * 1024); // 2 GB
}
let mut sys = System::new();
sys.refresh_memory();
Some(sys.total_memory() as i64)

View File

@@ -42,39 +42,13 @@ pub fn extract_resource_types_from_schema(schema: &SchemaType) -> HashSet<String
/// Transform a JSON schema for maximum MCP client compatibility.
///
/// Ensures schemas conform to JSON Schema draft 2020-12 by:
/// - Converting `integer` type to `number` (some clients don't support integer)
/// - Removing invalid non-array `enum` values
/// - Stripping non-standard keywords (`originalType`, `format` with `resource-*` prefix)
/// - Fixing contradictory schemas (`type: "string"` with `properties` → `type: "object"`)
/// - Removing `default: null` when the type doesn't include `null`
/// - Adding `type: "object"` to empty schemas that have no type
/// Some MCP clients (e.g., n8n) have limited JSON Schema support:
/// - `integer` type is not supported (convert to `number`)
/// - invalid non-array `enum` values are removed
pub fn make_schema_compatible(schema: &mut Value) {
let Value::Object(obj) = schema else { return };
// 1. Strip non-standard keywords that aren't part of JSON Schema
obj.remove("originalType");
// 2. Strip non-standard format values (resource-* is Windmill-internal)
if obj
.get("format")
.and_then(|v| v.as_str())
.is_some_and(|s| s.starts_with("resource-"))
{
obj.remove("format");
}
// 3. Fix contradictory type: if `properties` is present, type must be "object"
if obj.contains_key("properties") {
match obj.get("type").and_then(|v| v.as_str()) {
Some("object") => {}
_ => {
obj.insert("type".to_string(), Value::String("object".to_string()));
}
}
}
// 4. Convert integer to number
// 1. Convert integer to number
if let Some(type_val) = obj.get_mut("type") {
match type_val {
Value::String(s) if s == "integer" => *s = "number".to_string(),
@@ -91,28 +65,11 @@ pub fn make_schema_compatible(schema: &mut Value) {
}
}
// 5. Remove `default: null` when type doesn't include "null"
if obj.get("default").is_some_and(|v| v.is_null()) {
let type_includes_null = match obj.get("type") {
Some(Value::String(s)) => s == "null",
Some(Value::Array(arr)) => arr.iter().any(|v| v.as_str() == Some("null")),
_ => false,
};
if !type_includes_null {
obj.remove("default");
}
}
// 6. Invalid enum values like `enum: null` are not valid draft 2020-12.
// 2. Invalid enum values like `enum: null` are not valid draft 2020-12.
if obj.get("enum").is_some_and(|enum_val| !enum_val.is_array()) {
obj.remove("enum");
}
// 7. Ensure schemas with no type but with properties get type: "object"
if !obj.contains_key("type") && !obj.is_empty() {
obj.insert("type".to_string(), Value::String("object".to_string()));
}
// Recursively process nested schemas
if let Some(Value::Object(props)) = obj.get_mut("properties") {
for value in props.values_mut() {
@@ -217,155 +174,4 @@ mod tests {
json!(["open", "closed"])
);
}
#[test]
fn strips_original_type() {
let mut schema = json!({
"type": "object",
"properties": {
"name": {
"type": "string",
"originalType": "string"
},
"data": {
"type": "string",
"originalType": "bytes"
}
}
});
make_schema_compatible(&mut schema);
assert!(schema["properties"]["name"].get("originalType").is_none());
assert!(schema["properties"]["data"].get("originalType").is_none());
assert_eq!(schema["properties"]["name"]["type"], json!("string"));
}
#[test]
fn strips_resource_format() {
let mut schema = json!({
"type": "object",
"properties": {
"db": {
"type": "string",
"format": "resource-postgresql"
}
}
});
make_schema_compatible(&mut schema);
assert!(schema["properties"]["db"].get("format").is_none());
assert_eq!(schema["properties"]["db"]["type"], json!("string"));
}
#[test]
fn preserves_standard_format() {
let mut schema = json!({
"type": "object",
"properties": {
"email": {
"type": "string",
"format": "email"
}
}
});
make_schema_compatible(&mut schema);
assert_eq!(schema["properties"]["email"]["format"], json!("email"));
}
#[test]
fn fixes_string_type_with_properties() {
let mut schema = json!({
"type": "object",
"properties": {
"config": {
"type": "string",
"format": "resource-record",
"properties": {}
}
}
});
make_schema_compatible(&mut schema);
assert_eq!(schema["properties"]["config"]["type"], json!("object"));
assert!(schema["properties"]["config"].get("format").is_none());
}
#[test]
fn removes_null_default_on_string_type() {
let mut schema = json!({
"type": "object",
"properties": {
"name": {
"type": "string",
"default": null
}
}
});
make_schema_compatible(&mut schema);
assert!(schema["properties"]["name"].get("default").is_none());
}
#[test]
fn preserves_null_default_when_type_includes_null() {
let mut schema = json!({
"type": "object",
"properties": {
"name": {
"type": ["string", "null"],
"default": null
}
}
});
make_schema_compatible(&mut schema);
assert_eq!(schema["properties"]["name"]["default"], json!(null));
}
#[test]
fn preserves_non_null_default() {
let mut schema = json!({
"type": "object",
"properties": {
"name": {
"type": "string",
"default": "hello"
}
}
});
make_schema_compatible(&mut schema);
assert_eq!(schema["properties"]["name"]["default"], json!("hello"));
}
#[test]
fn adds_type_object_to_empty_schema() {
let mut schema = json!({});
make_schema_compatible(&mut schema);
// Empty schema stays empty (no keys = truly empty)
assert_eq!(schema, json!({}));
}
#[test]
fn adds_type_to_schema_with_properties_but_no_type() {
let mut schema = json!({
"properties": {
"value": {}
}
});
make_schema_compatible(&mut schema);
assert_eq!(schema["type"], json!("object"));
}
}

View File

@@ -146,9 +146,6 @@ hyper-tls = { workspace = true, optional = true }
hyper-util = { workspace = true, optional = true }
rcgen = { workspace = true, optional = true }
[target.'cfg(windows)'.dependencies]
windows = { version = "0.61", features = ["Win32_System_JobObjects", "Win32_System_Threading"] }
[dev-dependencies]
tempfile.workspace = true
x509-parser.workspace = true

View File

@@ -6,15 +6,8 @@ try {
external: ["*"],
});
} catch (err) {
const msgs = (err?.errors ?? []).map((e) => e?.message ?? String(e));
msgs.push(err?.message ?? String(err));
const full = msgs.join(" ");
if (full.includes("Unexpected end of file") || full.includes("Unterminated")) {
console.log("Failed to build bundle: syntax error in the script (e.g. unclosed bracket, string, or comment)");
} else {
console.log("Failed to build bundle");
}
console.log(err);
console.log("Failed to build bundle");
process.exit(1);
}

View File

@@ -702,120 +702,6 @@ lazy_static! {
static ref DISABLE_PROCESS_GROUP: bool = std::env::var("DISABLE_PROCESS_GROUP").is_ok();
}
/// 2 GB memory limit in bytes for LIMIT_WINDOWS_TO_1CU
#[cfg(windows)]
const MEMORY_LIMIT_1CU: usize = 2 * 1024 * 1024 * 1024;
/// Wrapper that holds a Windows Job Object handle alongside the child process.
/// The job object enforces memory limits and is closed when the child is dropped.
#[cfg(windows)]
struct MemoryLimitedChild {
inner: Box<dyn TokioChildWrapper>,
_job_handle: Win32JobHandle,
}
#[cfg(windows)]
impl std::fmt::Debug for MemoryLimitedChild {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MemoryLimitedChild").finish()
}
}
/// RAII wrapper for a raw Win32 HANDLE that closes it on drop.
#[cfg(windows)]
struct Win32JobHandle(windows::Win32::Foundation::HANDLE);
// SAFETY: Win32 HANDLEs are plain pointer-sized values with no thread affinity;
// the kernel ref-counts the underlying object, so sending/sharing the handle is safe.
#[cfg(windows)]
unsafe impl Send for Win32JobHandle {}
#[cfg(windows)]
unsafe impl Sync for Win32JobHandle {}
#[cfg(windows)]
impl Drop for Win32JobHandle {
fn drop(&mut self) {
let _ = unsafe { windows::Win32::Foundation::CloseHandle(self.0) };
}
}
#[cfg(windows)]
impl process_wrap::tokio::TokioChildWrapper for MemoryLimitedChild {
fn inner(&self) -> &tokio::process::Child {
self.inner.inner()
}
fn inner_mut(&mut self) -> &mut tokio::process::Child {
self.inner.inner_mut()
}
fn into_inner(self: Box<Self>) -> tokio::process::Child {
self.inner.into_inner()
}
fn start_kill(&mut self) -> std::io::Result<()> {
self.inner.start_kill()
}
fn wait(
&mut self,
) -> Box<dyn std::future::Future<Output = std::io::Result<std::process::ExitStatus>> + Send + '_>
{
self.inner.wait()
}
fn try_wait(&mut self) -> std::io::Result<Option<std::process::ExitStatus>> {
self.inner.try_wait()
}
}
/// Create a Windows Job Object with a memory limit and assign the process to it.
#[cfg(windows)]
fn apply_job_memory_limit(pid: u32, memory_limit: usize) -> Result<Win32JobHandle, std::io::Error> {
use windows::Win32::System::JobObjects::*;
use windows::Win32::System::Threading::{OpenProcess, PROCESS_SET_QUOTA, PROCESS_TERMINATE};
unsafe {
let job = CreateJobObjectW(None, None).map_err(|e| {
std::io::Error::new(std::io::ErrorKind::Other, format!("CreateJobObjectW: {e}"))
})?;
let mut info = JOBOBJECT_EXTENDED_LIMIT_INFORMATION::default();
info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_JOB_MEMORY;
info.JobMemoryLimit = memory_limit;
SetInformationJobObject(
job,
JobObjectExtendedLimitInformation,
&info as *const _ as _,
std::mem::size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32,
)
.map_err(|e| {
let _ = windows::Win32::Foundation::CloseHandle(job);
std::io::Error::new(
std::io::ErrorKind::Other,
format!("SetInformationJobObject: {e}"),
)
})?;
let process_handle = OpenProcess(PROCESS_SET_QUOTA | PROCESS_TERMINATE, false, pid)
.map_err(|e| {
let _ = windows::Win32::Foundation::CloseHandle(job);
std::io::Error::new(
std::io::ErrorKind::Other,
format!("OpenProcess({pid}): {e}"),
)
})?;
let assign_result = AssignProcessToJobObject(job, process_handle);
let _ = windows::Win32::Foundation::CloseHandle(process_handle);
assign_result.map_err(|e| {
let _ = windows::Win32::Foundation::CloseHandle(job);
std::io::Error::new(
std::io::ErrorKind::Other,
format!("AssignProcessToJobObject: {e}"),
)
})?;
Ok(Win32JobHandle(job))
}
}
pub fn build_command_with_isolation(program: &str, args: &[&str]) -> Command {
use tokio::process::Command;
@@ -881,31 +767,9 @@ pub async fn start_child_process(
}
}
let child: Box<dyn TokioChildWrapper> = cmd
return cmd
.spawn()
.map_err(|err| tentatively_improve_error(err.into(), executable))?;
#[cfg(windows)]
if *windmill_common::worker::LIMIT_WINDOWS_TO_1CU {
if let Some(pid) = child.inner().id() {
match apply_job_memory_limit(pid, MEMORY_LIMIT_1CU) {
Ok(job_handle) => {
tracing::info!(
"Applied 2GB memory limit (LIMIT_WINDOWS_TO_1CU) to child process {pid}"
);
return Ok(Box::new(MemoryLimitedChild {
inner: child,
_job_handle: job_handle,
}));
}
Err(e) => {
tracing::warn!("Failed to apply memory limit to child process {pid}: {e}");
}
}
}
}
Ok(child)
.map_err(|err| tentatively_improve_error(err.into(), executable));
}
pub async fn resolve_job_timeout(

View File

@@ -8,7 +8,7 @@ use anyhow::Context;
use base64::{engine, Engine as _};
use chrono::Utc;
use futures::future::BoxFuture;
use futures::{FutureExt, StreamExt, TryStreamExt};
use futures::{FutureExt, StreamExt, TryFutureExt, TryStreamExt};
use itertools::Itertools;
use rust_decimal::{prelude::FromPrimitive, Decimal};
use serde_json::value::RawValue;
@@ -134,45 +134,45 @@ fn do_postgresql_inner<'a>(
let result_f = async move {
let mut res: Vec<Box<serde_json::value::RawValue>> = vec![];
// Use query_typed_raw (unnamed prepared statement) when all param types are
// resolved. This avoids named prepared statements ("s0", "s1", ...) which break
// with transaction-mode connection poolers (e.g. PgBouncer/Supabase) since the
// prepare and query can land on different backend connections.
// Fall back to prepare + query_raw for custom/unsupported types.
let rows = if typed_schema {
let typed_params = query_params
.iter()
.zip(param_types.iter())
.map(|(p, t)| (&**p as &(dyn ToSql + Sync), t.clone()));
let query_params = query_params
.iter()
.map(|p| &**p as &(dyn ToSql + Sync))
.collect_vec();
let statement = if typed_schema {
client
.query_typed_raw(&query, typed_params)
.prepare_typed(&query, &param_types)
.await
.map_err(to_anyhow)?
} else {
let query_params = query_params
.iter()
.map(|p| &**p as &(dyn ToSql + Sync))
.collect_vec();
let statement = client.prepare(&query).await.map_err(to_anyhow)?;
client
.query_raw(&statement, query_params)
.await
.map_err(to_anyhow)?
client.prepare(&query).await.map_err(to_anyhow)?
};
if skip_collect {
futures::pin_mut!(rows);
while rows.try_next().await.map_err(to_anyhow)?.is_some() {}
client
.execute_raw(&statement, query_params)
.await
.map_err(to_anyhow)?;
} else if let Some(ref s3) = s3 {
let rows_stream = rows.map_err(to_anyhow).map(|row_result| {
row_result.and_then(|row| postgres_row_to_json_value(row).map_err(to_anyhow))
});
let rows_stream = client
.query_raw(&statement, query_params)
.map_err(to_anyhow)
.await?
.map_err(to_anyhow)
.map(|row_result| {
row_result.and_then(|row| postgres_row_to_json_value(row).map_err(to_anyhow))
});
let stream = convert_json_line_stream(rows_stream.boxed(), s3.format).await?;
s3.upload(stream.boxed()).await?;
return Ok(vec![to_raw_value(&s3.to_return_s3_obj())]);
} else {
let rows = client
.query_raw(&statement, query_params)
.await
.map_err(to_anyhow)?;
let rows = if first_row_only {
rows.take(1).boxed()
} else {

View File

@@ -429,7 +429,7 @@ pub async fn handle_flow_dependency_job(
// `JobKind::FlowDependencies` job store either:
// - A saved flow version `id` in the `script_hash` column.
// - Preview raw flow in the `queue` or `job` table.
let (mut flow, mut extras) = match job.runnable_id {
let (mut flow, extras) = match job.runnable_id {
Some(ScriptHash(id)) => {
let flow = cache::flow::fetch_version(db, id).await?;
(flow.value().clone(), flow.extras())
@@ -440,51 +440,6 @@ pub async fn handle_flow_dependency_job(
},
};
// When triggered by a relative import (e.g. a dependent script was updated),
// the version captured at job creation time may be stale if the flow was
// updated between job creation and execution. Re-query the latest version
// and read the current flow value from the flow table to avoid overwriting
// a newer flow definition with a stale one.
let version = if triggered_by_relative_import && !skip_flow_update {
let latest_version = sqlx::query_scalar!(
"SELECT id FROM flow_version WHERE path = $1 AND workspace_id = $2 ORDER BY created_at DESC LIMIT 1",
job_path,
job.workspace_id
)
.fetch_optional(db)
.await?;
if let Some(latest_version) = latest_version {
if version != Some(latest_version) {
tracing::info!(
"Flow version changed since dependency job was queued ({:?} -> {}), using latest",
version,
latest_version
);
}
// Read the current flow value from the flow table (not version cache).
// This ensures we have the latest committed state, including any locks
// computed by a concurrent FlowDependencies job from a direct flow update.
let raw_flow_value = sqlx::query_scalar!(
"SELECT value AS \"value!: Json<Box<RawValue>>\" FROM flow WHERE path = $1 AND workspace_id = $2",
job_path,
job.workspace_id
)
.fetch_one(db)
.await?;
let flow_data = cache::FlowData::from_raw(raw_flow_value.0)?;
flow = flow_data.value().clone();
extras = flow_data.extras();
Some(latest_version)
} else {
version
}
} else {
version
};
let mut tx = db.begin().await?;
let mut dependency_map = ScopedDependencyMap::fetch_maybe_rearranged(
@@ -620,33 +575,6 @@ pub async fn handle_flow_dependency_job(
tx = dependency_map.dissolve(tx).await;
// When triggered by a relative import, re-check that our version is still
// the latest before writing. Between reading the flow value and now (module
// locking can take significant time), another job may have created a newer
// version. If so, skip the update — the newer version's dep job will handle it.
if triggered_by_relative_import {
let current_latest = sqlx::query_scalar!(
"SELECT id FROM flow_version WHERE path = $1 AND workspace_id = $2 ORDER BY created_at DESC LIMIT 1",
job_path,
job.workspace_id
)
.fetch_optional(&mut *tx)
.await?;
if current_latest != Some(version) {
tracing::info!(
"Flow version changed during dependency locking ({} -> {:?}), skipping update to avoid overwriting newer version",
version,
current_latest
);
tx.commit().await?;
return Ok(to_raw_value_owned(json!({
"status": "Skipped: newer flow version exists",
"modified_ids": modified_ids,
})));
}
}
sqlx::query!(
"UPDATE flow SET value = $1 WHERE path = $2 AND workspace_id = $3",
&new_flow_value as &Json<Box<RawValue>>,
@@ -699,9 +627,7 @@ pub async fn handle_flow_dependency_job(
// Making new version viewable as the current one.
// This will also trigger `flow_versions_append_trigger` (check _flow_versions_update_notify.up.sql)
// which will invalidate cache for the latest flow versions for all workers.
// Only append if this version isn't already the last element in the array.
// This prevents duplicates when update_flow already appended this version.
sqlx::query!("UPDATE flow SET versions = array_append(versions, $1) WHERE path = $2 AND workspace_id = $3 AND (versions[array_upper(versions, 1)] IS DISTINCT FROM $1)",
sqlx::query!("UPDATE flow SET versions = array_append(versions, $1) WHERE path = $2 AND workspace_id = $3",
version,
&job_path,
&job.workspace_id,

View File

@@ -2,7 +2,7 @@ import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts";
import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts";
import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts";
export const VERSION = "v1.673.0";
export const VERSION = "v1.672.0";
export async function login(email: string, password: string): Promise<string> {
return await windmill.UserService.login({

View File

@@ -15,7 +15,7 @@ import { buildFolderPath, getMetadataFileName, loadNonDottedPathsSetting } from
import { requireLogin } from "../../core/auth.ts";
import { resolveWorkspace, validatePath } from "../../core/context.ts";
import { resolve, track_job, pollForJobResult } from "../script/script.ts";
import { resolve, track_job } from "../script/script.ts";
import { defaultFlowDefinition } from "../../../bootstrap/flow_bootstrap.ts";
import { SyncOptions, mergeConfigWithConfigFile } from "../../core/conf.ts";
import { FSFSElement, elementsToMap, ignoreF } from "../sync/sync.ts";
@@ -574,27 +574,32 @@ async function preview(
log.debug(`Flow value: ${JSON.stringify(localFlow.value, null, 2)}`);
// Run the flow preview — start the job, then poll for completion
const jobId = await wmill.runFlowPreview({
workspace: workspace.workspaceId,
requestBody: {
value: localFlow.value,
path: flowPath.substring(0, flowPath.indexOf(".flow")).replaceAll(SEP, "/"),
args: input,
},
});
const { result, success } = await pollForJobResult(workspace.workspaceId, jobId);
if (!success) {
if (opts.silent) {
console.log(JSON.stringify(result));
} else {
log.info(colors.yellow.bold("Flow failed, error handler result:"));
log.info(JSON.stringify(result, null, 2));
// Run the flow preview
let result;
try {
result = await wmill.runFlowPreviewAndWaitResult({
workspace: workspace.workspaceId,
requestBody: {
value: localFlow.value,
path: flowPath.substring(0, flowPath.indexOf(".flow")).replaceAll(SEP, "/"),
args: input,
},
});
} catch (e: any) {
if (e.body) {
// If a failure_module ran, the body contains its result — not an error
if (e.body.result !== undefined) {
if (opts.silent) {
console.log(JSON.stringify(e.body.result));
} else {
log.info(colors.yellow.bold("Flow failed, error handler result:"));
log.info(JSON.stringify(e.body.result, null, 2));
}
process.exitCode = 1;
return;
}
}
process.exitCode = 1;
return;
throw e;
}
if (opts.silent) {

View File

@@ -333,9 +333,8 @@ async function generateMetadata(
log.info("");
const isInteractive = process.stdin.isTTY ?? false;
if (
!opts.yes && isInteractive &&
!opts.yes &&
!(await Confirm.prompt({
message: "Update metadata?",
default: true,

View File

@@ -1118,27 +1118,6 @@ export async function track_job(workspace: string, id: string) {
}
}
const POLL_INTERVAL_MS = 2000;
export async function pollForJobResult(
workspace: string,
jobId: string,
): Promise<{ result: unknown; success: boolean }> {
while (true) {
const maybeResult = await wmill.getCompletedJobResultMaybe({
workspace,
id: jobId,
getStarted: false,
});
if (maybeResult.completed) {
return { result: maybeResult.result, success: maybeResult.success ?? false };
}
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
}
}
async function show(opts: GlobalOptions, path: string) {
const workspace = await resolveWorkspace(opts);
await requireLogin(opts);
@@ -1553,8 +1532,8 @@ async function preview(
}
}
} else {
// For regular scripts, start the preview job then poll for completion
const jobId = await wmill.runScriptPreview({
// For regular scripts, use the standard preview API
const result = await wmill.runScriptPreviewAndWaitResult({
workspace: workspace.workspaceId,
requestBody: {
content,
@@ -1565,21 +1544,8 @@ async function preview(
},
});
const { result, success } = await pollForJobResult(workspace.workspaceId, jobId);
if (!success) {
if (opts.silent) {
console.log(JSON.stringify(result));
} else {
log.info(colors.red.bold("Preview failed"));
log.info(JSON.stringify(result, null, 2));
}
process.exitCode = 1;
return;
}
if (opts.silent) {
console.log(JSON.stringify(result));
console.log(JSON.stringify(result, null, 2));
} else {
log.info(colors.bold.underline.green("Preview completed"));
log.info(JSON.stringify(result, null, 2));

View File

@@ -78,7 +78,7 @@ export {
token,
};
export const VERSION = "1.673.0";
export const VERSION = "1.672.0";
// Re-exported from constants.ts to maintain backwards compatibility
export { WM_FORK_PREFIX } from "./core/constants.ts";

View File

@@ -1053,12 +1053,12 @@ export async function parseMetadataFile(
}
}
// no metadata file at all. Create it
metadataFilePath = scriptPath + ".script.yaml";
log.info(
(await blueColor())(
`Creating script metadata file for ${metadataFilePath}`
)
);
metadataFilePath = scriptPath + ".script.yaml";
let scriptInitialMetadata = defaultScriptMetadata();
const lockPath = scriptPath + ".script.lock";
scriptInitialMetadata.lock = "!inline " + lockPath;

View File

@@ -385,145 +385,7 @@ lock: ""
});
// =============================================================================
// Test 4: Hash consistency - generate-metadata (tree) then deprecated check (legacy)
// =============================================================================
test("Workspace deps: hash consistency between tree-based and legacy staleness checks", async () => {
await withTestBackend(async (backend, tempDir) => {
const testWorkspace = {
remote: backend.baseUrl,
workspaceId: backend.workspace,
name: "workspace_deps_hash_consistency_test",
token: backend.token
};
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
includes:
- "**"
excludes: []`, "utf-8");
// Setup workspace deps
await mkdir(`${tempDir}/dependencies`, { recursive: true });
const bunDep = `{"dependencies": {"lodash": "4.17.21"}}`;
await writeFile(`${tempDir}/dependencies/package.json`, bunDep, "utf-8");
// Setup script
await mkdir(`${tempDir}/f/test`, { recursive: true });
const scriptContent = `export async function main() {
return "uses default deps";
}
`;
const scriptMetadata = `summary: "test"
schema:
type: object
properties: {}
lock: ""
`;
await writeFile(`${tempDir}/f/test/my_script.ts`, scriptContent, "utf-8");
await writeFile(`${tempDir}/f/test/my_script.script.yaml`, scriptMetadata, "utf-8");
// Step 1: Run the NEW generate-metadata command (tree-based, stores hash with {} for deps)
const genResult = await backend.runCLICommand(
["generate-metadata", "--yes", "--schema-only"],
tempDir,
"workspace_deps_hash_consistency_test"
);
expect(genResult.code).toEqual(0);
expect(genResult.stdout).toContain("Done. Updated");
// Step 2: Run the DEPRECATED script generate-metadata --dry-run (legacy, checks hash with deps)
// This simulates what sync push does internally — the legacy check path.
// If hashes are inconsistent, this would falsely report the script as stale.
const legacyResult = await backend.runCLICommand(
["script", "generate-metadata", "-i", "f/test/my_script*", "--yes", "--dry-run"],
tempDir,
"workspace_deps_hash_consistency_test"
);
expect(legacyResult.code).toEqual(0);
// The legacy check should NOT find it stale — generate-metadata persisted
// the dep hashes, so getRawWorkspaceDependencies(true) returns {} for the
// legacy path, making the hash formula equivalent.
expect(legacyResult.stdout).toContain("No metadata to update");
});
});
// =============================================================================
// Test 5: Dep change after generate-metadata — legacy check detects staleness
// =============================================================================
test("Workspace deps: dep change after generate-metadata is detected by legacy check", async () => {
await withTestBackend(async (backend, tempDir) => {
const testWorkspace = {
remote: backend.baseUrl,
workspaceId: backend.workspace,
name: "workspace_deps_dep_change_test",
token: backend.token
};
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
includes:
- "**"
excludes: []`, "utf-8");
// Setup workspace deps
await mkdir(`${tempDir}/dependencies`, { recursive: true });
const bunDep = `{"dependencies": {"lodash": "4.17.21"}}`;
await writeFile(`${tempDir}/dependencies/package.json`, bunDep, "utf-8");
// Setup script (no annotation = uses default dep)
await mkdir(`${tempDir}/f/test`, { recursive: true });
const scriptContent = `export async function main() {
return "uses default deps";
}
`;
const scriptMetadata = `summary: "test"
schema:
type: object
properties: {}
lock: ""
`;
await writeFile(`${tempDir}/f/test/my_script.ts`, scriptContent, "utf-8");
await writeFile(`${tempDir}/f/test/my_script.script.yaml`, scriptMetadata, "utf-8");
// Step 1: Run generate-metadata to establish baseline hashes
const genResult = await backend.runCLICommand(
["generate-metadata", "--yes", "--schema-only"],
tempDir,
"workspace_deps_dep_change_test"
);
expect(genResult.code).toEqual(0);
// Step 2: Verify baseline is up-to-date via legacy check
const checkResult = await backend.runCLICommand(
["script", "generate-metadata", "-i", "f/test/my_script*", "--yes", "--dry-run"],
tempDir,
"workspace_deps_dep_change_test"
);
expect(checkResult.code).toEqual(0);
expect(checkResult.stdout).toContain("No metadata to update");
// Step 3: Change the workspace dep
const newBunDep = `{"dependencies": {"lodash": "4.17.22"}}`;
await writeFile(`${tempDir}/dependencies/package.json`, newBunDep, "utf-8");
// Step 4: Legacy check should detect the dep change as stale
// This works because getRawWorkspaceDependencies(true) returns the changed dep
// (its content hash no longer matches the lockfile), and the legacy path
// includes deps in the hash formula.
const afterDepChangeResult = await backend.runCLICommand(
["script", "generate-metadata", "-i", "f/test/my_script*", "--yes", "--dry-run"],
tempDir,
"workspace_deps_dep_change_test"
);
expect(afterDepChangeResult.code).toEqual(0);
expect(afterDepChangeResult.stdout).toContain("my_script");
});
});
// =============================================================================
// Test 6: Apps - Create app via API and test filterWorkspaceDependenciesForApp
// Test 4: Apps - Create app via API and test filterWorkspaceDependenciesForApp
// =============================================================================
test("Workspace deps: Apps - filterWorkspaceDependenciesForApp with real app via API", async () => {

View File

@@ -1,12 +1,12 @@
{
"name": "windmill-components",
"version": "1.673.0",
"version": "1.672.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "windmill-components",
"version": "1.673.0",
"version": "1.672.0",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "windmill-components",
"version": "1.673.0",
"version": "1.672.0",
"scripts": {
"dev": "vite dev",
"build": "vite build",

View File

@@ -132,10 +132,14 @@
}
})
for (const dep of sortedSet) {
allAlreadyExists[computeStatusPath(dep.kind, dep.path)] = await checkAlreadyExists(
dep.kind,
dep.path
)
try {
allAlreadyExists[computeStatusPath(dep.kind, dep.path)] = await checkAlreadyExists(
dep.kind,
dep.path
)
} catch {
allAlreadyExists[computeStatusPath(dep.kind, dep.path)] = false
}
}
dependencies = sortedSet.map((x) => ({
...x,

View File

@@ -38,10 +38,7 @@
export async function run(overrideScheduledForStr?: string | undefined | null) {
let processedArgs: Record<string, any>
try {
processedArgs = await processSecretArgs(
enforceDisabledDefaults(args ?? {}, true),
runnable?.schema
)
processedArgs = await processSecretArgs(args ?? {}, runnable?.schema)
} catch (e) {
sendUserToast('Failed to process sensitive args: ' + e, true)
return
@@ -135,30 +132,6 @@
}
}
function enforceDisabledDefaults(
args: Record<string, any>,
notify: boolean = false
): Record<string, any> {
const schema = runnable?.schema
if (!schema?.properties) return args
const result = { ...args }
const resetKeys: string[] = []
for (const [key, prop] of Object.entries(schema.properties) as [string, any][]) {
if (prop?.disabled && 'default' in prop) {
if (notify && result[key] !== prop.default) {
resetKeys.push(key)
}
result[key] = prop.default
}
}
if (resetKeys.length > 0) {
sendUserToast(
`Disabled field${resetKeys.length > 1 ? 's' : ''} ${resetKeys.map((k) => `'${k}'`).join(', ')} reset to default value${resetKeys.length > 1 ? 's' : ''}`
)
}
return result
}
export function setCode(code: string) {
jsonEditor?.setCode(code)
}
@@ -276,7 +249,7 @@
bind:this={jsonEditor}
on:select={(e) => {
if (e.detail) {
args = enforceDisabledDefaults(e.detail)
args = e.detail
}
}}
updateOnBlur={false}

View File

@@ -6,7 +6,6 @@
RotateCcw,
Settings,
Trash,
Power,
X,
ExternalLink,
FileCode
@@ -15,13 +14,7 @@
import Badge from './common/badge/Badge.svelte'
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
import {
ConfigService,
SettingService,
WorkspaceService,
type WorkerPing,
type Workspace
} from '$lib/gen'
import { ConfigService, WorkspaceService, type WorkerPing, type Workspace } from '$lib/gen'
import ConfirmationModal from './common/confirmationModal/ConfirmationModal.svelte'
import { createEventDispatcher } from 'svelte'
import { sendUserToast } from '$lib/toast'
@@ -271,7 +264,6 @@
})
let openDelete = $state(false)
let openClean = $state(false)
let openRestart = $state(false)
// Compute hashed tags for display (actual tags used by the worker)
let hashedDedicatedTags: Map<string, string> = $state(new Map())
@@ -373,32 +365,6 @@
</div>
</ConfirmationModal>
<ConfirmationModal
open={openRestart}
title="Restart workers"
confirmationText="Restart"
on:canceled={() => {
openRestart = false
}}
on:confirmed={async () => {
try {
await SettingService.restartWorkerGroup({ workerGroup: name })
sendUserToast(`Restart signal sent to worker group '${name}'`)
dispatch('reload')
} catch (e) {
sendUserToast(`Failed to restart worker group: ${e}`, true)
}
openRestart = false
}}
>
<div class="flex flex-col w-full space-y-4">
<span
>Are you sure you want to restart all workers in worker group '{name}'? Workers will be
gracefully shut down and are expected to be restarted by their supervisor.</span
>
</div>
</ConfirmationModal>
<Drawer bind:this={drawer} size="800px">
<DrawerContent
on:close={() => drawer?.closeDrawer()}
@@ -1270,17 +1236,6 @@
>
Clean cache
</Button>
<Button
unifiedSize="sm"
variant="subtle"
on:click={() => {
openRestart = true
}}
startIcon={{ icon: Power }}
destructive
>
Restart workers
</Button>
{:else}
<Dropdown
items={[
@@ -1293,13 +1248,6 @@
disabled: !config,
type: 'delete'
},
{
displayName: 'Restart workers',
action: () => {
openRestart = true
},
type: 'delete'
},
{
displayName: 'Delete config',
action: () => {

View File

@@ -227,7 +227,6 @@ class AIChatManager {
pendingPrompt?: string,
options?: {
closeScriptSettings?: boolean
lang?: ScriptLang | 'bunnative'
}
) {
if (mode === AIMode.SCRIPT && !tryGetCurrentModel()) return
@@ -236,7 +235,7 @@ class AIChatManager {
if (mode === AIMode.SCRIPT) {
const currentModel = getCurrentModel()
const customPrompt = getCombinedCustomPrompt(mode)
const lang = options?.lang ?? this.scriptEditorOptions?.lang ?? 'bun'
const lang = this.scriptEditorOptions?.lang ?? 'bun'
const context = this.contextManager.getSelectedContext()
this.systemMessage = prepareScriptSystemMessage(currentModel, lang, {}, customPrompt)
this.systemMessage.content = this.systemMessage.content
@@ -446,7 +445,10 @@ class AIChatManager {
if (!pendingPrompt) return undefined
this.pendingPrompt = ''
if (this.mode === AIMode.SCRIPT) {
return prepareScriptUserMessage(pendingPrompt, this.contextManager.getSelectedContext())
return prepareScriptUserMessage(
pendingPrompt,
this.contextManager.getSelectedContext()
)
} else if (this.mode === AIMode.FLOW) {
return prepareFlowUserMessage(
pendingPrompt,
@@ -572,9 +574,9 @@ class AIChatManager {
} = {}
) => {
if (options.mode) {
this.changeMode(options.mode, undefined, { lang: options.lang })
this.changeMode(options.mode)
} else {
this.changeMode(this.mode, undefined, { lang: options.lang })
this.changeMode(this.mode)
}
if (options.instructions) {
this.instructions = options.instructions

View File

@@ -145,14 +145,6 @@
}}
options={{ right: 'Disable TLS' }}
/>
<Toggle
id="smtp_clicktracking_off"
{disabled}
bind:checked={$values['smtp_settings'].smtp_clicktracking_off}
size="xs"
options={{ right: 'Disable click tracking on links' }}
/>
</div>
<!-- Test Email -->

View File

@@ -4,7 +4,7 @@ verify_ssl = true
name = "pypi"
[packages]
wmill = ">=1.673.0"
wmill = ">=1.672.0"
sendgrid = "*"
mysql-connector-python = "*"
pymongo = "*"

View File

@@ -1,7 +1,7 @@
openapi: '3.0.3'
info:
version: 1.673.0
version: 1.672.0
title: OpenFlow Spec
contact:
name: Ruben Fiszel

View File

@@ -12,7 +12,7 @@
RootModule = 'WindmillClient.psm1'
# Version number of this module.
ModuleVersion = '1.673.0'
ModuleVersion = '1.672.0'
# Supported PSEditions
# CompatiblePSEditions = @()

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "wmill"
version = "1.673.0"
version = "1.672.0"
description = "A client library for accessing Windmill server wrapping the Windmill client API"
license = "Apache-2.0"
homepage = "https://windmill.dev"

View File

@@ -1,6 +1,6 @@
{
"name": "@windmill/windmill",
"version": "1.673.0",
"version": "1.672.0",
"exports": "./src/index.ts",
"publish": {
"exclude": ["!src", "./s3Types.ts", "./sqlUtils.ts", "./client.ts"]

View File

@@ -1,7 +1,7 @@
{
"name": "windmill-client",
"description": "Windmill SDK client for browsers and Node.js",
"version": "1.673.0",
"version": "1.672.0",
"author": "Ruben Fiszel",
"license": "Apache 2.0",
"sideEffects": false,

View File

@@ -1 +1 @@
1.673.0
1.672.0