Compare commits
3 Commits
v1.660.0
...
feat/php-v
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ace849e0b | ||
|
|
89d7d8d025 | ||
|
|
35e81eb23f |
74
.workmux.yaml
Normal file
74
.workmux.yaml
Normal file
@@ -0,0 +1,74 @@
|
||||
main_branch: main
|
||||
|
||||
merge_strategy: rebase
|
||||
# worktree_dir: .worktrees
|
||||
|
||||
worktree_naming: basename
|
||||
|
||||
worktree_prefix: ""
|
||||
|
||||
# Default: "wm-"
|
||||
window_prefix: "wm-"
|
||||
|
||||
auto_name:
|
||||
model: "gemini-2.5-flash-lite"
|
||||
system_prompt: |
|
||||
Generate a concise git branch name based on the task description.
|
||||
|
||||
Rules:
|
||||
- Use kebab-case (lowercase with hyphens)
|
||||
- Keep it short: 1-3 words, max 4 if necessary
|
||||
- Focus on the core task/feature, not implementation details
|
||||
- No prefixes like feat/, fix/, chore/
|
||||
|
||||
Examples of good branch names:
|
||||
- "Add dark mode toggle" → dark-mode
|
||||
- "Fix the search results not showing" → fix-search
|
||||
- "Refactor the authentication module" → auth-refactor
|
||||
- "Add CSV export to reports" → export-csv
|
||||
- "Shell completion is broken" → shell-completion
|
||||
|
||||
Output ONLY the branch name, nothing else.
|
||||
background: true
|
||||
|
||||
|
||||
# Commands to run in new worktree before tmux window opens.
|
||||
# These block window creation - use for short tasks only.
|
||||
# Use "<global>" to inherit from global config.
|
||||
# Set to empty list to disable: `post_create: []`
|
||||
# post_create:
|
||||
# - "<global>"
|
||||
# - mise use
|
||||
post_create:
|
||||
- ./scripts/worktree-env
|
||||
|
||||
pre_remove:
|
||||
- ./scripts/worktree-cleanup
|
||||
|
||||
panes:
|
||||
- command: >-
|
||||
claude --dangerously-skip-permissions --append-system-prompt
|
||||
"You are running inside a tmux session with other panes running services.\n
|
||||
Pane layout (current window):\n
|
||||
- Pane 0: this pane (claude agent)\n
|
||||
- Pane 1: backend (cargo watch -x run)\n
|
||||
- Pane 2: frontend (npm run dev)\n\n
|
||||
To check logs, use: \`tmux capture-pane -t .1 -p -S -50\` (backend) or \`tmux capture-pane -t .2 -p -S -50\` (frontend).\n
|
||||
When restarting backend or frontend, make sure to use the ports listed in .env.local.\n
|
||||
Because we are running backend with cargo watch, to verify your changes, just check the logs in the backend pane. No need for cargo check.\n\n
|
||||
IMPORTANT: Read docs/autonomous-mode.md before starting any work."
|
||||
focus: true
|
||||
- command: 'ROOT="$(git rev-parse --show-toplevel)"; [ -f "$ROOT/.env.local" ] && source "$ROOT/.env.local"; cd "$ROOT/backend" && PORT=${BACKEND_PORT:-8000} cargo watch -x "run ${CARGO_FEATURES:+--features $CARGO_FEATURES}"'
|
||||
split: horizontal
|
||||
- command: 'ROOT="$(git rev-parse --show-toplevel)"; [ -f "$ROOT/.env.local" ] && source "$ROOT/.env.local"; cd "$ROOT/frontend" && npm run generate-backend-client && REMOTE=${REMOTE:-http://localhost:${BACKEND_PORT:-8000}} npm run dev -- --port ${FRONTEND_PORT:-3000} --host 0.0.0.0'
|
||||
split: vertical
|
||||
|
||||
files:
|
||||
copy:
|
||||
- backend/.env
|
||||
- scripts/
|
||||
- wm-ts-nav/target/release/wm-ts-nav
|
||||
|
||||
sandbox:
|
||||
enabled: false
|
||||
toolchain: off
|
||||
20
CHANGELOG.md
20
CHANGELOG.md
@@ -1,25 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## [1.660.0](https://github.com/windmill-labs/windmill/compare/v1.659.1...v1.660.0) (2026-03-18)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **cli:** use local scripts when previewing flows ([#8365](https://github.com/windmill-labs/windmill/issues/8365)) ([435de95](https://github.com/windmill-labs/windmill/commit/435de95e7d5c9433dafac5369cfc533fd738fc22))
|
||||
* MCP server readiness for Anthropic connectors directory ([#8438](https://github.com/windmill-labs/windmill/issues/8438)) ([1cfb40b](https://github.com/windmill-labs/windmill/commit/1cfb40bdaa877f1616fc1c1cf5fb6b6aa1832b86))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* exclude wm_deployers group from CE group limit check ([#8429](https://github.com/windmill-labs/windmill/issues/8429)) ([9a6ce44](https://github.com/windmill-labs/windmill/commit/9a6ce44c8414810292ebc8a1ae64950ee2c76307))
|
||||
* prevent AI agent tool jobs from becoming zombies on cancellation ([#8437](https://github.com/windmill-labs/windmill/issues/8437)) ([f4489cb](https://github.com/windmill-labs/windmill/commit/f4489cbe645489a892994c70d17df2284b494568))
|
||||
* show cancelled WAC jobs as done in workflow timeline ([#8436](https://github.com/windmill-labs/windmill/issues/8436)) ([bee9282](https://github.com/windmill-labs/windmill/commit/bee928276e098ce7b17e20af74e34458e5c5353e))
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* cache composer vendor dir to skip reinstall on repeated php executions ([#8330](https://github.com/windmill-labs/windmill/issues/8330)) ([66a8e84](https://github.com/windmill-labs/windmill/commit/66a8e844a64d91d57dcabb7ad31d9308dec99032))
|
||||
|
||||
## [1.659.1](https://github.com/windmill-labs/windmill/compare/v1.659.0...v1.659.1) (2026-03-18)
|
||||
|
||||
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE v2_job_completed SET\n workflow_as_code_status = jsonb_set(\n jsonb_set(\n workflow_as_code_status,\n array[$1],\n COALESCE(workflow_as_code_status->$1, '{}'::jsonb)\n ),\n array[$1, 'duration_ms'],\n to_jsonb($2::bigint)\n )\n WHERE id = $3 AND workflow_as_code_status IS NOT NULL",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Int8",
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "29935e89475f637d765c516f1aa2be2f0f31fb50d519b42a056d0d73417599a3"
|
||||
}
|
||||
@@ -15,7 +15,7 @@
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55"
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT COUNT(*) FROM script s WHERE s.workspace_id = $1 AND s.hash NOT IN (\n SELECT DISTINCT ON (path) hash FROM script\n WHERE workspace_id = $1 AND deleted = false AND draft_only IS NOT TRUE\n ORDER BY path, created_at DESC\n )",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "count",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "9517395ac7230ab7c40c03ddd2a95fd6118b329a4421c9e8022df90ff7e775c8"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT COUNT(*) FROM group_ WHERE name != 'all' AND name != 'error_handler' AND name != 'slack' AND name != 'wm_deployers'",
|
||||
"query": "SELECT COUNT(*) FROM group_ WHERE name != 'all' AND name != 'error_handler' AND name != 'slack'",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -16,5 +16,5 @@
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "bc6ebef9d41aba232f115f95404922c6054df01fa7f38f5d15d6d4af6c726a3c"
|
||||
"hash": "cec906e3bcfe5b477f23e8bb44500b97166c789ab1cf6487679d092eebf3a48b"
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT COUNT(*) FROM app_version av\n JOIN app a ON a.id = av.app_id\n WHERE a.workspace_id = $1 AND av.id != a.versions[array_upper(a.versions, 1)]",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "count",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "d592ba371d4ad6e7f1bcffc01749f4753b6fe4e42413cce3d55f92797c856f35"
|
||||
}
|
||||
14
backend/.sqlx/query-e050734d7642b26f8859982c55ec2c8b1fc14a8de665b15a2f2dfd6e0b5b7fdf.json
generated
Normal file
14
backend/.sqlx/query-e050734d7642b26f8859982c55ec2c8b1fc14a8de665b15a2f2dfd6e0b5b7fdf.json
generated
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM v2_job_debounce_batch WHERE debounce_batch = (\n SELECT debounce_batch FROM v2_job_debounce_batch WHERE id = $1\n )",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "e050734d7642b26f8859982c55ec2c8b1fc14a8de665b15a2f2dfd6e0b5b7fdf"
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT COUNT(*) FROM flow_version fv\n JOIN flow f ON f.workspace_id = fv.workspace_id AND f.path = fv.path\n WHERE fv.workspace_id = $1 AND fv.id != f.versions[array_upper(f.versions, 1)]",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "count",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "ed202f69f5f0a8f21f2dbfcae98b9a94d039362537b050913ea2b7534f85a047"
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT j.id FROM v2_job j\n JOIN v2_job_queue q ON q.id = j.id\n WHERE j.parent_job = $1 AND j.workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "fc27f306a9399205eaa0946897d797e5b792674c353acb41bd4e7133e19e2a03"
|
||||
}
|
||||
8
backend/.workmux.yaml
Normal file
8
backend/.workmux.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
panes:
|
||||
# Pane 1: Install dependencies, then start dev server
|
||||
- command: cargo run
|
||||
|
||||
# Pane 2: AI agent
|
||||
- command: <agent>
|
||||
split: horizontal
|
||||
focus: true
|
||||
159
backend/Cargo.lock
generated
159
backend/Cargo.lock
generated
@@ -1885,20 +1885,19 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "borsh"
|
||||
version = "1.6.1"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a"
|
||||
checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f"
|
||||
dependencies = [
|
||||
"borsh-derive",
|
||||
"bytes",
|
||||
"cfg_aliases 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "borsh-derive"
|
||||
version = "1.6.1"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bfcfdc083699101d5a7965e49925975f2f55060f94f9a05e7187be95d530ca59"
|
||||
checksum = "0686c856aa6aac0c4498f936d7d6a02df690f614c03e4d906d1018062b5c5e2c"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"proc-macro-crate",
|
||||
@@ -15742,7 +15741,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-nats",
|
||||
@@ -15809,7 +15808,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-alerting"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"axum 0.7.9",
|
||||
"chrono",
|
||||
@@ -15822,7 +15821,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
@@ -15963,7 +15962,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-agent-workers"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"axum 0.7.9",
|
||||
"chrono",
|
||||
@@ -15986,7 +15985,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-assets"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"axum 0.7.9",
|
||||
"chrono",
|
||||
@@ -15999,7 +15998,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-auth"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.7.9",
|
||||
@@ -16025,7 +16024,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-client"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"reqwest 0.12.28",
|
||||
"serde",
|
||||
@@ -16035,7 +16034,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-configs"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"axum 0.7.9",
|
||||
"chrono",
|
||||
@@ -16052,7 +16051,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-debug"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"axum 0.7.9",
|
||||
"base64 0.22.1",
|
||||
@@ -16075,7 +16074,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-embeddings"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.7.9",
|
||||
@@ -16098,7 +16097,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-flow-conversations"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"axum 0.7.9",
|
||||
"chrono",
|
||||
@@ -16114,7 +16113,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-flows"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"axum 0.7.9",
|
||||
"chrono",
|
||||
@@ -16134,7 +16133,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-groups"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"axum 0.7.9",
|
||||
"chrono",
|
||||
@@ -16154,7 +16153,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-inputs"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"axum 0.7.9",
|
||||
"chrono",
|
||||
@@ -16168,7 +16167,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-integration-tests"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-nats",
|
||||
@@ -16196,7 +16195,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-jobs"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.7.9",
|
||||
@@ -16221,7 +16220,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-npm-proxy"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"axum 0.7.9",
|
||||
"flate2",
|
||||
@@ -16239,7 +16238,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-openapi"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.7.9",
|
||||
@@ -16260,7 +16259,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-schedule"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"axum 0.7.9",
|
||||
"chrono",
|
||||
@@ -16280,7 +16279,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-scripts"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"axum 0.7.9",
|
||||
"chrono",
|
||||
@@ -16310,7 +16309,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-settings"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.7.9",
|
||||
@@ -16337,7 +16336,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-sse"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"serde",
|
||||
@@ -16349,7 +16348,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-users"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"argon2",
|
||||
"axum 0.7.9",
|
||||
@@ -16372,7 +16371,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-workers"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"axum 0.7.9",
|
||||
"chrono",
|
||||
@@ -16386,7 +16385,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-workspaces"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"axum 0.7.9",
|
||||
"chrono",
|
||||
@@ -16417,7 +16416,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-audit"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"lazy_static",
|
||||
@@ -16431,7 +16430,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-autoscaling"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.7.9",
|
||||
@@ -16450,7 +16449,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-common"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"anyhow",
|
||||
@@ -16549,7 +16548,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-dep-map"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"itertools 0.14.0",
|
||||
@@ -16568,7 +16567,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-git-sync"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"regex",
|
||||
"serde",
|
||||
@@ -16583,7 +16582,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-indexer"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"astral-tokio-tar",
|
||||
@@ -16607,7 +16606,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-jseval"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"futures",
|
||||
@@ -16624,7 +16623,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-macros"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"itertools 0.14.0",
|
||||
"lazy_static",
|
||||
@@ -16640,7 +16639,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-mcp"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -16661,7 +16660,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-native-triggers"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -16692,7 +16691,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-oauth"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-oauth2",
|
||||
@@ -16716,7 +16715,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-object-store"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-stream",
|
||||
@@ -16750,7 +16749,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-operator"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"futures",
|
||||
@@ -16768,7 +16767,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"convert_case 0.6.0",
|
||||
"serde",
|
||||
@@ -16777,7 +16776,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-bash"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -16789,7 +16788,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-csharp"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -16801,7 +16800,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-go"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"gosyn",
|
||||
@@ -16813,7 +16812,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-graphql"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -16825,7 +16824,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-java"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -16837,7 +16836,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-nu"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"nu-parser",
|
||||
@@ -16848,7 +16847,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-php"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -16859,7 +16858,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -16871,7 +16870,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py-asset"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"rustpython-ast",
|
||||
@@ -16882,7 +16881,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py-imports"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -16906,7 +16905,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ruby"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -16920,7 +16919,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-rust"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"convert_case 0.6.0",
|
||||
@@ -16937,7 +16936,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-sql"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -16951,7 +16950,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-sql-asset"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
@@ -16963,7 +16962,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ts"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -16981,7 +16980,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ts-asset"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde-wasm-bindgen",
|
||||
@@ -16997,7 +16996,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-wac"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"rustpython-ast",
|
||||
@@ -17013,7 +17012,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-yaml"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
@@ -17024,7 +17023,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-queue"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -17061,7 +17060,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-runtime-nativets"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"const_format",
|
||||
@@ -17099,7 +17098,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-sql-datatype-parser-wasm"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
"wasm-bindgen",
|
||||
@@ -17110,7 +17109,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-store"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -17139,7 +17138,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-test-utils"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.7.9",
|
||||
@@ -17162,7 +17161,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17195,7 +17194,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-email"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17215,7 +17214,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-gcp"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17249,7 +17248,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-http"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17284,7 +17283,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-kafka"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17307,7 +17306,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-mqtt"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17331,7 +17330,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-nats"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-nats",
|
||||
@@ -17355,7 +17354,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-postgres"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17390,7 +17389,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-sqs"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17418,7 +17417,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-websocket"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17441,7 +17440,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-types"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.9.4",
|
||||
@@ -17459,7 +17458,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-worker"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-once-cell",
|
||||
@@ -17566,7 +17565,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-worker-volumes"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "windmill"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
@@ -82,7 +82,7 @@ members = [
|
||||
exclude = ["./windmill-duckdb-ffi-internal"]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
|
||||
edition = "2021"
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
06a848ab04e3f43978b92f0ceee019e9e2fe50f1
|
||||
878cc2044717e0177228529a50433fe2768e70b5
|
||||
|
||||
@@ -571,16 +571,16 @@ async fn test_volume_sql_worker_e2e(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
std::fs::write(vol_dir.join("hello.txt"), b"hello from volume")?;
|
||||
|
||||
// 3. Push the job and run with SQL-connected worker
|
||||
let code = r#"// volume: test-vol data
|
||||
let code = r#"// volume: test-vol /tmp/data
|
||||
|
||||
import { readFileSync, writeFileSync, existsSync } from "fs";
|
||||
|
||||
export function main() {
|
||||
const content = readFileSync("data/hello.txt", "utf-8");
|
||||
writeFileSync("data/output.txt", "written by sql worker");
|
||||
const content = readFileSync("/tmp/data/hello.txt", "utf-8");
|
||||
writeFileSync("/tmp/data/output.txt", "written by sql worker");
|
||||
return {
|
||||
read_content: content,
|
||||
output_exists: existsSync("data/output.txt"),
|
||||
output_exists: existsSync("/tmp/data/output.txt"),
|
||||
};
|
||||
}"#;
|
||||
|
||||
|
||||
@@ -452,7 +452,7 @@ async fn create_flow(
|
||||
.await?;
|
||||
if nb_flows.unwrap_or(0) >= 1000 {
|
||||
return Err(Error::BadRequest(
|
||||
"You have reached the maximum number of flows (1000) on cloud. Check your usage in Workspace Settings > General > Cloud Quotas. Contact support@windmill.dev to increase the limit"
|
||||
"You have reached the maximum number of flows (1000) on cloud. Contact support@windmill.dev to increase the limit"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -215,12 +215,12 @@ pub async fn require_is_owner(
|
||||
}
|
||||
|
||||
async fn _check_nb_of_groups(db: &DB) -> Result<()> {
|
||||
let nb_groups = sqlx::query_scalar!("SELECT COUNT(*) FROM group_ WHERE name != 'all' AND name != 'error_handler' AND name != 'slack' AND name != 'wm_deployers'",)
|
||||
let nb_groups = sqlx::query_scalar!("SELECT COUNT(*) FROM group_ WHERE name != 'all' AND name != 'error_handler' AND name != 'slack'",)
|
||||
.fetch_one(db)
|
||||
.await?;
|
||||
if nb_groups.unwrap_or(0) >= 3 {
|
||||
return Err(Error::BadRequest(
|
||||
"You have reached the maximum number of groups (3 outside of native groups 'all', 'slack', 'error_handler' and 'wm_deployers') without an enterprise license"
|
||||
"You have reached the maximum number of groups (3 outside of native groups 'all', 'slack' and 'error_handler') without an enterprise license"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -104,8 +104,6 @@ pub struct Input {
|
||||
created_by: String,
|
||||
is_public: bool,
|
||||
success: bool,
|
||||
#[serde(skip_serializing_if = "std::ops::Not::not")]
|
||||
is_preview: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, FromRow)]
|
||||
@@ -115,7 +113,6 @@ pub struct CompletedJobMini {
|
||||
args: Option<sqlx::types::Json<Box<serde_json::value::RawValue>>>,
|
||||
created_by: String,
|
||||
success: bool,
|
||||
is_preview: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -156,10 +153,9 @@ async fn get_input_history(
|
||||
let inner_limit = 2 * (per_page + offset);
|
||||
|
||||
let sql = &format!(
|
||||
"SELECT id, completed_at, created_by, args, success, is_preview FROM (\
|
||||
"SELECT id, completed_at, created_by, args, success FROM (\
|
||||
SELECT id, v2_job_completed.completed_at, created_by, 'null'::jsonb as args, \
|
||||
status = 'success' as success, \
|
||||
kind IN ('preview', 'flowpreview') as is_preview \
|
||||
status = 'success' as success \
|
||||
FROM v2_job JOIN v2_job_completed USING (id) \
|
||||
WHERE v2_job.workspace_id = $3 AND {} = $1 AND kind = any($2) \
|
||||
{args_query} AND v2_job_completed.status != 'skipped' {include_non_root} \
|
||||
@@ -213,7 +209,6 @@ async fn get_input_history(
|
||||
created_by: row.created_by,
|
||||
is_public: true,
|
||||
success: row.success,
|
||||
is_preview: row.is_preview,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -311,7 +306,6 @@ async fn list_saved_inputs(
|
||||
created_at: row.created_at,
|
||||
is_public: row.is_public,
|
||||
success: true,
|
||||
is_preview: false,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -636,7 +636,7 @@ async fn create_script_internal<'c>(
|
||||
.await?;
|
||||
if nb_scripts.unwrap_or(0) >= 5000 {
|
||||
return Err(Error::BadRequest(
|
||||
"You have reached the maximum number of scripts (5000) on cloud. Check your usage in Workspace Settings > General > Cloud Quotas. Contact support@windmill.dev to increase the limit"
|
||||
"You have reached the maximum number of scripts (5000) on cloud. Contact support@windmill.dev to increase the limit"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -158,8 +158,6 @@ pub fn workspaced_service() -> Router {
|
||||
post(update_protection_rule).delete(delete_protection_rule),
|
||||
)
|
||||
.route("/log_chat", post(log_ai_chat))
|
||||
.route("/cloud_quotas", get(get_cloud_quotas))
|
||||
.route("/prune_versions", post(prune_versions))
|
||||
}
|
||||
pub fn global_service() -> Router {
|
||||
Router::new()
|
||||
@@ -5400,199 +5398,3 @@ async fn log_ai_chat(
|
||||
.await?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct QuotaInfo {
|
||||
used: i64,
|
||||
limit: i64,
|
||||
prunable: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct CloudQuotas {
|
||||
scripts: QuotaInfo,
|
||||
flows: QuotaInfo,
|
||||
apps: QuotaInfo,
|
||||
variables: QuotaInfo,
|
||||
resources: QuotaInfo,
|
||||
}
|
||||
|
||||
async fn get_cloud_quotas(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Path(w_id): Path<String>,
|
||||
) -> JsonResult<CloudQuotas> {
|
||||
require_admin(authed.is_admin, &authed.username)?;
|
||||
|
||||
if !*CLOUD_HOSTED {
|
||||
return Err(Error::BadRequest(
|
||||
"Cloud quotas are only available on cloud-hosted instances".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let scripts_used =
|
||||
sqlx::query_scalar!("SELECT COUNT(*) FROM script WHERE workspace_id = $1", &w_id)
|
||||
.fetch_one(&db)
|
||||
.await?
|
||||
.unwrap_or(0);
|
||||
|
||||
let scripts_prunable = sqlx::query_scalar!(
|
||||
"SELECT COUNT(*) FROM script s WHERE s.workspace_id = $1 AND s.hash NOT IN (
|
||||
SELECT DISTINCT ON (path) hash FROM script
|
||||
WHERE workspace_id = $1 AND deleted = false AND draft_only IS NOT TRUE
|
||||
ORDER BY path, created_at DESC
|
||||
)",
|
||||
&w_id
|
||||
)
|
||||
.fetch_one(&db)
|
||||
.await?
|
||||
.unwrap_or(0);
|
||||
|
||||
let flows_used =
|
||||
sqlx::query_scalar!("SELECT COUNT(*) FROM flow WHERE workspace_id = $1", &w_id)
|
||||
.fetch_one(&db)
|
||||
.await?
|
||||
.unwrap_or(0);
|
||||
|
||||
let flows_prunable = sqlx::query_scalar!(
|
||||
"SELECT COUNT(*) FROM flow_version fv
|
||||
JOIN flow f ON f.workspace_id = fv.workspace_id AND f.path = fv.path
|
||||
WHERE fv.workspace_id = $1 AND fv.id != f.versions[array_upper(f.versions, 1)]",
|
||||
&w_id
|
||||
)
|
||||
.fetch_one(&db)
|
||||
.await?
|
||||
.unwrap_or(0);
|
||||
|
||||
let apps_used = sqlx::query_scalar!("SELECT COUNT(*) FROM app WHERE workspace_id = $1", &w_id)
|
||||
.fetch_one(&db)
|
||||
.await?
|
||||
.unwrap_or(0);
|
||||
|
||||
let apps_prunable = sqlx::query_scalar!(
|
||||
"SELECT COUNT(*) FROM app_version av
|
||||
JOIN app a ON a.id = av.app_id
|
||||
WHERE a.workspace_id = $1 AND av.id != a.versions[array_upper(a.versions, 1)]",
|
||||
&w_id
|
||||
)
|
||||
.fetch_one(&db)
|
||||
.await?
|
||||
.unwrap_or(0);
|
||||
|
||||
let variables_used = sqlx::query_scalar!(
|
||||
"SELECT COUNT(*) FROM variable WHERE workspace_id = $1",
|
||||
&w_id
|
||||
)
|
||||
.fetch_one(&db)
|
||||
.await?
|
||||
.unwrap_or(0);
|
||||
|
||||
let resources_used = sqlx::query_scalar!(
|
||||
"SELECT COUNT(*) FROM resource WHERE workspace_id = $1",
|
||||
&w_id
|
||||
)
|
||||
.fetch_one(&db)
|
||||
.await?
|
||||
.unwrap_or(0);
|
||||
|
||||
Ok(Json(CloudQuotas {
|
||||
scripts: QuotaInfo { used: scripts_used, limit: 5000, prunable: scripts_prunable },
|
||||
flows: QuotaInfo { used: flows_used, limit: 1000, prunable: flows_prunable },
|
||||
apps: QuotaInfo { used: apps_used, limit: 1000, prunable: apps_prunable },
|
||||
variables: QuotaInfo { used: variables_used, limit: 10000, prunable: 0 },
|
||||
resources: QuotaInfo { used: resources_used, limit: 10000, prunable: 0 },
|
||||
}))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct PruneVersionsRequest {
|
||||
resource_type: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct PruneVersionsResponse {
|
||||
pruned: u64,
|
||||
}
|
||||
|
||||
async fn prune_versions(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Path(w_id): Path<String>,
|
||||
Json(req): Json<PruneVersionsRequest>,
|
||||
) -> JsonResult<PruneVersionsResponse> {
|
||||
require_admin(authed.is_admin, &authed.username)?;
|
||||
|
||||
if !*CLOUD_HOSTED {
|
||||
return Err(Error::BadRequest(
|
||||
"Version pruning is only available on cloud-hosted instances".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let pruned = match req.resource_type.as_str() {
|
||||
"scripts" => {
|
||||
let result = sqlx::query(
|
||||
"DELETE FROM script
|
||||
WHERE workspace_id = $1 AND hash NOT IN (
|
||||
SELECT DISTINCT ON (path) hash FROM script
|
||||
WHERE workspace_id = $1 AND deleted = false AND draft_only IS NOT TRUE
|
||||
ORDER BY path, created_at DESC
|
||||
)",
|
||||
)
|
||||
.bind(&w_id)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
result.rows_affected()
|
||||
}
|
||||
"flows" => {
|
||||
let deleted = sqlx::query(
|
||||
"DELETE FROM flow_version fv
|
||||
USING flow f
|
||||
WHERE fv.workspace_id = f.workspace_id AND fv.path = f.path
|
||||
AND fv.workspace_id = $1
|
||||
AND fv.id != f.versions[array_upper(f.versions, 1)]",
|
||||
)
|
||||
.bind(&w_id)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
|
||||
sqlx::query(
|
||||
"UPDATE flow SET versions = ARRAY[versions[array_upper(versions, 1)]]
|
||||
WHERE workspace_id = $1 AND array_length(versions, 1) > 1",
|
||||
)
|
||||
.bind(&w_id)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
|
||||
deleted.rows_affected()
|
||||
}
|
||||
"apps" => {
|
||||
let deleted = sqlx::query(
|
||||
"DELETE FROM app_version av
|
||||
USING app a
|
||||
WHERE av.app_id = a.id AND a.workspace_id = $1
|
||||
AND av.id != a.versions[array_upper(a.versions, 1)]",
|
||||
)
|
||||
.bind(&w_id)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
|
||||
sqlx::query(
|
||||
"UPDATE app SET versions = ARRAY[versions[array_upper(versions, 1)]]
|
||||
WHERE workspace_id = $1 AND array_length(versions, 1) > 1",
|
||||
)
|
||||
.bind(&w_id)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
|
||||
deleted.rows_affected()
|
||||
}
|
||||
_ => {
|
||||
return Err(Error::BadRequest(format!(
|
||||
"Invalid resource type '{}'. Must be 'scripts', 'flows', or 'apps'",
|
||||
req.resource_type
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Json(PruneVersionsResponse { pruned }))
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: "3.0.3"
|
||||
|
||||
info:
|
||||
version: 1.660.0
|
||||
version: 1.659.1
|
||||
title: Windmill API
|
||||
|
||||
contact:
|
||||
@@ -4491,76 +4491,6 @@ paths:
|
||||
responses:
|
||||
"204":
|
||||
description: logged
|
||||
|
||||
/w/{workspace}/workspaces/cloud_quotas:
|
||||
get:
|
||||
summary: get cloud quota usage and limits for workspace
|
||||
operationId: getCloudQuotas
|
||||
tags:
|
||||
- workspace
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
responses:
|
||||
"200":
|
||||
description: cloud quota usage and limits
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
scripts:
|
||||
$ref: "#/components/schemas/QuotaInfo"
|
||||
flows:
|
||||
$ref: "#/components/schemas/QuotaInfo"
|
||||
apps:
|
||||
$ref: "#/components/schemas/QuotaInfo"
|
||||
variables:
|
||||
$ref: "#/components/schemas/QuotaInfo"
|
||||
resources:
|
||||
$ref: "#/components/schemas/QuotaInfo"
|
||||
required:
|
||||
- scripts
|
||||
- flows
|
||||
- apps
|
||||
- variables
|
||||
- resources
|
||||
|
||||
/w/{workspace}/workspaces/prune_versions:
|
||||
post:
|
||||
summary: prune old versions of scripts, flows, or apps
|
||||
operationId: pruneVersions
|
||||
tags:
|
||||
- workspace
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
resource_type:
|
||||
type: string
|
||||
enum:
|
||||
- scripts
|
||||
- flows
|
||||
- apps
|
||||
required:
|
||||
- resource_type
|
||||
responses:
|
||||
"200":
|
||||
description: number of pruned versions
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
pruned:
|
||||
type: integer
|
||||
required:
|
||||
- pruned
|
||||
|
||||
/w/{workspace}/workspaces/public_app_rate_limit:
|
||||
post:
|
||||
summary: Set public app rate limit for this workspace
|
||||
@@ -24464,19 +24394,6 @@ components:
|
||||
description: Users that can bypass this ruleset
|
||||
items:
|
||||
type: string
|
||||
QuotaInfo:
|
||||
type: object
|
||||
properties:
|
||||
used:
|
||||
type: integer
|
||||
limit:
|
||||
type: integer
|
||||
prunable:
|
||||
type: integer
|
||||
required:
|
||||
- used
|
||||
- limit
|
||||
- prunable
|
||||
NativeServiceName:
|
||||
type: string
|
||||
enum:
|
||||
|
||||
@@ -1179,7 +1179,7 @@ async fn create_app_internal<'a>(
|
||||
.await?;
|
||||
if nb_apps.unwrap_or(0) >= 1000 {
|
||||
return Err(Error::BadRequest(
|
||||
"You have reached the maximum number of apps (1000) on cloud. Check your usage in Workspace Settings > General > Cloud Quotas. Contact support@windmill.dev to increase the limit"
|
||||
"You have reached the maximum number of apps (1000) on cloud. Contact support@windmill.dev to increase the limit"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -64,6 +64,7 @@ use crate::scim_oss::has_scim_token;
|
||||
use windmill_common::error::AppError;
|
||||
|
||||
mod ai;
|
||||
mod google;
|
||||
mod apps;
|
||||
pub mod args;
|
||||
mod audit;
|
||||
@@ -76,7 +77,6 @@ mod bedrock;
|
||||
mod capture;
|
||||
mod concurrency_groups;
|
||||
mod db;
|
||||
mod google;
|
||||
|
||||
mod drafts;
|
||||
#[cfg(feature = "private")]
|
||||
@@ -376,7 +376,7 @@ pub async fn run_server(
|
||||
));
|
||||
|
||||
let cors = CorsLayer::new()
|
||||
.allow_methods([http::Method::GET, http::Method::POST, http::Method::DELETE])
|
||||
.allow_methods([http::Method::GET, http::Method::POST])
|
||||
.allow_headers([http::header::CONTENT_TYPE, http::header::AUTHORIZATION])
|
||||
.allow_origin(Any);
|
||||
|
||||
@@ -675,11 +675,8 @@ pub async fn run_server(
|
||||
)
|
||||
.layer(from_extractor::<OptAuthed>())
|
||||
// Deprecated, here for backwards compatibility: user should use /mcp/w/:workspace_id/mcp instead
|
||||
.nest(
|
||||
"/mcp/w/:workspace_id/sse",
|
||||
mcp_router.clone().layer(cors.clone()),
|
||||
)
|
||||
.nest("/mcp/w/:workspace_id/mcp", mcp_router.layer(cors.clone()))
|
||||
.nest("/mcp/w/:workspace_id/sse", mcp_router.clone())
|
||||
.nest("/mcp/w/:workspace_id/mcp", mcp_router)
|
||||
.nest("/agent_workers", {
|
||||
#[cfg(feature = "agent_worker_server")]
|
||||
{
|
||||
|
||||
@@ -386,9 +386,7 @@ async fn handle_authorization_code_grant(
|
||||
|
||||
let access_token = rd_string(32);
|
||||
let access_token_hash = hash_token(&access_token);
|
||||
let access_token_prefix = access_token
|
||||
.get(..TOKEN_PREFIX_LEN)
|
||||
.unwrap_or(&access_token);
|
||||
let access_token_prefix = access_token.get(..TOKEN_PREFIX_LEN).unwrap_or(&access_token);
|
||||
let plaintext: Option<&str> = if MIN_VERSION_SUPPORTS_TOKEN_HASH.met().await {
|
||||
None
|
||||
} else {
|
||||
@@ -532,9 +530,7 @@ async fn handle_refresh_token_grant(
|
||||
// Generate new tokens
|
||||
let new_access_token = rd_string(32);
|
||||
let new_access_token_hash = hash_token(&new_access_token);
|
||||
let new_access_token_prefix = new_access_token
|
||||
.get(..TOKEN_PREFIX_LEN)
|
||||
.unwrap_or(&new_access_token);
|
||||
let new_access_token_prefix = new_access_token.get(..TOKEN_PREFIX_LEN).unwrap_or(&new_access_token);
|
||||
let new_plaintext: Option<&str> = if MIN_VERSION_SUPPORTS_TOKEN_HASH.met().await {
|
||||
None
|
||||
} else {
|
||||
|
||||
@@ -28,25 +28,6 @@ use std::sync::Arc;
|
||||
// Re-export from http crate for extracting request parts
|
||||
use http::request::Parts as HttpParts;
|
||||
|
||||
/// Maximum size in bytes for tool result text content.
|
||||
/// Anthropic enforces a 25,000 token limit per tool result.
|
||||
/// At ~3.5 bytes/token, 87,500 bytes is a safe upper bound.
|
||||
const MAX_TOOL_RESULT_BYTES: usize = 87_500;
|
||||
|
||||
fn truncate_tool_result(text: String) -> String {
|
||||
if text.len() <= MAX_TOOL_RESULT_BYTES {
|
||||
return text;
|
||||
}
|
||||
// Truncate at a char boundary
|
||||
let mut end = MAX_TOOL_RESULT_BYTES;
|
||||
while !text.is_char_boundary(end) && end > 0 {
|
||||
end -= 1;
|
||||
}
|
||||
let mut truncated = text[..end].to_string();
|
||||
truncated.push_str("\n\n[truncated — result exceeded 25,000 token limit]");
|
||||
truncated
|
||||
}
|
||||
|
||||
/// MCP Server Runner - generic over the backend implementation
|
||||
///
|
||||
/// This struct implements the MCP ServerHandler trait and uses a McpBackend
|
||||
@@ -287,26 +268,29 @@ impl<B: McpBackend> ServerHandler for Runner<B> {
|
||||
.map_err(|e| ErrorData::internal_error(e.message, None))?;
|
||||
|
||||
return Ok(CallToolResult::success(vec![Content::text(
|
||||
truncate_tool_result(
|
||||
serde_json::to_string_pretty(&result).unwrap_or_else(|_| "{}".to_string()),
|
||||
),
|
||||
serde_json::to_string_pretty(&result).unwrap_or_else(|_| "{}".to_string()),
|
||||
)]));
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve the tool name to (type, path, is_hub)
|
||||
let (type_str, is_hub, is_hashed) = parse_tool_prefix(&request.name).map_err(|e| {
|
||||
ErrorData::internal_error(format!("Failed to parse tool name: {}", e), None)
|
||||
})?;
|
||||
let (type_str, is_hub, is_hashed) =
|
||||
parse_tool_prefix(&request.name).map_err(|e| {
|
||||
ErrorData::internal_error(format!("Failed to parse tool name: {}", e), None)
|
||||
})?;
|
||||
|
||||
let (tool_type, path, is_hub) = if !is_hashed {
|
||||
reverse_transform(&request.name).map_err(|e| {
|
||||
ErrorData::internal_error(format!("Failed to parse tool name: {}", e), None)
|
||||
})?
|
||||
} else if is_hub {
|
||||
let version_id = extract_hub_version_id_from_hashed(&request.name).map_err(|e| {
|
||||
ErrorData::internal_error(format!("Failed to extract hub version_id: {}", e), None)
|
||||
})?;
|
||||
let version_id =
|
||||
extract_hub_version_id_from_hashed(&request.name).map_err(|e| {
|
||||
ErrorData::internal_error(
|
||||
format!("Failed to extract hub version_id: {}", e),
|
||||
None,
|
||||
)
|
||||
})?;
|
||||
(type_str, version_id, true)
|
||||
} else {
|
||||
let path_prefix = extract_path_prefix_from_hashed(&request.name);
|
||||
@@ -401,9 +385,7 @@ impl<B: McpBackend> ServerHandler for Runner<B> {
|
||||
|
||||
match result {
|
||||
Ok(value) => Ok(CallToolResult::success(vec![Content::text(
|
||||
truncate_tool_result(
|
||||
serde_json::to_string_pretty(&value).unwrap_or_else(|_| "{}".to_string()),
|
||||
),
|
||||
serde_json::to_string_pretty(&value).unwrap_or_else(|_| "{}".to_string()),
|
||||
)])),
|
||||
Err(e) => Err(ErrorData::internal_error(
|
||||
format!("Failed to run {}: {}", tool_type, e.message),
|
||||
|
||||
@@ -1041,35 +1041,6 @@ async fn commit_completed_job<T: Serialize + Send + Sync + ValidableJson>(
|
||||
.ok()
|
||||
.flatten();
|
||||
wac_job_ids = row.flatten();
|
||||
|
||||
// If parent was already completed (e.g. cancelled), update v2_job_completed instead
|
||||
if wac_job_ids.is_none() {
|
||||
let _ = sqlx::query!(
|
||||
r#"UPDATE v2_job_completed SET
|
||||
workflow_as_code_status = jsonb_set(
|
||||
jsonb_set(
|
||||
workflow_as_code_status,
|
||||
array[$1],
|
||||
COALESCE(workflow_as_code_status->$1, '{}'::jsonb)
|
||||
),
|
||||
array[$1, 'duration_ms'],
|
||||
to_jsonb($2::bigint)
|
||||
)
|
||||
WHERE id = $3 AND workflow_as_code_status IS NOT NULL"#,
|
||||
&completed_job.id.to_string(),
|
||||
duration,
|
||||
parent_job
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.warn_after_seconds(10)
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
tracing::error!(
|
||||
"Could not update completed parent job `duration_ms` in workflow as code status: {}",
|
||||
e,
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
// tracing::error!("Added completed job {:#?}", queued_job);
|
||||
|
||||
@@ -768,7 +768,7 @@ async fn create_resource(
|
||||
.await?;
|
||||
if nb_resources.unwrap_or(0) >= 10000 {
|
||||
return Err(Error::BadRequest(
|
||||
"You have reached the maximum number of resources (10000) on cloud. Check your usage in Workspace Settings > General > Cloud Quotas. Contact support@windmill.dev to increase the limit"
|
||||
"You have reached the maximum number of resources (10000) on cloud. Contact support@windmill.dev to increase the limit"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
@@ -884,12 +884,13 @@ async fn delete_resource(
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
|
||||
// Fetch the resource value before deleting, so we can find linked $var: references
|
||||
let resource_value: Option<Option<serde_json::Value>> =
|
||||
sqlx::query_scalar("SELECT value FROM resource WHERE path = $1 AND workspace_id = $2")
|
||||
.bind(path)
|
||||
.bind(&w_id)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?;
|
||||
let resource_value: Option<Option<serde_json::Value>> = sqlx::query_scalar(
|
||||
"SELECT value FROM resource WHERE path = $1 AND workspace_id = $2",
|
||||
)
|
||||
.bind(path)
|
||||
.bind(&w_id)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?;
|
||||
|
||||
let deleted_path = sqlx::query_scalar!(
|
||||
"DELETE FROM resource WHERE path = $1 AND workspace_id = $2 RETURNING path",
|
||||
@@ -975,7 +976,10 @@ async fn delete_resource(
|
||||
|
||||
webhook.send_message(
|
||||
w_id.clone(),
|
||||
WebhookMessage::DeleteVariable { workspace: w_id.clone(), path: var_path.clone() },
|
||||
WebhookMessage::DeleteVariable {
|
||||
workspace: w_id.clone(),
|
||||
path: var_path.clone(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -414,7 +414,7 @@ async fn create_variable(
|
||||
.await?;
|
||||
if nb_variables.unwrap_or(0) >= 10000 {
|
||||
return Err(Error::BadRequest(
|
||||
"You have reached the maximum number of variables (10000) on cloud. Check your usage in Workspace Settings > General > Cloud Quotas. Contact support@windmill.dev to increase the limit"
|
||||
"You have reached the maximum number of variables (10000) on cloud. Contact support@windmill.dev to increase the limit"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ mount {
|
||||
|
||||
|
||||
mount {
|
||||
src: "{JOB_DIR}/out/Main"
|
||||
src: "{JOB_DIR}/Main"
|
||||
dst: "/tmp/main"
|
||||
is_bind: true
|
||||
mandatory: false
|
||||
|
||||
@@ -45,10 +45,6 @@ use windmill_queue::{
|
||||
MiniPulledJob, PushArgs, PushIsolationLevel,
|
||||
};
|
||||
|
||||
/// Shared collection of abort handles for spawned tool tasks.
|
||||
/// Used to abort in-flight tasks when the parent agent is force-cancelled.
|
||||
pub type ToolAbortHandles = Arc<std::sync::Mutex<Vec<tokio::task::AbortHandle>>>;
|
||||
|
||||
/// Context for tool execution containing all required references and state
|
||||
pub struct ToolExecutionContext<'a> {
|
||||
// Database & connections
|
||||
@@ -77,9 +73,6 @@ pub struct ToolExecutionContext<'a> {
|
||||
pub flow_context: &'a mut FlowContext,
|
||||
pub previous_result: &'a Option<Box<RawValue>>,
|
||||
pub id_context: &'a Option<crate::js_eval::IdContext>,
|
||||
|
||||
// Abort handles for spawned tool tasks (used for force-cancel cleanup)
|
||||
pub tool_abort_handles: ToolAbortHandles,
|
||||
}
|
||||
|
||||
/// Execute all tool calls from an AI response
|
||||
@@ -556,19 +549,10 @@ async fn execute_windmill_tool(
|
||||
(result, occupancy_metrics_spawn)
|
||||
});
|
||||
|
||||
// Register abort handle so the task can be killed on force-cancel
|
||||
let abort_handle = join_handle.abort_handle();
|
||||
// unwrap safe: lock is only held briefly for push/drain, no panic possible inside
|
||||
ctx.tool_abort_handles.lock().unwrap().push(abort_handle);
|
||||
|
||||
// Await the spawned task
|
||||
let (handle_result, updated_occupancy) = join_handle.await.map_err(|e| {
|
||||
if e.is_cancelled() {
|
||||
Error::ExecutionErr("Tool execution task was cancelled".to_string())
|
||||
} else {
|
||||
Error::internal_err(format!("Tool execution task failed: {}", e))
|
||||
}
|
||||
})?;
|
||||
let (handle_result, updated_occupancy) = join_handle
|
||||
.await
|
||||
.map_err(|e| Error::internal_err(format!("Tool execution task failed: {}", e)))?;
|
||||
|
||||
// Merge occupancy metrics back
|
||||
ctx.occupancy_metrics.total_duration_of_running_jobs =
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#[cfg(feature = "bedrock")]
|
||||
use crate::ai::providers::bedrock::check_env_credentials;
|
||||
use crate::ai::tools::{execute_tool_calls, ToolAbortHandles, ToolExecutionContext};
|
||||
use crate::ai::tools::{execute_tool_calls, ToolExecutionContext};
|
||||
use crate::ai::utils::{
|
||||
add_message_to_conversation, any_tool_needs_previous_result, cleanup_mcp_clients,
|
||||
filter_schema_by_input_transforms, find_unique_tool_name, get_flow_context,
|
||||
@@ -35,7 +35,7 @@ use windmill_common::{
|
||||
utils::{StripPath, HTTP_CLIENT},
|
||||
worker::{to_raw_value, Connection},
|
||||
};
|
||||
use windmill_queue::{cancel_single_job, CanceledBy, MiniPulledJob};
|
||||
use windmill_queue::{CanceledBy, MiniPulledJob};
|
||||
|
||||
use crate::{
|
||||
ai::{
|
||||
@@ -46,7 +46,7 @@ use crate::{
|
||||
types::*,
|
||||
},
|
||||
common::{build_args_map, resolve_job_timeout, OccupancyMetrics, StreamNotifier},
|
||||
handle_child::{run_future_with_polling_update_job_poller_graceful, GracefulPollOutcome},
|
||||
handle_child::run_future_with_polling_update_job_poller,
|
||||
};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
@@ -445,102 +445,45 @@ pub async fn handle_ai_agent_job(
|
||||
Some(flow_job_id)
|
||||
};
|
||||
|
||||
// Create cancellation signal for graceful shutdown
|
||||
let (cancel_tx, cancel_rx) = tokio::sync::watch::channel(false);
|
||||
let tool_abort_handles: ToolAbortHandles = Arc::new(std::sync::Mutex::new(Vec::new()));
|
||||
let agent_fut = run_agent(
|
||||
db,
|
||||
conn,
|
||||
job,
|
||||
flow_status_job.as_ref(),
|
||||
Some(flow_step_id.as_str()),
|
||||
&args,
|
||||
&tools,
|
||||
&mcp_clients,
|
||||
summary.as_deref(),
|
||||
client,
|
||||
&mut inner_occupancy_metrics,
|
||||
worker_dir,
|
||||
base_internal_url,
|
||||
worker_name,
|
||||
hostname,
|
||||
killpill_rx,
|
||||
has_stream,
|
||||
has_websearch,
|
||||
);
|
||||
|
||||
/// Grace period for in-flight tool calls to complete after cancellation.
|
||||
const CANCEL_GRACE_PERIOD: std::time::Duration = std::time::Duration::from_secs(30);
|
||||
|
||||
let outcome = {
|
||||
let agent_fut = run_agent(
|
||||
db,
|
||||
conn,
|
||||
job,
|
||||
flow_status_job.as_ref(),
|
||||
Some(flow_step_id.as_str()),
|
||||
&args,
|
||||
&tools,
|
||||
&mcp_clients,
|
||||
summary.as_deref(),
|
||||
client,
|
||||
&mut inner_occupancy_metrics,
|
||||
worker_dir,
|
||||
base_internal_url,
|
||||
worker_name,
|
||||
hostname,
|
||||
killpill_rx,
|
||||
has_stream,
|
||||
has_websearch,
|
||||
cancel_rx,
|
||||
tool_abort_handles.clone(),
|
||||
);
|
||||
|
||||
let mut occupancy_opt = Some(occupancy_metrics);
|
||||
|
||||
run_future_with_polling_update_job_poller_graceful(
|
||||
job.id,
|
||||
job.timeout,
|
||||
conn,
|
||||
mem_peak,
|
||||
canceled_by,
|
||||
agent_fut,
|
||||
worker_name,
|
||||
&job.workspace_id,
|
||||
&mut occupancy_opt,
|
||||
Box::pin(futures::stream::once(async { 0 })),
|
||||
cancel_tx,
|
||||
CANCEL_GRACE_PERIOD,
|
||||
)
|
||||
.await?
|
||||
};
|
||||
// agent_fut and update_job are now dropped — borrows on mcp_clients and canceled_by released
|
||||
let result = run_future_with_polling_update_job_poller(
|
||||
job.id,
|
||||
job.timeout,
|
||||
conn,
|
||||
mem_peak,
|
||||
canceled_by,
|
||||
agent_fut,
|
||||
worker_name,
|
||||
&job.workspace_id,
|
||||
&mut Some(occupancy_metrics),
|
||||
Box::pin(futures::stream::once(async { 0 })),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Cleanup MCP clients
|
||||
cleanup_mcp_clients(mcp_clients).await;
|
||||
|
||||
let format_cancel_info = |cb: &Option<CanceledBy>| {
|
||||
cb.as_ref()
|
||||
.map_or(("unknown".to_string(), "unknown".to_string()), |x| {
|
||||
(
|
||||
x.username.clone().unwrap_or_default(),
|
||||
x.reason.clone().unwrap_or_default(),
|
||||
)
|
||||
})
|
||||
};
|
||||
|
||||
match outcome {
|
||||
GracefulPollOutcome::Ok(result) => Ok(result),
|
||||
GracefulPollOutcome::Timeout(ms) => {
|
||||
tracing::error!("AI agent timeout after {}s", ms / 1000);
|
||||
Err(Error::ExecutionErr(format!(
|
||||
"AI agent timeout after (>{}s)",
|
||||
ms / 1000
|
||||
)))
|
||||
}
|
||||
GracefulPollOutcome::Cancelled { canceled_by: cb } => {
|
||||
let (by, reason) = format_cancel_info(&cb);
|
||||
Err(Error::ExecutionErr(format!(
|
||||
"Job cancelled by {by} (reason: {reason})"
|
||||
)))
|
||||
}
|
||||
GracefulPollOutcome::CancelledTimeout { canceled_by: cb } => {
|
||||
let (by, reason) = format_cancel_info(&cb);
|
||||
// Abort any still-running spawned tool tasks
|
||||
// unwrap safe: lock is only held briefly for push/drain, no panic possible inside
|
||||
for handle in tool_abort_handles.lock().unwrap().drain(..) {
|
||||
handle.abort();
|
||||
}
|
||||
// Hard timeout: clean up orphaned jobs still stuck in v2_job_queue
|
||||
cleanup_orphaned_tool_jobs(db, &job.id, &job.workspace_id, cb).await;
|
||||
Err(Error::ExecutionErr(format!(
|
||||
"Job cancelled by {by} (reason: {reason}, timed out waiting for tool calls)"
|
||||
)))
|
||||
}
|
||||
GracefulPollOutcome::AlreadyCompleted => {
|
||||
Err(Error::AlreadyCompleted("Job already completed".to_string()))
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[async_recursion]
|
||||
@@ -568,12 +511,6 @@ pub async fn run_agent(
|
||||
killpill_rx: &mut tokio::sync::broadcast::Receiver<()>,
|
||||
has_stream: &mut bool,
|
||||
has_websearch: bool,
|
||||
|
||||
// cancellation signal from parent
|
||||
cancel_rx: tokio::sync::watch::Receiver<bool>,
|
||||
|
||||
// abort handles for spawned tool tasks
|
||||
tool_abort_handles: ToolAbortHandles,
|
||||
) -> error::Result<Box<RawValue>> {
|
||||
let output_type = args.output_type.as_ref().unwrap_or(&OutputType::Text);
|
||||
// Skip get_base_url for Bedrock - it uses SDK directly, not HTTP
|
||||
@@ -825,12 +762,9 @@ pub async fn run_agent(
|
||||
.map(|m| m.clamp(1, HARD_MAX_AGENT_ITERATIONS))
|
||||
.unwrap_or(DEFAULT_MAX_AGENT_ITERATIONS);
|
||||
|
||||
|
||||
// Main agent loop
|
||||
for i in 0..max_iterations {
|
||||
// Check if parent was canceled — stop iterating but let current tool calls finish
|
||||
if *cancel_rx.borrow() {
|
||||
return Err(Error::ExecutionErr("Job cancelled".to_string()));
|
||||
}
|
||||
|
||||
if used_structured_output_tool {
|
||||
break;
|
||||
@@ -1154,7 +1088,6 @@ pub async fn run_agent(
|
||||
flow_context: &mut flow_context,
|
||||
previous_result: &previous_result,
|
||||
id_context: &id_context,
|
||||
tool_abort_handles: tool_abort_handles.clone(),
|
||||
};
|
||||
|
||||
let (tool_messages, tool_content, tool_used_structured_output) =
|
||||
@@ -1174,11 +1107,6 @@ pub async fn run_agent(
|
||||
content = Some(tc);
|
||||
}
|
||||
used_structured_output_tool = tool_used_structured_output;
|
||||
|
||||
// Check cancellation after tool calls complete to avoid a wasted LLM call
|
||||
if *cancel_rx.borrow() {
|
||||
return Err(Error::ExecutionErr("Job cancelled".to_string()));
|
||||
}
|
||||
}
|
||||
ParsedResponse::Image { base64_data } => {
|
||||
// For image output, upload to S3 and track in conversation
|
||||
@@ -1364,107 +1292,3 @@ async fn handle_credentials_check(provider: &ProviderWithResource) -> Result<Box
|
||||
|
||||
serde_json::value::to_raw_value(&result).map_err(|e| Error::internal_err(e.to_string()))
|
||||
}
|
||||
|
||||
/// Hard-timeout fallback: force-cancel any descendant jobs still in v2_job_queue
|
||||
/// so they don't stay as zombies.
|
||||
async fn cleanup_orphaned_tool_jobs(
|
||||
db: &DB,
|
||||
parent_job_id: &Uuid,
|
||||
w_id: &str,
|
||||
canceled_by: Option<CanceledBy>,
|
||||
) {
|
||||
let username = canceled_by
|
||||
.as_ref()
|
||||
.and_then(|cb| cb.username.clone())
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
let reason = canceled_by
|
||||
.as_ref()
|
||||
.and_then(|cb| cb.reason.clone())
|
||||
.unwrap_or_else(|| {
|
||||
format!(
|
||||
"parent AI agent {} was cancelled and tool call did not complete in time",
|
||||
parent_job_id
|
||||
)
|
||||
});
|
||||
|
||||
// Find direct child jobs still in v2_job_queue (agent tool jobs are always direct children)
|
||||
let orphaned_ids: Vec<Uuid> = match sqlx::query_scalar!(
|
||||
r#"SELECT j.id FROM v2_job j
|
||||
JOIN v2_job_queue q ON q.id = j.id
|
||||
WHERE j.parent_job = $1 AND j.workspace_id = $2"#,
|
||||
parent_job_id,
|
||||
w_id,
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
{
|
||||
Ok(ids) => ids,
|
||||
Err(e) => {
|
||||
tracing::error!(
|
||||
"Failed to find orphaned tool jobs for {}: {}",
|
||||
parent_job_id,
|
||||
e
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if orphaned_ids.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
tracing::warn!(
|
||||
"Cleaning up {} orphaned tool jobs for cancelled AI agent {}",
|
||||
orphaned_ids.len(),
|
||||
parent_job_id,
|
||||
);
|
||||
|
||||
for job_id in &orphaned_ids {
|
||||
let queued_job = match windmill_queue::get_queued_job_v2(db, job_id).await {
|
||||
Ok(Some(j)) => j,
|
||||
Ok(None) => continue,
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to fetch orphaned tool job {}: {}", job_id, e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let tx = match db.begin().await {
|
||||
Ok(tx) => tx,
|
||||
Err(e) => {
|
||||
tracing::error!(
|
||||
"Failed to begin transaction for orphaned job {}: {}",
|
||||
job_id,
|
||||
e
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
match cancel_single_job(
|
||||
&username,
|
||||
Some(reason.clone()),
|
||||
queued_job,
|
||||
w_id,
|
||||
tx,
|
||||
db,
|
||||
true,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok((tx, _)) => {
|
||||
if let Err(e) = tx.commit().await {
|
||||
tracing::error!(
|
||||
"Failed to commit cancel for orphaned tool job {}: {}",
|
||||
job_id,
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
// warn not error: job may have completed between fetch and cancel (expected race)
|
||||
tracing::warn!("Failed to force-cancel orphaned tool job {}: {}", job_id, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -379,7 +379,7 @@ async fn build_cs_proj(
|
||||
"--configuration",
|
||||
"Release",
|
||||
"-o",
|
||||
&format!("{job_dir}/out"),
|
||||
job_dir,
|
||||
"--no-self-contained",
|
||||
"-p:PublishSingleFile=true",
|
||||
"-p:IncludeNativeLibrariesForSelfExtract=true",
|
||||
@@ -437,9 +437,9 @@ async fn build_cs_proj(
|
||||
|
||||
let bin_path = format!("{}/{hash}", *CSHARP_CACHE_DIR);
|
||||
#[cfg(unix)]
|
||||
let target = format!("{job_dir}/out/Main");
|
||||
let target = format!("{job_dir}/Main");
|
||||
#[cfg(windows)]
|
||||
let target = format!("{job_dir}/out/Main.exe");
|
||||
let target = format!("{job_dir}/Main.exe");
|
||||
|
||||
match save_cache(
|
||||
&bin_path,
|
||||
@@ -450,7 +450,7 @@ async fn build_cs_proj(
|
||||
.await
|
||||
{
|
||||
Err(e) => {
|
||||
let em = format!("could not save {job_dir}/out/Main to C# cache: {e:?}",);
|
||||
let em = format!("could not save {job_dir}/Main to C# cache: {e:?}",);
|
||||
tracing::error!(em);
|
||||
Ok(em)
|
||||
}
|
||||
@@ -527,17 +527,11 @@ pub async fn handle_csharp_job(
|
||||
let cache_logs = if cache {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let out_dir = format!("{job_dir}/out");
|
||||
std::fs::create_dir_all(&out_dir).map_err(|e| {
|
||||
Error::ExecutionErr(format!(
|
||||
"could not create output directory {out_dir}: {e:?}"
|
||||
))
|
||||
})?;
|
||||
let target = format!("{out_dir}/Main");
|
||||
let target = format!("{job_dir}/Main");
|
||||
let symlink = std::os::unix::fs::symlink(&bin_path, &target);
|
||||
symlink.map_err(|e| {
|
||||
Error::ExecutionErr(format!(
|
||||
"could not copy cached binary from {bin_path} to {target}: {e:?}"
|
||||
"could not copy cached binary from {bin_path} to {job_dir}/Main: {e:?}"
|
||||
))
|
||||
})?;
|
||||
}
|
||||
@@ -637,12 +631,12 @@ pub async fn handle_csharp_job(
|
||||
start_child_process(nsjail_cmd, NSJAIL_PATH.as_str(), true).await?
|
||||
} else {
|
||||
#[cfg(unix)]
|
||||
let compiled_executable_name = "./out/Main".to_string();
|
||||
let compiled_executable_name = "./Main".to_string();
|
||||
#[cfg(windows)]
|
||||
let compiled_executable_name = if cache {
|
||||
bin_path.to_string()
|
||||
} else {
|
||||
format!("{job_dir}/out/Main.exe")
|
||||
format!("{job_dir}/Main.exe")
|
||||
};
|
||||
|
||||
let mut run_csharp = build_command_with_isolation(&compiled_executable_name, &[]);
|
||||
|
||||
@@ -745,96 +745,6 @@ where
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
/// Outcome of [`run_future_with_polling_update_job_poller_graceful`].
|
||||
pub enum GracefulPollOutcome<T> {
|
||||
/// The future completed normally.
|
||||
Ok(T),
|
||||
/// The job timed out.
|
||||
Timeout(u64),
|
||||
/// The job was cancelled and the future finished within the grace period.
|
||||
Cancelled { canceled_by: Option<CanceledBy> },
|
||||
/// The job was cancelled but the future did NOT finish within the grace period.
|
||||
CancelledTimeout { canceled_by: Option<CanceledBy> },
|
||||
/// The job was already moved to v2_job_completed externally.
|
||||
AlreadyCompleted,
|
||||
}
|
||||
|
||||
/// Like [`run_future_with_polling_update_job_poller`] but on cancellation, signals
|
||||
/// `cancel_tx` and waits up to `grace_period` for the future to finish instead of
|
||||
/// dropping it immediately. This lets in-flight work (e.g. AI tool calls) complete
|
||||
/// and clean up properly.
|
||||
pub async fn run_future_with_polling_update_job_poller_graceful<Fut, T, S>(
|
||||
job_id: Uuid,
|
||||
timeout: Option<i32>,
|
||||
conn: &Connection,
|
||||
mem_peak: &mut i32,
|
||||
canceled_by_ref: &mut Option<CanceledBy>,
|
||||
result_f: Fut,
|
||||
worker_name: &str,
|
||||
w_id: &str,
|
||||
occupancy_metrics: &mut Option<&mut OccupancyMetrics>,
|
||||
get_mem: S,
|
||||
cancel_tx: watch::Sender<bool>,
|
||||
grace_period: std::time::Duration,
|
||||
) -> error::Result<GracefulPollOutcome<T>>
|
||||
where
|
||||
Fut: Future<Output = error::Result<T>>,
|
||||
S: stream::Stream<Item = i32> + Unpin,
|
||||
{
|
||||
let (tx, rx) = broadcast::channel::<()>(3);
|
||||
|
||||
let mut update_job = Box::pin(update_job_poller(
|
||||
job_id,
|
||||
conn,
|
||||
mem_peak,
|
||||
canceled_by_ref,
|
||||
get_mem,
|
||||
worker_name,
|
||||
w_id,
|
||||
rx,
|
||||
occupancy_metrics,
|
||||
));
|
||||
|
||||
let timeout_ms = u64::try_from(
|
||||
resolve_job_timeout(conn, w_id, job_id, timeout)
|
||||
.await
|
||||
.0
|
||||
.as_millis(),
|
||||
)
|
||||
.unwrap_or(200_000);
|
||||
|
||||
let mut result_f = Box::pin(result_f);
|
||||
|
||||
let outcome = tokio::select! {
|
||||
biased;
|
||||
result = tokio::time::timeout(
|
||||
std::time::Duration::from_millis(timeout_ms),
|
||||
&mut result_f,
|
||||
) => {
|
||||
match result {
|
||||
Ok(Ok(v)) => GracefulPollOutcome::Ok(v),
|
||||
Ok(Err(e)) => return Err(e),
|
||||
Err(_) => GracefulPollOutcome::Timeout(timeout_ms),
|
||||
}
|
||||
}
|
||||
ex = &mut update_job, if job_id != Uuid::nil() => {
|
||||
match ex {
|
||||
UpdateJobPollingExit::Done(cb) => {
|
||||
let _ = cancel_tx.send(true);
|
||||
match tokio::time::timeout(grace_period, &mut result_f).await {
|
||||
Ok(_) => GracefulPollOutcome::Cancelled { canceled_by: cb },
|
||||
Err(_) => GracefulPollOutcome::CancelledTimeout { canceled_by: cb },
|
||||
}
|
||||
}
|
||||
UpdateJobPollingExit::AlreadyCompleted => GracefulPollOutcome::AlreadyCompleted,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
drop(tx);
|
||||
Ok(outcome)
|
||||
}
|
||||
|
||||
pub enum UpdateJobPollingExit {
|
||||
Done(Option<CanceledBy>),
|
||||
AlreadyCompleted,
|
||||
|
||||
@@ -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.660.0";
|
||||
export const VERSION = "v1.659.1";
|
||||
|
||||
export async function login(email: string, password: string): Promise<string> {
|
||||
return await windmill.UserService.login({
|
||||
|
||||
@@ -16,34 +16,33 @@ import { resolveWorkspace } from "../../core/context.ts";
|
||||
import {
|
||||
SyncOptions,
|
||||
mergeConfigWithConfigFile,
|
||||
readConfigFile,
|
||||
} from "../../core/conf.ts";
|
||||
import { exts, removeExtensionToPath } from "../script/script.ts";
|
||||
import { inferContentTypeFromFilePath } from "../../utils/script_common.ts";
|
||||
import { OpenFlow } from "../../../gen/types.gen.ts";
|
||||
import { FlowFile } from "../flow/flow.ts";
|
||||
import { replaceInlineScripts, replaceAllPathScriptsWithLocal } from "../../../windmill-utils-internal/src/inline-scripts/replacer.ts";
|
||||
import { replaceInlineScripts } from "../../../windmill-utils-internal/src/inline-scripts/replacer.ts";
|
||||
import { parseMetadataFile } from "../../utils/metadata.ts";
|
||||
import {
|
||||
getFolderSuffixWithSep,
|
||||
getMetadataFileName,
|
||||
extractFolderPath,
|
||||
} from "../../utils/resource_folders.ts";
|
||||
import { listSyncCodebases } from "../../utils/codebase.ts";
|
||||
import { createPreviewLocalScriptReader } from "../../utils/local_path_scripts.ts";
|
||||
|
||||
const PORT = 3001;
|
||||
async function dev(opts: GlobalOptions & SyncOptions) {
|
||||
opts = await mergeConfigWithConfigFile(opts);
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
log.info("Started dev mode");
|
||||
const conf = await readConfigFile();
|
||||
let currentLastEdit: LastEditScript | LastEditFlow | undefined = undefined;
|
||||
|
||||
const fsWatcher = watch(".", { recursive: true });
|
||||
const base = await realpath(".");
|
||||
opts = await mergeConfigWithConfigFile(opts);
|
||||
const ignore = await ignoreF(opts);
|
||||
const codebases = await listSyncCodebases(opts);
|
||||
|
||||
const changesTimeouts: Record<string, ReturnType<typeof setTimeout>> = {};
|
||||
function watchChanges() {
|
||||
@@ -57,11 +56,7 @@ async function dev(opts: GlobalOptions & SyncOptions) {
|
||||
}
|
||||
changesTimeouts[key] = setTimeout(async () => {
|
||||
delete changesTimeouts[key];
|
||||
await loadPaths([filePath]).catch((error) => {
|
||||
log.error(
|
||||
`Failed to reload ${filePath}: ${error instanceof Error ? error.message : error}`
|
||||
);
|
||||
});
|
||||
await loadPaths([filePath]);
|
||||
}, 100);
|
||||
});
|
||||
fsWatcher.on("error", (err) => {
|
||||
@@ -99,13 +94,6 @@ async function dev(opts: GlobalOptions & SyncOptions) {
|
||||
SEP,
|
||||
undefined,
|
||||
);
|
||||
// Replace PathScript modules with local file content so dev mode uses local versions
|
||||
const localScriptReader = createPreviewLocalScriptReader({
|
||||
exts,
|
||||
defaultTs: opts.defaultTs,
|
||||
codebases,
|
||||
});
|
||||
await replaceAllPathScriptsWithLocal(localFlow.value, localScriptReader, log);
|
||||
currentLastEdit = {
|
||||
type: "flow",
|
||||
flow: localFlow,
|
||||
@@ -117,7 +105,7 @@ async function dev(opts: GlobalOptions & SyncOptions) {
|
||||
const content = await readFile(cpath, "utf-8");
|
||||
const splitted = cpath.split(".");
|
||||
const wmPath = splitted[0];
|
||||
const lang = inferContentTypeFromFilePath(cpath, opts.defaultTs);
|
||||
const lang = inferContentTypeFromFilePath(cpath, conf.defaultTs);
|
||||
const typed =
|
||||
(await parseMetadataFile(
|
||||
removeExtensionToPath(cpath),
|
||||
|
||||
@@ -18,19 +18,8 @@ import { defaultFlowDefinition } from "../../../bootstrap/flow_bootstrap.ts";
|
||||
import { SyncOptions, mergeConfigWithConfigFile } from "../../core/conf.ts";
|
||||
import { FSFSElement, elementsToMap, ignoreF } from "../sync/sync.ts";
|
||||
import { Flow } from "../../../gen/types.gen.ts";
|
||||
import {
|
||||
collectPathScriptPaths,
|
||||
replaceInlineScripts,
|
||||
replaceAllPathScriptsWithLocal,
|
||||
} from "../../../windmill-utils-internal/src/inline-scripts/replacer.ts";
|
||||
import { replaceInlineScripts } from "../../../windmill-utils-internal/src/inline-scripts/replacer.ts";
|
||||
import { generateFlowLockInternal } from "./flow_metadata.ts";
|
||||
import { exts } from "../script/script.ts";
|
||||
import type { SyncCodebase } from "../../utils/codebase.ts";
|
||||
import { listSyncCodebases } from "../../utils/codebase.ts";
|
||||
import {
|
||||
createPreviewLocalScriptReader,
|
||||
resolvePreviewLocalScriptState,
|
||||
} from "../../utils/local_path_scripts.ts";
|
||||
|
||||
export interface FlowFile {
|
||||
summary: string;
|
||||
@@ -39,90 +28,6 @@ export interface FlowFile {
|
||||
schema?: any;
|
||||
}
|
||||
|
||||
function normalizeOptionalString(value: string | null | undefined): string | undefined {
|
||||
return typeof value === "string" && value.trim() === "" ? undefined : value ?? undefined;
|
||||
}
|
||||
|
||||
function normalizeComparableContent(value: string | undefined): string | undefined {
|
||||
return value?.replaceAll("\r\n", "\n").replace(/\n$/, "");
|
||||
}
|
||||
|
||||
async function findDivergedLocalPathScripts(
|
||||
workspaceId: string,
|
||||
scriptPaths: string[],
|
||||
opts: {
|
||||
exts: string[];
|
||||
defaultTs?: "bun" | "deno";
|
||||
codebases: SyncCodebase[];
|
||||
}
|
||||
): Promise<{ changed: string[]; missing: string[] }> {
|
||||
const changed: string[] = [];
|
||||
const missing: string[] = [];
|
||||
|
||||
for (const scriptPath of scriptPaths) {
|
||||
const localScript = await resolvePreviewLocalScriptState(scriptPath, opts);
|
||||
if (!localScript) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let remoteScript;
|
||||
try {
|
||||
remoteScript = await wmill.getScriptByPath({
|
||||
workspace: workspaceId,
|
||||
path: scriptPath,
|
||||
});
|
||||
} catch {
|
||||
missing.push(scriptPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
const remoteLock = normalizeOptionalString(remoteScript.lock);
|
||||
const diverged =
|
||||
normalizeComparableContent(localScript.content) !==
|
||||
normalizeComparableContent(remoteScript.content) ||
|
||||
localScript.language !== remoteScript.language ||
|
||||
(localScript.lock !== undefined &&
|
||||
normalizeComparableContent(localScript.lock) !==
|
||||
normalizeComparableContent(remoteLock)) ||
|
||||
localScript.tag !== normalizeOptionalString(remoteScript.tag) ||
|
||||
localScript.codebaseDigest !== normalizeOptionalString(remoteScript.codebase);
|
||||
|
||||
if (diverged) {
|
||||
changed.push(scriptPath);
|
||||
}
|
||||
}
|
||||
|
||||
return { changed, missing };
|
||||
}
|
||||
|
||||
function warnAboutLocalPathScriptDivergence(
|
||||
divergence: { changed: string[]; missing: string[] }
|
||||
): void {
|
||||
if (divergence.changed.length === 0 && divergence.missing.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const details: string[] = [];
|
||||
if (divergence.changed.length > 0) {
|
||||
details.push(
|
||||
`These workspace scripts differ from the deployed version:\n${divergence.changed
|
||||
.map((path) => `- ${path}`)
|
||||
.join("\n")}`
|
||||
);
|
||||
}
|
||||
if (divergence.missing.length > 0) {
|
||||
details.push(
|
||||
`These scripts do not exist in the workspace yet:\n${divergence.missing
|
||||
.map((path) => `- ${path}`)
|
||||
.join("\n")}`
|
||||
);
|
||||
}
|
||||
|
||||
log.warn(
|
||||
`Using local PathScript files for flow preview.\n${details.join("\n")}\nUse --remote to preview deployed workspace scripts instead.`
|
||||
);
|
||||
}
|
||||
|
||||
const alreadySynced: string[] = [];
|
||||
|
||||
export async function pushFlow(
|
||||
@@ -328,17 +233,11 @@ async function preview(
|
||||
opts: GlobalOptions & {
|
||||
data?: string;
|
||||
silent: boolean;
|
||||
remote?: boolean;
|
||||
} & SyncOptions,
|
||||
},
|
||||
flowPath: string
|
||||
) {
|
||||
const useLocalPathScripts = !opts.remote;
|
||||
if (useLocalPathScripts) {
|
||||
opts = await mergeConfigWithConfigFile(opts);
|
||||
}
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
const codebases = useLocalPathScripts ? listSyncCodebases(opts) : [];
|
||||
|
||||
// Normalize path - ensure it's a directory path to a .flow folder
|
||||
if (!flowPath.endsWith(".flow") && !flowPath.endsWith(".flow" + SEP)) {
|
||||
@@ -375,31 +274,6 @@ async function preview(
|
||||
await replaceInlineScripts([localFlow.value.preprocessor_module], fileReader, log, flowPath, SEP);
|
||||
}
|
||||
|
||||
if (useLocalPathScripts) {
|
||||
const scriptPaths = collectPathScriptPaths(localFlow.value);
|
||||
if (scriptPaths.length > 0) {
|
||||
const divergence = await findDivergedLocalPathScripts(
|
||||
workspace.workspaceId,
|
||||
scriptPaths,
|
||||
{
|
||||
exts,
|
||||
defaultTs: opts.defaultTs,
|
||||
codebases,
|
||||
}
|
||||
);
|
||||
if (!opts.silent) {
|
||||
warnAboutLocalPathScriptDivergence(divergence);
|
||||
}
|
||||
}
|
||||
|
||||
const localScriptReader = createPreviewLocalScriptReader({
|
||||
exts,
|
||||
defaultTs: opts.defaultTs,
|
||||
codebases,
|
||||
});
|
||||
await replaceAllPathScriptsWithLocal(localFlow.value, localScriptReader, log);
|
||||
}
|
||||
|
||||
const input = opts.data ? await resolve(opts.data) : {};
|
||||
|
||||
if (!opts.silent) {
|
||||
@@ -570,7 +444,7 @@ const command = new Command()
|
||||
.action(run as any)
|
||||
.command(
|
||||
"preview",
|
||||
"preview a local flow without deploying it. Runs the flow definition from local files and uses local PathScripts by default."
|
||||
"preview a local flow without deploying it. Runs the flow definition from local files."
|
||||
)
|
||||
.arguments("<flow_path:string>")
|
||||
.option(
|
||||
@@ -581,10 +455,6 @@ const command = new Command()
|
||||
"-s --silent",
|
||||
"Do not output anything other then the final output. Useful for scripting."
|
||||
)
|
||||
.option(
|
||||
"--remote",
|
||||
"Use deployed workspace scripts for PathScript steps instead of local files."
|
||||
)
|
||||
.action(preview as any)
|
||||
.command(
|
||||
"generate-locks",
|
||||
|
||||
@@ -5039,10 +5039,9 @@ flow related commands
|
||||
- \`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.
|
||||
- \`flow preview <flow_path:string>\` - preview a local flow without deploying it. Runs the flow definition from local files and uses local PathScripts by default.
|
||||
- \`flow preview <flow_path:string>\` - preview a local flow without deploying it. Runs the flow definition from local files.
|
||||
- \`-d --data <data:string>\` - Inputs specified as a JSON string or a file using @<filename> or stdin using @-.
|
||||
- \`-s --silent\` - Do not output anything other then the final output. Useful for scripting.
|
||||
- \`--remote\` - Use deployed workspace scripts for PathScript steps instead of local files.
|
||||
- \`flow generate-locks [flow:file]\` - re-generate the lock files of all inline scripts of all updated flows
|
||||
- \`--yes\` - Skip confirmation prompt
|
||||
- \`-i --includes <patterns:file[]>\` - Comma separated patterns to specify which file to take into account (among files that are compatible with windmill). Patterns can include * (any string until '/') and ** (any string)
|
||||
|
||||
@@ -68,7 +68,7 @@ export {
|
||||
workspaceAdd,
|
||||
};
|
||||
|
||||
export const VERSION = "1.660.0";
|
||||
export const VERSION = "1.659.1";
|
||||
|
||||
// Re-exported from constants.ts to maintain backwards compatibility
|
||||
export { WM_FORK_PREFIX } from "./core/constants.ts";
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { readFile, stat } from "node:fs/promises";
|
||||
import type { SyncCodebase } from "./codebase.ts";
|
||||
import { parseMetadataFileIfExists } from "./metadata.ts";
|
||||
import { inferContentTypeFromFilePath } from "./script_common.ts";
|
||||
import { findCodebase } from "../commands/sync/sync.ts";
|
||||
import type { LocalScriptInfo } from "../../windmill-utils-internal/src/inline-scripts/replacer.ts";
|
||||
import type { RawScript } from "../../../gen/types.gen.ts";
|
||||
|
||||
export class UnsupportedLocalPathScriptPreviewError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "UnsupportedLocalPathScriptPreviewError";
|
||||
}
|
||||
}
|
||||
|
||||
async function readOptionalLock(scriptPath: string): Promise<string | undefined> {
|
||||
try {
|
||||
return await readFile(scriptPath + ".script.lock", "utf-8");
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeOptionalLock(lock: string | undefined): string | undefined {
|
||||
return typeof lock === "string" && lock.trim() === "" ? undefined : lock;
|
||||
}
|
||||
|
||||
async function bundleSingleFileCodebaseScript(
|
||||
filePath: string,
|
||||
codebase: SyncCodebase
|
||||
): Promise<string> {
|
||||
if (codebase.customBundler) {
|
||||
// Pass the script path as a positional shell argument so existing shell-based
|
||||
// custom bundlers still work without interpolating the path into the command.
|
||||
return execFileSync(
|
||||
"sh",
|
||||
["-lc", `${codebase.customBundler} "$1"`, "sh", filePath],
|
||||
{
|
||||
maxBuffer: 1024 * 1024 * 50,
|
||||
}
|
||||
).toString();
|
||||
}
|
||||
|
||||
const esbuild = await import("esbuild");
|
||||
const out = await esbuild.build({
|
||||
entryPoints: [filePath],
|
||||
// Inline rawscripts are executed through the standard module wrapper,
|
||||
// so the bundle must expose `main` as an ESM export.
|
||||
format: "esm",
|
||||
bundle: true,
|
||||
write: false,
|
||||
external: codebase.external,
|
||||
inject: codebase.inject,
|
||||
define: codebase.define,
|
||||
loader: codebase.loader ?? { ".node": "file" },
|
||||
outdir: "/",
|
||||
platform: "node",
|
||||
packages: "bundle",
|
||||
target: "esnext",
|
||||
banner: codebase.banner,
|
||||
});
|
||||
|
||||
if (out.outputFiles.length === 0) {
|
||||
throw new Error(`No output files found for ${filePath}`);
|
||||
}
|
||||
if (out.outputFiles.length > 1) {
|
||||
throw new UnsupportedLocalPathScriptPreviewError(
|
||||
`Local PathScript ${filePath} requires a multi-file bundle, which flow preview/dev cannot inline yet`
|
||||
);
|
||||
}
|
||||
if (Array.isArray(codebase.assets) && codebase.assets.length > 0) {
|
||||
throw new UnsupportedLocalPathScriptPreviewError(
|
||||
`Local PathScript ${filePath} requires codebase assets, which flow preview/dev cannot inline yet`
|
||||
);
|
||||
}
|
||||
|
||||
return out.outputFiles[0].text;
|
||||
}
|
||||
|
||||
export function createPreviewLocalScriptReader(opts: {
|
||||
exts: string[];
|
||||
defaultTs?: "bun" | "deno";
|
||||
codebases: SyncCodebase[];
|
||||
}): (scriptPath: string) => Promise<LocalScriptInfo | undefined> {
|
||||
return async (scriptPath) => {
|
||||
const localScript = await resolvePreviewLocalScriptState(scriptPath, opts);
|
||||
if (!localScript) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const content = localScript.codebase
|
||||
? await bundleSingleFileCodebaseScript(localScript.filePath, localScript.codebase)
|
||||
: localScript.content;
|
||||
|
||||
return {
|
||||
content,
|
||||
language: localScript.language,
|
||||
lock: localScript.lock,
|
||||
tag: localScript.tag,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export type PreviewLocalScriptState = {
|
||||
filePath: string;
|
||||
content: string;
|
||||
language: RawScript["language"];
|
||||
lock?: string;
|
||||
tag?: string;
|
||||
codebase?: SyncCodebase;
|
||||
codebaseDigest?: string;
|
||||
};
|
||||
|
||||
export async function resolvePreviewLocalScriptState(
|
||||
scriptPath: string,
|
||||
opts: {
|
||||
exts: string[];
|
||||
defaultTs?: "bun" | "deno";
|
||||
codebases: SyncCodebase[];
|
||||
}
|
||||
): Promise<PreviewLocalScriptState | undefined> {
|
||||
for (const ext of opts.exts) {
|
||||
const filePath = scriptPath + ext;
|
||||
let fileStat;
|
||||
try {
|
||||
fileStat = await stat(filePath);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
if (!fileStat.isFile()) continue;
|
||||
|
||||
const language = inferContentTypeFromFilePath(filePath, opts.defaultTs);
|
||||
const metadata = await parseMetadataFileIfExists(scriptPath);
|
||||
const rawLock = metadata?.payload?.lock ?? (await readOptionalLock(scriptPath));
|
||||
const codebase =
|
||||
language === "bun" ? findCodebase(filePath, opts.codebases) : undefined;
|
||||
|
||||
return {
|
||||
filePath,
|
||||
content: await readFile(filePath, "utf-8"),
|
||||
language,
|
||||
lock: normalizeOptionalLock(rawLock),
|
||||
tag: metadata?.payload?.tag,
|
||||
codebase,
|
||||
codebaseDigest: codebase
|
||||
? await codebase.getDigest(
|
||||
Array.isArray(codebase.assets) && codebase.assets.length > 0
|
||||
)
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
@@ -921,38 +921,6 @@ export function replaceLock(o?: { lock?: string | string[] }) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function parseMetadataFileIfExists(
|
||||
scriptPath: string
|
||||
): Promise<{ isJson: boolean; payload: any; path: string } | undefined> {
|
||||
let metadataFilePath = scriptPath + ".script.json";
|
||||
try {
|
||||
await stat(metadataFilePath);
|
||||
const payload = JSON.parse(await readFile(metadataFilePath, "utf-8"));
|
||||
replaceLock(payload);
|
||||
return {
|
||||
path: metadataFilePath,
|
||||
payload,
|
||||
isJson: true,
|
||||
};
|
||||
} catch {
|
||||
try {
|
||||
metadataFilePath = scriptPath + ".script.yaml";
|
||||
await stat(metadataFilePath);
|
||||
const payload: any = await yamlParseFile(metadataFilePath);
|
||||
replaceLock(payload);
|
||||
|
||||
return {
|
||||
path: metadataFilePath,
|
||||
payload,
|
||||
isJson: false,
|
||||
};
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function parseMetadataFile(
|
||||
scriptPath: string,
|
||||
generateMetadataIfMissing:
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { withTestBackend } from "./test_backend.ts";
|
||||
import { shouldSkipOnCI } from "./cargo_backend.ts";
|
||||
|
||||
// =============================================================================
|
||||
// PREVIEW COMMAND INTEGRATION TESTS
|
||||
@@ -28,7 +27,6 @@ async function createWmillConfig(
|
||||
relative_path: string;
|
||||
includes?: string[];
|
||||
format?: "cjs" | "esm";
|
||||
customBundler?: string;
|
||||
assets?: Array<{ from: string; to: string }>;
|
||||
}>;
|
||||
}
|
||||
@@ -46,9 +44,6 @@ async function createWmillConfig(
|
||||
if (cb.format) {
|
||||
yamlContent += ` format: ${cb.format}\n`;
|
||||
}
|
||||
if (cb.customBundler) {
|
||||
yamlContent += ` customBundler: ${JSON.stringify(cb.customBundler)}\n`;
|
||||
}
|
||||
if (cb.assets && cb.assets.length > 0) {
|
||||
yamlContent += " assets:\n";
|
||||
for (const asset of cb.assets) {
|
||||
@@ -127,39 +122,6 @@ schema:
|
||||
await writeFile(`${dir}/flow.yaml`, flowYaml, "utf-8");
|
||||
}
|
||||
|
||||
async function createPathScriptFlow(
|
||||
tempDir: string,
|
||||
flowPath: string,
|
||||
options: {
|
||||
summary: string;
|
||||
scriptPath: string;
|
||||
inputTransforms?: string;
|
||||
}
|
||||
): Promise<void> {
|
||||
const dir = `${tempDir}/${flowPath}`;
|
||||
await mkdir(dir, { recursive: true });
|
||||
const inputTransforms = options.inputTransforms
|
||||
? ` input_transforms:\n${options.inputTransforms}`
|
||||
: " input_transforms: {}\n";
|
||||
|
||||
const flowYaml = `summary: "${options.summary}"
|
||||
description: "Test flow"
|
||||
value:
|
||||
modules:
|
||||
- id: "a"
|
||||
value:
|
||||
type: "script"
|
||||
path: "${options.scriptPath}"
|
||||
${inputTransforms}
|
||||
schema:
|
||||
$schema: "https://json-schema.org/draft/2020-12/schema"
|
||||
type: object
|
||||
properties: {}
|
||||
required: []
|
||||
`;
|
||||
await writeFile(`${dir}/flow.yaml`, flowYaml, "utf-8");
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SCRIPT PREVIEW TESTS
|
||||
// =============================================================================
|
||||
@@ -529,280 +491,3 @@ test("flow preview: simple flow", async () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("flow preview: uses local PathScript by default and remote PathScript with --remote", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await createWmillConfig(tempDir, { defaultTs: "bun" });
|
||||
|
||||
await createScript(
|
||||
tempDir,
|
||||
"f/test/helper_script.ts",
|
||||
`export function main(name: string = "World") { return \`Remote script says: \${name}!\`; }`
|
||||
);
|
||||
|
||||
const pushResult = await backend.runCLICommand(
|
||||
["script", "push", "f/test/helper_script.ts"],
|
||||
tempDir
|
||||
);
|
||||
expect(pushResult.code).toEqual(0);
|
||||
|
||||
await writeFile(
|
||||
`${tempDir}/f/test/helper_script.ts`,
|
||||
`export function main(name: string = "World") { return \`Local script says: \${name}!\`; }`,
|
||||
"utf-8"
|
||||
);
|
||||
|
||||
await createPathScriptFlow(tempDir, "f/test/path_flow.flow", {
|
||||
summary: "Flow with PathScript",
|
||||
scriptPath: "f/test/helper_script",
|
||||
inputTransforms: ` name:
|
||||
type: "static"
|
||||
value: "PathTest"
|
||||
`,
|
||||
});
|
||||
|
||||
const localResult = await backend.runCLICommand(
|
||||
["flow", "preview", "f/test/path_flow.flow"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(localResult.code).toEqual(0);
|
||||
expect(localResult.stdout + localResult.stderr).toContain(
|
||||
"Local script says: PathTest!"
|
||||
);
|
||||
expect(localResult.stdout + localResult.stderr).toContain(
|
||||
"Using local PathScript files for flow preview."
|
||||
);
|
||||
expect(localResult.stdout + localResult.stderr).toContain(
|
||||
"These workspace scripts differ from the deployed version:\n- f/test/helper_script"
|
||||
);
|
||||
|
||||
const remoteResult = await backend.runCLICommand(
|
||||
["flow", "preview", "--remote", "f/test/path_flow.flow"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(remoteResult.code).toEqual(0);
|
||||
expect(remoteResult.stdout + remoteResult.stderr).toContain(
|
||||
"Remote script says: PathTest!"
|
||||
);
|
||||
expect(remoteResult.stdout + remoteResult.stderr).not.toContain(
|
||||
"Using local PathScript files for flow preview."
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test.skipIf(shouldSkipOnCI())("flow preview: respects defaultTs when resolving local PathScripts", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await createWmillConfig(tempDir, { defaultTs: "deno" });
|
||||
|
||||
await createScript(
|
||||
tempDir,
|
||||
"f/test/deno_helper.ts",
|
||||
`export function main() { return Deno.version.deno ? "deno-runtime" : "missing"; }`
|
||||
);
|
||||
|
||||
await createPathScriptFlow(tempDir, "f/test/deno_path_flow.flow", {
|
||||
summary: "Flow with Deno PathScript",
|
||||
scriptPath: "f/test/deno_helper",
|
||||
});
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["flow", "preview", "f/test/deno_path_flow.flow"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
expect(result.stdout + result.stderr).toContain("deno-runtime");
|
||||
});
|
||||
});
|
||||
|
||||
test("flow preview: bundles local PathScripts with local imports", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await createWmillConfig(tempDir, {
|
||||
defaultTs: "bun",
|
||||
codebases: [{ relative_path: "f/flow_codebase", includes: ["**"] }],
|
||||
});
|
||||
|
||||
await mkdir(`${tempDir}/f/flow_codebase`, { recursive: true });
|
||||
await writeFile(
|
||||
`${tempDir}/f/flow_codebase/helper.ts`,
|
||||
`export function greet(name: string): string {
|
||||
return \`Hello from local flow codebase, \${name}!\`;
|
||||
}`,
|
||||
"utf-8"
|
||||
);
|
||||
|
||||
await createScript(
|
||||
tempDir,
|
||||
"f/flow_codebase/main_script.ts",
|
||||
`import { greet } from "./helper";
|
||||
|
||||
export function main(name: string = "World") {
|
||||
return greet(name);
|
||||
}`
|
||||
);
|
||||
|
||||
await createPathScriptFlow(tempDir, "f/test/importing_path_flow.flow", {
|
||||
summary: "Flow with imported PathScript",
|
||||
scriptPath: "f/flow_codebase/main_script",
|
||||
inputTransforms: ` name:
|
||||
type: "static"
|
||||
value: "FlowTest"
|
||||
`,
|
||||
});
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["flow", "preview", "f/test/importing_path_flow.flow"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
expect(result.stdout + result.stderr).toContain(
|
||||
"Hello from local flow codebase, FlowTest!"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test("flow preview: customBundler handles script paths with spaces", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await createWmillConfig(tempDir, {
|
||||
defaultTs: "bun",
|
||||
codebases: [{
|
||||
relative_path: "f/codebase custom",
|
||||
includes: ["f/codebase custom/**"],
|
||||
customBundler: "cat",
|
||||
}],
|
||||
});
|
||||
|
||||
await createScript(
|
||||
tempDir,
|
||||
"f/codebase custom/custom bundler.ts",
|
||||
`export function main() {
|
||||
return "Custom bundler path with spaces";
|
||||
}`
|
||||
);
|
||||
|
||||
await createPathScriptFlow(tempDir, "f/test/custom_bundler_path.flow", {
|
||||
summary: "Flow with customBundler path",
|
||||
scriptPath: "f/codebase custom/custom bundler",
|
||||
});
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["flow", "preview", "f/test/custom_bundler_path.flow"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
expect(result.stdout + result.stderr).toContain(
|
||||
"Custom bundler path with spaces"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test("flow preview: warns when local PathScript is not deployed remotely", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await createWmillConfig(tempDir, { defaultTs: "bun" });
|
||||
|
||||
await createScript(
|
||||
tempDir,
|
||||
"f/test/undeployed_helper.ts",
|
||||
`export function main() { return "Local only script"; }`
|
||||
);
|
||||
|
||||
await createPathScriptFlow(tempDir, "f/test/undeployed_path_flow.flow", {
|
||||
summary: "Flow with undeployed PathScript",
|
||||
scriptPath: "f/test/undeployed_helper",
|
||||
});
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["flow", "preview", "f/test/undeployed_path_flow.flow"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
expect(result.stdout + result.stderr).toContain("Local only script");
|
||||
expect(result.stdout + result.stderr).toContain(
|
||||
"These scripts do not exist in the workspace yet:\n- f/test/undeployed_helper"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test("flow preview: does not warn when local and deployed PathScripts match", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await createWmillConfig(tempDir, { defaultTs: "bun" });
|
||||
|
||||
await createScript(
|
||||
tempDir,
|
||||
"f/test/matching_helper.ts",
|
||||
`export function main() { return "Matching script"; }`
|
||||
);
|
||||
|
||||
const pushResult = await backend.runCLICommand(
|
||||
["script", "push", "f/test/matching_helper.ts"],
|
||||
tempDir
|
||||
);
|
||||
expect(pushResult.code).toEqual(0);
|
||||
|
||||
await createPathScriptFlow(tempDir, "f/test/matching_path_flow.flow", {
|
||||
summary: "Flow with matching PathScript",
|
||||
scriptPath: "f/test/matching_helper",
|
||||
});
|
||||
|
||||
const result = await backend.runCLICommand(
|
||||
["flow", "preview", "f/test/matching_path_flow.flow"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(result.code).toEqual(0);
|
||||
expect(result.stdout + result.stderr).toContain("Matching script");
|
||||
expect(result.stdout + result.stderr).not.toContain(
|
||||
"Using local PathScript files for flow preview."
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test("flow preview: fails loudly for asset-backed codebase scripts", async () => {
|
||||
await withTestBackend(async (backend, tempDir) => {
|
||||
await createWmillConfig(tempDir, {
|
||||
defaultTs: "bun",
|
||||
codebases: [{
|
||||
relative_path: "f/codebase_tar",
|
||||
includes: ["**"],
|
||||
assets: [{ from: "f/codebase_tar/data.json", to: "data.json" }],
|
||||
}],
|
||||
});
|
||||
|
||||
await mkdir(`${tempDir}/f/codebase_tar`, { recursive: true });
|
||||
await writeFile(
|
||||
`${tempDir}/f/codebase_tar/data.json`,
|
||||
JSON.stringify({ message: "Hello from asset!" }),
|
||||
"utf-8"
|
||||
);
|
||||
|
||||
await createScript(
|
||||
tempDir,
|
||||
"f/codebase_tar/main_script.ts",
|
||||
`import * as fs from "fs";
|
||||
|
||||
export function main() {
|
||||
const data = JSON.parse(fs.readFileSync("data.json", "utf-8"));
|
||||
return data.message;
|
||||
}`
|
||||
);
|
||||
|
||||
await createPathScriptFlow(tempDir, "f/test/assets_path_flow.flow", {
|
||||
summary: "Flow with asset-backed PathScript",
|
||||
scriptPath: "f/codebase_tar/main_script",
|
||||
});
|
||||
|
||||
const localResult = await backend.runCLICommand(
|
||||
["flow", "preview", "f/test/assets_path_flow.flow"],
|
||||
tempDir
|
||||
);
|
||||
|
||||
expect(localResult.code).not.toEqual(0);
|
||||
expect(localResult.stdout + localResult.stderr).toContain(
|
||||
"requires codebase assets"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,322 +0,0 @@
|
||||
/**
|
||||
* Unit tests for replacePathScriptsWithLocal.
|
||||
*
|
||||
* Tests that PathScript ("script" type) modules are correctly converted
|
||||
* to RawScript ("rawscript" type) using local file content during
|
||||
* flow preview / dev mode.
|
||||
*/
|
||||
|
||||
import { expect, test, describe } from "bun:test";
|
||||
import { replacePathScriptsWithLocal, type LocalScriptInfo } from "../windmill-utils-internal/src/inline-scripts/replacer.ts";
|
||||
import type { FlowModule } from "../windmill-utils-internal/src/gen/types.gen.ts";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function makePathScriptModule(
|
||||
id: string,
|
||||
path: string,
|
||||
inputTransforms: Record<string, any> = {},
|
||||
tagOverride?: string,
|
||||
): FlowModule {
|
||||
return {
|
||||
id,
|
||||
value: {
|
||||
type: "script" as const,
|
||||
path,
|
||||
input_transforms: inputTransforms,
|
||||
tag_override: tagOverride,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function makeRawscriptModule(
|
||||
id: string,
|
||||
content: string,
|
||||
language: "bun" | "python3" | "deno" = "bun",
|
||||
): FlowModule {
|
||||
return {
|
||||
id,
|
||||
value: {
|
||||
type: "rawscript" as const,
|
||||
content,
|
||||
language,
|
||||
input_transforms: {},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const noopLogger = {
|
||||
info: () => {},
|
||||
error: () => {},
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Basic conversion tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("replacePathScriptsWithLocal", () => {
|
||||
test("converts PathScript to RawScript when local file exists", async () => {
|
||||
const module = makePathScriptModule("a", "f/scripts/my_script", {
|
||||
x: { type: "static", value: 42 },
|
||||
});
|
||||
|
||||
const scriptReader = async (scriptPath: string): Promise<LocalScriptInfo | undefined> => {
|
||||
if (scriptPath === "f/scripts/my_script") {
|
||||
return {
|
||||
content: 'export function main() { return "hello"; }',
|
||||
language: "bun",
|
||||
lock: "some-lock",
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
await replacePathScriptsWithLocal([module], scriptReader, noopLogger);
|
||||
|
||||
expect(module.value.type).toBe("rawscript");
|
||||
expect((module.value as any).content).toBe('export function main() { return "hello"; }');
|
||||
expect((module.value as any).language).toBe("bun");
|
||||
expect((module.value as any).lock).toBe("some-lock");
|
||||
expect((module.value as any).path).toBe("f/scripts/my_script");
|
||||
expect((module.value as any).input_transforms).toEqual({ x: { type: "static", value: 42 } });
|
||||
});
|
||||
|
||||
test("preserves tag_override as tag", async () => {
|
||||
const module = makePathScriptModule("a", "f/scripts/tagged", {}, "my-worker");
|
||||
|
||||
const scriptReader = async (scriptPath: string): Promise<LocalScriptInfo | undefined> => {
|
||||
if (scriptPath === "f/scripts/tagged") {
|
||||
return { content: "code", language: "python3", tag: "script-worker" };
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
await replacePathScriptsWithLocal([module], scriptReader, noopLogger);
|
||||
|
||||
expect(module.value.type).toBe("rawscript");
|
||||
expect((module.value as any).tag).toBe("my-worker");
|
||||
});
|
||||
|
||||
test("uses local script tag when no tag_override is set", async () => {
|
||||
const module = makePathScriptModule("a", "f/scripts/tagged");
|
||||
|
||||
const scriptReader = async (scriptPath: string): Promise<LocalScriptInfo | undefined> => {
|
||||
if (scriptPath === "f/scripts/tagged") {
|
||||
return { content: "code", language: "python3", tag: "script-worker" };
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
await replacePathScriptsWithLocal([module], scriptReader, noopLogger);
|
||||
|
||||
expect(module.value.type).toBe("rawscript");
|
||||
expect((module.value as any).tag).toBe("script-worker");
|
||||
});
|
||||
|
||||
test("leaves PathScript untouched when local file not found", async () => {
|
||||
const module = makePathScriptModule("a", "f/scripts/remote_only");
|
||||
|
||||
const scriptReader = async (): Promise<LocalScriptInfo | undefined> => undefined;
|
||||
|
||||
await replacePathScriptsWithLocal([module], scriptReader, noopLogger);
|
||||
|
||||
expect(module.value.type).toBe("script");
|
||||
expect((module.value as any).path).toBe("f/scripts/remote_only");
|
||||
});
|
||||
|
||||
test("does not affect rawscript modules", async () => {
|
||||
const module = makeRawscriptModule("a", "existing code", "bun");
|
||||
|
||||
const scriptReader = async (): Promise<LocalScriptInfo | undefined> => {
|
||||
throw new Error("should not be called for rawscript");
|
||||
};
|
||||
|
||||
await replacePathScriptsWithLocal([module], scriptReader, noopLogger);
|
||||
|
||||
expect(module.value.type).toBe("rawscript");
|
||||
expect((module.value as any).content).toBe("existing code");
|
||||
});
|
||||
|
||||
test("handles mixed PathScript and RawScript modules", async () => {
|
||||
const pathModule = makePathScriptModule("a", "f/scripts/local_script");
|
||||
const rawModule = makeRawscriptModule("b", "inline code", "bun");
|
||||
const remoteModule = makePathScriptModule("c", "f/scripts/remote_only");
|
||||
|
||||
const scriptReader = async (scriptPath: string): Promise<LocalScriptInfo | undefined> => {
|
||||
if (scriptPath === "f/scripts/local_script") {
|
||||
return { content: "local code", language: "python3" };
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
await replacePathScriptsWithLocal(
|
||||
[pathModule, rawModule, remoteModule],
|
||||
scriptReader,
|
||||
noopLogger,
|
||||
);
|
||||
|
||||
// PathScript with local file → converted
|
||||
expect(pathModule.value.type).toBe("rawscript");
|
||||
expect((pathModule.value as any).content).toBe("local code");
|
||||
|
||||
// RawScript → untouched
|
||||
expect(rawModule.value.type).toBe("rawscript");
|
||||
expect((rawModule.value as any).content).toBe("inline code");
|
||||
|
||||
// PathScript without local file → untouched
|
||||
expect(remoteModule.value.type).toBe("script");
|
||||
expect((remoteModule.value as any).path).toBe("f/scripts/remote_only");
|
||||
});
|
||||
|
||||
test("handles module without lock", async () => {
|
||||
const module = makePathScriptModule("a", "f/scripts/no_lock");
|
||||
|
||||
const scriptReader = async (scriptPath: string): Promise<LocalScriptInfo | undefined> => {
|
||||
if (scriptPath === "f/scripts/no_lock") {
|
||||
return { content: "code", language: "go" };
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
await replacePathScriptsWithLocal([module], scriptReader, noopLogger);
|
||||
|
||||
expect(module.value.type).toBe("rawscript");
|
||||
expect((module.value as any).lock).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Nested structure tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("replacePathScriptsWithLocal nested structures", () => {
|
||||
test("processes modules inside forloopflow", async () => {
|
||||
const innerModule = makePathScriptModule("inner", "f/scripts/loop_script");
|
||||
const loopModule: FlowModule = {
|
||||
id: "loop",
|
||||
value: {
|
||||
type: "forloopflow" as const,
|
||||
iterator: { type: "static" as const, value: "" },
|
||||
modules: [innerModule],
|
||||
skip_failures: false,
|
||||
},
|
||||
};
|
||||
|
||||
const scriptReader = async (scriptPath: string): Promise<LocalScriptInfo | undefined> => {
|
||||
if (scriptPath === "f/scripts/loop_script") {
|
||||
return { content: "loop code", language: "bun" };
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
await replacePathScriptsWithLocal([loopModule], scriptReader, noopLogger);
|
||||
|
||||
expect(innerModule.value.type).toBe("rawscript");
|
||||
expect((innerModule.value as any).content).toBe("loop code");
|
||||
});
|
||||
|
||||
test("processes modules inside whileloopflow", async () => {
|
||||
const innerModule = makePathScriptModule("inner", "f/scripts/while_script");
|
||||
const whileModule: FlowModule = {
|
||||
id: "while",
|
||||
value: {
|
||||
type: "whileloopflow" as const,
|
||||
modules: [innerModule],
|
||||
skip_failures: false,
|
||||
},
|
||||
};
|
||||
|
||||
const scriptReader = async (scriptPath: string): Promise<LocalScriptInfo | undefined> => {
|
||||
if (scriptPath === "f/scripts/while_script") {
|
||||
return { content: "while code", language: "bun" };
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
await replacePathScriptsWithLocal([whileModule], scriptReader, noopLogger);
|
||||
|
||||
expect(innerModule.value.type).toBe("rawscript");
|
||||
expect((innerModule.value as any).content).toBe("while code");
|
||||
});
|
||||
|
||||
test("processes modules inside branchall", async () => {
|
||||
const branch1Module = makePathScriptModule("b1", "f/scripts/branch1");
|
||||
const branch2Module = makePathScriptModule("b2", "f/scripts/branch2");
|
||||
const branchAllModule: FlowModule = {
|
||||
id: "branches",
|
||||
value: {
|
||||
type: "branchall" as const,
|
||||
branches: [
|
||||
{ modules: [branch1Module], skip_failure: false },
|
||||
{ modules: [branch2Module], skip_failure: false },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const scriptReader = async (scriptPath: string): Promise<LocalScriptInfo | undefined> => {
|
||||
if (scriptPath === "f/scripts/branch1") {
|
||||
return { content: "branch1 code", language: "bun" };
|
||||
}
|
||||
if (scriptPath === "f/scripts/branch2") {
|
||||
return { content: "branch2 code", language: "python3" };
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
await replacePathScriptsWithLocal([branchAllModule], scriptReader, noopLogger);
|
||||
|
||||
expect(branch1Module.value.type).toBe("rawscript");
|
||||
expect((branch1Module.value as any).content).toBe("branch1 code");
|
||||
expect(branch2Module.value.type).toBe("rawscript");
|
||||
expect((branch2Module.value as any).content).toBe("branch2 code");
|
||||
expect((branch2Module.value as any).language).toBe("python3");
|
||||
});
|
||||
|
||||
test("processes modules inside branchone (branches + default)", async () => {
|
||||
const branchModule = makePathScriptModule("b1", "f/scripts/branch");
|
||||
const defaultModule = makePathScriptModule("d1", "f/scripts/default");
|
||||
const branchOneModule: FlowModule = {
|
||||
id: "branchone",
|
||||
value: {
|
||||
type: "branchone" as const,
|
||||
branches: [
|
||||
{ modules: [branchModule], expr: "true" },
|
||||
],
|
||||
default: [defaultModule],
|
||||
},
|
||||
};
|
||||
|
||||
const scriptReader = async (scriptPath: string): Promise<LocalScriptInfo | undefined> => {
|
||||
if (scriptPath === "f/scripts/branch") {
|
||||
return { content: "branch code", language: "bun" };
|
||||
}
|
||||
if (scriptPath === "f/scripts/default") {
|
||||
return { content: "default code", language: "bash" };
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
await replacePathScriptsWithLocal([branchOneModule], scriptReader, noopLogger);
|
||||
|
||||
expect(branchModule.value.type).toBe("rawscript");
|
||||
expect((branchModule.value as any).content).toBe("branch code");
|
||||
expect(defaultModule.value.type).toBe("rawscript");
|
||||
expect((defaultModule.value as any).content).toBe("default code");
|
||||
expect((defaultModule.value as any).language).toBe("bash");
|
||||
});
|
||||
|
||||
test("handles empty modules array", async () => {
|
||||
const scriptReader = async (): Promise<LocalScriptInfo | undefined> => undefined;
|
||||
// Should not throw
|
||||
await replacePathScriptsWithLocal([], scriptReader, noopLogger);
|
||||
});
|
||||
|
||||
test("handles module with undefined value gracefully", async () => {
|
||||
const module: FlowModule = { id: "x", value: undefined as any };
|
||||
const scriptReader = async (): Promise<LocalScriptInfo | undefined> => undefined;
|
||||
// Should not throw
|
||||
await replacePathScriptsWithLocal([module], scriptReader, noopLogger);
|
||||
});
|
||||
});
|
||||
4
cli/windmill-utils-internal/package-lock.json
generated
4
cli/windmill-utils-internal/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "windmill-utils-internal",
|
||||
"version": "1.3.5",
|
||||
"version": "1.3.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "windmill-utils-internal",
|
||||
"version": "1.3.5",
|
||||
"version": "1.3.4",
|
||||
"license": "Apache 2.0",
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.2.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "windmill-utils-internal",
|
||||
"version": "1.3.5",
|
||||
"version": "1.3.4",
|
||||
"description": "Internal utility functions for Windmill",
|
||||
"main": "dist/cjs/index.js",
|
||||
"module": "dist/esm/index.js",
|
||||
@@ -31,4 +31,4 @@
|
||||
"files": [
|
||||
"dist/**/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
import { AiAgent, FlowModule, FlowValue, RawScript } from "../gen/types.gen.ts";
|
||||
|
||||
export type LocalScriptInfo = {
|
||||
content: string;
|
||||
language: RawScript["language"];
|
||||
lock?: string;
|
||||
tag?: string;
|
||||
};
|
||||
import { FlowModule, RawScript } from "../gen/types.gen.ts";
|
||||
|
||||
async function replaceRawscriptInline(
|
||||
id: string,
|
||||
@@ -125,154 +118,4 @@ export async function replaceInlineScripts(
|
||||
}));
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces PathScript ("script" type) modules with RawScript ("rawscript" type) using local file content.
|
||||
* This is used during flow preview so that local script changes are tested instead of remote versions.
|
||||
*
|
||||
* @param modules - Array of flow modules to process
|
||||
* @param scriptReader - Function that takes a script path and returns local content/language/lock, or undefined if not found locally
|
||||
* @param logger - Logger for info/error messages
|
||||
*/
|
||||
export async function replacePathScriptsWithLocal(
|
||||
modules: FlowModule[],
|
||||
scriptReader: (scriptPath: string) => Promise<LocalScriptInfo | undefined>,
|
||||
logger: {
|
||||
info: (message: string) => void;
|
||||
error: (message: string) => void;
|
||||
} = {
|
||||
info: () => {},
|
||||
error: () => {},
|
||||
}
|
||||
): Promise<void> {
|
||||
await Promise.all(modules.map(async (module) => {
|
||||
if (!module.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (module.value.type === "script") {
|
||||
const scriptPath = module.value.path;
|
||||
const localScript = await scriptReader(scriptPath);
|
||||
if (localScript) {
|
||||
const pathScript = module.value;
|
||||
module.value = {
|
||||
type: "rawscript",
|
||||
content: localScript.content,
|
||||
language: localScript.language,
|
||||
lock: localScript.lock,
|
||||
path: scriptPath,
|
||||
input_transforms: pathScript.input_transforms,
|
||||
tag: pathScript.tag_override ?? localScript.tag,
|
||||
} satisfies RawScript;
|
||||
}
|
||||
} else if (module.value.type === "forloopflow" || module.value.type === "whileloopflow") {
|
||||
await replacePathScriptsWithLocal(module.value.modules, scriptReader, logger);
|
||||
} else if (module.value.type === "branchall") {
|
||||
await Promise.all(module.value.branches.map(async (branch) => {
|
||||
await replacePathScriptsWithLocal(branch.modules, scriptReader, logger);
|
||||
}));
|
||||
} else if (module.value.type === "branchone") {
|
||||
await Promise.all(module.value.branches.map(async (branch) => {
|
||||
await replacePathScriptsWithLocal(branch.modules, scriptReader, logger);
|
||||
}));
|
||||
await replacePathScriptsWithLocal(module.value.default, scriptReader, logger);
|
||||
} else if (module.value.type === "aiagent") {
|
||||
await Promise.all((module.value.tools ?? []).map(async (tool) => {
|
||||
const toolValue = tool.value;
|
||||
if (!toolValue || toolValue.tool_type !== "flowmodule" || toolValue.type !== "script") {
|
||||
return;
|
||||
}
|
||||
const localScript = await scriptReader(toolValue.path);
|
||||
if (localScript) {
|
||||
(tool as AiAgent["tools"][number]).value = {
|
||||
tool_type: "flowmodule",
|
||||
type: "rawscript",
|
||||
content: localScript.content,
|
||||
language: localScript.language,
|
||||
lock: localScript.lock,
|
||||
path: toolValue.path,
|
||||
input_transforms: toolValue.input_transforms,
|
||||
tag: toolValue.tag_override ?? localScript.tag,
|
||||
};
|
||||
}
|
||||
}));
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
function collectPathScriptPathsFromModules(
|
||||
modules: FlowModule[],
|
||||
paths: Set<string>
|
||||
): void {
|
||||
for (const module of modules) {
|
||||
if (!module.value) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (module.value.type === "script") {
|
||||
paths.add(module.value.path);
|
||||
} else if (
|
||||
module.value.type === "forloopflow" ||
|
||||
module.value.type === "whileloopflow"
|
||||
) {
|
||||
collectPathScriptPathsFromModules(module.value.modules, paths);
|
||||
} else if (module.value.type === "branchall") {
|
||||
for (const branch of module.value.branches) {
|
||||
collectPathScriptPathsFromModules(branch.modules, paths);
|
||||
}
|
||||
} else if (module.value.type === "branchone") {
|
||||
for (const branch of module.value.branches) {
|
||||
collectPathScriptPathsFromModules(branch.modules, paths);
|
||||
}
|
||||
collectPathScriptPathsFromModules(module.value.default, paths);
|
||||
} else if (module.value.type === "aiagent") {
|
||||
for (const tool of module.value.tools ?? []) {
|
||||
const toolValue = tool.value;
|
||||
if (
|
||||
toolValue &&
|
||||
toolValue.tool_type === "flowmodule" &&
|
||||
toolValue.type === "script"
|
||||
) {
|
||||
paths.add(toolValue.path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces all PathScript modules in a flow value (modules, failure_module, preprocessor_module)
|
||||
* with RawScript using local file content.
|
||||
*/
|
||||
export async function replaceAllPathScriptsWithLocal(
|
||||
flowValue: FlowValue,
|
||||
scriptReader: (scriptPath: string) => Promise<LocalScriptInfo | undefined>,
|
||||
logger: {
|
||||
info: (message: string) => void;
|
||||
error: (message: string) => void;
|
||||
} = {
|
||||
info: () => {},
|
||||
error: () => {},
|
||||
}
|
||||
): Promise<void> {
|
||||
await replacePathScriptsWithLocal(flowValue.modules, scriptReader, logger);
|
||||
if (flowValue.failure_module) {
|
||||
await replacePathScriptsWithLocal([flowValue.failure_module], scriptReader, logger);
|
||||
}
|
||||
if (flowValue.preprocessor_module) {
|
||||
await replacePathScriptsWithLocal([flowValue.preprocessor_module], scriptReader, logger);
|
||||
}
|
||||
}
|
||||
|
||||
export function collectPathScriptPaths(flowValue: FlowValue): string[] {
|
||||
const paths = new Set<string>();
|
||||
collectPathScriptPathsFromModules(flowValue.modules, paths);
|
||||
if (flowValue.failure_module) {
|
||||
collectPathScriptPathsFromModules([flowValue.failure_module], paths);
|
||||
}
|
||||
if (flowValue.preprocessor_module) {
|
||||
collectPathScriptPathsFromModules([flowValue.preprocessor_module], paths);
|
||||
}
|
||||
return [...paths];
|
||||
}
|
||||
}
|
||||
@@ -1069,14 +1069,11 @@ const server = Bun.serve({
|
||||
|
||||
// Handle WebSocket upgrade with path-based routing
|
||||
if (server.upgrade(req, { data: { path } })) {
|
||||
logger.info(`WS upgrade: ${path}`)
|
||||
return undefined as unknown as Response
|
||||
}
|
||||
|
||||
logger.info(`HTTP ${req.method} ${path}`)
|
||||
|
||||
// Health check endpoint
|
||||
if (path === '/health' || path === '/ws_debug/health') {
|
||||
if (path === '/health') {
|
||||
return new Response(JSON.stringify({
|
||||
status: 'ok',
|
||||
service: 'debugger',
|
||||
@@ -1105,7 +1102,6 @@ const server = Bun.serve({
|
||||
|
||||
// Handle ping test — respond and close immediately
|
||||
if (path === '/ping') {
|
||||
logger.info(`WS ping test`)
|
||||
ws.send(JSON.stringify({ type: 'pong', service: 'debugger' }))
|
||||
ws.close()
|
||||
return
|
||||
|
||||
15
frontend/.workmux.yaml
Normal file
15
frontend/.workmux.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
panes:
|
||||
# Pane 1: Install dependencies, then start dev server
|
||||
- command: npm install && npm run generate-backend-client && npm run dev
|
||||
|
||||
# Pane 2: AI agent
|
||||
- command: <agent>
|
||||
split: horizontal
|
||||
focus: true
|
||||
|
||||
files:
|
||||
copy:
|
||||
- .env
|
||||
|
||||
symlink:
|
||||
- node_modules
|
||||
4
frontend/package-lock.json
generated
4
frontend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "windmill-components",
|
||||
"version": "1.660.0",
|
||||
"version": "1.659.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "windmill-components",
|
||||
"version": "1.660.0",
|
||||
"version": "1.659.1",
|
||||
"hasInstallScript": true,
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "windmill-components",
|
||||
"version": "1.660.0",
|
||||
"version": "1.659.1",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
node ./scripts/untar_ui_builder.js
|
||||
# node ./scripts/untar_ui_builder.js
|
||||
|
||||
mkdir ui_builder_serve || true
|
||||
cp -r static/ui_builder ui_builder_serve/ui_builder || true
|
||||
rm -rf static/ui_builder || true
|
||||
# mkdir ui_builder_serve || true
|
||||
# cp -r static/ui_builder ui_builder_serve/ui_builder || true
|
||||
# rm -rf static/ui_builder || true
|
||||
python3 -c "
|
||||
import os
|
||||
os.chdir('ui_builder_serve')
|
||||
@@ -14,4 +14,4 @@ class H(SimpleHTTPRequestHandler):
|
||||
self.send_header('Cross-Origin-Resource-Policy', 'cross-origin')
|
||||
super().end_headers()
|
||||
HTTPServer(('', 4000), H).serve_forever()
|
||||
"
|
||||
"
|
||||
@@ -63,19 +63,18 @@
|
||||
|
||||
const inputsWithPayload = await Promise.all(
|
||||
inputs.map(async (input) => {
|
||||
const isPreview = !!(input as any).is_preview
|
||||
if (cachedArgs[input.id]) {
|
||||
return {
|
||||
...input,
|
||||
payloadData: cachedArgs[input.id]
|
||||
}
|
||||
}
|
||||
const payloadData = await loadArgsFromHistory(input.id, undefined, false, isPreview)
|
||||
const payloadData = await loadArgsFromHistory(input.id, undefined, false)
|
||||
if (payloadData === 'WINDMILL_TOO_BIG') {
|
||||
return {
|
||||
...input,
|
||||
payloadData: 'WINDMILL_TOO_BIG',
|
||||
getFullPayload: () => loadArgsFromHistory(input.id, undefined, true, isPreview)
|
||||
getFullPayload: () => loadArgsFromHistory(input.id, undefined, true)
|
||||
}
|
||||
}
|
||||
cachedArgs[input.id] = payloadData
|
||||
@@ -94,17 +93,10 @@
|
||||
timeout && clearTimeout(timeout)
|
||||
})
|
||||
|
||||
function filterPreviewArgs(args: any, isPreview: boolean): any {
|
||||
if (!isPreview || !args || typeof args !== 'object' || Array.isArray(args)) return args
|
||||
const { _MODULES, ...rest } = args
|
||||
return rest
|
||||
}
|
||||
|
||||
async function loadArgsFromHistory(
|
||||
id: string | undefined,
|
||||
input: boolean | undefined,
|
||||
allowLarge: boolean,
|
||||
isPreview: boolean = false
|
||||
allowLarge: boolean
|
||||
): Promise<any> {
|
||||
if (!id) return
|
||||
const payloadData = await InputService.getArgsFromHistoryOrSavedInput({
|
||||
@@ -113,7 +105,7 @@
|
||||
input,
|
||||
allowLarge
|
||||
})
|
||||
return filterPreviewArgs(payloadData, isPreview)
|
||||
return payloadData
|
||||
}
|
||||
|
||||
function updateViewerOpenState(itemId: string, isOpen: boolean) {
|
||||
@@ -139,7 +131,6 @@
|
||||
job={item}
|
||||
hovering={hover}
|
||||
payloadData={item.payloadData}
|
||||
isPreview={!!item.is_preview}
|
||||
{showAuthor}
|
||||
{placement}
|
||||
{viewerOpen}
|
||||
|
||||
@@ -59,13 +59,11 @@
|
||||
)
|
||||
let max = $derived(
|
||||
flowDone
|
||||
? Object.values(flow_status).reduce((a, b) => {
|
||||
if (!b.started_at) return a
|
||||
const startedAt = new Date(b.started_at).getTime()
|
||||
// For cancelled steps without duration_ms, use `now` as end time
|
||||
const endAt = b.duration_ms != undefined ? startedAt + b.duration_ms : now
|
||||
return Math.max(a, endAt)
|
||||
}, 0)
|
||||
? Object.values(flow_status).reduce(
|
||||
(a, b) =>
|
||||
Math.max(a, b.started_at ? new Date(b.started_at).getTime() + (b.duration_ms ?? 0) : 0),
|
||||
0
|
||||
)
|
||||
: undefined
|
||||
)
|
||||
let total = $derived(flowDone && max ? max - min : Math.max(now - min, 2000))
|
||||
@@ -108,7 +106,7 @@
|
||||
let pollInterval = setInterval(() => {
|
||||
for (const [id, v] of Object.entries(flow_status)) {
|
||||
if (isStep(id)) continue
|
||||
const isRunning = !flowDone && v.duration_ms == undefined && v.started_at != undefined
|
||||
const isRunning = v.duration_ms == undefined && v.started_at != undefined
|
||||
if (expandedRows[id] && isRunning) {
|
||||
fetchChildJob(id)
|
||||
}
|
||||
@@ -156,8 +154,8 @@
|
||||
{@const isInlineStep = isStep(k)}
|
||||
{@const isSleep = (v as any).sleep_duration_s != undefined}
|
||||
{@const isApproval = (v as any).approval === true}
|
||||
{@const isRunning = !flowDone && v.duration_ms == undefined && v.started_at != undefined}
|
||||
{@const isDone = v.duration_ms != undefined || flowDone}
|
||||
{@const isRunning = v.duration_ms == undefined && v.started_at != undefined}
|
||||
{@const isDone = v.duration_ms != undefined}
|
||||
{@const isExpanded = expandedRows[k] ?? false}
|
||||
<div class="border-b last:border-b-0">
|
||||
{#if isSleep}
|
||||
@@ -236,7 +234,7 @@
|
||||
{min}
|
||||
started_at={startedAt}
|
||||
len={v.duration_ms ?? now - startedAt}
|
||||
running={!flowDone && v.duration_ms == undefined}
|
||||
running={v.duration_ms == undefined}
|
||||
/>
|
||||
{/if}
|
||||
{:else}
|
||||
@@ -248,7 +246,7 @@
|
||||
gray
|
||||
started_at={scheduledFor}
|
||||
len={waitingLen < 100 ? 0 : waitingLen - 100}
|
||||
running={!flowDone && startedAt == undefined}
|
||||
running={startedAt == undefined}
|
||||
/>
|
||||
{#if startedAt}
|
||||
<TimelineBar
|
||||
@@ -259,7 +257,7 @@
|
||||
concat
|
||||
started_at={startedAt}
|
||||
len={v.duration_ms ?? now - startedAt}
|
||||
running={!flowDone && v.duration_ms == undefined}
|
||||
running={v.duration_ms == undefined}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
payloadData?: any | undefined
|
||||
hovering?: boolean
|
||||
showAuthor?: boolean
|
||||
isPreview?: boolean
|
||||
placement?: 'bottom-start' | 'top-start' | 'bottom-end' | 'top-end'
|
||||
viewerOpen?: boolean
|
||||
limitPayloadSize?: boolean
|
||||
@@ -20,7 +19,6 @@
|
||||
payloadData = undefined,
|
||||
hovering = false,
|
||||
showAuthor = false,
|
||||
isPreview = false,
|
||||
placement = 'bottom-start',
|
||||
viewerOpen = false,
|
||||
limitPayloadSize = false
|
||||
@@ -37,17 +35,11 @@
|
||||
{limitPayloadSize}
|
||||
>
|
||||
{#snippet start()}
|
||||
<div class="center-center relative">
|
||||
<div class="center-center">
|
||||
<div
|
||||
class="rounded-full w-2 h-2 {job.success ? 'bg-green-400' : 'bg-red-400'}"
|
||||
title={job.success ? 'Success' : 'Failed'}
|
||||
></div>
|
||||
{#if isPreview}
|
||||
<span
|
||||
class="absolute -top-1.5 right-0.5 text-[8px] font-bold leading-none text-tertiary"
|
||||
title="Preview run">P</span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
{#snippet extra()}
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
showCustomResultPanel = false
|
||||
}: Props = $props()
|
||||
|
||||
|
||||
type DContent = {
|
||||
mode: 'json' | Preview['language'] | 'plain'
|
||||
title: string
|
||||
@@ -106,10 +107,7 @@
|
||||
|
||||
let forceJson = $state(false)
|
||||
let isWac = $derived(!!previewJob?.workflow_as_code_status)
|
||||
let wacDone = $derived(
|
||||
previewJob?.type == 'CompletedJob' ||
|
||||
(previewJob != undefined && !previewIsLoading && !!previewJob.workflow_as_code_status)
|
||||
)
|
||||
let wacDone = $derived(previewJob?.type == 'CompletedJob')
|
||||
</script>
|
||||
|
||||
<Drawer bind:open={drawerOpen} size="800px">
|
||||
@@ -161,67 +159,67 @@
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<SplitPanesWrapper>
|
||||
<Splitpanes horizontal>
|
||||
<Pane class="relative">
|
||||
<LogViewer
|
||||
jobId={previewJob?.id}
|
||||
duration={previewJob?.['duration_ms']}
|
||||
mem={previewJob?.['mem_peak']}
|
||||
content={previewJob?.logs}
|
||||
isLoading={previewJob?.['running'] == false && previewIsLoading}
|
||||
tag={previewJob?.tag}
|
||||
download={customUi?.disableDownload !== true}
|
||||
tagLabel={customUi?.tagLabel}
|
||||
/>
|
||||
</Pane>
|
||||
<Pane>
|
||||
{@render children?.()}
|
||||
{#if showCustomResultPanel && customResultPanel}
|
||||
<div class="h-full">
|
||||
{@render customResultPanel()}
|
||||
</div>
|
||||
{:else if previewJob != undefined && (previewJob.result_stream || previewJob.result)}
|
||||
<div class="relative w-full h-full p-2">
|
||||
<div class="relative h-full">
|
||||
<DisplayResult
|
||||
bind:forceJson
|
||||
workspaceId={previewJob?.workspace_id}
|
||||
jobId={previewJob?.id}
|
||||
result={previewJob.result}
|
||||
customUi={customUi?.displayResult}
|
||||
language={lang}
|
||||
result_stream={previewJob?.result_stream}
|
||||
fixTableSizingToParent
|
||||
>
|
||||
{#snippet copilot_fix()}
|
||||
{#if lang && editor && diffEditor && args && previewJob && !previewJob.success && getStringError(previewJob.result)}
|
||||
<ScriptFix {lang} />
|
||||
{/if}
|
||||
{/snippet}
|
||||
</DisplayResult>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-sm text-primary p-2 flex justify-between items-center">
|
||||
<span>
|
||||
{#if previewIsLoading}
|
||||
<Loader2 class="animate-spin" />
|
||||
{:else}
|
||||
Test to see the result here
|
||||
{/if}
|
||||
</span>
|
||||
<Tooltip
|
||||
documentationLink="https://www.windmill.dev/docs/core_concepts/rich_display_rendering"
|
||||
<SplitPanesWrapper>
|
||||
<Splitpanes horizontal>
|
||||
<Pane class="relative">
|
||||
<LogViewer
|
||||
jobId={previewJob?.id}
|
||||
duration={previewJob?.['duration_ms']}
|
||||
mem={previewJob?.['mem_peak']}
|
||||
content={previewJob?.logs}
|
||||
isLoading={previewJob?.['running'] == false && previewIsLoading}
|
||||
tag={previewJob?.tag}
|
||||
download={customUi?.disableDownload !== true}
|
||||
tagLabel={customUi?.tagLabel}
|
||||
/>
|
||||
</Pane>
|
||||
<Pane>
|
||||
{@render children?.()}
|
||||
{#if showCustomResultPanel && customResultPanel}
|
||||
<div class="h-full">
|
||||
{@render customResultPanel()}
|
||||
</div>
|
||||
{:else if previewJob != undefined && (previewJob.result_stream || previewJob.result)}
|
||||
<div class="relative w-full h-full p-2">
|
||||
<div class="relative h-full">
|
||||
<DisplayResult
|
||||
bind:forceJson
|
||||
workspaceId={previewJob?.workspace_id}
|
||||
jobId={previewJob?.id}
|
||||
result={previewJob.result}
|
||||
customUi={customUi?.displayResult}
|
||||
language={lang}
|
||||
result_stream={previewJob?.result_stream}
|
||||
fixTableSizingToParent
|
||||
>
|
||||
The result renderer in Windmill supports rich display rendering, allowing
|
||||
you to customize the display format of your results.
|
||||
</Tooltip>
|
||||
{#snippet copilot_fix()}
|
||||
{#if lang && editor && diffEditor && args && previewJob && !previewJob.success && getStringError(previewJob.result)}
|
||||
<ScriptFix {lang} />
|
||||
{/if}
|
||||
{/snippet}
|
||||
</DisplayResult>
|
||||
</div>
|
||||
{/if}
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
</SplitPanesWrapper>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-sm text-primary p-2 flex justify-between items-center">
|
||||
<span>
|
||||
{#if previewIsLoading}
|
||||
<Loader2 class="animate-spin" />
|
||||
{:else}
|
||||
Test to see the result here
|
||||
{/if}
|
||||
</span>
|
||||
<Tooltip
|
||||
documentationLink="https://www.windmill.dev/docs/core_concepts/rich_display_rendering"
|
||||
>
|
||||
The result renderer in Windmill supports rich display rendering, allowing you
|
||||
to customize the display format of your results.
|
||||
</Tooltip>
|
||||
</div>
|
||||
{/if}
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
</SplitPanesWrapper>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if selectedTab === 'history'}
|
||||
@@ -325,7 +323,9 @@
|
||||
{#if previewJob?.id}
|
||||
<JobOtelTraces jobId={previewJob.id} />
|
||||
{:else}
|
||||
<div class="p-4 text-secondary"> Run a preview to see HTTP request traces </div>
|
||||
<div class="p-4 text-secondary">
|
||||
Run a preview to see HTTP request traces
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,167 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import Button from '../common/button/Button.svelte'
|
||||
import Drawer from '../common/drawer/Drawer.svelte'
|
||||
import DrawerContent from '../common/drawer/DrawerContent.svelte'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { WorkspaceService } from '$lib/gen'
|
||||
import type { QuotaInfo } from '$lib/gen'
|
||||
import { untrack } from 'svelte'
|
||||
import { Trash2 } from 'lucide-svelte'
|
||||
|
||||
type ResourceType = 'scripts' | 'flows' | 'apps'
|
||||
|
||||
let quotas:
|
||||
| {
|
||||
scripts: QuotaInfo
|
||||
flows: QuotaInfo
|
||||
apps: QuotaInfo
|
||||
variables: QuotaInfo
|
||||
resources: QuotaInfo
|
||||
}
|
||||
| undefined = $state(undefined)
|
||||
|
||||
let loading = $state(false)
|
||||
let pruning = $state(false)
|
||||
let drawer: Drawer | undefined = $state()
|
||||
let pruneTarget: ResourceType | undefined = $state(undefined)
|
||||
|
||||
$effect(() => {
|
||||
if ($workspaceStore) {
|
||||
untrack(() => loadQuotas())
|
||||
}
|
||||
})
|
||||
|
||||
async function loadQuotas() {
|
||||
loading = true
|
||||
try {
|
||||
quotas = await WorkspaceService.getCloudQuotas({ workspace: $workspaceStore! })
|
||||
} catch (e) {
|
||||
sendUserToast(`Failed to load cloud quotas: ${e}`, true)
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
function openPruneDrawer(type: ResourceType) {
|
||||
pruneTarget = type
|
||||
drawer?.openDrawer()
|
||||
}
|
||||
|
||||
async function confirmPrune() {
|
||||
if (!pruneTarget) return
|
||||
pruning = true
|
||||
try {
|
||||
const result = await WorkspaceService.pruneVersions({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: { resource_type: pruneTarget }
|
||||
})
|
||||
sendUserToast(`Pruned ${result.pruned} old ${pruneTarget} versions`)
|
||||
drawer?.closeDrawer()
|
||||
await loadQuotas()
|
||||
} catch (e) {
|
||||
sendUserToast(`Failed to prune: ${e}`, true)
|
||||
} finally {
|
||||
pruning = false
|
||||
}
|
||||
}
|
||||
|
||||
function getPrunableCount(type: ResourceType): number {
|
||||
if (!quotas) return 0
|
||||
return quotas[type].prunable
|
||||
}
|
||||
|
||||
function getPruneDescription(type: ResourceType): string {
|
||||
switch (type) {
|
||||
case 'scripts':
|
||||
return 'This will permanently delete all non-HEAD script versions (old edits). The latest deployed version of each script will be preserved. This directly frees up quota space since each script edit creates a new row counted against the limit.'
|
||||
case 'flows':
|
||||
return 'This will permanently delete all non-HEAD flow versions. Only the latest version of each flow will be kept. This frees up storage but does not reduce the flow count (quota counts unique flows, not versions).'
|
||||
case 'apps':
|
||||
return 'This will permanently delete all non-HEAD app versions. Only the latest version of each app will be kept. This frees up storage but does not reduce the app count (quota counts unique apps, not versions).'
|
||||
}
|
||||
}
|
||||
|
||||
const rows: { label: string; key: keyof NonNullable<typeof quotas>; prunable: boolean }[] = [
|
||||
{ label: 'Scripts', key: 'scripts', prunable: true },
|
||||
{ label: 'Flows', key: 'flows', prunable: true },
|
||||
{ label: 'Apps', key: 'apps', prunable: true },
|
||||
{ label: 'Variables', key: 'variables', prunable: false },
|
||||
{ label: 'Resources', key: 'resources', prunable: false }
|
||||
]
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="font-semibold text-xs text-emphasis">Cloud Quotas</p>
|
||||
<p class="text-xs text-secondary font-normal">
|
||||
Current usage and limits for this workspace. Prune old versions to free up space.
|
||||
</p>
|
||||
|
||||
{#if loading && !quotas}
|
||||
<p class="text-xs text-tertiary">Loading...</p>
|
||||
{:else if quotas}
|
||||
<div class="border rounded-md overflow-hidden">
|
||||
<table class="w-full text-xs">
|
||||
<thead>
|
||||
<tr class="bg-surface-secondary border-b">
|
||||
<th class="text-left px-3 py-2 text-secondary font-medium">Resource</th>
|
||||
<th class="text-left px-3 py-2 text-secondary font-medium">Usage</th>
|
||||
<th class="text-right px-3 py-2 text-secondary font-medium">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each rows as row (row.key)}
|
||||
{@const info = quotas[row.key]}
|
||||
<tr class="border-b last:border-b-0">
|
||||
<td class="px-3 py-2 text-primary font-medium">{row.label}</td>
|
||||
<td class="px-3 py-2">
|
||||
<span
|
||||
class={info.used >= info.limit ? 'text-red-500 font-semibold' : 'text-primary'}
|
||||
>
|
||||
{info.used}
|
||||
</span>
|
||||
<span class="text-tertiary">/ {info.limit}</span>
|
||||
</td>
|
||||
<td class="px-3 py-2 text-right">
|
||||
{#if row.prunable && info.prunable > 0}
|
||||
<Button
|
||||
unifiedSize="sm"
|
||||
variant="default"
|
||||
startIcon={{ icon: Trash2 }}
|
||||
on:click={() => openPruneDrawer(row.key as ResourceType)}
|
||||
>
|
||||
Prune {info.prunable} old versions
|
||||
</Button>
|
||||
{:else if row.prunable}
|
||||
<span class="text-tertiary">No old versions</span>
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<Drawer bind:this={drawer}>
|
||||
<DrawerContent title="Prune old versions" on:close={drawer?.closeDrawer}>
|
||||
{#if pruneTarget}
|
||||
<div class="flex flex-col gap-4">
|
||||
<p class="text-sm text-primary">
|
||||
You are about to prune <span class="font-semibold">{getPrunableCount(pruneTarget)}</span>
|
||||
old {pruneTarget} versions.
|
||||
</p>
|
||||
<p class="text-xs text-secondary">
|
||||
{getPruneDescription(pruneTarget)}
|
||||
</p>
|
||||
<p class="text-xs text-red-500 font-medium">This action cannot be undone.</p>
|
||||
</div>
|
||||
{/if}
|
||||
{#snippet actions()}
|
||||
<Button variant="accent" on:click={confirmPrune} disabled={pruning}>
|
||||
{pruning ? 'Pruning...' : 'Confirm Prune'}
|
||||
</Button>
|
||||
{/snippet}
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
@@ -43,7 +43,6 @@
|
||||
import ChangeWorkspaceName from '$lib/components/settings/ChangeWorkspaceName.svelte'
|
||||
import ChangeWorkspaceId from '$lib/components/settings/ChangeWorkspaceId.svelte'
|
||||
import ChangeWorkspaceColor from '$lib/components/settings/ChangeWorkspaceColor.svelte'
|
||||
import CloudQuotas from '$lib/components/settings/CloudQuotas.svelte'
|
||||
import {
|
||||
convertBackendSettingsToFrontendSettings,
|
||||
type S3ResourceSettings
|
||||
@@ -1526,11 +1525,6 @@
|
||||
link="https://www.windmill.dev/docs/core_concepts/workspace_settings"
|
||||
/>
|
||||
|
||||
{#if isCloudHosted()}
|
||||
<CloudQuotas />
|
||||
<div class="my-4 border-b"></div>
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-col gap-6">
|
||||
<ChangeWorkspaceName />
|
||||
<ChangeWorkspaceId />
|
||||
|
||||
@@ -51,11 +51,13 @@
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
(!page.url.pathname.startsWith('/user/') || page.url.pathname.startsWith('/user/cli')) &&
|
||||
!page.url.pathname.startsWith('/oauth/mcp_authorize')
|
||||
!page.url.pathname.startsWith('/user/') ||
|
||||
page.url.pathname.startsWith('/user/cli')
|
||||
) {
|
||||
goto(
|
||||
`/user/workspaces?rd=${encodeURIComponent(page.url.href.replace(page.url.origin, ''))}`
|
||||
`/user/workspaces?rd=${encodeURIComponent(
|
||||
page.url.href.replace(page.url.origin, '')
|
||||
)}`
|
||||
)
|
||||
}
|
||||
let user = await UserService.globalWhoami()
|
||||
|
||||
@@ -4,7 +4,7 @@ verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
wmill = ">=1.660.0"
|
||||
wmill = ">=1.659.1"
|
||||
sendgrid = "*"
|
||||
mysql-connector-python = "*"
|
||||
pymongo = "*"
|
||||
|
||||
@@ -96,7 +96,6 @@ class HealthHandler(web.RequestHandler):
|
||||
self.set_header("Content-Type", "application/json")
|
||||
|
||||
def get(self):
|
||||
log.info("HTTP GET %s", self.request.uri)
|
||||
self.write(json.dumps({"status": "ok", "service": "lsp"}))
|
||||
|
||||
def options(self):
|
||||
@@ -105,7 +104,6 @@ class HealthHandler(web.RequestHandler):
|
||||
|
||||
class PingHandler(websocket.WebSocketHandler):
|
||||
def open(self):
|
||||
log.info("WS ping from %s", self.request.remote_ip)
|
||||
self.write_message(json.dumps({"type": "pong", "service": "lsp"}))
|
||||
self.close()
|
||||
|
||||
|
||||
@@ -122,7 +122,6 @@ const setupWSConnection = (conn, req, docName) => {
|
||||
}
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
console.log(`[${new Date().toISOString()}] HTTP ${req.method} ${req.url} from=${req.socket.remoteAddress}`)
|
||||
if (req.url === '/' || req.url === '/health' || req.url === '/ws_mp/health') {
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -145,16 +144,15 @@ wss.on('connection', (ws, req) => {
|
||||
docName = docName.slice('ws_mp/'.length)
|
||||
}
|
||||
|
||||
const clientIp = req.socket.remoteAddress
|
||||
|
||||
// Handle ping test — respond and close immediately
|
||||
if (docName === '__ping__') {
|
||||
console.log(`[${new Date().toISOString()}] WS ping from=${clientIp}`)
|
||||
ws.send(JSON.stringify({ type: 'pong', service: 'multiplayer' }))
|
||||
ws.close()
|
||||
return
|
||||
}
|
||||
|
||||
const clientIp = req.socket.remoteAddress
|
||||
|
||||
console.log(`[${new Date().toISOString()}] CONNECT: doc="${docName}" from=${clientIp}`)
|
||||
|
||||
ws.on('close', () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: '3.0.3'
|
||||
|
||||
info:
|
||||
version: 1.660.0
|
||||
version: 1.659.1
|
||||
title: OpenFlow Spec
|
||||
contact:
|
||||
name: Ruben Fiszel
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
RootModule = 'WindmillClient.psm1'
|
||||
|
||||
# Version number of this module.
|
||||
ModuleVersion = '1.660.0'
|
||||
ModuleVersion = '1.659.1'
|
||||
|
||||
# Supported PSEditions
|
||||
# CompatiblePSEditions = @()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "wmill"
|
||||
version = "1.660.0"
|
||||
version = "1.659.1"
|
||||
description = "A client library for accessing Windmill server wrapping the Windmill client API"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://windmill.dev"
|
||||
|
||||
@@ -86,10 +86,9 @@ flow related commands
|
||||
- `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.
|
||||
- `flow preview <flow_path:string>` - preview a local flow without deploying it. Runs the flow definition from local files and uses local PathScripts by default.
|
||||
- `flow preview <flow_path:string>` - preview a local flow without deploying it. Runs the flow definition from local files.
|
||||
- `-d --data <data:string>` - Inputs specified as a JSON string or a file using @<filename> or stdin using @-.
|
||||
- `-s --silent` - Do not output anything other then the final output. Useful for scripting.
|
||||
- `--remote` - Use deployed workspace scripts for PathScript steps instead of local files.
|
||||
- `flow generate-locks [flow:file]` - re-generate the lock files of all inline scripts of all updated flows
|
||||
- `--yes` - Skip confirmation prompt
|
||||
- `-i --includes <patterns:file[]>` - Comma separated patterns to specify which file to take into account (among files that are compatible with windmill). Patterns can include * (any string until '/') and ** (any string)
|
||||
|
||||
@@ -1460,10 +1460,9 @@ flow related commands
|
||||
- \`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.
|
||||
- \`flow preview <flow_path:string>\` - preview a local flow without deploying it. Runs the flow definition from local files and uses local PathScripts by default.
|
||||
- \`flow preview <flow_path:string>\` - preview a local flow without deploying it. Runs the flow definition from local files.
|
||||
- \`-d --data <data:string>\` - Inputs specified as a JSON string or a file using @<filename> or stdin using @-.
|
||||
- \`-s --silent\` - Do not output anything other then the final output. Useful for scripting.
|
||||
- \`--remote\` - Use deployed workspace scripts for PathScript steps instead of local files.
|
||||
- \`flow generate-locks [flow:file]\` - re-generate the lock files of all inline scripts of all updated flows
|
||||
- \`--yes\` - Skip confirmation prompt
|
||||
- \`-i --includes <patterns:file[]>\` - Comma separated patterns to specify which file to take into account (among files that are compatible with windmill). Patterns can include * (any string until '/') and ** (any string)
|
||||
|
||||
@@ -91,10 +91,9 @@ flow related commands
|
||||
- `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.
|
||||
- `flow preview <flow_path:string>` - preview a local flow without deploying it. Runs the flow definition from local files and uses local PathScripts by default.
|
||||
- `flow preview <flow_path:string>` - preview a local flow without deploying it. Runs the flow definition from local files.
|
||||
- `-d --data <data:string>` - Inputs specified as a JSON string or a file using @<filename> or stdin using @-.
|
||||
- `-s --silent` - Do not output anything other then the final output. Useful for scripting.
|
||||
- `--remote` - Use deployed workspace scripts for PathScript steps instead of local files.
|
||||
- `flow generate-locks [flow:file]` - re-generate the lock files of all inline scripts of all updated flows
|
||||
- `--yes` - Skip confirmation prompt
|
||||
- `-i --includes <patterns:file[]>` - Comma separated patterns to specify which file to take into account (among files that are compatible with windmill). Patterns can include * (any string until '/') and ** (any string)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@windmill/windmill",
|
||||
"version": "1.660.0",
|
||||
"version": "1.659.1",
|
||||
"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.660.0",
|
||||
"version": "1.659.1",
|
||||
"author": "Ruben Fiszel",
|
||||
"license": "Apache 2.0",
|
||||
"sideEffects": false,
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.660.0
|
||||
1.659.1
|
||||
|
||||
Reference in New Issue
Block a user