Merge remote-tracking branch 'origin/main' into remove-workspace-drafts

This commit is contained in:
Diego Imbert
2026-06-09 16:09:18 +02:00
28 changed files with 849 additions and 136 deletions
+14
View File
@@ -1,5 +1,19 @@
# Changelog
## [1.721.0](https://github.com/windmill-labs/windmill/compare/v1.720.0...v1.721.0) (2026-06-09)
### Features
* deployed↔draft compare + AI-session draft bar ([#9435](https://github.com/windmill-labs/windmill/issues/9435)) ([b0b330c](https://github.com/windmill-labs/windmill/commit/b0b330c7864d0159af4b0f17dbb3c09bd015145b))
### Bug Fixes
* **cli:** reconcile case-only path drift during sync on case-insensitive filesystems (WIN-2020) ([#9485](https://github.com/windmill-labs/windmill/issues/9485)) ([c258928](https://github.com/windmill-labs/windmill/commit/c258928ab62adc1327913c21556520dfd1e5c24c))
* drop archived items from fork compare (spurious 'not visible' warning) ([#9481](https://github.com/windmill-labs/windmill/issues/9481)) ([92c21bb](https://github.com/windmill-labs/windmill/commit/92c21bbe6586f3c285796a98692e976515a629d5))
* require auth to view approval details when user_auth_required ([#9482](https://github.com/windmill-labs/windmill/issues/9482)) ([5f41ddd](https://github.com/windmill-labs/windmill/commit/5f41ddd3a592bcd504f94fc99060ca5d79c36190))
## [1.720.0](https://github.com/windmill-labs/windmill/compare/v1.719.0...v1.720.0) (2026-06-08)
+78 -78
View File
@@ -13782,7 +13782,7 @@ dependencies = [
[[package]]
name = "windmill"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"async-nats",
@@ -13864,7 +13864,7 @@ dependencies = [
[[package]]
name = "windmill-ai"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"async-stream",
"async-trait",
@@ -13897,7 +13897,7 @@ dependencies = [
[[package]]
name = "windmill-alerting"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"axum 0.8.9",
"chrono",
@@ -13910,7 +13910,7 @@ dependencies = [
[[package]]
name = "windmill-api"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"argon2",
@@ -14048,7 +14048,7 @@ dependencies = [
[[package]]
name = "windmill-api-agent-workers"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"axum 0.8.9",
"chrono",
@@ -14071,7 +14071,7 @@ dependencies = [
[[package]]
name = "windmill-api-assets"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"axum 0.8.9",
"chrono",
@@ -14084,7 +14084,7 @@ dependencies = [
[[package]]
name = "windmill-api-auth"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"axum 0.8.9",
@@ -14110,7 +14110,7 @@ dependencies = [
[[package]]
name = "windmill-api-client"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"reqwest 0.12.28",
"serde",
@@ -14120,7 +14120,7 @@ dependencies = [
[[package]]
name = "windmill-api-configs"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"axum 0.8.9",
"chrono",
@@ -14137,7 +14137,7 @@ dependencies = [
[[package]]
name = "windmill-api-debug"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"axum 0.8.9",
"base64 0.22.1",
@@ -14159,7 +14159,7 @@ dependencies = [
[[package]]
name = "windmill-api-embeddings"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"axum 0.8.9",
@@ -14182,7 +14182,7 @@ dependencies = [
[[package]]
name = "windmill-api-flow-conversations"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"axum 0.8.9",
"chrono",
@@ -14198,7 +14198,7 @@ dependencies = [
[[package]]
name = "windmill-api-flows"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"axum 0.8.9",
"chrono",
@@ -14219,7 +14219,7 @@ dependencies = [
[[package]]
name = "windmill-api-groups"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"axum 0.8.9",
"chrono",
@@ -14240,7 +14240,7 @@ dependencies = [
[[package]]
name = "windmill-api-inputs"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"axum 0.8.9",
"chrono",
@@ -14254,7 +14254,7 @@ dependencies = [
[[package]]
name = "windmill-api-integration-tests"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"async-nats",
@@ -14289,7 +14289,7 @@ dependencies = [
[[package]]
name = "windmill-api-jobs"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"axum 0.8.9",
@@ -14314,7 +14314,7 @@ dependencies = [
[[package]]
name = "windmill-api-npm-proxy"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"axum 0.8.9",
"flate2",
@@ -14332,7 +14332,7 @@ dependencies = [
[[package]]
name = "windmill-api-openapi"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"axum 0.8.9",
@@ -14354,7 +14354,7 @@ dependencies = [
[[package]]
name = "windmill-api-schedule"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"axum 0.8.9",
"chrono",
@@ -14374,7 +14374,7 @@ dependencies = [
[[package]]
name = "windmill-api-scripts"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"axum 0.8.9",
"chrono",
@@ -14405,7 +14405,7 @@ dependencies = [
[[package]]
name = "windmill-api-settings"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"axum 0.8.9",
@@ -14433,7 +14433,7 @@ dependencies = [
[[package]]
name = "windmill-api-sse"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"lazy_static",
"serde",
@@ -14445,7 +14445,7 @@ dependencies = [
[[package]]
name = "windmill-api-users"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"argon2",
"axum 0.8.9",
@@ -14470,7 +14470,7 @@ dependencies = [
[[package]]
name = "windmill-api-workers"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"axum 0.8.9",
"chrono",
@@ -14484,7 +14484,7 @@ dependencies = [
[[package]]
name = "windmill-api-workspaces"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"axum 0.8.9",
"chrono",
@@ -14517,7 +14517,7 @@ dependencies = [
[[package]]
name = "windmill-audit"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"chrono",
"lazy_static",
@@ -14531,7 +14531,7 @@ dependencies = [
[[package]]
name = "windmill-autoscaling"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"axum 0.8.9",
@@ -14550,7 +14550,7 @@ dependencies = [
[[package]]
name = "windmill-common"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"aes-gcm",
"aho-corasick",
@@ -14651,7 +14651,7 @@ dependencies = [
[[package]]
name = "windmill-dep-map"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"chrono",
"itertools 0.14.0",
@@ -14670,7 +14670,7 @@ dependencies = [
[[package]]
name = "windmill-git-sync"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"regex",
"serde",
@@ -14685,7 +14685,7 @@ dependencies = [
[[package]]
name = "windmill-indexer"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"astral-tokio-tar",
@@ -14709,7 +14709,7 @@ dependencies = [
[[package]]
name = "windmill-jseval"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"futures",
@@ -14726,7 +14726,7 @@ dependencies = [
[[package]]
name = "windmill-macros"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"itertools 0.14.0",
"lazy_static",
@@ -14742,7 +14742,7 @@ dependencies = [
[[package]]
name = "windmill-mcp"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"async-trait",
@@ -14763,7 +14763,7 @@ dependencies = [
[[package]]
name = "windmill-native-triggers"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"async-trait",
@@ -14794,7 +14794,7 @@ dependencies = [
[[package]]
name = "windmill-oauth"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"arc-swap",
@@ -14819,7 +14819,7 @@ dependencies = [
[[package]]
name = "windmill-object-store"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"async-stream",
@@ -14853,7 +14853,7 @@ dependencies = [
[[package]]
name = "windmill-operator"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"futures",
@@ -14871,7 +14871,7 @@ dependencies = [
[[package]]
name = "windmill-parser"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"convert_case 0.6.0",
"serde",
@@ -14880,7 +14880,7 @@ dependencies = [
[[package]]
name = "windmill-parser-bash"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -14892,7 +14892,7 @@ dependencies = [
[[package]]
name = "windmill-parser-csharp"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"serde_json",
@@ -14904,7 +14904,7 @@ dependencies = [
[[package]]
name = "windmill-parser-go"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"gosyn",
@@ -14916,7 +14916,7 @@ dependencies = [
[[package]]
name = "windmill-parser-graphql"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -14928,7 +14928,7 @@ dependencies = [
[[package]]
name = "windmill-parser-java"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"serde_json",
@@ -14940,7 +14940,7 @@ dependencies = [
[[package]]
name = "windmill-parser-nu"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"nu-parser",
@@ -14951,7 +14951,7 @@ dependencies = [
[[package]]
name = "windmill-parser-php"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -14962,7 +14962,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -14974,7 +14974,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py-asset"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"rustpython-ast",
@@ -14985,7 +14985,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py-imports"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -15007,7 +15007,7 @@ dependencies = [
[[package]]
name = "windmill-parser-r"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"serde_json",
@@ -15019,7 +15019,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ruby"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -15033,7 +15033,7 @@ dependencies = [
[[package]]
name = "windmill-parser-rust"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"convert_case 0.6.0",
@@ -15050,7 +15050,7 @@ dependencies = [
[[package]]
name = "windmill-parser-sql"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -15063,7 +15063,7 @@ dependencies = [
[[package]]
name = "windmill-parser-sql-asset"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"serde",
@@ -15075,7 +15075,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -15093,7 +15093,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts-asset"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"serde-wasm-bindgen",
@@ -15109,7 +15109,7 @@ dependencies = [
[[package]]
name = "windmill-parser-wac"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"rustpython-ast",
@@ -15125,7 +15125,7 @@ dependencies = [
[[package]]
name = "windmill-parser-yaml"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"serde",
@@ -15136,7 +15136,7 @@ dependencies = [
[[package]]
name = "windmill-queue"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -15174,7 +15174,7 @@ dependencies = [
[[package]]
name = "windmill-runtime-nativets"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"const_format",
@@ -15212,7 +15212,7 @@ dependencies = [
[[package]]
name = "windmill-sql-datatype-parser-wasm"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"getrandom 0.3.4",
"wasm-bindgen",
@@ -15223,7 +15223,7 @@ dependencies = [
[[package]]
name = "windmill-store"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -15255,7 +15255,7 @@ dependencies = [
[[package]]
name = "windmill-test-utils"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"async-trait",
@@ -15279,7 +15279,7 @@ dependencies = [
[[package]]
name = "windmill-trigger"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"async-trait",
@@ -15312,7 +15312,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-azure"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"async-trait",
@@ -15345,7 +15345,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-email"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"async-trait",
@@ -15365,7 +15365,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-gcp"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"async-trait",
@@ -15399,7 +15399,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-http"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"async-trait",
@@ -15435,7 +15435,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-kafka"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"async-trait",
@@ -15458,7 +15458,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-mqtt"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"async-trait",
@@ -15482,7 +15482,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-nats"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"async-nats",
@@ -15506,7 +15506,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-postgres"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"async-trait",
@@ -15541,7 +15541,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-sqs"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"async-trait",
@@ -15569,7 +15569,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-websocket"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"async-trait",
@@ -15594,7 +15594,7 @@ dependencies = [
[[package]]
name = "windmill-types"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"bitflags 2.13.0",
@@ -15613,7 +15613,7 @@ dependencies = [
[[package]]
name = "windmill-worker"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"async-once-cell",
@@ -15723,7 +15723,7 @@ dependencies = [
[[package]]
name = "windmill-worker-volumes"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"bytes",
"futures",
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.720.0"
version = "1.721.0"
authors.workspace = true
edition.workspace = true
@@ -87,7 +87,7 @@ members = [
exclude = ["./windmill-duckdb-ffi-internal", "./parsers/windmill-parser-wasm"]
[workspace.package]
version = "1.720.0"
version = "1.721.0"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"
+24 -24
View File
@@ -6183,7 +6183,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windmill-common"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"aho-corasick",
"anyhow",
@@ -6263,7 +6263,7 @@ dependencies = [
[[package]]
name = "windmill-macros"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"proc-macro2",
"quote",
@@ -6275,7 +6275,7 @@ dependencies = [
[[package]]
name = "windmill-parser"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"convert_case",
"serde",
@@ -6284,7 +6284,7 @@ dependencies = [
[[package]]
name = "windmill-parser-bash"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -6296,7 +6296,7 @@ dependencies = [
[[package]]
name = "windmill-parser-csharp"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"serde_json",
@@ -6308,7 +6308,7 @@ dependencies = [
[[package]]
name = "windmill-parser-go"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"gosyn",
@@ -6320,7 +6320,7 @@ dependencies = [
[[package]]
name = "windmill-parser-graphql"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -6332,7 +6332,7 @@ dependencies = [
[[package]]
name = "windmill-parser-java"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"serde_json",
@@ -6344,7 +6344,7 @@ dependencies = [
[[package]]
name = "windmill-parser-nu"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"nu-parser",
@@ -6355,7 +6355,7 @@ dependencies = [
[[package]]
name = "windmill-parser-php"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -6366,7 +6366,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -6378,7 +6378,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py-asset"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"rustpython-ast",
@@ -6389,7 +6389,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py-imports"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -6411,7 +6411,7 @@ dependencies = [
[[package]]
name = "windmill-parser-r"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"serde_json",
@@ -6423,7 +6423,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ruby"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -6437,7 +6437,7 @@ dependencies = [
[[package]]
name = "windmill-parser-rust"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"convert_case",
@@ -6454,7 +6454,7 @@ dependencies = [
[[package]]
name = "windmill-parser-sql"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -6467,7 +6467,7 @@ dependencies = [
[[package]]
name = "windmill-parser-sql-asset"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"serde",
@@ -6479,7 +6479,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -6497,7 +6497,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts-asset"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"serde-wasm-bindgen",
@@ -6513,7 +6513,7 @@ dependencies = [
[[package]]
name = "windmill-parser-wac"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"rustpython-ast",
@@ -6529,7 +6529,7 @@ dependencies = [
[[package]]
name = "windmill-parser-wasm"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"getrandom 0.2.17",
@@ -6561,7 +6561,7 @@ dependencies = [
[[package]]
name = "windmill-parser-yaml"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"serde",
@@ -6572,7 +6572,7 @@ dependencies = [
[[package]]
name = "windmill-types"
version = "1.720.0"
version = "1.721.0"
dependencies = [
"anyhow",
"bitflags",
@@ -12,7 +12,7 @@ resolver = "2"
members = ["."]
[workspace.package]
version = "1.720.0"
version = "1.721.0"
edition = "2021"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
+1 -1
View File
@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.720.0
version: 1.721.0
title: Windmill API
contact:
+1 -1
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.720.0";
export const VERSION = "v1.721.0";
export async function login(email: string, password: string): Promise<string> {
return await windmill.UserService.login({
+297 -1
View File
@@ -1836,6 +1836,247 @@ export interface Skips {
includeKey?: boolean | undefined;
}
// Detect paths (and their parent directories) within a single set that differ
// only by letter case — e.g. a remote workspace that genuinely holds both
// f/Caps/a and f/caps/b. On a case-insensitive filesystem (Windows, default
// macOS) these cannot be represented as two distinct files/directories at all,
// so we can only warn. Returns one group per collision, each listing the
// distinct casings sorted for stable output.
//
// Only the shallowest clash is reported: when two case-variant folders also
// contain same-named files (f/Caps/main.ts + f/caps/main.ts), the folder clash
// is the root cause, so the nested per-file group is suppressed rather than
// inflating the count with one entry per duplicated leaf.
export function findCaseInsensitiveCollisions(
paths: Iterable<string>,
): string[][] {
// lowercased prefix -> set of distinct original casings observed
const byLower = new Map<string, Set<string>>();
for (const full of paths) {
// Compare on normalized forward-slash prefixes so a Windows-style "\" map
// key and a remote "/" key collapse to the same prefix.
const segs = full.split(/[\\/]/).filter((s) => s.length > 0);
let acc = "";
for (let i = 0; i < segs.length; i++) {
// Accumulate every directory prefix as well as the full file path, so a
// "f/Caps" vs "f/caps" folder clash is caught even when the leaf files
// (e.g. a.ts vs b.ts) don't themselves collide.
acc = i === 0 ? segs[i] : `${acc}/${segs[i]}`;
const lower = acc.toLowerCase();
let set = byLower.get(lower);
if (!set) {
set = new Set();
byLower.set(lower, set);
}
set.add(acc);
}
}
const collidingLowers = new Set<string>();
for (const [lower, set] of byLower) {
if (set.size > 1) collidingLowers.add(lower);
}
const collisions: string[][] = [];
for (const lower of collidingLowers) {
// Drop this group if any ancestor prefix is itself a collision — the
// shallower folder clash already names the root cause.
const parts = lower.split("/");
let hasCollidingAncestor = false;
for (let i = 1; i < parts.length; i++) {
if (collidingLowers.has(parts.slice(0, i).join("/"))) {
hasCollidingAncestor = true;
break;
}
}
if (!hasCollidingAncestor) {
collisions.push([...byLower.get(lower)!].sort());
}
}
return collisions;
}
type CaseTrieNode = {
// lowercased segment -> child, recording the canonical (server) casing and
// whether the server holds more than one casing of that segment (ambiguous).
children: Map<
string,
{ canonical: string; ambiguous: boolean; node: CaseTrieNode }
>;
};
// Rewrite `localMap` keys to the canonical casing recorded on the server
// (`remoteMap`) when they differ only by letter case. This is the core
// WIN-2020 fix: on a case-insensitive filesystem a folder such as `f/Caps`
// can have its on-disk casing silently drift (e.g. to `f/caps`) — Windows
// stores whatever case the directory was first created with and reports that
// from readdir, regardless of the server's path. Without this, the diff sees
// the drifted local path as an entirely different item and emits a destructive
// "delete f/Caps + add f/caps" pair, so a single capitalized folder appears to
// vanish and a lowercase clone shows up out of nowhere. Adopting the server
// casing collapses that phantom and leaves the canonical path on the server
// untouched.
//
// Canonicalization is segment-by-segment against a trie of remote paths, so it
// also applies the server's folder casing to brand-new local files that have no
// exact remote match (e.g. adding f/caps/New.ts under a drifted f/Caps folder
// becomes f/Caps/New.ts) — otherwise the push would recreate the case-only
// collision the fix is meant to prevent. A segment is only adopted when the
// server casing is unambiguous; at the first ambiguous or unknown segment the
// remainder of the path keeps its local casing.
//
// Returns the rewritten map, the per-key rewrites, and any genuinely ambiguous
// server-side groups (two distinct remote paths differing only by case) — those
// can't be canonicalized to a single target and are left for the caller to warn
// about.
export function canonicalizeCaseInsensitiveKeys(
localMap: Record<string, string>,
remoteMap: Record<string, string>,
): {
map: Record<string, string>;
ambiguous: string[][];
rewritten: { from: string; to: string }[];
} {
const root: CaseTrieNode = { children: new Map() };
for (const k of Object.keys(remoteMap)) {
let node = root;
for (const seg of k.split(/[\\/]/)) {
if (seg.length === 0) continue;
const lk = seg.toLowerCase();
let entry = node.children.get(lk);
if (!entry) {
entry = { canonical: seg, ambiguous: false, node: { children: new Map() } };
node.children.set(lk, entry);
} else if (entry.canonical !== seg) {
entry.ambiguous = true;
}
node = entry.node;
}
}
const out: Record<string, string> = {};
const rewritten: { from: string; to: string }[] = [];
for (const [k, v] of Object.entries(localMap)) {
// Preserve the key's own separator style so the rewritten key still matches
// the rest of the map (and round-trips through push) on every platform.
const sep = k.includes("\\") ? "\\" : "/";
const segs = k.split(/[\\/]/);
const canonSegs: string[] = [];
let node: CaseTrieNode | undefined = root;
let changed = false;
for (const seg of segs) {
if (seg.length === 0) {
canonSegs.push(seg);
continue;
}
const entry = node?.children.get(seg.toLowerCase());
if (entry && !entry.ambiguous) {
if (entry.canonical !== seg) changed = true;
canonSegs.push(entry.canonical);
node = entry.node;
} else {
// No unambiguous server guidance for this segment: keep the local
// casing here and below (deeper server structure is unknown).
canonSegs.push(seg);
node = undefined;
}
}
const canonKey = canonSegs.join(sep);
if (changed && canonKey !== k) {
out[canonKey] = v;
rewritten.push({ from: k, to: canonKey });
} else {
out[k] = v;
}
}
return {
map: out,
ambiguous: findCaseInsensitiveCollisions(Object.keys(remoteMap)),
rewritten,
};
}
// Summarize case-only key rewrites by their differing path prefix (typically a
// folder such as f/caps -> f/Caps) so a folder whose casing drifted is reported
// once instead of once per contained file.
export function summarizeCaseRewrites(
rewritten: { from: string; to: string }[],
): string[] {
const seen = new Set<string>();
const out: string[] = [];
for (const { from, to } of rewritten) {
const fromSegs = from.split(/[\\/]/);
const toSegs = to.split(/[\\/]/);
// Find the shortest prefix at which the two casings first differ; that is
// the folder (or file) whose casing actually changed.
let i = 0;
while (
i < fromSegs.length &&
i < toSegs.length &&
fromSegs[i] === toSegs[i]
) {
i++;
}
const fromPrefix = fromSegs.slice(0, i + 1).join("/");
const toPrefix = toSegs.slice(0, i + 1).join("/");
const key = `${fromPrefix} -> ${toPrefix}`;
if (!seen.has(key)) {
seen.add(key);
out.push(key);
}
}
return out;
}
// Emit a single grouped warning for case-only collisions that cannot be
// represented on a case-insensitive filesystem (two distinct server paths
// differing only by case). Unlike the drift handled by
// canonicalizeCaseInsensitiveKeys, these require the user to rename one side.
function warnUnrepresentableCaseCollisions(collisions: string[][]): void {
if (collisions.length === 0) return;
const groups = collisions.map((g) => ` - ${g.join(" <-> ")}`).join("\n");
log.warn(
`Found ${collisions.length} path(s) that differ only by letter case:\n` +
`${groups}\n` +
`On case-insensitive filesystems (Windows, default macOS) these collapse ` +
`into a single file/directory and cannot both be synced. Rename one side ` +
`to a distinct path to make the tree sync reliably across platforms.`,
);
}
// Probe (and cache) whether `dir` lives on a case-insensitive filesystem.
// Auto-detected by round-tripping a probe file under two casings, with an
// explicit WMILL_CASE_INSENSITIVE_FS=true/false override so Windows behaviour
// can be forced (or emulated for cross-platform repos / tests) on any host.
let _caseInsensitiveFsCache: boolean | undefined;
export async function isCaseInsensitiveFilesystem(
dir: string,
): Promise<boolean> {
const override = (process.env.WMILL_CASE_INSENSITIVE_FS ?? "")
.trim()
.toLowerCase();
if (override === "true" || override === "1") return true;
if (override === "false" || override === "0") return false;
if (_caseInsensitiveFsCache !== undefined) return _caseInsensitiveFsCache;
let result = false;
try {
const upper = path.join(dir, `.wmill-CASEPROBE-${process.pid}.tmp`);
const lower = path.join(dir, `.wmill-caseprobe-${process.pid}.tmp`);
await writeFile(upper, "", "utf-8");
try {
await stat(lower);
result = true; // lowercase name resolves to the file we wrote uppercase
} catch {
result = false;
}
await rm(upper).catch(() => {});
await rm(lower).catch(() => {});
} catch {
result = false;
}
_caseInsensitiveFsCache = result;
return result;
}
async function compareDynFSElement(
els1: DynFSElement,
els2: DynFSElement | undefined,
@@ -1848,14 +2089,55 @@ async function compareDynFSElement(
specificItems?: SpecificItemsConfig,
branchOverride?: string,
isEls1Remote?: boolean,
caseInsensitiveFs?: boolean,
): Promise<{ changes: Change[]; localMap: Record<string, string> }> {
const [m1, m2] = els2
let [m1, m2] = els2
? await Promise.all([
elementsToMap(els1, ignore, json, skips, specificItems, branchOverride, isEls1Remote),
elementsToMap(els2, ignore, json, skips, specificItems, branchOverride, !isEls1Remote),
])
: [await elementsToMap(els1, ignore, json, skips, specificItems, branchOverride, isEls1Remote), {}];
// Reconcile letter-case differences between the local tree and the
// authoritative server casing. Only meaningful for an actual two-sided diff
// (els2 defined) where we know which side is the remote.
if (els2 && isEls1Remote !== undefined) {
const remoteMap = isEls1Remote ? m1 : m2;
// Always warn about server paths that differ only by case (e.g. f/Caps and
// f/caps as two distinct items). These cannot coexist on a case-insensitive
// filesystem, so flag them on every platform — a Linux author needs to know
// their tree won't round-trip for a Windows/macOS teammate.
warnUnrepresentableCaseCollisions(
findCaseInsensitiveCollisions(Object.keys(remoteMap)),
);
// On a case-insensitive filesystem, the local on-disk casing of a folder
// can drift from the server's (Windows reports the case the directory was
// first created with). Rewrite those drifted local keys to the server
// casing so the diff treats them as the same item instead of a destructive
// delete+add pair. This is the WIN-2020 fix.
if (caseInsensitiveFs) {
const { map, rewritten } = canonicalizeCaseInsensitiveKeys(
isEls1Remote ? m2 : m1,
remoteMap,
);
if (isEls1Remote) {
m2 = map;
} else {
m1 = map;
}
const summary = summarizeCaseRewrites(rewritten);
if (summary.length > 0) {
log.info(
`Reconciled ${summary.length} local path(s) to the server's casing ` +
`(case-insensitive filesystem):\n` +
summary.map((s) => ` ${s}`).join("\n"),
);
}
}
}
const changes: Change[] = [];
function parseYaml(k: string, v: string) {
@@ -2613,6 +2895,7 @@ export async function pull(
specificItems,
wsNameForFiles,
true, // els1 (remote) is the remote source
await isCaseInsensitiveFilesystem(process.cwd()),
);
log.info(
@@ -3182,6 +3465,14 @@ function removeSuffix(str: string, suffix: string) {
return str.slice(0, str.length - suffix.length);
}
// Shown after a `wmill sync push --dry-run` preview that has changes. `sync push`
// deploys to the remote workspace and is destructive (it overwrites and prunes
// remote items that differ from or are absent locally), so the preview reminds
// the caller — especially an AI agent that ran the dry-run to inspect changes —
// to get explicit user confirmation before applying it for real.
const SYNC_PUSH_DESTRUCTIVE_WARNING =
"`wmill sync push` is destructive: applying it deploys these changes to the remote workspace and overwrites or deletes remote items that differ from or are absent locally — this is not automatically reversible. If you are an AI agent, do NOT run `wmill sync push` (without --dry-run) until the user has explicitly confirmed this deploy, unless your custom instructions explicitly allow bypassing that confirmation.";
export async function push(
opts: GlobalOptions & SyncOptions & { repository?: string; branch?: string; acceptOverridingPermissionedAsWithSelf?: boolean },
) {
@@ -3361,6 +3652,7 @@ export async function push(
specificItems,
wsNameForFiles,
false, // els1 (local) is not the remote source
await isCaseInsensitiveFilesystem(process.cwd()),
);
// Detect resources/variables that the local config flags as ws_specific
@@ -3717,6 +4009,9 @@ export async function push(
: {}),
})),
total: changes.length,
...(changes.length > 0
? { warning: SYNC_PUSH_DESTRUCTIVE_WARNING }
: {}),
};
console.log(JSON.stringify(result, null, 2));
return;
@@ -3760,6 +4055,7 @@ export async function push(
if (opts.dryRun) {
log.info(colors.gray(`Dry run complete.`));
log.warn(colors.yellow(`\n⚠ ${SYNC_PUSH_DESTRUCTIVE_WARNING}`));
return;
}
+1 -1
View File
@@ -10,4 +10,4 @@ export const WM_FORK_PREFIX = "wm-fork";
// (e.g. utils.ts) can read it without importing main.ts and creating a circular
// dependency (main → workspace → utils → main) that triggers a TDZ.
// Re-exported from main.ts for backwards compatibility.
export const VERSION = "1.720.0";
export const VERSION = "1.721.0";
+6 -5
View File
@@ -5266,10 +5266,10 @@ Text/HTML/inline parts are placed inline in \`body\` as strings.
## CLI Commands
After writing, tell the user they can run these commands (do NOT run them yourself):
\`wmill sync push\` deploys local changes to the workspace and can be destructive to remote state — only suggest/run it when the user explicitly asks to deploy/publish/push, not when they say "run", "try", or "test". \`sync pull\` is safe to run yourself — it never mutates remote state, though it does overwrite local files to match the remote (use \`sync pull --dry-run\` to only preview).
\`\`\`bash
# Push trigger configuration
# Push trigger configuration only when the user explicitly asks to deploy
wmill sync push
# Pull triggers from Windmill
@@ -5317,10 +5317,10 @@ Windmill uses 6-field cron expressions (includes seconds):
## CLI Commands
After writing, tell the user they can run these commands (do NOT run them yourself):
\`wmill sync push\` deploys local changes to the workspace and can be destructive to remote state — only suggest/run it when the user explicitly asks to deploy/publish/push, not when they say "run", "try", or "test". The commands below never mutate remote state, so they're safe to run yourself — note that \`sync pull\` does overwrite local files to match the remote (use \`sync pull --dry-run\` to only preview), while \`schedule\` just lists.
\`\`\`bash
# Push schedules to Windmill
# Push schedules to Windmill only when the user explicitly asks to deploy
wmill sync push
# Pull schedules from Windmill
@@ -5574,7 +5574,8 @@ wmill resource-type list --schema
# Get specific resource type schema
wmill resource-type get postgresql
# Push resources (tell the user to run this, do NOT run it yourself)
# Push resources to Windmill deploys to the workspace and can be destructive to
# remote state, so only run it when the user explicitly asks to deploy/publish/push
wmill sync push
\`\`\`
`,
@@ -0,0 +1,232 @@
import { expect, test } from "bun:test";
import {
findCaseInsensitiveCollisions,
canonicalizeCaseInsensitiveKeys,
summarizeCaseRewrites,
} from "../src/commands/sync/sync.ts";
// =============================================================================
// Case-insensitive sync handling (WIN-2020)
//
// Windmill paths are case-sensitive, but Windows and the default macOS setup
// use case-insensitive filesystems. The real-world failure is NOT a user
// deliberately authoring both f/Caps and f/caps — it is a single capitalized
// folder whose on-disk casing drifts (Windows reports the case the directory
// was first created with), so the diff sees a brand-new lowercase path and a
// destructive delete of the real one. These tests pin:
// 1. findCaseInsensitiveCollisions — warn about genuinely unrepresentable
// server-side collisions (two distinct remote paths differing only by
// case).
// 2. canonicalizeCaseInsensitiveKeys — the fix: rewrite drifted local keys
// to the server's casing so no phantom delete+add is produced.
// =============================================================================
function normalize(groups: string[][]): string[][] {
return groups
.map((g) => [...g])
.sort((a, b) => a.join().localeCompare(b.join()));
}
// ---------------------------------------------------------------------------
// findCaseInsensitiveCollisions
// ---------------------------------------------------------------------------
test("collision detection: sibling folders differing only by case", () => {
const collisions = findCaseInsensitiveCollisions([
"f/Caps/a.script.ts",
"f/caps/b.script.ts",
]);
expect(normalize(collisions)).toEqual([["f/Caps", "f/caps"]]);
});
test("collision detection: leaf files differing only by case", () => {
const collisions = findCaseInsensitiveCollisions([
"f/team/Report.script.ts",
"f/team/report.script.ts",
]);
expect(normalize(collisions)).toEqual([
["f/team/Report.script.ts", "f/team/report.script.ts"],
]);
});
test("collision detection: none for case-consistent distinct paths", () => {
const collisions = findCaseInsensitiveCollisions([
"f/caps/foo.script.ts",
"f/caps/bar.script.ts",
"f/other/baz.script.ts",
]);
expect(collisions).toEqual([]);
});
test("collision detection: normalizes mixed forward/back slashes", () => {
const collisions = findCaseInsensitiveCollisions([
"f\\Caps\\a.script.ts",
"f/caps/b.script.ts",
]);
expect(normalize(collisions)).toEqual([["f/Caps", "f/caps"]]);
});
test("collision detection: reports only the shallowest clash for same-named leaves", () => {
// Two case-variant folders that ALSO hold a same-named file must report the
// single folder clash, not the folder group plus a nested per-file group.
const collisions = findCaseInsensitiveCollisions([
"f/Caps/main.script.ts",
"f/caps/main.script.ts",
]);
expect(normalize(collisions)).toEqual([["f/Caps", "f/caps"]]);
});
test("collision detection: groups three distinct casings together", () => {
const collisions = findCaseInsensitiveCollisions([
"f/caps/a.script.ts",
"f/CAPS/b.script.ts",
"f/Caps/c.script.ts",
]);
expect(normalize(collisions)).toEqual([["f/CAPS", "f/Caps", "f/caps"]]);
});
// ---------------------------------------------------------------------------
// canonicalizeCaseInsensitiveKeys (the WIN-2020 fix)
// ---------------------------------------------------------------------------
test("canonicalize: drifted local folder casing adopts the server casing", () => {
// Server (remote) is authoritative: f/Caps. The local tree drifted to
// f/caps on a case-insensitive FS.
const remote = {
"f/Caps/x.script.ts": "content",
"f/Caps/x.script.yaml": "meta",
};
const local = {
"f/caps/x.script.ts": "content",
"f/caps/x.script.yaml": "meta",
};
const { map, ambiguous, rewritten } = canonicalizeCaseInsensitiveKeys(
local,
remote,
);
// Local keys are rewritten to the server casing, so a subsequent exact-key
// diff sees identical paths — no phantom delete+add.
expect(Object.keys(map).sort()).toEqual([
"f/Caps/x.script.ts",
"f/Caps/x.script.yaml",
]);
expect(ambiguous).toEqual([]);
expect(rewritten).toEqual([
{ from: "f/caps/x.script.ts", to: "f/Caps/x.script.ts" },
{ from: "f/caps/x.script.yaml", to: "f/Caps/x.script.yaml" },
]);
});
test("canonicalize: preserves content values while rewriting keys", () => {
const remote = { "f/MyFolder/Script.script.ts": "remote" };
const local = { "f/myfolder/Script.script.ts": "LOCAL EDIT" };
const { map } = canonicalizeCaseInsensitiveKeys(local, remote);
expect(map["f/MyFolder/Script.script.ts"]).toEqual("LOCAL EDIT");
expect(map["f/myfolder/Script.script.ts"]).toBeUndefined();
});
test("canonicalize: leaves keys with no case-insensitive remote match", () => {
const remote = { "f/Caps/x.script.ts": "a" };
const local = {
"f/Caps/x.script.ts": "a",
"f/brand_new/y.script.ts": "b", // genuinely local-only add
};
const { map, rewritten } = canonicalizeCaseInsensitiveKeys(local, remote);
expect(rewritten).toEqual([]);
expect(Object.keys(map).sort()).toEqual([
"f/Caps/x.script.ts",
"f/brand_new/y.script.ts",
]);
});
test("canonicalize: does NOT rewrite when the server casing is ambiguous", () => {
// The server itself holds two paths differing only by case — we must not
// silently pick one. Leave the local key untouched and report the ambiguity.
const remote = {
"f/Caps/x.script.ts": "a",
"f/caps/x.script.ts": "b",
};
const local = { "f/CAPS/x.script.ts": "local" };
const { map, ambiguous, rewritten } = canonicalizeCaseInsensitiveKeys(
local,
remote,
);
expect(rewritten).toEqual([]);
expect(Object.keys(map)).toEqual(["f/CAPS/x.script.ts"]);
// Reported as the shallowest (folder) clash, not the nested per-file group.
expect(normalize(ambiguous)).toEqual([["f/Caps", "f/caps"]]);
});
test("canonicalize: new local file under a drifted folder adopts the server folder casing", () => {
// Regression for the P1 review finding: a brand-new local file has no exact
// remote match, but it still lives under a folder whose casing drifted. It
// must inherit the server's folder casing (f/Caps), otherwise push would
// create f/caps/New beside f/Caps/* and reintroduce the case-only collision.
const remote = { "f/Caps/Existing.script.ts": "remote" };
const local = {
"f/caps/Existing.script.ts": "remote", // drifted, existing
"f/caps/New.script.ts": "brand new", // drifted folder, local-only file
};
const { map, rewritten } = canonicalizeCaseInsensitiveKeys(local, remote);
expect(Object.keys(map).sort()).toEqual([
"f/Caps/Existing.script.ts",
"f/Caps/New.script.ts",
]);
expect(map["f/Caps/New.script.ts"]).toEqual("brand new");
expect(rewritten).toContainEqual({
from: "f/caps/New.script.ts",
to: "f/Caps/New.script.ts",
});
});
test("canonicalize: stops at the first segment with no server guidance", () => {
// Only the folder prefix that exists on the server is canonicalized; deeper
// local-only directories keep their own casing.
const remote = { "f/Caps/x.script.ts": "a" };
const local = { "f/caps/SubDir/y.script.ts": "b" };
const { map } = canonicalizeCaseInsensitiveKeys(local, remote);
expect(Object.keys(map)).toEqual(["f/Caps/SubDir/y.script.ts"]);
});
test("canonicalize: identical casing is a no-op", () => {
const remote = { "f/Caps/x.script.ts": "a" };
const local = { "f/Caps/x.script.ts": "a" };
const { map, rewritten, ambiguous } = canonicalizeCaseInsensitiveKeys(
local,
remote,
);
expect(rewritten).toEqual([]);
expect(ambiguous).toEqual([]);
expect(map).toEqual({ "f/Caps/x.script.ts": "a" });
});
// ---------------------------------------------------------------------------
// summarizeCaseRewrites
// ---------------------------------------------------------------------------
test("summarize: collapses per-file rewrites into one folder entry", () => {
const summary = summarizeCaseRewrites([
{ from: "f/caps/x.script.ts", to: "f/Caps/x.script.ts" },
{ from: "f/caps/x.script.yaml", to: "f/Caps/x.script.yaml" },
{ from: "f/caps/y.script.ts", to: "f/Caps/y.script.ts" },
]);
expect(summary).toEqual(["f/caps -> f/Caps"]);
});
test("summarize: reports a leaf-file casing change at file granularity", () => {
const summary = summarizeCaseRewrites([
{ from: "f/team/report.script.ts", to: "f/team/Report.script.ts" },
]);
expect(summary).toEqual([
"f/team/report.script.ts -> f/team/Report.script.ts",
]);
});
test("summarize: reports independent folder drifts separately", () => {
const summary = summarizeCaseRewrites([
{ from: "f/caps/x.script.ts", to: "f/Caps/x.script.ts" },
{ from: "u/alice/y.script.ts", to: "u/Alice/y.script.ts" },
]);
expect(summary.sort()).toEqual(["f/caps -> f/Caps", "u/alice -> u/Alice"]);
});
+168 -1
View File
@@ -14,7 +14,7 @@
import { expect, test } from "bun:test";
import * as path from "node:path";
import { writeFile, readFile, stat } from "node:fs/promises";
import { writeFile, readFile, stat, rename } from "node:fs/promises";
import { withTestBackend } from "./test_backend.ts";
import { addWorkspace } from "../workspace.ts";
import { parseJsonFromCLIOutput } from "./test_config_helpers.ts";
@@ -686,6 +686,173 @@ excludes: []
});
});
// The core WIN-2020 reproduction. The real failure is NOT a user authoring
// both f/Caps and f/caps — it is a SINGLE capitalized folder whose on-disk
// casing drifts on a case-insensitive filesystem (Windows stores/reports the
// case the directory was first created with). The diff then sees a brand-new
// lowercase path plus a destructive delete of the real one.
//
// This test runs on BOTH the Linux and Windows CI jobs:
// - On the Windows runner (real case-insensitive NTFS) the CLI auto-detects
// case-insensitivity via its filesystem probe — no override, real behavior.
// - On Linux (case-sensitive) we reproduce the drift with an explicit rename
// and force the same code path with WMILL_CASE_INSENSITIVE_FS=true, and we
// additionally assert that WITHOUT the fix the destructive phantom appears
// (which can only be observed on a case-sensitive FS).
test("Mixed Case Paths: case-only folder drift reconciles to server casing (WIN-2020)", async () => {
await withTestBackend(async (backend, tempDir) => {
await setupWorkspaceProfile(backend);
await createFolder(backend, "Caps");
await createScript(
backend,
"f/Caps/MyScript",
'export async function main() { return "drift repro"; }',
"Drift Script"
);
await writeFile(
path.join(tempDir, "wmill.yaml"),
`defaultTs: bun
includes:
- "**"
excludes: []
`,
"utf-8"
);
const pullResult = await backend.runCLICommand(["sync", "pull", "--yes"], tempDir);
expect(pullResult.code).toEqual(0);
const upperDir = path.join(tempDir, "f", "Caps");
const lowerDir = path.join(tempDir, "f", "caps");
const pulledExists = await stat(path.join(upperDir, "MyScript.ts"))
.then(() => true)
.catch(() => false);
expect(pulledExists).toBeTruthy();
// Simulate the on-disk casing drift (a no-op-in-spirit case-only rename).
// On Windows this is a real case-only rename of the same directory; on
// Linux it produces a genuinely lowercase sibling.
await rename(upperDir, lowerDir);
const onWindows = process.platform === "win32";
// Without case-insensitive handling, the drift is a destructive
// delete(f/Caps) + add(f/caps) phantom. Only observable on a
// case-sensitive FS (Linux), where the override defaults off.
if (!onWindows) {
const buggy = await backend.runCLICommand(
["sync", "push", "--yes", "--dry-run", "--json-output"],
tempDir
);
expect(buggy.code).toEqual(0);
const buggyChanges = parseJsonFromCLIOutput(buggy.stdout).changes || [];
const hasDelete = buggyChanges.some(
(c: any) => c.type === "deleted" && c.path.replace(/\\/g, "/").startsWith("f/Caps/")
);
const hasAdd = buggyChanges.some(
(c: any) => c.type === "added" && c.path.replace(/\\/g, "/").startsWith("f/caps/")
);
expect(hasDelete).toBeTruthy();
expect(hasAdd).toBeTruthy();
}
// With case-insensitive handling in effect (auto on Windows via the FS
// probe, forced via env on Linux) the drifted local casing is reconciled
// to the server's casing, so the push is a clean no-op.
if (!onWindows) process.env.WMILL_CASE_INSENSITIVE_FS = "true";
try {
const fixed = await backend.runCLICommand(
["sync", "push", "--yes", "--dry-run", "--json-output"],
tempDir
);
expect(fixed.code).toEqual(0);
const fixedChanges = parseJsonFromCLIOutput(fixed.stdout).changes || [];
if (fixedChanges.length !== 0) {
console.error(
"Expected no changes after reconciliation, got:",
JSON.stringify(fixedChanges, null, 2)
);
}
expect(fixedChanges.length).toEqual(0);
// P1 regression: a brand-new item added under the drifted (lowercase)
// folder must be pushed under the server's folder casing (f/Caps), not
// recreate the case-only collision as f/caps. A self-contained variable
// YAML is used so the assertion targets path canonicalization without
// dragging in script lock/metadata generation.
await writeFile(
path.join(lowerDir, "NewVar.variable.yaml"),
`value: hello\nis_secret: false\ndescription: new under drifted folder\nis_oauth: false\n`,
"utf-8"
);
const added = await backend.runCLICommand(
["sync", "push", "--yes", "--dry-run", "--json-output"],
tempDir
);
expect(added.code).toEqual(0);
const addedChanges = parseJsonFromCLIOutput(added.stdout).changes || [];
const addedPaths = addedChanges.map((c: any) =>
c.path.replace(/\\/g, "/")
);
expect(
addedPaths.some((p: string) => p === "f/Caps/NewVar.variable.yaml")
).toBeTruthy();
expect(
addedPaths.some((p: string) => p.startsWith("f/caps/"))
).toBeFalsy();
} finally {
if (!onWindows) delete process.env.WMILL_CASE_INSENSITIVE_FS;
}
});
});
// A genuine, unrepresentable server-side collision: two DISTINCT server folders
// that differ only by case (f/Caps and f/caps). These cannot both exist on a
// case-insensitive filesystem, so the CLI must warn. Skipped on Windows, where
// the pull physically cannot lay both folders down; the warning string itself
// is exercised platform-independently by the unit tests.
const collisionTest = process.platform === "win32" ? test.skip : test;
collisionTest("Mixed Case Paths: distinct server folders differing only by case warn", async () => {
await withTestBackend(async (backend, tempDir) => {
await setupWorkspaceProfile(backend);
await createFolder(backend, "Caps");
await createFolder(backend, "caps");
await createScript(
backend,
"f/Caps/upper",
'export async function main() { return "upper"; }',
"Upper"
);
await createScript(
backend,
"f/caps/lower",
'export async function main() { return "lower"; }',
"Lower"
);
await writeFile(
path.join(tempDir, "wmill.yaml"),
`defaultTs: bun
includes:
- "**"
excludes: []
`,
"utf-8"
);
const pullResult = await backend.runCLICommand(["sync", "pull", "--yes"], tempDir);
expect(pullResult.code).toEqual(0);
const combinedOutput = pullResult.stdout + pullResult.stderr;
expect(combinedOutput.includes("differ only by letter case")).toBeTruthy();
expect(combinedOutput.includes("f/Caps")).toBeTruthy();
expect(combinedOutput.includes("f/caps")).toBeTruthy();
});
});
test("Mixed Case Paths: CamelCase folder names with numbers", async () => {
await withTestBackend(async (backend, tempDir) => {
await setupWorkspaceProfile(backend);
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "@windmill-labs/components",
"version": "1.720.0",
"version": "1.721.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@windmill-labs/components",
"version": "1.720.0",
"version": "1.721.0",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@windmill-labs/components",
"version": "1.720.0",
"version": "1.721.0",
"scripts": {
"dev": "vite dev",
"dev:ui-builder": "mv static/ui_builder static/ui_builder.dev-disabled 2>/dev/null || true ; trap 'mv static/ui_builder.dev-disabled static/ui_builder 2>/dev/null || true' EXIT ; vite dev",
+1 -1
View File
@@ -4,7 +4,7 @@ verify_ssl = true
name = "pypi"
[packages]
wmill = ">=1.720.0"
wmill = ">=1.721.0"
sendgrid = "*"
mysql-connector-python = "*"
pymongo = "*"
+1 -1
View File
@@ -1,7 +1,7 @@
openapi: '3.0.3'
info:
version: 1.720.0
version: 1.721.0
title: OpenFlow Spec
contact:
name: Ruben Fiszel
@@ -12,7 +12,7 @@
RootModule = 'WindmillClient.psm1'
# Version number of this module.
ModuleVersion = '1.720.0'
ModuleVersion = '1.721.0'
# Supported PSEditions
# CompatiblePSEditions = @()
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "wmill"
version = "1.720.0"
version = "1.721.0"
description = "A client library for accessing Windmill server wrapping the Windmill client API"
license = "Apache-2.0"
homepage = "https://windmill.dev"
+2 -1
View File
@@ -563,7 +563,8 @@ wmill resource-type list --schema
# Get specific resource type schema
wmill resource-type get postgresql
# Push resources (tell the user to run this, do NOT run it yourself)
# Push resources to Windmill — deploys to the workspace and can be destructive to
# remote state, so only run it when the user explicitly asks to deploy/publish/push
wmill sync push
\`\`\`
`;
@@ -242,6 +242,7 @@ wmill resource-type list --schema
# Get specific resource type schema
wmill resource-type get postgresql
# Push resources (tell the user to run this, do NOT run it yourself)
# Push resources to Windmill — deploys to the workspace and can be destructive to
# remote state, so only run it when the user explicitly asks to deploy/publish/push
wmill sync push
```
@@ -39,10 +39,10 @@ Windmill uses 6-field cron expressions (includes seconds):
## CLI Commands
After writing, tell the user they can run these commands (do NOT run them yourself):
`wmill sync push` deploys local changes to the workspace and can be destructive to remote state — only suggest/run it when the user explicitly asks to deploy/publish/push, not when they say "run", "try", or "test". The commands below never mutate remote state, so they're safe to run yourself — note that `sync pull` does overwrite local files to match the remote (use `sync pull --dry-run` to only preview), while `schedule` just lists.
```bash
# Push schedules to Windmill
# Push schedules to Windmill — only when the user explicitly asks to deploy
wmill sync push
# Pull schedules from Windmill
@@ -61,10 +61,10 @@ Text/HTML/inline parts are placed inline in `body` as strings.
## CLI Commands
After writing, tell the user they can run these commands (do NOT run them yourself):
`wmill sync push` deploys local changes to the workspace and can be destructive to remote state — only suggest/run it when the user explicitly asks to deploy/publish/push, not when they say "run", "try", or "test". `sync pull` is safe to run yourself — it never mutates remote state, though it does overwrite local files to match the remote (use `sync pull --dry-run` to only preview).
```bash
# Push trigger configuration
# Push trigger configuration — only when the user explicitly asks to deploy
wmill sync push
# Pull triggers from Windmill
+2 -1
View File
@@ -237,6 +237,7 @@ wmill resource-type list --schema
# Get specific resource type schema
wmill resource-type get postgresql
# Push resources (tell the user to run this, do NOT run it yourself)
# Push resources to Windmill — deploys to the workspace and can be destructive to
# remote state, so only run it when the user explicitly asks to deploy/publish/push
wmill sync push
```
+2 -2
View File
@@ -34,10 +34,10 @@ Windmill uses 6-field cron expressions (includes seconds):
## CLI Commands
After writing, tell the user they can run these commands (do NOT run them yourself):
`wmill sync push` deploys local changes to the workspace and can be destructive to remote state — only suggest/run it when the user explicitly asks to deploy/publish/push, not when they say "run", "try", or "test". The commands below never mutate remote state, so they're safe to run yourself — note that `sync pull` does overwrite local files to match the remote (use `sync pull --dry-run` to only preview), while `schedule` just lists.
```bash
# Push schedules to Windmill
# Push schedules to Windmill — only when the user explicitly asks to deploy
wmill sync push
# Pull schedules from Windmill
+2 -2
View File
@@ -56,10 +56,10 @@ Text/HTML/inline parts are placed inline in `body` as strings.
## CLI Commands
After writing, tell the user they can run these commands (do NOT run them yourself):
`wmill sync push` deploys local changes to the workspace and can be destructive to remote state — only suggest/run it when the user explicitly asks to deploy/publish/push, not when they say "run", "try", or "test". `sync pull` is safe to run yourself — it never mutates remote state, though it does overwrite local files to match the remote (use `sync pull --dry-run` to only preview).
```bash
# Push trigger configuration
# Push trigger configuration — only when the user explicitly asks to deploy
wmill sync push
# Pull triggers from Windmill
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@windmill/windmill",
"version": "1.720.0",
"version": "1.721.0",
"exports": "./src/index.ts",
"publish": {
"exclude": ["!src", "./s3Types.ts", "./sqlUtils.ts", "./client.ts"]
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "windmill-client",
"description": "Windmill SDK client for browsers and Node.js",
"version": "1.720.0",
"version": "1.721.0",
"author": "Ruben Fiszel",
"license": "Apache 2.0",
"sideEffects": false,
+1 -1
View File
@@ -1 +1 @@
1.720.0
1.721.0