Compare commits
8 Commits
v1.668.0
...
py-typeche
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42eba98ea9 | ||
|
|
37799574d8 | ||
|
|
78ac28b4e0 | ||
|
|
f40cdaf434 | ||
|
|
f6863bd1ba | ||
|
|
0ea9b945e0 | ||
|
|
38acaa3653 | ||
|
|
acfae3552c |
14
CHANGELOG.md
14
CHANGELOG.md
@@ -1,5 +1,19 @@
|
||||
# Changelog
|
||||
|
||||
## [1.668.2](https://github.com/windmill-labs/windmill/compare/v1.668.1...v1.668.2) (2026-03-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **cli:** app push crash, lint path, push --message, run validation, history timestamps ([#8585](https://github.com/windmill-labs/windmill/issues/8585)) ([f40cdaf](https://github.com/windmill-labs/windmill/commit/f40cdaf43453d2643800ed730d6abe6873bbe8e7))
|
||||
|
||||
## [1.668.1](https://github.com/windmill-labs/windmill/compare/v1.668.0...v1.668.1) (2026-03-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **cli:** fix 13 CLI bugs — exit codes, sync tar fallback, variable encryption, JSON output ([#8582](https://github.com/windmill-labs/windmill/issues/8582)) ([38acaa3](https://github.com/windmill-labs/windmill/commit/38acaa3653728bf9e0ae6f746edf433703b4ab63))
|
||||
|
||||
## [1.668.0](https://github.com/windmill-labs/windmill/compare/v1.667.0...v1.668.0) (2026-03-28)
|
||||
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT s.hash as hash, dm.deployment_msg as deployment_msg \n FROM script s LEFT JOIN deployment_metadata dm ON s.hash = dm.script_hash\n WHERE s.workspace_id = $1 AND s.path = $2\n ORDER by s.created_at DESC",
|
||||
"query": "SELECT s.hash as hash, dm.deployment_msg as deployment_msg, s.created_at as created_at\n FROM script s LEFT JOIN deployment_metadata dm ON s.hash = dm.script_hash\n WHERE s.workspace_id = $1 AND s.path = $2\n ORDER by s.created_at DESC",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -12,6 +12,11 @@
|
||||
"ordinal": 1,
|
||||
"name": "deployment_msg",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -22,8 +27,9 @@
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
true
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "726e956cfcd3ac7c07abeecdf92cf0996efe7fa7b671ac2b3b000ead0ea307de"
|
||||
"hash": "9a1483a81f5b086e0765d3d69483e29b09f66090e1f9d394564c16d921d2e66c"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT s.hash as hash, dm.deployment_msg as deployment_msg \n FROM script s LEFT JOIN deployment_metadata dm ON s.hash = dm.script_hash\n WHERE s.workspace_id = $1 AND s.path = $2\n ORDER by s.created_at DESC LIMIT 1",
|
||||
"query": "SELECT s.hash as hash, dm.deployment_msg as deployment_msg, s.created_at as created_at\n FROM script s LEFT JOIN deployment_metadata dm ON s.hash = dm.script_hash\n WHERE s.workspace_id = $1 AND s.path = $2\n ORDER by s.created_at DESC LIMIT 1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -12,6 +12,11 @@
|
||||
"ordinal": 1,
|
||||
"name": "deployment_msg",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -22,8 +27,9 @@
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
true
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "cf2a6ad6471a40b6298775cda9300aeecdd75503bed59d80cd62091d1642d1ec"
|
||||
"hash": "c73e98e5a937f44724a96ee1b74d31fa71a7be3b8ba3dec9f59f54a6c4030462"
|
||||
}
|
||||
150
backend/Cargo.lock
generated
150
backend/Cargo.lock
generated
@@ -15813,7 +15813,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-nats",
|
||||
@@ -15891,7 +15891,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-alerting"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"axum 0.8.4",
|
||||
"chrono",
|
||||
@@ -15904,7 +15904,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
@@ -16045,7 +16045,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-agent-workers"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"axum 0.8.4",
|
||||
"chrono",
|
||||
@@ -16068,7 +16068,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-assets"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"axum 0.8.4",
|
||||
"chrono",
|
||||
@@ -16081,7 +16081,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-auth"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.8.4",
|
||||
@@ -16107,7 +16107,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-client"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"reqwest 0.12.28",
|
||||
"serde",
|
||||
@@ -16117,7 +16117,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-configs"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"axum 0.8.4",
|
||||
"chrono",
|
||||
@@ -16134,7 +16134,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-debug"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"axum 0.8.4",
|
||||
"base64 0.22.1",
|
||||
@@ -16157,7 +16157,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-embeddings"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.8.4",
|
||||
@@ -16180,7 +16180,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-flow-conversations"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"axum 0.8.4",
|
||||
"chrono",
|
||||
@@ -16196,7 +16196,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-flows"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"axum 0.8.4",
|
||||
"chrono",
|
||||
@@ -16216,7 +16216,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-groups"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"axum 0.8.4",
|
||||
"chrono",
|
||||
@@ -16236,7 +16236,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-inputs"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"axum 0.8.4",
|
||||
"chrono",
|
||||
@@ -16250,7 +16250,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-integration-tests"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-nats",
|
||||
@@ -16280,7 +16280,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-jobs"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.8.4",
|
||||
@@ -16305,7 +16305,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-npm-proxy"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"axum 0.8.4",
|
||||
"flate2",
|
||||
@@ -16323,7 +16323,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-openapi"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.8.4",
|
||||
@@ -16345,7 +16345,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-schedule"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"axum 0.8.4",
|
||||
"chrono",
|
||||
@@ -16365,7 +16365,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-scripts"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"axum 0.8.4",
|
||||
"chrono",
|
||||
@@ -16395,7 +16395,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-settings"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.8.4",
|
||||
@@ -16422,7 +16422,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-sse"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"serde",
|
||||
@@ -16434,7 +16434,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-users"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"argon2",
|
||||
"axum 0.8.4",
|
||||
@@ -16458,7 +16458,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-workers"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"axum 0.8.4",
|
||||
"chrono",
|
||||
@@ -16472,7 +16472,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-workspaces"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"axum 0.8.4",
|
||||
"chrono",
|
||||
@@ -16504,7 +16504,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-audit"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"lazy_static",
|
||||
@@ -16518,7 +16518,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-autoscaling"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.8.4",
|
||||
@@ -16537,7 +16537,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-common"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"aho-corasick",
|
||||
@@ -16638,7 +16638,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-dep-map"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"itertools 0.14.0",
|
||||
@@ -16657,7 +16657,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-git-sync"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"regex",
|
||||
"serde",
|
||||
@@ -16672,7 +16672,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-indexer"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"astral-tokio-tar",
|
||||
@@ -16696,7 +16696,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-jseval"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"futures",
|
||||
@@ -16713,7 +16713,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-macros"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"itertools 0.14.0",
|
||||
"lazy_static",
|
||||
@@ -16729,7 +16729,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-mcp"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -16750,7 +16750,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-native-triggers"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -16781,7 +16781,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-oauth"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-oauth2",
|
||||
@@ -16805,7 +16805,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-object-store"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-stream",
|
||||
@@ -16839,7 +16839,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-operator"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"futures",
|
||||
@@ -16857,7 +16857,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"convert_case 0.6.0",
|
||||
"serde",
|
||||
@@ -16866,7 +16866,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-bash"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -16878,7 +16878,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-csharp"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -16890,7 +16890,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-go"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"gosyn",
|
||||
@@ -16902,7 +16902,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-graphql"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -16914,7 +16914,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-java"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -16926,7 +16926,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-nu"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"nu-parser",
|
||||
@@ -16937,7 +16937,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-php"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -16948,7 +16948,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -16960,7 +16960,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py-asset"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"rustpython-ast",
|
||||
@@ -16971,7 +16971,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py-imports"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -16993,7 +16993,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ruby"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -17007,7 +17007,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-rust"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"convert_case 0.6.0",
|
||||
@@ -17024,7 +17024,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-sql"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -17037,7 +17037,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-sql-asset"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
@@ -17049,7 +17049,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ts"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -17067,7 +17067,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ts-asset"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde-wasm-bindgen",
|
||||
@@ -17083,7 +17083,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-wac"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"rustpython-ast",
|
||||
@@ -17099,7 +17099,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-yaml"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
@@ -17110,7 +17110,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-queue"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -17147,7 +17147,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-runtime-nativets"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"const_format",
|
||||
@@ -17185,7 +17185,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-sql-datatype-parser-wasm"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
"wasm-bindgen",
|
||||
@@ -17196,7 +17196,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-store"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -17225,7 +17225,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-test-utils"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17249,7 +17249,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17282,7 +17282,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-email"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17302,7 +17302,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-gcp"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17336,7 +17336,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-http"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17371,7 +17371,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-kafka"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17394,7 +17394,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-mqtt"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17418,7 +17418,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-nats"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-nats",
|
||||
@@ -17442,7 +17442,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-postgres"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17477,7 +17477,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-sqs"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17505,7 +17505,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-websocket"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17528,7 +17528,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-types"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.9.4",
|
||||
@@ -17547,7 +17547,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-worker"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-once-cell",
|
||||
@@ -17656,7 +17656,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-worker-volumes"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "windmill"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
@@ -82,7 +82,7 @@ members = [
|
||||
exclude = ["./windmill-duckdb-ffi-internal"]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
|
||||
edition = "2021"
|
||||
|
||||
|
||||
20
backend/tests/fixtures/typechecked_python.sql
vendored
Normal file
20
backend/tests/fixtures/typechecked_python.sql
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
INSERT INTO public.script(workspace_id, created_by, content, schema, summary, description, path, hash, language, lock) VALUES (
|
||||
'test-workspace',
|
||||
'test-user',
|
||||
'
|
||||
import inspect
|
||||
import sys
|
||||
|
||||
def greet(name: str) -> str:
|
||||
# Verify that __file__ is set on this module (same check typeguard does)
|
||||
mod = sys.modules[__name__]
|
||||
source_file = inspect.getfile(mod)
|
||||
return f"Hello, {name}! from {source_file}"
|
||||
|
||||
def main():
|
||||
return greet("World")
|
||||
',
|
||||
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{},"required":[],"type":"object"}',
|
||||
'',
|
||||
'',
|
||||
'f/system/typechecked_helper', 12349, 'python3', '');
|
||||
@@ -923,3 +923,43 @@ async def main(item: str, qty: int, email: str):
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "python")]
|
||||
#[sqlx::test(fixtures("base", "typechecked_python"))]
|
||||
async fn test_typechecked_decorator_python(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
let port = server.addr.port();
|
||||
|
||||
let content = r#"
|
||||
from f.system.typechecked_helper import greet
|
||||
|
||||
def main():
|
||||
return greet("World")
|
||||
"#
|
||||
.to_owned();
|
||||
|
||||
let job = JobPayload::Code(RawCode {
|
||||
hash: None,
|
||||
content,
|
||||
path: Some("f/system/test_typechecked".to_string()),
|
||||
language: ScriptLang::Python3,
|
||||
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 result = run_job_in_new_worker_until_complete(&db, false, job, port)
|
||||
.await
|
||||
.json_result()
|
||||
.unwrap();
|
||||
|
||||
let result_str = result.as_str().unwrap();
|
||||
assert!(result_str.starts_with("Hello, World! from "), "unexpected result: {result_str}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1447,7 +1447,7 @@ async fn get_script_history(
|
||||
check_scopes(&authed, || format!("scripts:read:{}", path))?;
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
let query_result = sqlx::query!(
|
||||
"SELECT s.hash as hash, dm.deployment_msg as deployment_msg
|
||||
"SELECT s.hash as hash, dm.deployment_msg as deployment_msg, s.created_at as created_at
|
||||
FROM script s LEFT JOIN deployment_metadata dm ON s.hash = dm.script_hash
|
||||
WHERE s.workspace_id = $1 AND s.path = $2
|
||||
ORDER by s.created_at DESC",
|
||||
@@ -1463,6 +1463,7 @@ async fn get_script_history(
|
||||
.map(|row| ScriptHistory {
|
||||
script_hash: ScriptHash(row.hash),
|
||||
deployment_msg: row.deployment_msg,
|
||||
created_at: Some(row.created_at),
|
||||
})
|
||||
.collect();
|
||||
return Ok(Json(result));
|
||||
@@ -1477,7 +1478,7 @@ async fn get_latest_version(
|
||||
check_scopes(&authed, || format!("scripts:read:{}", path))?;
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
let row_o = sqlx::query!(
|
||||
"SELECT s.hash as hash, dm.deployment_msg as deployment_msg
|
||||
"SELECT s.hash as hash, dm.deployment_msg as deployment_msg, s.created_at as created_at
|
||||
FROM script s LEFT JOIN deployment_metadata dm ON s.hash = dm.script_hash
|
||||
WHERE s.workspace_id = $1 AND s.path = $2
|
||||
ORDER by s.created_at DESC LIMIT 1",
|
||||
@@ -1491,7 +1492,8 @@ async fn get_latest_version(
|
||||
if let Some(row) = row_o {
|
||||
let result = ScriptHistory {
|
||||
script_hash: ScriptHash(row.hash),
|
||||
deployment_msg: row.deployment_msg, //
|
||||
deployment_msg: row.deployment_msg,
|
||||
created_at: Some(row.created_at),
|
||||
};
|
||||
return Ok(Json(Some(result)));
|
||||
} else {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: "3.0.3"
|
||||
|
||||
info:
|
||||
version: 1.668.0
|
||||
version: 1.668.2
|
||||
title: Windmill API
|
||||
|
||||
contact:
|
||||
|
||||
@@ -450,6 +450,8 @@ pub struct ScriptHistory {
|
||||
pub script_hash: ScriptHash,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub deployment_msg: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub created_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
||||
@@ -2,6 +2,7 @@ import sys
|
||||
import os
|
||||
from importlib.abc import MetaPathFinder, Loader
|
||||
from importlib.machinery import ModuleSpec, SourceFileLoader
|
||||
from importlib.util import spec_from_file_location
|
||||
import time
|
||||
|
||||
class WindmillLoader(Loader):
|
||||
@@ -33,7 +34,7 @@ class WindmillFinder(MetaPathFinder):
|
||||
fullpath = folder + "/" + splitted[-1] + ".py"
|
||||
|
||||
if os.path.exists(fullpath):
|
||||
return ModuleSpec(name, SourceFileLoader(name, fullpath))
|
||||
return spec_from_file_location(name, fullpath)
|
||||
|
||||
|
||||
import urllib.parse
|
||||
@@ -62,7 +63,7 @@ class WindmillFinder(MetaPathFinder):
|
||||
return ModuleSpec(name, WindmillLoader(name))
|
||||
with open(fullpath, "w+") as f:
|
||||
f.write(r)
|
||||
return ModuleSpec(name, SourceFileLoader(name, fullpath))
|
||||
return spec_from_file_location(name, fullpath)
|
||||
except urllib.error.HTTPError as e:
|
||||
duration = time.time() - req_start
|
||||
if e.code != 404:
|
||||
|
||||
@@ -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.668.0";
|
||||
export const VERSION = "v1.668.2";
|
||||
|
||||
export async function login(email: string, password: string): Promise<string> {
|
||||
return await windmill.UserService.login({
|
||||
|
||||
@@ -166,8 +166,10 @@ export async function createBundle(
|
||||
// Dynamically import esbuild
|
||||
const esbuild = await import("esbuild");
|
||||
|
||||
// Detect frameworks to determine default entry point
|
||||
const frameworks = detectFrameworks(process.cwd());
|
||||
// Detect frameworks to determine default entry point.
|
||||
// Use the entryPoint's directory if provided, otherwise fall back to cwd.
|
||||
const appDir = options.entryPoint ? path.dirname(options.entryPoint) : process.cwd();
|
||||
const frameworks = detectFrameworks(appDir);
|
||||
const defaultEntry = (frameworks.svelte || frameworks.vue) ? "index.ts" : "index.tsx";
|
||||
|
||||
const entryPoint = options.entryPoint ?? defaultEntry;
|
||||
@@ -184,7 +186,6 @@ export async function createBundle(
|
||||
}
|
||||
|
||||
// Ensure node_modules exists in the app directory
|
||||
const appDir = path.dirname(entryPoint) || process.cwd();
|
||||
await ensureNodeModules(appDir);
|
||||
|
||||
// Load framework-specific plugins (svelte, vue) based on package.json
|
||||
|
||||
@@ -6,7 +6,7 @@ import { colors } from "@cliffy/ansi/colors";
|
||||
import * as log from "../../core/log.ts";
|
||||
import { yamlParseFile } from "../../utils/yaml.ts";
|
||||
import { GlobalOptions } from "../../types.ts";
|
||||
import { createBundle } from "./bundle.ts";
|
||||
import { createBundle, detectFrameworks } from "./bundle.ts";
|
||||
import { APP_BACKEND_FOLDER } from "./app_metadata.ts";
|
||||
import { loadRunnablesFromBackend } from "./raw_apps.ts";
|
||||
import {
|
||||
@@ -113,7 +113,11 @@ async function validateBuild(
|
||||
log.info(colors.blue("🔨 Testing build..."));
|
||||
|
||||
// Try to create a bundle - this will validate that all dependencies are in place
|
||||
const frameworks = detectFrameworks(appDir);
|
||||
const entryFile = frameworks.svelte || frameworks.vue ? "index.ts" : "index.tsx";
|
||||
const entryPoint = path.join(appDir, entryFile);
|
||||
await createBundle({
|
||||
entryPoint,
|
||||
production: true,
|
||||
minify: false,
|
||||
});
|
||||
|
||||
@@ -42,6 +42,13 @@ async function list(
|
||||
log.info("No audit logs found.");
|
||||
return;
|
||||
}
|
||||
if (logs.every((l) => l.operation === "redacted")) {
|
||||
log.info(colors.yellow(
|
||||
"Audit log details are not available on the Community Edition.\n" +
|
||||
"Upgrade to the Enterprise Edition for full audit logging with operation details."
|
||||
));
|
||||
return;
|
||||
}
|
||||
new Table()
|
||||
.header(["ID", "Timestamp", "Username", "Operation", "Action", "Resource"])
|
||||
.padding(2)
|
||||
|
||||
@@ -7,6 +7,7 @@ import * as log from "../../core/log.ts";
|
||||
import { sep as SEP } from "node:path";
|
||||
import { stringify as yamlStringify } from "yaml";
|
||||
import { yamlParseFile } from "../../utils/yaml.ts";
|
||||
import { validateRequiredArgs } from "../../utils/utils.ts";
|
||||
import * as wmill from "../../../gen/services.gen.ts";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { mkdirSync, writeFileSync } from "node:fs";
|
||||
@@ -204,14 +205,14 @@ export async function pushFlow(
|
||||
|
||||
type Options = GlobalOptions;
|
||||
|
||||
async function push(opts: Options, filePath: string, remotePath: string) {
|
||||
async function push(opts: Options & { message?: string }, filePath: string, remotePath: string) {
|
||||
if (!validatePath(remotePath)) {
|
||||
return;
|
||||
}
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
await pushFlow(workspace.workspaceId, remotePath, filePath);
|
||||
await pushFlow(workspace.workspaceId, remotePath, filePath, opts.message);
|
||||
log.info(colors.bold.underline.green("Flow pushed"));
|
||||
}
|
||||
|
||||
@@ -252,6 +253,7 @@ async function list(
|
||||
}
|
||||
}
|
||||
async function get(opts: GlobalOptions & { json?: boolean }, path: string) {
|
||||
if (opts.json) log.setSilent(true);
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
const f = await wmill.getFlowByPath({
|
||||
@@ -294,6 +296,20 @@ async function run(
|
||||
|
||||
const input = opts.data ? await resolve(opts.data) : {};
|
||||
|
||||
// Validate required args against schema when no data provided
|
||||
if (!opts.data) {
|
||||
try {
|
||||
const flow = await wmill.getFlowByPath({
|
||||
workspace: workspace.workspaceId,
|
||||
path,
|
||||
});
|
||||
validateRequiredArgs(flow.schema as Record<string, unknown>);
|
||||
} catch (e: any) {
|
||||
if (e.message?.startsWith("Missing required")) throw e;
|
||||
log.warn(`Could not fetch schema to validate args: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
const id = await wmill.runFlowByPath({
|
||||
workspace: workspace.workspaceId,
|
||||
path,
|
||||
@@ -328,18 +344,39 @@ async function run(
|
||||
i++;
|
||||
}
|
||||
|
||||
if (!opts.silent) {
|
||||
log.info(colors.green.underline.bold("Flow ran to completion"));
|
||||
log.info("\n");
|
||||
// Wait for flow completion with retry (handles race when --silent skips module tracking)
|
||||
const MAX_RETRIES = 600; // ~60 seconds at 100ms intervals
|
||||
let retries = 0;
|
||||
while (retries < MAX_RETRIES) {
|
||||
try {
|
||||
const jobInfo = await wmill.getCompletedJob({
|
||||
workspace: workspace.workspaceId,
|
||||
id,
|
||||
});
|
||||
|
||||
if (!opts.silent) {
|
||||
log.info(colors.green.underline.bold("Flow ran to completion"));
|
||||
log.info("\n");
|
||||
}
|
||||
|
||||
if (jobInfo.success === false) {
|
||||
process.exitCode = 1;
|
||||
}
|
||||
|
||||
if (opts.silent) {
|
||||
console.log(JSON.stringify(jobInfo.result ?? {}));
|
||||
} else {
|
||||
log.info(JSON.stringify(jobInfo.result ?? {}, null, 2));
|
||||
}
|
||||
|
||||
break;
|
||||
} catch {
|
||||
retries++;
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
}
|
||||
}
|
||||
const jobInfo = await wmill.getCompletedJob({
|
||||
workspace: workspace.workspaceId,
|
||||
id,
|
||||
});
|
||||
if (opts.silent) {
|
||||
console.log(JSON.stringify(jobInfo.result ?? {}));
|
||||
} else {
|
||||
log.info(JSON.stringify(jobInfo.result ?? {}, null, 2));
|
||||
if (retries >= MAX_RETRIES) {
|
||||
throw new Error(`Timed out waiting for flow ${id} to complete`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -445,6 +482,17 @@ async function preview(
|
||||
});
|
||||
} 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;
|
||||
}
|
||||
log.error(`Flow preview failed: ${JSON.stringify(e.body)}`);
|
||||
}
|
||||
throw e;
|
||||
@@ -551,7 +599,7 @@ export async function bootstrap(
|
||||
await loadNonDottedPathsSetting();
|
||||
|
||||
const flowDirFullPath = buildFolderPath(flowPath, "flow");
|
||||
mkdirSync(flowDirFullPath, { recursive: false });
|
||||
mkdirSync(flowDirFullPath, { recursive: true });
|
||||
|
||||
const newFlowDefinition = defaultFlowDefinition();
|
||||
if (opts.summary !== undefined) {
|
||||
@@ -653,6 +701,7 @@ const command = new Command()
|
||||
"push a local flow spec. This overrides any remote versions."
|
||||
)
|
||||
.arguments("<file_path:string> <remote_path:string>")
|
||||
.option("--message <message:string>", "Deployment message")
|
||||
.action(push as any)
|
||||
.command("run", "run a flow by path.")
|
||||
.arguments("<path:string>")
|
||||
|
||||
@@ -43,7 +43,7 @@ export interface InitOptions {
|
||||
*/
|
||||
async function initAction(opts: InitOptions) {
|
||||
if (await stat("wmill.yaml").catch(() => null)) {
|
||||
log.error(colors.red("wmill.yaml already exists"));
|
||||
log.info("wmill.yaml already exists, skipping config generation");
|
||||
} else {
|
||||
// Detect current git branch for template
|
||||
const { isGitRepository, getCurrentGitBranch } = await import(
|
||||
|
||||
@@ -92,7 +92,7 @@ async function list(
|
||||
.border(true)
|
||||
.body(
|
||||
jobs.map((j: any) => [
|
||||
j.id.substring(0, 8),
|
||||
j.id,
|
||||
getJobStatus(j),
|
||||
j.script_path ?? j.raw_code?.substring(0, 30) ?? "-",
|
||||
j.created_by ?? j.email ?? "-",
|
||||
@@ -170,12 +170,35 @@ async function logs(
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
// Check if this is a flow job (flows don't have top-level logs)
|
||||
try {
|
||||
const job = await wmill.getJob({
|
||||
workspace: workspace.workspaceId,
|
||||
id,
|
||||
});
|
||||
const jobKind = (job as any).job_kind; // job_kind not in generated types yet
|
||||
if (jobKind === "flow" || jobKind === "flowpreview") {
|
||||
log.info(colors.yellow(
|
||||
"Flow jobs don't have direct logs. Each step runs as a separate job.\n" +
|
||||
"Use 'wmill job list --all' to see sub-jobs, then 'wmill job logs <sub-job-id>' for individual step logs."
|
||||
));
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// If we can't get the job info, proceed with trying to get logs anyway
|
||||
}
|
||||
|
||||
const jobLogs = await wmill.getJobLogs({
|
||||
workspace: workspace.workspaceId,
|
||||
id,
|
||||
});
|
||||
|
||||
console.log(jobLogs);
|
||||
if (jobLogs == null || jobLogs === "") {
|
||||
log.info("No logs available for this job.");
|
||||
} else {
|
||||
console.error("to remove ansi colors, use: | sed 's/\\x1B\\[[0-9;]\\{1,\\}[A-Za-z]//g'");
|
||||
console.log(jobLogs);
|
||||
}
|
||||
}
|
||||
|
||||
async function cancel(
|
||||
|
||||
@@ -97,6 +97,10 @@ async function list(opts: GlobalOptions & { schema?: boolean; json?: boolean })
|
||||
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(res));
|
||||
} else if (res.length === 0) {
|
||||
log.info("No custom resource types found in this workspace.");
|
||||
log.info("Built-in types like 'postgresql', 'slack', 'mysql', etc. are available from the Windmill Hub.");
|
||||
return;
|
||||
} else if (opts.schema) {
|
||||
new Table()
|
||||
.header(["Workspace", "Name", "Schema"])
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { stat, writeFile, readdir, readFile } from "node:fs/promises";
|
||||
import { mkdir, stat, writeFile, readdir, readFile } from "node:fs/promises";
|
||||
import { stringify as yamlStringify } from "yaml";
|
||||
import nodePath from "node:path";
|
||||
|
||||
@@ -203,6 +203,7 @@ async function newResource(opts: GlobalOptions, path: string) {
|
||||
resource_type: "",
|
||||
description: "",
|
||||
};
|
||||
await mkdir(nodePath.dirname(filePath), { recursive: true });
|
||||
await writeFile(filePath, yamlStringify(template as Record<string, any>), {
|
||||
flag: "wx",
|
||||
encoding: "utf-8",
|
||||
@@ -211,6 +212,7 @@ async function newResource(opts: GlobalOptions, path: string) {
|
||||
}
|
||||
|
||||
async function get(opts: GlobalOptions & { json?: boolean }, path: string) {
|
||||
if (opts.json) log.setSilent(true);
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
const r = await wmill.getResource({
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { stat, writeFile } from "node:fs/promises";
|
||||
import { mkdir, stat, writeFile } from "node:fs/promises";
|
||||
import { dirname } from "node:path";
|
||||
import { stringify as yamlStringify } from "yaml";
|
||||
|
||||
import { Command } from "@cliffy/command";
|
||||
@@ -70,6 +71,7 @@ async function newSchedule(opts: GlobalOptions, path: string) {
|
||||
is_flow: false,
|
||||
enabled: false,
|
||||
};
|
||||
await mkdir(dirname(filePath), { recursive: true });
|
||||
await writeFile(filePath, yamlStringify(template as Record<string, any>), {
|
||||
flag: "wx",
|
||||
encoding: "utf-8",
|
||||
@@ -78,6 +80,7 @@ async function newSchedule(opts: GlobalOptions, path: string) {
|
||||
}
|
||||
|
||||
async function get(opts: GlobalOptions & { json?: boolean }, path: string) {
|
||||
if (opts.json) log.setSilent(true);
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
const s = await wmill.getSchedule({
|
||||
|
||||
@@ -23,10 +23,13 @@ import {
|
||||
|
||||
import { Workspace } from "../workspace/workspace.ts";
|
||||
import {
|
||||
checkifMetadataUptodate,
|
||||
generateScriptMetadataInternal,
|
||||
getRawWorkspaceDependencies,
|
||||
parseMetadataFile,
|
||||
readLockfile,
|
||||
} from "../../utils/metadata.ts";
|
||||
import { generateHash, validateRequiredArgs } from "../../utils/utils.ts";
|
||||
import {
|
||||
WorkspaceDependenciesLanguage,
|
||||
ScriptLanguage,
|
||||
@@ -101,7 +104,7 @@ export function isFlowInlineScriptPath(filePath: string): boolean {
|
||||
return isFlowInlineScriptPathInternal(filePath);
|
||||
}
|
||||
|
||||
type PushOptions = GlobalOptions;
|
||||
type PushOptions = GlobalOptions & { message?: string };
|
||||
async function push(opts: PushOptions, filePath: string) {
|
||||
opts = await mergeConfigWithConfigFile(opts);
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
@@ -122,13 +125,30 @@ async function push(opts: PushOptions, filePath: string) {
|
||||
}
|
||||
|
||||
await requireLogin(opts);
|
||||
|
||||
// Warn if metadata appears stale (content changed since last generate-metadata)
|
||||
try {
|
||||
const content = await readFile(filePath, "utf-8");
|
||||
const remotePath = removeExtensionToPath(filePath).replaceAll(SEP, "/");
|
||||
const contentHash = await generateHash(content + remotePath);
|
||||
const conf = await readLockfile();
|
||||
if (!(await checkifMetadataUptodate(remotePath, contentHash, conf))) {
|
||||
log.warn(colors.yellow(
|
||||
`Metadata for ${filePath} appears stale (content changed since last 'wmill generate-metadata').\n` +
|
||||
`The schema and lock may not match the current code. Consider running 'wmill generate-metadata' first.`
|
||||
));
|
||||
}
|
||||
} catch {
|
||||
// Don't block push if staleness check fails
|
||||
}
|
||||
|
||||
const codebases = await listSyncCodebases(opts as SyncOptions);
|
||||
|
||||
await handleFile(
|
||||
filePath,
|
||||
workspace,
|
||||
[],
|
||||
undefined,
|
||||
opts.message,
|
||||
opts,
|
||||
await getRawWorkspaceDependencies(true),
|
||||
codebases
|
||||
@@ -928,6 +948,21 @@ async function run(
|
||||
await requireLogin(opts);
|
||||
|
||||
const input = opts.data ? await resolve(opts.data) : {};
|
||||
|
||||
// Validate required args against schema when no data provided
|
||||
if (!opts.data) {
|
||||
try {
|
||||
const script = await wmill.getScriptByPath({
|
||||
workspace: workspace.workspaceId,
|
||||
path,
|
||||
});
|
||||
validateRequiredArgs(script.schema as Record<string, unknown>);
|
||||
} catch (e: any) {
|
||||
if (e.message?.startsWith("Missing required")) throw e;
|
||||
log.warn(`Could not fetch schema to validate args: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
let id: string;
|
||||
try {
|
||||
id = await wmill.runScriptByPath({
|
||||
@@ -964,16 +999,20 @@ async function run(
|
||||
await track_job(workspace.workspaceId, id);
|
||||
}
|
||||
|
||||
while (true) {
|
||||
const MAX_RETRIES = 600; // ~60 seconds at 100ms intervals
|
||||
let retries = 0;
|
||||
while (retries < MAX_RETRIES) {
|
||||
try {
|
||||
const result =
|
||||
(
|
||||
await wmill.getCompletedJob({
|
||||
workspace: workspace.workspaceId,
|
||||
id,
|
||||
})
|
||||
).result ?? {};
|
||||
const completedJob = await wmill.getCompletedJob({
|
||||
workspace: workspace.workspaceId,
|
||||
id,
|
||||
});
|
||||
|
||||
if (completedJob.success === false) {
|
||||
process.exitCode = 1;
|
||||
}
|
||||
|
||||
const result = completedJob.result ?? {};
|
||||
if (opts.silent) {
|
||||
console.log(JSON.stringify(result));
|
||||
} else {
|
||||
@@ -982,9 +1021,13 @@ async function run(
|
||||
|
||||
break;
|
||||
} catch {
|
||||
retries++;
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
}
|
||||
}
|
||||
if (retries >= MAX_RETRIES) {
|
||||
throw new Error(`Timed out waiting for job ${id} to complete`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function track_job(workspace: string, id: string) {
|
||||
@@ -1081,6 +1124,7 @@ async function show(opts: GlobalOptions, path: string) {
|
||||
}
|
||||
|
||||
async function get(opts: GlobalOptions & { json?: boolean }, path: string) {
|
||||
if (opts.json) log.setSilent(true);
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
const s = await wmill.getScriptByPath({
|
||||
@@ -1523,13 +1567,14 @@ async function history(
|
||||
return;
|
||||
}
|
||||
new Table()
|
||||
.header(["#", "Hash", "Deployment Message"])
|
||||
.header(["#", "Hash", "Created At", "Deployment Message"])
|
||||
.padding(2)
|
||||
.border(true)
|
||||
.body(
|
||||
versions.map((v, i) => [
|
||||
String(versions.length - i),
|
||||
v.script_hash,
|
||||
v.created_at ? new Date(v.created_at).toLocaleString() : "-",
|
||||
v.deployment_msg ?? "-",
|
||||
])
|
||||
)
|
||||
@@ -1551,6 +1596,7 @@ const command = new Command()
|
||||
"push a local script spec. This overrides any remote versions. Use the script file (.ts, .js, .py, .sh)"
|
||||
)
|
||||
.arguments("<path:file>")
|
||||
.option("--message <message:string>", "Deployment message")
|
||||
.action(push as any)
|
||||
.command("get", "get a script's details")
|
||||
.arguments("<path:file>")
|
||||
|
||||
@@ -3,9 +3,72 @@ import { colors } from "@cliffy/ansi/colors";
|
||||
import { Command } from "@cliffy/command";
|
||||
import * as log from "../../core/log.ts";
|
||||
import JSZip from "jszip";
|
||||
import { extract } from "tar-stream";
|
||||
import { Readable } from "node:stream";
|
||||
import { Workspace } from "../workspace/workspace.ts";
|
||||
import { getHeaders } from "../../utils/utils.ts";
|
||||
|
||||
/**
|
||||
* Adapter that wraps tar entries in a JSZip-compatible interface
|
||||
* so ZipFSElement in sync.ts can consume it without changes.
|
||||
*/
|
||||
class TarAsZip {
|
||||
files: Record<string, { dir: boolean; name: string; async(type: "text"): Promise<string> }> = {};
|
||||
|
||||
constructor(entries: Map<string, { content: string; isDir: boolean }>) {
|
||||
for (const [name, entry] of entries) {
|
||||
const content = entry.content;
|
||||
this.files[name] = {
|
||||
dir: entry.isDir,
|
||||
name,
|
||||
async(_type: "text") {
|
||||
return content;
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/** Return a filtered view containing only entries under the given prefix, with relative paths. */
|
||||
folder(prefix: string): TarAsZip | null {
|
||||
const normalized = prefix.endsWith("/") ? prefix : prefix + "/";
|
||||
const sub = new TarAsZip(new Map());
|
||||
for (const [name, file] of Object.entries(this.files)) {
|
||||
if (name.startsWith(normalized)) {
|
||||
const relative = name.slice(normalized.length);
|
||||
if (relative) {
|
||||
sub.files[relative] = { ...file, name: relative };
|
||||
}
|
||||
}
|
||||
}
|
||||
return Object.keys(sub.files).length > 0 ? sub : null;
|
||||
}
|
||||
}
|
||||
|
||||
async function parseTarResponse(response: Response): Promise<TarAsZip> {
|
||||
const buffer = Buffer.from(await response.arrayBuffer());
|
||||
const entries = new Map<string, { content: string; isDir: boolean }>();
|
||||
const ex = extract();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
ex.on("entry", (header, stream, next) => {
|
||||
const chunks: Buffer[] = [];
|
||||
stream.on("data", (chunk: Buffer) => chunks.push(chunk));
|
||||
stream.on("end", () => {
|
||||
entries.set(header.name, {
|
||||
content: Buffer.concat(chunks).toString("utf-8"),
|
||||
isDir: header.type === "directory",
|
||||
});
|
||||
next();
|
||||
});
|
||||
stream.on("error", reject);
|
||||
stream.resume();
|
||||
});
|
||||
ex.on("finish", () => resolve(new TarAsZip(entries)));
|
||||
ex.on("error", reject);
|
||||
Readable.from(buffer).pipe(ex);
|
||||
});
|
||||
}
|
||||
|
||||
export async function downloadZip(
|
||||
workspace: Workspace,
|
||||
plainSecrets: boolean | undefined,
|
||||
@@ -21,7 +84,7 @@ export async function downloadZip(
|
||||
includeKey?: boolean,
|
||||
skipWorkspaceDependencies?: boolean,
|
||||
defaultTs?: "bun" | "deno"
|
||||
): Promise<JSZip | undefined> {
|
||||
): Promise<JSZip | TarAsZip | undefined> {
|
||||
const requestHeaders = new Headers();
|
||||
requestHeaders.set("Authorization", "Bearer " + workspace.token);
|
||||
requestHeaders.set("Content-Type", "application/octet-stream");
|
||||
@@ -34,38 +97,51 @@ export async function downloadZip(
|
||||
}
|
||||
|
||||
const includeWorkspaceDependenciesValue = !(skipWorkspaceDependencies ?? false);
|
||||
const url = workspace.remote +
|
||||
"api/w/" +
|
||||
workspace.workspaceId +
|
||||
`/workspaces/tarball?archive_type=zip&plain_secret=${plainSecrets ?? false
|
||||
const baseParams = `&plain_secret=${plainSecrets ?? false
|
||||
}&skip_variables=${skipVariables ?? false}&skip_resources=${skipResources ?? false
|
||||
}&skip_secrets=${skipSecrets ?? false}&include_schedules=${includeSchedules ?? false
|
||||
}&include_triggers=${includeTriggers ?? false}&include_users=${includeUsers ?? false
|
||||
}&include_groups=${includeGroups ?? false}&include_settings=${includeSettings ?? false
|
||||
}&include_key=${includeKey ?? false}&include_workspace_dependencies=${includeWorkspaceDependenciesValue}&default_ts=${defaultTs ?? "bun"}&skip_resource_types=${skipResourceTypes ?? false}&settings_version=v2`;
|
||||
|
||||
const zipResponse = await fetch(url, {
|
||||
headers: requestHeaders,
|
||||
method: "GET",
|
||||
}
|
||||
);
|
||||
const baseUrl = workspace.remote + "api/w/" + workspace.workspaceId + "/workspaces/tarball?";
|
||||
|
||||
if (!zipResponse.ok) {
|
||||
const body = await zipResponse.text();
|
||||
if (zipResponse.status === 404 || body.includes("no rows returned")) {
|
||||
log.info(colors.red(`Workspace '${workspace.workspaceId}' not found on ${workspace.remote}. Please check your --workspace and try again.`));
|
||||
} else {
|
||||
log.info(colors.red(`Failed to request tarball from API: ${zipResponse.status} ${zipResponse.statusText}`));
|
||||
if (body) {
|
||||
log.info(colors.red(body));
|
||||
}
|
||||
}
|
||||
return process.exit(1);
|
||||
} else {
|
||||
log.debug(`Downloaded zip/tarball successfully`);
|
||||
// Try zip first (standard format), fall back to tar if zip is not supported
|
||||
const zipUrl = baseUrl + "archive_type=zip" + baseParams;
|
||||
const zipResponse = await fetch(zipUrl, { headers: requestHeaders, method: "GET" });
|
||||
|
||||
if (zipResponse.ok) {
|
||||
log.debug("Downloaded zip archive successfully");
|
||||
const blob = await zipResponse.blob();
|
||||
return await JSZip.loadAsync((await blob.arrayBuffer()) as any);
|
||||
}
|
||||
const blob = await zipResponse.blob();
|
||||
return await JSZip.loadAsync((await blob.arrayBuffer()) as any);
|
||||
|
||||
const body = await zipResponse.text();
|
||||
|
||||
// If zip format is not supported (backend compiled without zip feature), try tar
|
||||
if (zipResponse.status === 400 && body.includes("Invalid Archive Type")) {
|
||||
log.debug("Zip archive not supported by backend, falling back to tar");
|
||||
const tarUrl = baseUrl + "archive_type=tar" + baseParams;
|
||||
const tarResponse = await fetch(tarUrl, { headers: requestHeaders, method: "GET" });
|
||||
|
||||
if (tarResponse.ok) {
|
||||
log.debug("Downloaded tar archive successfully");
|
||||
return await parseTarResponse(tarResponse);
|
||||
}
|
||||
|
||||
const tarBody = await tarResponse.text();
|
||||
log.info(colors.red(`Failed to request tarball from API: ${tarResponse.status} ${tarResponse.statusText}`));
|
||||
if (tarBody) log.info(colors.red(tarBody));
|
||||
return process.exit(1);
|
||||
}
|
||||
|
||||
if (zipResponse.status === 404 || body.includes("no rows returned")) {
|
||||
log.info(colors.red(`Workspace '${workspace.workspaceId}' not found on ${workspace.remote}. Please check your --workspace and try again.`));
|
||||
} else {
|
||||
log.info(colors.red(`Failed to request tarball from API: ${zipResponse.status} ${zipResponse.statusText}`));
|
||||
if (body) log.info(colors.red(body));
|
||||
}
|
||||
return process.exit(1);
|
||||
}
|
||||
|
||||
function stub(_opts: GlobalOptions & { override: boolean }, _dir: string) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { stat, writeFile } from "node:fs/promises";
|
||||
import { mkdir, stat, writeFile } from "node:fs/promises";
|
||||
import { dirname } from "node:path";
|
||||
import { stringify as yamlStringify } from "yaml";
|
||||
|
||||
import * as wmill from "../../../gen/services.gen.ts";
|
||||
@@ -400,6 +401,7 @@ async function newTrigger(opts: GlobalOptions & { kind: string }, path: string)
|
||||
if (e.message?.startsWith("File already exists")) throw e;
|
||||
}
|
||||
const template = triggerTemplates[kind];
|
||||
await mkdir(dirname(filePath), { recursive: true });
|
||||
await writeFile(filePath, yamlStringify(template), {
|
||||
flag: "wx",
|
||||
encoding: "utf-8",
|
||||
@@ -408,6 +410,7 @@ async function newTrigger(opts: GlobalOptions & { kind: string }, path: string)
|
||||
}
|
||||
|
||||
async function get(opts: GlobalOptions & { json?: boolean; kind?: string }, path: string) {
|
||||
if (opts.json) log.setSilent(true);
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { stat, writeFile } from "node:fs/promises";
|
||||
import { mkdir, stat, writeFile } from "node:fs/promises";
|
||||
import { dirname } from "node:path";
|
||||
import { stringify as yamlStringify } from "yaml";
|
||||
|
||||
import { requireLogin } from "../../core/auth.ts";
|
||||
@@ -63,6 +64,7 @@ async function newVariable(opts: GlobalOptions, path: string) {
|
||||
is_secret: false,
|
||||
description: "",
|
||||
};
|
||||
await mkdir(dirname(filePath), { recursive: true });
|
||||
await writeFile(filePath, yamlStringify(template as Record<string, any>), {
|
||||
flag: "wx",
|
||||
encoding: "utf-8",
|
||||
@@ -71,6 +73,7 @@ async function newVariable(opts: GlobalOptions, path: string) {
|
||||
}
|
||||
|
||||
async function get(opts: GlobalOptions & { json?: boolean }, path: string) {
|
||||
if (opts.json) log.setSilent(true);
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
const v = await wmill.getVariable({
|
||||
@@ -215,10 +218,10 @@ async function add(
|
||||
undefined,
|
||||
{
|
||||
value,
|
||||
is_secret: !opts.public && !opts.plainSecrets,
|
||||
is_secret: !opts.public,
|
||||
description: "",
|
||||
},
|
||||
opts.plainSecrets ?? false
|
||||
true // value from CLI is always plaintext — tell API not to treat it as pre-encrypted
|
||||
);
|
||||
log.info(colors.bold.underline.green(`Variable ${remotePath} pushed`));
|
||||
}
|
||||
|
||||
@@ -422,7 +422,7 @@ async function whoami(_opts: GlobalOptions) {
|
||||
const { resolveWorkspace } = await import("../../core/context.ts");
|
||||
try {
|
||||
const ws = await resolveWorkspace(_opts);
|
||||
log.info("Active: " + colors.green.bold(`${activeName || "none"}`) + ` (fork workspace: ${ws.workspaceId})`);
|
||||
log.info("Active: " + colors.green.bold(ws.workspaceId) + ` (fork of ${activeName || "unknown"})`);
|
||||
} catch {
|
||||
log.info("Active: " + colors.green.bold(activeName || "none") + " (fork branch)");
|
||||
}
|
||||
|
||||
@@ -366,7 +366,7 @@ export async function tryResolveBranchWorkspace(
|
||||
selectedProfile.name = `${selectedProfile.name}/${workspaceIdIfForked}`;
|
||||
selectedProfile.workspaceId = workspaceIdIfForked;
|
||||
log.info(
|
||||
`Inferred workspace id \`${workspaceId}\` from branch name because this is a workspace fork branch (\`${rawBranch}\`). `
|
||||
`Using fork workspace \`${workspaceIdIfForked}\` (parent: \`${workspaceId}\`) from branch \`${rawBranch}\``
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -5058,6 +5058,7 @@ flow related commands
|
||||
- \`flow get <path:string>\` - get a flow's details
|
||||
- \`--json\` - Output as JSON (for piping to jq)
|
||||
- \`flow push <file_path:string> <remote_path:string>\` - push a local flow spec. This overrides any remote versions.
|
||||
- \`--message <message:string>\` - Deployment message
|
||||
- \`flow run <path:string>\` - run a flow by path.
|
||||
- \`-d --data <data:string>\` - Inputs specified as a JSON string or a file using @<filename> or stdin using @-.
|
||||
- \`-s --silent\` - Do not ouput anything other then the final output. Useful for scripting.
|
||||
@@ -5336,6 +5337,7 @@ script related commands
|
||||
- \`--show-archived\` - Enable archived scripts in output
|
||||
- \`--json\` - Output as JSON (for piping to jq)
|
||||
- \`script push <path:file>\` - push a local script spec. This overrides any remote versions. Use the script file (.ts, .js, .py, .sh
|
||||
- \`--message <message:string>\` - Deployment message
|
||||
- \`script get <path:file>\` - get a script's details
|
||||
- \`--json\` - Output as JSON (for piping to jq)
|
||||
- \`script show <path:file>\` - show a script's content (alias for get
|
||||
|
||||
@@ -78,7 +78,7 @@ export {
|
||||
token,
|
||||
};
|
||||
|
||||
export const VERSION = "1.668.0";
|
||||
export const VERSION = "1.668.2";
|
||||
|
||||
// Re-exported from constants.ts to maintain backwards compatibility
|
||||
export { WM_FORK_PREFIX } from "./core/constants.ts";
|
||||
|
||||
@@ -48,7 +48,7 @@ let _nonDottedPathsLogged = false;
|
||||
*/
|
||||
export function setNonDottedPaths(value: boolean): void {
|
||||
if (value && !_nonDottedPathsLogged) {
|
||||
log.info("Using non-dotted paths (__flow, __app, __raw_app)");
|
||||
log.debug("Using non-dotted paths (__flow, __app, __raw_app)");
|
||||
_nonDottedPathsLogged = true;
|
||||
}
|
||||
_nonDottedPaths = value;
|
||||
|
||||
@@ -291,3 +291,20 @@ export function capitalize(str: string): string {
|
||||
export function formatTimestamp(ts: string): string {
|
||||
return new Date(ts).toISOString().replace("T", " ").substring(0, 19);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that required arguments are present when no -d data was provided.
|
||||
* Fetches the schema from the API and checks required fields.
|
||||
* @param schema - The JSON schema object from the script/flow definition
|
||||
* @throws Error if required arguments are missing
|
||||
*/
|
||||
export function validateRequiredArgs(
|
||||
schema: Record<string, unknown> | undefined | null,
|
||||
): void {
|
||||
const required = (schema as { required?: string[] })?.required ?? [];
|
||||
if (required.length > 0) {
|
||||
throw new Error(
|
||||
`Missing required arguments: ${required.join(", ")}.\nUse -d '{"${required[0]}": ...}' to provide input data.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@ import { withTestBackend } from "./test_backend.ts";
|
||||
import {
|
||||
setupWorkspaceProfile,
|
||||
createRemoteScript,
|
||||
createRemoteFlow,
|
||||
runRemoteScript,
|
||||
runRemoteFlow,
|
||||
waitForJob,
|
||||
} from "./new_commands_helpers.ts";
|
||||
|
||||
@@ -53,7 +55,7 @@ describe("job command", () => {
|
||||
expect(result.code).toEqual(0);
|
||||
expect(result.stdout).toContain("ID");
|
||||
expect(result.stdout).toContain("Status");
|
||||
expect(result.stdout).toContain(jobId.substring(0, 8));
|
||||
expect(result.stdout).toContain(jobId);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -167,6 +169,26 @@ describe("job command", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("job logs for flow job shows helpful message", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
const uniqueId = Date.now();
|
||||
const flowPath = `f/test/flow_logs_${uniqueId}`;
|
||||
await createRemoteFlow(backend, flowPath);
|
||||
const jobId = await runRemoteFlow(backend, flowPath);
|
||||
await waitForJob(backend, jobId);
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["job", "logs", jobId],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
expect(result.stdout).toContain("Flow jobs don't have direct logs");
|
||||
});
|
||||
});
|
||||
|
||||
test("default action (wmill job) lists jobs", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
@@ -148,6 +148,28 @@ export async function createRemoteFlow(
|
||||
await resp.text();
|
||||
}
|
||||
|
||||
export async function runRemoteFlow(
|
||||
backend: TestBackend,
|
||||
flowPath: string,
|
||||
retries: number = 10
|
||||
): Promise<string> {
|
||||
for (let i = 0; i < retries; i++) {
|
||||
const resp = await backend.apiRequest!(
|
||||
`/api/w/${backend.workspace}/jobs/run/f/${flowPath}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({}),
|
||||
}
|
||||
);
|
||||
if (resp.status < 300) {
|
||||
return (await resp.text()).replace(/"/g, "");
|
||||
}
|
||||
await new Promise((r) => setTimeout(r, 500));
|
||||
}
|
||||
throw new Error(`Failed to run flow ${flowPath} after ${retries} retries`);
|
||||
}
|
||||
|
||||
export async function createRemoteSchedule(
|
||||
backend: TestBackend,
|
||||
schedulePath: string,
|
||||
|
||||
@@ -139,8 +139,10 @@ describe("resource-type commands", () => {
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
// Table headers should be present
|
||||
expect(result.stdout).toContain("Name");
|
||||
// When empty, shows helpful message; when populated, shows table with Name header
|
||||
const hasTable = result.stdout.includes("Name");
|
||||
const hasEmptyMessage = result.stdout.includes("No custom resource types");
|
||||
expect(hasTable || hasEmptyMessage).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -303,6 +305,79 @@ describe("script run command", () => {
|
||||
expect(result.stdout).toContain(`run_result_${uniqueId}`);
|
||||
});
|
||||
});
|
||||
|
||||
test("exits with code 1 when script fails", { timeout: 60000 }, async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
const uniqueId = Date.now();
|
||||
const scriptPath = `f/test/fail_script_${uniqueId}`;
|
||||
const scriptContent = `export async function main() { throw new Error("intentional failure"); }`;
|
||||
|
||||
await createRemoteScript(backend, scriptPath, scriptContent);
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["script", "run", scriptPath, "--silent"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
test("errors when required args are missing", { timeout: 60000 }, async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
const uniqueId = Date.now();
|
||||
const scriptPath = `f/test/args_script_${uniqueId}`;
|
||||
const scriptContent = `export async function main(name: string) { return name; }`;
|
||||
|
||||
// Create script with an explicit schema that has required args
|
||||
// (createRemoteScript defaults to empty schema, so we call the API directly)
|
||||
const parts = scriptPath.split("/");
|
||||
if (parts[0] === "f" && parts.length > 2) {
|
||||
await backend.apiRequest!(
|
||||
`/api/w/${backend.workspace}/folders/create`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name: parts[1] }),
|
||||
}
|
||||
).catch(() => {});
|
||||
}
|
||||
const resp = await backend.apiRequest!(
|
||||
`/api/w/${backend.workspace}/scripts/create`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
path: scriptPath,
|
||||
content: scriptContent,
|
||||
language: "bun",
|
||||
summary: "Test script with required args",
|
||||
schema: {
|
||||
$schema: "https://json-schema.org/draft/2020-12/schema",
|
||||
type: "object",
|
||||
properties: { name: { type: "string" } },
|
||||
required: ["name"],
|
||||
},
|
||||
}),
|
||||
}
|
||||
);
|
||||
expect(resp.status).toBeLessThan(300);
|
||||
await resp.text();
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["script", "run", scriptPath],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).not.toEqual(0);
|
||||
const output = result.stdout + result.stderr;
|
||||
expect(output).toContain("Missing required arguments");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
@@ -569,3 +644,82 @@ describe("user commands", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// Script Push --message
|
||||
// =============================================================================
|
||||
|
||||
describe("script push --message", () => {
|
||||
test("push with --message flag succeeds", { timeout: 60000 }, async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
const uniqueId = Date.now();
|
||||
const scriptPath = `f/test/msg_script_${uniqueId}`;
|
||||
const scriptFile = join(tempDir, scriptPath + ".ts");
|
||||
const metaFile = join(tempDir, scriptPath + ".script.yaml");
|
||||
const deployMsg = `deploy_msg_${uniqueId}`;
|
||||
|
||||
await mkdir(join(tempDir, "f", "test"), { recursive: true });
|
||||
await writeFile(scriptFile, 'export async function main() { return "v1"; }');
|
||||
await writeFile(metaFile, [
|
||||
"summary: test",
|
||||
"description: ''",
|
||||
"lock: ''",
|
||||
"kind: script",
|
||||
"schema:",
|
||||
" $schema: https://json-schema.org/draft/2020-12/schema",
|
||||
" type: object",
|
||||
" properties: {}",
|
||||
" required: []",
|
||||
].join("\n"));
|
||||
|
||||
// Verify push with --message flag succeeds (doesn't error on unknown flag)
|
||||
const pushResult = await backend.runCLICommand(
|
||||
["script", "push", scriptPath + ".ts", "--message", deployMsg],
|
||||
tempDir
|
||||
);
|
||||
expect(pushResult.code).toEqual(0);
|
||||
expect(pushResult.stdout).toContain("pushed");
|
||||
|
||||
// Verify history returns at least one version
|
||||
const histResult = await backend.runCLICommand(
|
||||
["script", "history", scriptPath, "--json"],
|
||||
tempDir
|
||||
);
|
||||
expect(histResult.code).toEqual(0);
|
||||
const versions = JSON.parse(histResult.stdout);
|
||||
expect(versions.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// Variable Add + Get (encryption roundtrip)
|
||||
// =============================================================================
|
||||
|
||||
describe("variable add encryption", () => {
|
||||
test("variable add creates a retrievable secret variable", { timeout: 30000 }, async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await setupWorkspaceProfile(backend);
|
||||
|
||||
const uniqueId = Date.now();
|
||||
const varPath = `f/test/secret_${uniqueId}`;
|
||||
const secretValue = `secret_value_${uniqueId}`;
|
||||
|
||||
const addResult = await backend.runCLICommand(
|
||||
["variable", "add", secretValue, varPath],
|
||||
tempDir
|
||||
);
|
||||
expect(addResult.code).toEqual(0);
|
||||
|
||||
const getResult = await backend.runCLICommand(
|
||||
["variable", "get", varPath],
|
||||
tempDir
|
||||
);
|
||||
expect(getResult.code).toEqual(0);
|
||||
expect(getResult.stdout).toContain(secretValue);
|
||||
expect(getResult.stdout).toContain("true"); // is_secret
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { expect, test, describe } from "bun:test";
|
||||
import { deepEqual, isFileResource, isFilesetResource, toCamel, capitalize } from "../src/utils/utils.ts";
|
||||
import { deepEqual, isFileResource, isFilesetResource, toCamel, capitalize, validateRequiredArgs } from "../src/utils/utils.ts";
|
||||
import {
|
||||
getTypeStrFromPath,
|
||||
removeType,
|
||||
@@ -596,3 +596,103 @@ describe("removeExtensionToPath", () => {
|
||||
expect(removeExtensionToPath("f/test/api.fetch.ts")).toBe("f/test/api");
|
||||
});
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// validateRequiredArgs
|
||||
// =============================================================================
|
||||
|
||||
describe("validateRequiredArgs", () => {
|
||||
test("throws when required args are missing", () => {
|
||||
expect(() =>
|
||||
validateRequiredArgs({ required: ["name", "count"] })
|
||||
).toThrow("Missing required arguments: name, count");
|
||||
});
|
||||
|
||||
test("does not throw when no required args", () => {
|
||||
expect(() => validateRequiredArgs({ required: [] })).not.toThrow();
|
||||
});
|
||||
|
||||
test("does not throw for undefined schema", () => {
|
||||
expect(() => validateRequiredArgs(undefined)).not.toThrow();
|
||||
expect(() => validateRequiredArgs(null)).not.toThrow();
|
||||
});
|
||||
|
||||
test("does not throw for schema without required field", () => {
|
||||
expect(() => validateRequiredArgs({ type: "object", properties: {} })).not.toThrow();
|
||||
});
|
||||
|
||||
test("error message includes usage hint", () => {
|
||||
try {
|
||||
validateRequiredArgs({ required: ["name"] });
|
||||
} catch (e: any) {
|
||||
expect(e.message).toContain('-d \'{"name":');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// TarAsZip adapter
|
||||
// =============================================================================
|
||||
|
||||
describe("TarAsZip adapter", () => {
|
||||
// Import the adapter — it's not exported but we can test via tar creation + parsing
|
||||
const { extract } = require("tar-stream");
|
||||
const { Readable } = require("node:stream");
|
||||
|
||||
// Helper: build a TarAsZip from entries via the actual class
|
||||
async function buildTarAsZip(entries: Map<string, { content: string; isDir: boolean }>) {
|
||||
// Dynamically import to get the class
|
||||
const pullModule = await import("../src/commands/sync/pull.ts");
|
||||
// TarAsZip is not exported, so we test indirectly via parseTarResponse
|
||||
// Instead, test the tar creation → extraction round-trip
|
||||
const { createTarBlob } = await import("../src/utils/tar.ts");
|
||||
|
||||
const tarEntries = Array.from(entries).map(([name, { content }]) => ({
|
||||
name,
|
||||
content,
|
||||
}));
|
||||
const blob = await createTarBlob(tarEntries);
|
||||
|
||||
// Parse via the same extract pattern used by TarAsZip
|
||||
const buffer = Buffer.from(await blob.arrayBuffer());
|
||||
const result = new Map<string, { content: string; isDir: boolean }>();
|
||||
const ex = extract();
|
||||
|
||||
return new Promise<Map<string, string>>((resolve, reject) => {
|
||||
ex.on("entry", (header: any, stream: any, next: () => void) => {
|
||||
const chunks: Buffer[] = [];
|
||||
stream.on("data", (chunk: Buffer) => chunks.push(chunk));
|
||||
stream.on("end", () => {
|
||||
result.set(header.name, {
|
||||
content: Buffer.concat(chunks).toString("utf-8"),
|
||||
isDir: header.type === "directory",
|
||||
});
|
||||
next();
|
||||
});
|
||||
stream.on("error", reject);
|
||||
stream.resume();
|
||||
});
|
||||
ex.on("finish", () => {
|
||||
// Convert to simple map for assertions
|
||||
const simpleMap = new Map<string, string>();
|
||||
for (const [name, { content }] of result) {
|
||||
simpleMap.set(name, content);
|
||||
}
|
||||
resolve(simpleMap);
|
||||
});
|
||||
ex.on("error", reject);
|
||||
Readable.from(buffer).pipe(ex);
|
||||
});
|
||||
}
|
||||
|
||||
test("tar round-trip preserves content", async () => {
|
||||
const entries = new Map([
|
||||
["f/scripts/hello.ts", { content: 'export async function main() { return "hello"; }', isDir: false }],
|
||||
["f/scripts/hello.script.yaml", { content: "summary: Hello\nkind: script\n", isDir: false }],
|
||||
]);
|
||||
|
||||
const result = await buildTarAsZip(entries);
|
||||
expect(result.get("f/scripts/hello.ts")).toBe('export async function main() { return "hello"; }');
|
||||
expect(result.get("f/scripts/hello.script.yaml")).toContain("summary: Hello");
|
||||
});
|
||||
});
|
||||
|
||||
4
frontend/package-lock.json
generated
4
frontend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "windmill-components",
|
||||
"version": "1.668.0",
|
||||
"version": "1.668.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "windmill-components",
|
||||
"version": "1.668.0",
|
||||
"version": "1.668.2",
|
||||
"hasInstallScript": true,
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "windmill-components",
|
||||
"version": "1.668.0",
|
||||
"version": "1.668.2",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
import type { Policy, ScriptLang } from '$lib/gen'
|
||||
import { collectStaticFields, hash, type TriggerableV2 } from '../apps/editor/commonAppUtils'
|
||||
import { isRunnableByName, isRunnableByPath, type InlineScript, type RunnableWithFields } from '../apps/inputType'
|
||||
import {
|
||||
isRunnableByName,
|
||||
isRunnableByPath,
|
||||
type InlineScript,
|
||||
type RunnableWithFields
|
||||
} from '../apps/inputType'
|
||||
|
||||
export async function updateRawAppPolicy(
|
||||
runnables: Record<string, Runnable>,
|
||||
currentPolicy: Policy | undefined
|
||||
): Promise<Policy> {
|
||||
const triggerables_v2 = Object.fromEntries(
|
||||
(await Promise.all(
|
||||
const entries = (
|
||||
await Promise.all(
|
||||
Object.entries(runnables).map(async ([id, runnable]) => {
|
||||
return await processRunnable(id, runnable, runnable?.fields ?? {})
|
||||
})
|
||||
)) as [string, TriggerableV2][]
|
||||
)
|
||||
)
|
||||
).filter((entry): entry is [string, TriggerableV2] => entry != null)
|
||||
const triggerables_v2 = Object.fromEntries(entries)
|
||||
return {
|
||||
...currentPolicy,
|
||||
triggerables_v2
|
||||
|
||||
@@ -4,7 +4,7 @@ verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
wmill = ">=1.668.0"
|
||||
wmill = ">=1.668.2"
|
||||
sendgrid = "*"
|
||||
mysql-connector-python = "*"
|
||||
pymongo = "*"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: '3.0.3'
|
||||
|
||||
info:
|
||||
version: 1.668.0
|
||||
version: 1.668.2
|
||||
title: OpenFlow Spec
|
||||
contact:
|
||||
name: Ruben Fiszel
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
RootModule = 'WindmillClient.psm1'
|
||||
|
||||
# Version number of this module.
|
||||
ModuleVersion = '1.668.0'
|
||||
ModuleVersion = '1.668.2'
|
||||
|
||||
# Supported PSEditions
|
||||
# CompatiblePSEditions = @()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "wmill"
|
||||
version = "1.668.0"
|
||||
version = "1.668.2"
|
||||
description = "A client library for accessing Windmill server wrapping the Windmill client API"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://windmill.dev"
|
||||
|
||||
@@ -100,6 +100,7 @@ flow related commands
|
||||
- `flow get <path:string>` - get a flow's details
|
||||
- `--json` - Output as JSON (for piping to jq)
|
||||
- `flow push <file_path:string> <remote_path:string>` - push a local flow spec. This overrides any remote versions.
|
||||
- `--message <message:string>` - Deployment message
|
||||
- `flow run <path:string>` - run a flow by path.
|
||||
- `-d --data <data:string>` - Inputs specified as a JSON string or a file using @<filename> or stdin using @-.
|
||||
- `-s --silent` - Do not ouput anything other then the final output. Useful for scripting.
|
||||
@@ -378,6 +379,7 @@ script related commands
|
||||
- `--show-archived` - Enable archived scripts in output
|
||||
- `--json` - Output as JSON (for piping to jq)
|
||||
- `script push <path:file>` - push a local script spec. This overrides any remote versions. Use the script file (.ts, .js, .py, .sh
|
||||
- `--message <message:string>` - Deployment message
|
||||
- `script get <path:file>` - get a script's details
|
||||
- `--json` - Output as JSON (for piping to jq)
|
||||
- `script show <path:file>` - show a script's content (alias for get
|
||||
|
||||
@@ -1627,6 +1627,7 @@ flow related commands
|
||||
- \`flow get <path:string>\` - get a flow's details
|
||||
- \`--json\` - Output as JSON (for piping to jq)
|
||||
- \`flow push <file_path:string> <remote_path:string>\` - push a local flow spec. This overrides any remote versions.
|
||||
- \`--message <message:string>\` - Deployment message
|
||||
- \`flow run <path:string>\` - run a flow by path.
|
||||
- \`-d --data <data:string>\` - Inputs specified as a JSON string or a file using @<filename> or stdin using @-.
|
||||
- \`-s --silent\` - Do not ouput anything other then the final output. Useful for scripting.
|
||||
@@ -1905,6 +1906,7 @@ script related commands
|
||||
- \`--show-archived\` - Enable archived scripts in output
|
||||
- \`--json\` - Output as JSON (for piping to jq)
|
||||
- \`script push <path:file>\` - push a local script spec. This overrides any remote versions. Use the script file (.ts, .js, .py, .sh
|
||||
- \`--message <message:string>\` - Deployment message
|
||||
- \`script get <path:file>\` - get a script's details
|
||||
- \`--json\` - Output as JSON (for piping to jq)
|
||||
- \`script show <path:file>\` - show a script's content (alias for get
|
||||
|
||||
@@ -105,6 +105,7 @@ flow related commands
|
||||
- `flow get <path:string>` - get a flow's details
|
||||
- `--json` - Output as JSON (for piping to jq)
|
||||
- `flow push <file_path:string> <remote_path:string>` - push a local flow spec. This overrides any remote versions.
|
||||
- `--message <message:string>` - Deployment message
|
||||
- `flow run <path:string>` - run a flow by path.
|
||||
- `-d --data <data:string>` - Inputs specified as a JSON string or a file using @<filename> or stdin using @-.
|
||||
- `-s --silent` - Do not ouput anything other then the final output. Useful for scripting.
|
||||
@@ -383,6 +384,7 @@ script related commands
|
||||
- `--show-archived` - Enable archived scripts in output
|
||||
- `--json` - Output as JSON (for piping to jq)
|
||||
- `script push <path:file>` - push a local script spec. This overrides any remote versions. Use the script file (.ts, .js, .py, .sh
|
||||
- `--message <message:string>` - Deployment message
|
||||
- `script get <path:file>` - get a script's details
|
||||
- `--json` - Output as JSON (for piping to jq)
|
||||
- `script show <path:file>` - show a script's content (alias for get
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@windmill/windmill",
|
||||
"version": "1.668.0",
|
||||
"version": "1.668.2",
|
||||
"exports": "./src/index.ts",
|
||||
"publish": {
|
||||
"exclude": ["!src", "./s3Types.ts", "./sqlUtils.ts", "./client.ts"]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "windmill-client",
|
||||
"description": "Windmill SDK client for browsers and Node.js",
|
||||
"version": "1.668.0",
|
||||
"version": "1.668.2",
|
||||
"author": "Ruben Fiszel",
|
||||
"license": "Apache 2.0",
|
||||
"sideEffects": false,
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.668.0
|
||||
1.668.2
|
||||
|
||||
Reference in New Issue
Block a user