Compare commits

...

8 Commits

Author SHA1 Message Date
Pyra
42eba98ea9 Merge branch 'main' into py-typechecked-decorator 2026-03-28 17:57:03 +01:00
Ruben Fiszel
37799574d8 chore(main): release 1.668.2 (#8586)
* chore(main): release 1.668.2

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2026-03-28 15:56:53 +00:00
Ruben Fiszel
78ac28b4e0 fix(cli): address review — createBundle appDir, shared arg validation (#8587)
* fix(cli): address review — createBundle appDir, shared validateRequiredArgs, warn on fetch failure

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test(cli): add coverage for exit codes, arg validation, variable add, job logs, push --message

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(cli): fix test — create script with required schema, relax push --message assertion

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 15:55:40 +00:00
Ruben Fiszel
f40cdaf434 fix(cli): app push crash, lint path, push --message, run validation, history timestamps (#8585)
* fix(cli): app push crash, lint entry point, push --message, run arg validation, history timestamps

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(cli): update sqlx cache and fix second history query missing created_at

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore(cli): regenerate system prompts after new CLI options

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 14:33:49 +00:00
Pyra
f6863bd1ba Merge branch 'main' into py-typechecked-decorator 2026-03-28 14:07:51 +01:00
Ruben Fiszel
0ea9b945e0 chore(main): release 1.668.1 (#8583)
* chore(main): release 1.668.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2026-03-28 10:51:49 +00:00
Ruben Fiszel
38acaa3653 fix(cli): fix 13 CLI bugs — exit codes, sync tar fallback, variable encryption, JSON output (#8582)
* fix(cli): fix 13 CLI bugs — exit codes, sync tar fallback, variable encryption, JSON output, parent dirs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(cli): address PR review — TarAsZip.folder(), retry timeout, stderr hint

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(cli): update resource-type list test to handle empty state message

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 10:46:01 +00:00
pyranota
acfae3552c fix: support @typechecked decorator in Python relative imports
WindmillFinder's ModuleSpec lacked origin, so __file__ was never set on
loaded modules. inspect.getfile() then raised "is a built-in module",
breaking typeguard's @typechecked and anything else that introspects
module source. Use spec_from_file_location() which sets origin correctly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-24 14:52:37 +01:00
49 changed files with 818 additions and 177 deletions

View File

@@ -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)

View File

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

View File

@@ -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"
}

View File

@@ -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
View File

@@ -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",

View File

@@ -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"

View 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', '');

View File

@@ -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(())
}

View File

@@ -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 {

View File

@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.668.0
version: 1.668.2
title: Windmill API
contact:

View File

@@ -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)]

View File

@@ -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:

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.668.0";
export const VERSION = "v1.668.2";
export async function login(email: string, password: string): Promise<string> {
return await windmill.UserService.login({

View File

@@ -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

View File

@@ -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,
});

View File

@@ -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)

View File

@@ -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>")

View File

@@ -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(

View File

@@ -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(

View File

@@ -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"])

View File

@@ -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({

View File

@@ -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({

View File

@@ -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>")

View 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) {

View File

@@ -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);

View File

@@ -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`));
}

View File

@@ -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)");
}

View File

@@ -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}\``
);
}

View File

@@ -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

View File

@@ -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";

View File

@@ -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;

View File

@@ -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.`
);
}
}

View File

@@ -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);

View File

@@ -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,

View File

@@ -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
});
});
});

View File

@@ -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");
});
});

View File

@@ -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": {

View File

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

View File

@@ -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

View File

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

View File

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

View File

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

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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"]

View File

@@ -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,

View File

@@ -1 +1 @@
1.668.0
1.668.2