Compare commits

..
Author SHA1 Message Date
Ruben FiszelandClaude Opus 4.6 1aac43977e test(cli): add non-dotted path tests for generate-metadata and sync pull
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 04:30:21 +00:00
Travis 05d4d6dd48 fix(cli): preserve non-dotted flow lock filenames 2026-03-14 22:41:59 -04:00
97 changed files with 376 additions and 1553 deletions
-6
View File
@@ -1,7 +1 @@
use flake
# Per-worktree overrides (ports, DATABASE_URL, etc.) written by webmux/workmux
# post-create hooks. Must come after `use flake` so they take precedence over
# the flake's defaults.
# shellcheck source=/dev/null
[ -f .env.local ] && source .env.local
-53
View File
@@ -212,59 +212,6 @@ jobs:
${{ steps.extract-ee.outputs.destination }}/*
${{ steps.extract-duckdb-ffi-internal.outputs.destination }}/*
attach_ee_debug_to_release:
needs: [build_ee]
runs-on: ubicloud
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
strategy:
matrix:
platform: [linux/amd64, linux/arm64]
include:
- platform: linux/amd64
arch: amd64
- platform: linux/arm64
arch: arm64
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.ref }}
- name: Read EE repo commit hash
run: |
echo "ee_repo_ref=$(cat ./backend/ee-repo-ref.txt)" >> "$GITHUB_ENV"
- uses: actions/checkout@v4
with:
repository: windmill-labs/windmill-ee-private
path: ./windmill-ee-private
ref: ${{ env.ee_repo_ref }}
token: ${{ secrets.WINDMILL_EE_PRIVATE_ACCESS }}
- name: Substitute EE code
run: |
./backend/substitute_ee_code.sh --copy --dir ./windmill-ee-private
- uses: depot/setup-action@v1
- name: Extract EE debug info from builder stage (depot cache hit)
uses: depot/build-push-action@v1
with:
context: .
platforms: ${{ matrix.platform }}
target: debuginfo
build-args: |
features=ee
outputs: type=local,dest=./debuginfo
- name: Rename debug file with corresponding architecture
run: |
mv ./debuginfo/windmill.debug ./debuginfo/windmill-ee-${{ matrix.arch }}.debug
- name: Attach debug file to release
uses: softprops/action-gh-release@v2
with:
files: ./debuginfo/windmill-ee-${{ matrix.arch }}.debug
# attach_arm64_binary_to_release:
# needs: [build, build_ee]
# runs-on: ubicoud
-13
View File
@@ -106,19 +106,6 @@ jobs:
git config --local user.name "windmill-internal-app[bot]"
git config pull.rebase true
git pull origin $BRANCH_NAME
# Checkout the correct windmill-ee-private commit from ee-repo-ref.txt
if [ -f backend/ee-repo-ref.txt ]; then
EE_REF=$(cat backend/ee-repo-ref.txt | tr -d '[:space:]')
echo "Checking out windmill-ee-private at commit: $EE_REF"
cd windmill-ee-private
git fetch origin $EE_REF
git checkout $EE_REF
cd ..
else
echo "Warning: ee-repo-ref.txt not found, using default branch"
fi
mkdir -p frontend/build
cd backend
cargo install sqlx-cli --version 0.8.5
-1
View File
@@ -16,7 +16,6 @@ rust-client/Cargo.toml
# Worktree-generated port isolation
.env.local
.webmux.local.yaml
# Worktree-specific Claude Code settings (generated by scripts/worktree-env)
.claude/settings.local.json
-34
View File
@@ -1,39 +1,5 @@
# Changelog
## [1.658.0](https://github.com/windmill-labs/windmill/compare/v1.657.2...v1.658.0) (2026-03-16)
### Features
* add GET /api/saml/metadata endpoint ([#8394](https://github.com/windmill-labs/windmill/issues/8394)) ([50b24cf](https://github.com/windmill-labs/windmill/commit/50b24cfdc8bf54656adbdc3315037aa773632076))
* support custom headers in customai resource type ([#8364](https://github.com/windmill-labs/windmill/issues/8364)) ([5acb367](https://github.com/windmill-labs/windmill/commit/5acb367cf9b4b96ac7129c91df229d1a25258f5b))
* support multiple secret variables during resource creation ([#8386](https://github.com/windmill-labs/windmill/issues/8386)) ([54841b7](https://github.com/windmill-labs/windmill/commit/54841b7549d5c9719d4dc3cb43e282ba057cd0f3))
### Bug Fixes
* /updatesqlx now uses ee-repo-ref.txt commit hash ([#8387](https://github.com/windmill-labs/windmill/issues/8387)) ([a519d41](https://github.com/windmill-labs/windmill/commit/a519d4113086430ace1d7ac8795bd2c2a8cf99e9))
* **native-triggers:** preserve API error response body in HttpRequestError ([#8392](https://github.com/windmill-labs/windmill/issues/8392)) ([1eee89d](https://github.com/windmill-labs/windmill/commit/1eee89d99fbf31751d6257a4015e0b22e3871372))
* OutputPicker shows stale result after 'Test up to here' ([#8390](https://github.com/windmill-labs/windmill/issues/8390)) ([2907084](https://github.com/windmill-labs/windmill/commit/2907084ca653fc5540bb04a409d2789ddaeec05b))
* propagate enterprise feature to windmill-api-schedule ([#8391](https://github.com/windmill-labs/windmill/issues/8391)) ([50ef9e7](https://github.com/windmill-labs/windmill/commit/50ef9e79fcef8ee2cccd789b5eb1aacf5647365f))
* set nsjail time_limit from job timeout so configured defaults are respected ([#8389](https://github.com/windmill-labs/windmill/issues/8389)) ([65a92d9](https://github.com/windmill-labs/windmill/commit/65a92d98994dbe4ae90a5e554e55b3ab44463f86))
* soft error on AI agent max iterations + rename retries tab to error handling ([#8366](https://github.com/windmill-labs/windmill/issues/8366)) ([1a1e8a1](https://github.com/windmill-labs/windmill/commit/1a1e8a164cccbfcc663b963cb062af9208ff51be))
* use bookworm-based php image to fix glibc 2.38 incompatibility ([#8381](https://github.com/windmill-labs/windmill/issues/8381)) ([68fd900](https://github.com/windmill-labs/windmill/commit/68fd900076ecf8b20f6622cd5794f1b52c0f5cab))
## [1.657.2](https://github.com/windmill-labs/windmill/compare/v1.657.1...v1.657.2) (2026-03-15)
### Bug Fixes
* **cli:** Fix nonDottedPaths handling in cli flow lock generation ([#8375](https://github.com/windmill-labs/windmill/issues/8375)) ([eb03ebb](https://github.com/windmill-labs/windmill/commit/eb03ebbb0486b33c290fba3c34ea959e6e82fd13))
## [1.657.1](https://github.com/windmill-labs/windmill/compare/v1.657.0...v1.657.1) (2026-03-14)
### Bug Fixes
* powershell WindmillClient module loading on Windows workers ([#8370](https://github.com/windmill-labs/windmill/issues/8370)) ([3a268a9](https://github.com/windmill-labs/windmill/commit/3a268a9cf16add2ea2530e6eab247120a4d4754e))
## [1.657.0](https://github.com/windmill-labs/windmill/compare/v1.656.0...v1.657.0) (2026-03-14)
+1 -13
View File
@@ -118,18 +118,6 @@ RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=$SCCACHE_DIR,sharing=locked \
CARGO_NET_GIT_FETCH_WITH_CLI=true cargo build --release --features "$features"
# Split debug info into a separate file, then strip the binary.
# The .debug file can be extracted as a CI artifact for production debugging.
# The debuglink allows gdb to auto-discover the debug file when placed next to the binary.
RUN objcopy --only-keep-debug /windmill/target/release/windmill /windmill/target/release/windmill.debug \
&& strip /windmill/target/release/windmill \
&& objcopy --add-gnu-debuglink=/windmill/target/release/windmill.debug /windmill/target/release/windmill
# Standalone stage for extracting the .debug file without including it in the final image.
# Build with: docker build --target debuginfo --output type=local,dest=./out .
FROM scratch AS debuginfo
COPY --from=builder /windmill/target/release/windmill.debug /windmill.debug
FROM ${DEBIAN_IMAGE}
ARG TARGETPLATFORM
@@ -280,7 +268,7 @@ RUN bun install -g windmill-cli \
RUN curl -fsSL https://claude.ai/install.sh | bash \
&& cp /root/.local/share/claude/versions/* /usr/bin/claude
COPY --from=php:8.3.30-cli-bookworm /usr/local/bin/php /usr/bin/php
COPY --from=php:8.3.30-cli /usr/local/bin/php /usr/bin/php
COPY --from=composer:2.9.5 /usr/bin/composer /usr/bin/composer
# add the docker client to call docker from a worker if enabled
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO native_trigger (\n external_id,\n workspace_id,\n service_name,\n script_path,\n is_flow,\n webhook_token_prefix,\n service_config,\n summary\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8\n )\n ON CONFLICT (external_id, workspace_id, service_name)\n DO UPDATE SET script_path = $4, is_flow = $5, webhook_token_prefix = $6, service_config = $7, summary = $8, error = NULL, updated_at = NOW()\n ",
"query": "\n INSERT INTO native_trigger (\n external_id,\n workspace_id,\n service_name,\n script_path,\n is_flow,\n webhook_token_prefix,\n service_config\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7\n )\n ON CONFLICT (external_id, workspace_id, service_name)\n DO UPDATE SET script_path = $4, is_flow = $5, webhook_token_prefix = $6, service_config = $7, error = NULL, updated_at = NOW()\n ",
"describe": {
"columns": [],
"parameters": {
@@ -21,11 +21,10 @@
"Varchar",
"Bool",
"Varchar",
"Jsonb",
"Varchar"
"Jsonb"
]
},
"nullable": []
},
"hash": "71327558c8f5f48f9da5d2585db9e4702ed7eddef41dbf0bb1bf66b5b567d481"
"hash": "023cdbc77ea9e2c17a1aa92a5b9001f29e58e81b3f782887db6e0a627dd8ad75"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE native_trigger\n SET script_path = $1, is_flow = $2, webhook_token_prefix = $3, service_config = $4, summary = $8, error = NULL, updated_at = NOW()\n WHERE\n workspace_id = $5\n AND service_name = $6\n AND external_id = $7\n ",
"query": "\n UPDATE native_trigger\n SET script_path = $1, is_flow = $2, webhook_token_prefix = $3, service_config = $4, error = NULL, updated_at = NOW()\n WHERE\n workspace_id = $5\n AND service_name = $6\n AND external_id = $7\n ",
"describe": {
"columns": [],
"parameters": {
@@ -21,11 +21,10 @@
}
}
},
"Text",
"Varchar"
"Text"
]
},
"nullable": []
},
"hash": "0eea6bb9d8d77fafbb4a0132a7b977bd37760e82efca322149814ad65a5be2bb"
"hash": "27ada97cb533c8595f1d73987c7823d8e54c96889e06895c57cafae9ca27bf8b"
}
@@ -15,7 +15,7 @@
]
},
"nullable": [
true
false
]
},
"hash": "2d6607b3c38fe72b5663c32de58dacbabed4c5ae28101e3ae2694f96fd055a91"
@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM variable WHERE path = $1 AND workspace_id = $2 RETURNING path",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "path",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false
]
},
"hash": "3317484a9c09c07c2c9db9debaecc4a4d518093ab48e79365dbb808068e0b8ff"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n external_id,\n workspace_id,\n service_name AS \"service_name!: ServiceName\",\n script_path,\n is_flow,\n webhook_token_prefix,\n service_config,\n summary,\n error,\n created_at,\n updated_at\n FROM\n native_trigger\n WHERE\n workspace_id = $1\n AND service_name = $2\n AND external_id = $3\n ",
"query": "\n SELECT\n external_id,\n workspace_id,\n service_name AS \"service_name!: ServiceName\",\n script_path,\n is_flow,\n webhook_token_prefix,\n service_config,\n error,\n created_at,\n updated_at\n FROM\n native_trigger\n WHERE\n workspace_id = $1\n AND service_name = $2\n AND external_id = $3\n ",
"describe": {
"columns": [
{
@@ -50,21 +50,16 @@
},
{
"ordinal": 7,
"name": "summary",
"type_info": "Varchar"
},
{
"ordinal": 8,
"name": "error",
"type_info": "Text"
},
{
"ordinal": 9,
"ordinal": 8,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 10,
"ordinal": 9,
"name": "updated_at",
"type_info": "Timestamptz"
}
@@ -95,10 +90,9 @@
false,
true,
true,
true,
false,
false
]
},
"hash": "88b27601cb7af37e3ddf79d308dbb2f6edb4c7c4214ee1e4ee0731e5175ed74b"
"hash": "b615d73ddb43e9d655b86a0cf98f892bf40e629ee11ee4845199481755f2789d"
}
@@ -15,7 +15,7 @@
]
},
"nullable": [
true
false
]
},
"hash": "eba16eb819e2644284fb073c891706d78a6f24cb0e614d7d81ba1b643805bf06"
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n nt.external_id,\n nt.workspace_id,\n nt.service_name AS \"service_name!: ServiceName\",\n nt.script_path,\n nt.is_flow,\n nt.webhook_token_prefix,\n nt.service_config,\n nt.summary,\n nt.error,\n nt.created_at,\n nt.updated_at\n FROM\n native_trigger nt\n WHERE\n nt.workspace_id = $1 AND\n nt.service_name = $2 AND\n ($5::text IS NULL OR nt.script_path = $5) AND\n ($6::bool IS NULL OR nt.is_flow = $6) AND\n (\n (nt.is_flow = false AND EXISTS (\n SELECT 1 FROM script s\n WHERE s.workspace_id = nt.workspace_id\n AND s.path = nt.script_path\n ))\n OR\n (nt.is_flow = true AND EXISTS (\n SELECT 1 FROM flow f\n WHERE f.workspace_id = nt.workspace_id\n AND f.path = nt.script_path\n ))\n )\n LIMIT $3\n OFFSET $4\n ",
"query": "\n SELECT\n nt.external_id,\n nt.workspace_id,\n nt.service_name AS \"service_name!: ServiceName\",\n nt.script_path,\n nt.is_flow,\n nt.webhook_token_prefix,\n nt.service_config,\n nt.error,\n nt.created_at,\n nt.updated_at\n FROM\n native_trigger nt\n WHERE\n nt.workspace_id = $1 AND\n nt.service_name = $2 AND\n ($5::text IS NULL OR nt.script_path = $5) AND\n ($6::bool IS NULL OR nt.is_flow = $6) AND\n (\n (nt.is_flow = false AND EXISTS (\n SELECT 1 FROM script s\n WHERE s.workspace_id = nt.workspace_id\n AND s.path = nt.script_path\n ))\n OR\n (nt.is_flow = true AND EXISTS (\n SELECT 1 FROM flow f\n WHERE f.workspace_id = nt.workspace_id\n AND f.path = nt.script_path\n ))\n )\n LIMIT $3\n OFFSET $4\n ",
"describe": {
"columns": [
{
@@ -50,21 +50,16 @@
},
{
"ordinal": 7,
"name": "summary",
"type_info": "Varchar"
},
{
"ordinal": 8,
"name": "error",
"type_info": "Text"
},
{
"ordinal": 9,
"ordinal": 8,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 10,
"ordinal": 9,
"name": "updated_at",
"type_info": "Timestamptz"
}
@@ -98,10 +93,9 @@
false,
true,
true,
true,
false,
false
]
},
"hash": "bab3d5e34abc8e998ea9cf721da1c08cc01c313543c3bff90d7f41a4c51bcd5e"
"hash": "ecab1af12a7afa685c056b9d0e526275203fc8ecddf83ca6d05c9fb77e46e7ee"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n external_id,\n workspace_id,\n service_name AS \"service_name!: ServiceName\",\n script_path,\n is_flow,\n webhook_token_prefix,\n service_config,\n summary,\n error,\n created_at,\n updated_at\n FROM\n native_trigger\n WHERE\n workspace_id = $1\n AND service_name = $2\n AND script_path = $3\n AND is_flow = $4\n LIMIT 1\n ",
"query": "\n SELECT\n external_id,\n workspace_id,\n service_name AS \"service_name!: ServiceName\",\n script_path,\n is_flow,\n webhook_token_prefix,\n service_config,\n error,\n created_at,\n updated_at\n FROM\n native_trigger\n WHERE\n workspace_id = $1\n AND service_name = $2\n AND script_path = $3\n AND is_flow = $4\n LIMIT 1\n ",
"describe": {
"columns": [
{
@@ -50,21 +50,16 @@
},
{
"ordinal": 7,
"name": "summary",
"type_info": "Varchar"
},
{
"ordinal": 8,
"name": "error",
"type_info": "Text"
},
{
"ordinal": 9,
"ordinal": 8,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 10,
"ordinal": 9,
"name": "updated_at",
"type_info": "Timestamptz"
}
@@ -96,10 +91,9 @@
false,
true,
true,
true,
false,
false
]
},
"hash": "1d1d70387f6da3181d2547146cd4897608932a6c0ff9bb9a2698c0d30c039665"
"hash": "ee537def1ead8bee48bb9f5c1f57d42e7add6011c34d91761ba23e2c74c4032c"
}
+83 -84
View File
@@ -8208,9 +8208,9 @@ dependencies = [
[[package]]
name = "lz4_flex"
version = "0.11.6"
version = "0.11.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "373f5eceeeab7925e0c1098212f2fbc4d416adec9d35051a6ab251e824c1854a"
checksum = "08ab2867e3eeeca90e844d1940eab391c9dc5228783db2ed999acbc0a9ed375a"
dependencies = [
"twox-hash 2.1.2",
]
@@ -9247,9 +9247,9 @@ dependencies = [
[[package]]
name = "num_enum"
version = "0.7.6"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26"
checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c"
dependencies = [
"num_enum_derive",
"rustversion",
@@ -9257,9 +9257,9 @@ dependencies = [
[[package]]
name = "num_enum_derive"
version = "0.7.6"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8"
checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7"
dependencies = [
"proc-macro-crate",
"proc-macro2",
@@ -14087,9 +14087,9 @@ dependencies = [
[[package]]
name = "tinyvec"
version = "1.11.0"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"
checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa"
dependencies = [
"tinyvec_macros",
]
@@ -15741,7 +15741,7 @@ dependencies = [
[[package]]
name = "windmill"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"anyhow",
"async-nats",
@@ -15808,7 +15808,7 @@ dependencies = [
[[package]]
name = "windmill-alerting"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -15821,7 +15821,7 @@ dependencies = [
[[package]]
name = "windmill-api"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"anyhow",
"argon2",
@@ -15962,7 +15962,7 @@ dependencies = [
[[package]]
name = "windmill-api-agent-workers"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -15985,7 +15985,7 @@ dependencies = [
[[package]]
name = "windmill-api-assets"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -15998,7 +15998,7 @@ dependencies = [
[[package]]
name = "windmill-api-auth"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16024,7 +16024,7 @@ dependencies = [
[[package]]
name = "windmill-api-client"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"reqwest 0.12.28",
"serde",
@@ -16034,7 +16034,7 @@ dependencies = [
[[package]]
name = "windmill-api-configs"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16051,7 +16051,7 @@ dependencies = [
[[package]]
name = "windmill-api-debug"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"axum 0.7.9",
"base64 0.22.1",
@@ -16074,7 +16074,7 @@ dependencies = [
[[package]]
name = "windmill-api-embeddings"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16097,7 +16097,7 @@ dependencies = [
[[package]]
name = "windmill-api-flow-conversations"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16113,7 +16113,7 @@ dependencies = [
[[package]]
name = "windmill-api-flows"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16133,7 +16133,7 @@ dependencies = [
[[package]]
name = "windmill-api-groups"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16153,7 +16153,7 @@ dependencies = [
[[package]]
name = "windmill-api-inputs"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16167,7 +16167,7 @@ dependencies = [
[[package]]
name = "windmill-api-integration-tests"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"anyhow",
"async-nats",
@@ -16195,7 +16195,7 @@ dependencies = [
[[package]]
name = "windmill-api-jobs"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16220,7 +16220,7 @@ dependencies = [
[[package]]
name = "windmill-api-npm-proxy"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"axum 0.7.9",
"flate2",
@@ -16238,7 +16238,7 @@ dependencies = [
[[package]]
name = "windmill-api-openapi"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16259,7 +16259,7 @@ dependencies = [
[[package]]
name = "windmill-api-schedule"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16279,7 +16279,7 @@ dependencies = [
[[package]]
name = "windmill-api-scripts"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16309,7 +16309,7 @@ dependencies = [
[[package]]
name = "windmill-api-settings"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16336,7 +16336,7 @@ dependencies = [
[[package]]
name = "windmill-api-sse"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"lazy_static",
"serde",
@@ -16348,7 +16348,7 @@ dependencies = [
[[package]]
name = "windmill-api-users"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"argon2",
"axum 0.7.9",
@@ -16371,7 +16371,7 @@ dependencies = [
[[package]]
name = "windmill-api-workers"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16385,7 +16385,7 @@ dependencies = [
[[package]]
name = "windmill-api-workspaces"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16416,7 +16416,7 @@ dependencies = [
[[package]]
name = "windmill-audit"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"chrono",
"lazy_static",
@@ -16430,7 +16430,7 @@ dependencies = [
[[package]]
name = "windmill-autoscaling"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16449,7 +16449,7 @@ dependencies = [
[[package]]
name = "windmill-common"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"aes-gcm",
"anyhow",
@@ -16548,7 +16548,7 @@ dependencies = [
[[package]]
name = "windmill-dep-map"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"chrono",
"itertools 0.14.0",
@@ -16567,7 +16567,7 @@ dependencies = [
[[package]]
name = "windmill-git-sync"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"regex",
"serde",
@@ -16582,7 +16582,7 @@ dependencies = [
[[package]]
name = "windmill-indexer"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"anyhow",
"astral-tokio-tar",
@@ -16606,7 +16606,7 @@ dependencies = [
[[package]]
name = "windmill-jseval"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"anyhow",
"futures",
@@ -16623,7 +16623,7 @@ dependencies = [
[[package]]
name = "windmill-macros"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"itertools 0.14.0",
"lazy_static",
@@ -16639,7 +16639,7 @@ dependencies = [
[[package]]
name = "windmill-mcp"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"anyhow",
"async-trait",
@@ -16660,7 +16660,7 @@ dependencies = [
[[package]]
name = "windmill-native-triggers"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"anyhow",
"async-trait",
@@ -16691,7 +16691,7 @@ dependencies = [
[[package]]
name = "windmill-oauth"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"anyhow",
"async-oauth2",
@@ -16715,7 +16715,7 @@ dependencies = [
[[package]]
name = "windmill-object-store"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"anyhow",
"async-stream",
@@ -16749,7 +16749,7 @@ dependencies = [
[[package]]
name = "windmill-operator"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"anyhow",
"futures",
@@ -16767,7 +16767,7 @@ dependencies = [
[[package]]
name = "windmill-parser"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"convert_case 0.6.0",
"serde",
@@ -16776,7 +16776,7 @@ dependencies = [
[[package]]
name = "windmill-parser-bash"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -16788,7 +16788,7 @@ dependencies = [
[[package]]
name = "windmill-parser-csharp"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"anyhow",
"serde_json",
@@ -16800,7 +16800,7 @@ dependencies = [
[[package]]
name = "windmill-parser-go"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"anyhow",
"gosyn",
@@ -16812,7 +16812,7 @@ dependencies = [
[[package]]
name = "windmill-parser-graphql"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -16824,7 +16824,7 @@ dependencies = [
[[package]]
name = "windmill-parser-java"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"anyhow",
"serde_json",
@@ -16836,7 +16836,7 @@ dependencies = [
[[package]]
name = "windmill-parser-nu"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"anyhow",
"nu-parser",
@@ -16847,7 +16847,7 @@ dependencies = [
[[package]]
name = "windmill-parser-php"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -16858,7 +16858,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -16870,7 +16870,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py-asset"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"anyhow",
"rustpython-ast",
@@ -16881,7 +16881,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py-imports"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -16905,7 +16905,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ruby"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -16919,7 +16919,7 @@ dependencies = [
[[package]]
name = "windmill-parser-rust"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"anyhow",
"convert_case 0.6.0",
@@ -16936,7 +16936,7 @@ dependencies = [
[[package]]
name = "windmill-parser-sql"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -16950,7 +16950,7 @@ dependencies = [
[[package]]
name = "windmill-parser-sql-asset"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"anyhow",
"serde",
@@ -16962,7 +16962,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -16980,7 +16980,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts-asset"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"anyhow",
"serde-wasm-bindgen",
@@ -16996,7 +16996,7 @@ dependencies = [
[[package]]
name = "windmill-parser-wac"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"anyhow",
"rustpython-ast",
@@ -17012,7 +17012,7 @@ dependencies = [
[[package]]
name = "windmill-parser-yaml"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"anyhow",
"serde",
@@ -17023,7 +17023,7 @@ dependencies = [
[[package]]
name = "windmill-queue"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -17060,7 +17060,7 @@ dependencies = [
[[package]]
name = "windmill-runtime-nativets"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"anyhow",
"const_format",
@@ -17098,7 +17098,7 @@ dependencies = [
[[package]]
name = "windmill-sql-datatype-parser-wasm"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"getrandom 0.3.4",
"wasm-bindgen",
@@ -17109,7 +17109,7 @@ dependencies = [
[[package]]
name = "windmill-store"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -17138,7 +17138,7 @@ dependencies = [
[[package]]
name = "windmill-test-utils"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -17161,7 +17161,7 @@ dependencies = [
[[package]]
name = "windmill-trigger"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17194,7 +17194,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-email"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17214,7 +17214,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-gcp"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17248,7 +17248,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-http"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17283,7 +17283,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-kafka"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17306,7 +17306,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-mqtt"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17330,7 +17330,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-nats"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"anyhow",
"async-nats",
@@ -17354,7 +17354,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-postgres"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17389,7 +17389,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-sqs"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17417,7 +17417,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-websocket"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17440,7 +17440,7 @@ dependencies = [
[[package]]
name = "windmill-types"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"anyhow",
"bitflags 2.9.4",
@@ -17458,7 +17458,7 @@ dependencies = [
[[package]]
name = "windmill-worker"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"anyhow",
"async-once-cell",
@@ -17521,7 +17521,6 @@ dependencies = [
"sha2 0.10.9",
"sqlx",
"tar",
"tempfile",
"tiberius",
"tokio",
"tokio-postgres 0.7.13",
@@ -17565,7 +17564,7 @@ dependencies = [
[[package]]
name = "windmill-worker-volumes"
version = "1.658.0"
version = "1.657.0"
dependencies = [
"bytes",
"futures",
+2 -4
View File
@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.658.0"
version = "1.657.0"
authors.workspace = true
edition.workspace = true
@@ -82,7 +82,7 @@ members = [
exclude = ["./windmill-duckdb-ffi-internal"]
[workspace.package]
version = "1.658.0"
version = "1.657.0"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"
@@ -100,8 +100,6 @@ debug = false
[profile.release]
lto = "thin"
debug = "line-tables-only"
strip = "none"
[features]
default = []
+1 -1
View File
@@ -1 +1 @@
b2fde51087d0d0ee0223c15cafb4e8badddd2d13
c74c86b78a66b976fd9968b21f77903723e668ec
@@ -1 +0,0 @@
ALTER TABLE native_trigger DROP COLUMN summary;
@@ -1 +0,0 @@
ALTER TABLE native_trigger ADD COLUMN summary VARCHAR(512);
-86
View File
@@ -1518,92 +1518,6 @@ Write-Output "hello $msg"
Ok(())
}
#[sqlx::test(fixtures("base"))]
async fn test_powershell_param_block_with_attributes(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
let port = server.addr.port();
let content = r#"
param(
[Parameter(Mandatory=$true)]
[string]$Name,
[int]$Count = 3
)
Write-Output "$Name-$Count"
"#
.to_owned();
let job = RunJob::from(JobPayload::Code(RawCode {
hash: None,
content,
path: None,
lock: None,
language: ScriptLang::Powershell,
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
}))
.arg("Name", json!("test"))
.arg("Count", json!(7))
.run_until_complete(&db, false, port)
.await;
assert_eq!(job.json_result(), Some(json!("test-7")));
Ok(())
}
#[sqlx::test(fixtures("base"))]
async fn test_powershell_error_caught(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
let port = server.addr.port();
// Script with param block that throws an error — verifies the catch block works
let content = r#"
param($x)
throw "intentional error"
"#
.to_owned();
let job = RunJob::from(JobPayload::Code(RawCode {
hash: None,
content,
path: None,
lock: None,
language: ScriptLang::Powershell,
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
}))
.arg("x", json!(1))
.run_until_complete(&db, false, port)
.await;
assert!(!job.success, "job should fail on thrown error");
let result_str = serde_json::to_string(&job.result).unwrap_or_default();
assert!(
result_str.contains("An error occurred:"),
"catch block should output 'An error occurred:', got: {result_str}"
);
assert!(
result_str.contains("intentional error"),
"catch block should output the error message, got: {result_str}"
);
// Verify the catch block doesn't leak "Write-Output" as literal text
// (regression from the old broken line continuation in strict_termination_end)
let after_marker = result_str.split("An error occurred:").nth(1).unwrap_or("");
assert!(
!after_marker.starts_with("\\nWrite-Output"),
"catch block should not output literal 'Write-Output' text, got: {result_str}"
);
Ok(())
}
#[cfg(feature = "php")]
#[sqlx::test(fixtures("base"))]
async fn test_php_job(db: Pool<Postgres>) -> anyhow::Result<()> {
@@ -422,7 +422,6 @@ async fn test_delete_integration_full_cascade(db: Pool<Postgres>) -> anyhow::Res
"ext-1",
&trigger_config,
json!({"triggerType": "drive"}),
None,
)
.await?;
@@ -512,7 +511,6 @@ async fn test_cleanup_preserves_triggers(db: Pool<Postgres>) -> anyhow::Result<(
"ext-1",
&trigger_config,
json!({"triggerType": "drive"}),
None,
)
.await?;
+1 -1
View File
@@ -11,7 +11,7 @@ path = "src/lib.rs"
[features]
default = []
private = ["windmill-audit/private", "windmill-common/private", "windmill-api-auth/private", "windmill-store/private", "windmill-api-users/private", "windmill-api-workspaces/private", "windmill-api-groups/private", "windmill-api-configs/private", "windmill-api-settings/private", "windmill-api-agent-workers?/private", "windmill-trigger-kafka?/private", "windmill-trigger-postgres?/private", "windmill-trigger-mqtt?/private", "windmill-trigger-websocket?/private", "windmill-trigger-nats?/private", "windmill-trigger-sqs?/private", "windmill-trigger-gcp?/private", "windmill-trigger-email?/private", "windmill-git-sync/private", "windmill-autoscaling?/private"]
enterprise = ["windmill-queue/enterprise", "windmill-audit/enterprise", "windmill-git-sync/enterprise", "windmill-common/enterprise", "windmill-worker?/enterprise", "windmill-api-auth/enterprise", "windmill-store/enterprise", "windmill-api-jobs/enterprise", "windmill-api-scripts/enterprise", "windmill-api-flows/enterprise", "windmill-api-users/enterprise", "windmill-api-workspaces/enterprise", "windmill-api-groups/enterprise", "windmill-api-configs/enterprise", "windmill-api-settings/enterprise", "windmill-api-schedule/enterprise", "windmill-api-agent-workers?/enterprise", "windmill-trigger/enterprise", "windmill-trigger-kafka?/enterprise", "windmill-trigger-postgres?/enterprise", "windmill-trigger-mqtt?/enterprise", "windmill-trigger-websocket?/enterprise", "windmill-trigger-email?/enterprise", "windmill-trigger-nats?/enterprise", "windmill-trigger-sqs?/enterprise", "windmill-trigger-gcp?/enterprise", "windmill-trigger-http?/enterprise", "windmill-native-triggers?/enterprise", "dep:windmill-autoscaling", "windmill-autoscaling/enterprise"]
enterprise = ["windmill-queue/enterprise", "windmill-audit/enterprise", "windmill-git-sync/enterprise", "windmill-common/enterprise", "windmill-worker?/enterprise", "windmill-api-auth/enterprise", "windmill-store/enterprise", "windmill-api-jobs/enterprise", "windmill-api-scripts/enterprise", "windmill-api-flows/enterprise", "windmill-api-users/enterprise", "windmill-api-workspaces/enterprise", "windmill-api-groups/enterprise", "windmill-api-configs/enterprise", "windmill-api-settings/enterprise", "windmill-api-agent-workers?/enterprise", "windmill-trigger/enterprise", "windmill-trigger-kafka?/enterprise", "windmill-trigger-postgres?/enterprise", "windmill-trigger-mqtt?/enterprise", "windmill-trigger-websocket?/enterprise", "windmill-trigger-email?/enterprise", "windmill-trigger-nats?/enterprise", "windmill-trigger-sqs?/enterprise", "windmill-trigger-gcp?/enterprise", "windmill-trigger-http?/enterprise", "windmill-native-triggers?/enterprise", "dep:windmill-autoscaling", "windmill-autoscaling/enterprise"]
stripe = []
run_inline = ["dep:windmill-worker", "windmill-api-configs/run_inline"]
agent_worker_server = ["dep:windmill-worker", "dep:windmill-api-agent-workers"]
+1 -13
View File
@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.658.0
version: 1.657.0
title: Windmill API
contact:
@@ -24326,10 +24326,6 @@ components:
type: object
description: Configuration for the trigger including event_type and service_config
additionalProperties: true
summary:
type: string
nullable: true
description: Short summary describing the purpose of this trigger
error:
type: string
nullable: true
@@ -24364,10 +24360,6 @@ components:
type: object
description: Configuration for the trigger including event_type and service_config
additionalProperties: true
summary:
type: string
nullable: true
description: Short summary describing the purpose of this trigger
error:
type: string
nullable: true
@@ -24464,10 +24456,6 @@ components:
type: object
description: Service-specific configuration (e.g., event types, filters)
additionalProperties: true
summary:
type: string
nullable: true
description: Short summary describing the purpose of this trigger
required:
- script_path
- is_flow
-14
View File
@@ -169,9 +169,6 @@ struct AIStandardResource {
/// Enable 1M context window for Anthropic
#[serde(alias = "enable_1M_context", default)]
enable_1m_context: bool,
/// Custom HTTP headers to include in AI requests
#[serde(default)]
headers: HashMap<String, String>,
}
#[derive(Deserialize, Debug)]
@@ -203,7 +200,6 @@ struct AIRequestConfig {
pub aws_session_token: Option<String>,
pub platform: AIPlatform,
pub enable_1m_context: bool,
pub custom_headers: HashMap<String, String>,
}
impl AIRequestConfig {
@@ -225,13 +221,11 @@ impl AIRequestConfig {
aws_session_token,
platform,
enable_1m_context,
custom_headers,
) = match resource {
AIResource::Standard(resource) => {
let region = resource.region.clone();
let platform = resource.platform.clone();
let enable_1m_context = resource.enable_1m_context;
let custom_headers = resource.headers.clone();
// Skip get_base_url for Bedrock - it uses SDK directly, not HTTP
let base_url = if matches!(provider, AIProvider::AWSBedrock) {
String::new()
@@ -277,7 +271,6 @@ impl AIRequestConfig {
aws_session_token,
platform,
enable_1m_context,
custom_headers,
)
}
AIResource::OAuth(resource) => {
@@ -301,7 +294,6 @@ impl AIRequestConfig {
None,
AIPlatform::Standard,
false,
HashMap::new(),
)
}
};
@@ -318,7 +310,6 @@ impl AIRequestConfig {
aws_session_token,
platform,
enable_1m_context,
custom_headers,
})
}
@@ -452,11 +443,6 @@ impl AIRequestConfig {
request = request.header(header_name.as_str(), header_value.as_str());
}
// Apply custom headers from the resource
for (header_name, header_value) in &self.custom_headers {
request = request.header(header_name.as_str(), header_value.as_str());
}
Ok(request)
}
-7
View File
@@ -57,13 +57,6 @@ pub fn extract_workspace_dependencies_annotated_refs(
None,
runnable_path,
),
Powershell => WorkspaceDependenciesAnnotatedRefs::parse(
"#",
"modules_json",
code,
None,
runnable_path,
),
_ => return None,
}
}
@@ -548,23 +548,6 @@ impl WorkspaceDependenciesPrefetched {
})
}
pub fn get_powershell(&self) -> error::Result<Option<String>> {
use WorkspaceDependenciesPrefetchedInternal::*;
self.internal.assert_no_extra_mode().map_err(map_err)?;
Ok(match &self.internal {
Explicit(wdar @ WorkspaceDependenciesAnnotatedRefs { external, .. }) => {
wdar.assert_no_inline().map_err(map_err)?;
wdar.assert_external_less_than(2).map_err(map_err)?;
external
.get(0)
.map(|wd| wd.content.clone())
.or(Some(r#"{"modules": {}}"#.to_owned()))
}
Implicit { workspace_dependencies, .. } => Some(workspace_dependencies.content.clone()),
None => Option::None,
})
}
/// Is the runnable permitted to have external references
pub fn is_external_references_permitted(runnable_path: &str) -> bool {
!BLACKLIST.contains(runnable_path) && !runnable_path.starts_with("hub/")
@@ -605,9 +588,7 @@ impl WorkspaceDependenciesPrefetched {
use WorkspaceDependenciesPrefetchedInternal::*;
match (self.language, &self.internal) {
// These languages except for python had none of this functionality
(Php | Bun | Bunnative | Go | Powershell, wdp) => {
wdp.assert_no_workspace_dependencies()?
}
(Php | Bun | Bunnative | Go, wdp) => wdp.assert_no_workspace_dependencies()?,
// Python, had #(extra_)requirements:
// but it had no external requirements.
@@ -1280,130 +1261,6 @@ def main():
assert!(result.external.is_empty());
assert!(result.inline.is_none());
}
#[test]
fn test_parse_annotation_powershell_modules_json_manual_mode() {
let code = r#"
# modules_json: default
param()
Write-Host "Hello"
"#;
let result = WorkspaceDependenciesAnnotatedRefs::<String>::parse(
"#",
"modules_json",
code,
None,
"",
)
.unwrap();
assert!(matches!(result.mode, Mode::manual));
assert_eq!(result.external, vec!["default".to_owned()]);
assert!(result.inline.is_none());
}
#[test]
fn test_parse_annotation_powershell_modules_json_extra_mode() {
let code = r#"
# extra_modules_json: my_deps
param()
Write-Host "Hello"
"#;
let result = WorkspaceDependenciesAnnotatedRefs::<String>::parse(
"#",
"modules_json",
code,
None,
"",
)
.unwrap();
assert!(matches!(result.mode, Mode::extra));
assert_eq!(result.external, vec!["my_deps".to_owned()]);
assert!(result.inline.is_none());
}
#[test]
fn test_parse_annotation_powershell_modules_json_extra_hyphen() {
let code = r#"
# extra-modules_json: my_deps
param()
Write-Host "Hello"
"#;
let result = WorkspaceDependenciesAnnotatedRefs::<String>::parse(
"#",
"modules_json",
code,
None,
"",
)
.unwrap();
assert!(matches!(result.mode, Mode::extra));
assert_eq!(result.external, vec!["my_deps".to_owned()]);
}
#[test]
fn test_parse_annotation_powershell_modules_json_multiple_refs() {
let code = r#"
# modules_json: default, extra_modules
param()
"#;
let result = WorkspaceDependenciesAnnotatedRefs::<String>::parse(
"#",
"modules_json",
code,
None,
"",
)
.unwrap();
assert!(matches!(result.mode, Mode::manual));
assert_eq!(
result.external,
vec!["default".to_owned(), "extra_modules".to_owned()]
);
}
#[test]
fn test_parse_annotation_powershell_no_match() {
let code = r#"
param()
Import-Module PSWriteColor
Write-Host "Hello"
"#;
let result = WorkspaceDependenciesAnnotatedRefs::<String>::parse(
"#",
"modules_json",
code,
None,
"",
);
assert!(result.is_none());
}
#[test]
fn test_parse_annotation_powershell_modules_json_with_inline() {
let code = r#"
# modules_json: default
#{ "modules": { "Extra": "1.0" } }
param()
"#;
let result = WorkspaceDependenciesAnnotatedRefs::<String>::parse(
"#",
"modules_json",
code,
None,
"",
)
.unwrap();
assert!(matches!(result.mode, Mode::manual));
assert_eq!(result.external, vec!["default".to_owned()]);
assert!(result.inline.is_some());
assert!(result.inline.as_ref().unwrap().contains("Extra"));
}
#[test]
fn test_parse_annotation_blacklisted() {
let code = r#"
@@ -181,7 +181,6 @@ async fn create_native_trigger<T: External>(
&external_id,
&config,
service_config,
data.summary.as_deref(),
)
.await?;
@@ -288,7 +287,6 @@ async fn update_native_trigger_handler<T: External>(
&external_id,
&config,
service_config,
data.summary.as_deref(),
)
.await?;
+6 -28
View File
@@ -192,7 +192,6 @@ pub struct NativeTrigger {
pub is_flow: bool,
pub webhook_token_prefix: String,
pub service_config: Option<serde_json::Value>,
pub summary: Option<String>,
pub error: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
@@ -210,7 +209,6 @@ pub struct NativeTriggerData<C> {
pub script_path: String,
pub is_flow: bool,
pub service_config: C,
pub summary: Option<String>,
}
#[derive(Debug, Clone, FromRow, Serialize, Deserialize)]
@@ -445,16 +443,10 @@ pub async fn make_http_request<T: DeserializeOwned + Send, B: Serialize>(
request = request.json(body_content);
}
let response = request.send().await?;
let status = response.status();
let bytes = response.bytes().await?;
if !status.is_success() {
let body = String::from_utf8_lossy(&bytes);
return Err(HttpRequestError::ApiError { status, body: body.into_owned() });
}
let response = request.send().await?.error_for_status()?;
// Handle empty responses (e.g. 204 No Content from Google channels/stop)
let bytes = response.bytes().await?;
if bytes.is_empty() {
serde_json::from_str("null").map_err(HttpRequestError::Json)
} else {
@@ -466,7 +458,6 @@ pub async fn make_http_request<T: DeserializeOwned + Send, B: Serialize>(
pub enum HttpRequestError {
Reqwest(reqwest::Error),
Json(serde_json::Error),
ApiError { status: StatusCode, body: String },
}
impl std::fmt::Display for HttpRequestError {
@@ -474,9 +465,6 @@ impl std::fmt::Display for HttpRequestError {
match self {
HttpRequestError::Reqwest(e) => write!(f, "{}", e),
HttpRequestError::Json(e) => write!(f, "JSON decode error: {}", e),
HttpRequestError::ApiError { status, body } => {
write!(f, "HTTP {} error: {}", status.as_u16(), body)
}
}
}
}
@@ -486,7 +474,6 @@ impl std::error::Error for HttpRequestError {
match self {
HttpRequestError::Reqwest(e) => Some(e),
HttpRequestError::Json(e) => Some(e),
HttpRequestError::ApiError { .. } => None,
}
}
}
@@ -502,7 +489,6 @@ impl HttpRequestError {
match self {
HttpRequestError::Reqwest(e) => e.status(),
HttpRequestError::Json(_) => None,
HttpRequestError::ApiError { status, .. } => Some(*status),
}
}
}
@@ -779,7 +765,6 @@ pub async fn store_native_trigger<'c, E: sqlx::Executor<'c, Database = Postgres>
external_id: &str,
config: &NativeTriggerConfig,
service_config: C,
summary: Option<&str>,
) -> Result<()> {
// Store only the first 10 characters of the webhook token as a prefix
let webhook_token_prefix: String = config.webhook_token.chars().take(10).collect();
@@ -793,13 +778,12 @@ pub async fn store_native_trigger<'c, E: sqlx::Executor<'c, Database = Postgres>
script_path,
is_flow,
webhook_token_prefix,
service_config,
summary
service_config
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8
$1, $2, $3, $4, $5, $6, $7
)
ON CONFLICT (external_id, workspace_id, service_name)
DO UPDATE SET script_path = $4, is_flow = $5, webhook_token_prefix = $6, service_config = $7, summary = $8, error = NULL, updated_at = NOW()
DO UPDATE SET script_path = $4, is_flow = $5, webhook_token_prefix = $6, service_config = $7, error = NULL, updated_at = NOW()
"#,
external_id,
workspace_id,
@@ -808,7 +792,6 @@ pub async fn store_native_trigger<'c, E: sqlx::Executor<'c, Database = Postgres>
config.is_flow,
webhook_token_prefix,
sqlx::types::Json(service_config) as _,
summary,
)
.execute(db)
.await?;
@@ -823,7 +806,6 @@ pub async fn update_native_trigger<'c, E: sqlx::Executor<'c, Database = Postgres
external_id: &str,
config: &NativeTriggerConfig,
service_config: Option<&RawValue>,
summary: Option<&str>,
) -> Result<()> {
// Store only the first 10 characters of the webhook token as a prefix
let webhook_token_prefix: String = config.webhook_token.chars().take(10).collect();
@@ -831,7 +813,7 @@ pub async fn update_native_trigger<'c, E: sqlx::Executor<'c, Database = Postgres
sqlx::query!(
r#"
UPDATE native_trigger
SET script_path = $1, is_flow = $2, webhook_token_prefix = $3, service_config = $4, summary = $8, error = NULL, updated_at = NOW()
SET script_path = $1, is_flow = $2, webhook_token_prefix = $3, service_config = $4, error = NULL, updated_at = NOW()
WHERE
workspace_id = $5
AND service_name = $6
@@ -844,7 +826,6 @@ pub async fn update_native_trigger<'c, E: sqlx::Executor<'c, Database = Postgres
workspace_id,
service_name as ServiceName,
external_id,
summary,
)
.execute(db)
.await?;
@@ -893,7 +874,6 @@ pub async fn get_native_trigger<'c, E: sqlx::Executor<'c, Database = Postgres>>(
is_flow,
webhook_token_prefix,
service_config,
summary,
error,
created_at,
updated_at
@@ -932,7 +912,6 @@ pub async fn get_native_trigger_by_script<'c, E: sqlx::Executor<'c, Database = P
is_flow,
webhook_token_prefix,
service_config,
summary,
error,
created_at,
updated_at
@@ -979,7 +958,6 @@ pub async fn list_native_triggers<'c, E: sqlx::Executor<'c, Database = Postgres>
nt.is_flow,
nt.webhook_token_prefix,
nt.service_config,
nt.summary,
nt.error,
nt.created_at,
nt.updated_at
+12 -65
View File
@@ -883,15 +883,6 @@ 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 deleted_path = sqlx::query_scalar!(
"DELETE FROM resource WHERE path = $1 AND workspace_id = $2 RETURNING path",
path,
@@ -900,32 +891,13 @@ async fn delete_resource(
.fetch_optional(&mut *tx)
.await?;
not_found_if_none(deleted_path, "Resource", &path)?;
// Collect all $var: paths referenced in the resource value
let mut linked_var_paths: Vec<String> = Vec::new();
if let Some(Some(value)) = resource_value {
collect_var_refs(&value, &mut linked_var_paths);
}
// Delete linked variables that are actually referenced in the resource value
let deleted_linked_variables: Vec<String> = if linked_var_paths.is_empty() {
Vec::new()
} else {
let placeholders: Vec<String> = linked_var_paths
.iter()
.enumerate()
.map(|(i, _)| format!("${}", i + 2))
.collect();
let query = format!(
"DELETE FROM variable WHERE workspace_id = $1 AND path IN ({}) RETURNING path",
placeholders.join(", ")
);
let mut q = sqlx::query_scalar::<_, String>(&query).bind(&w_id);
for var_path in &linked_var_paths {
q = q.bind(var_path);
}
q.fetch_all(&mut *tx).await?
};
let deleted_linked_variable = sqlx::query_scalar!(
"DELETE FROM variable WHERE path = $1 AND workspace_id = $2 RETURNING path",
path,
w_id
)
.fetch_optional(&mut *tx)
.await?;
audit_log(
&mut *tx,
&authed,
@@ -955,19 +927,19 @@ async fn delete_resource(
WebhookMessage::DeleteResource { workspace: w_id.clone(), path: path.to_owned() },
);
for var_path in &deleted_linked_variables {
if deleted_linked_variable.is_some() {
handle_deployment_metadata(
&authed.email,
&authed.username,
&db,
&w_id,
DeployedObject::Variable {
path: var_path.clone(),
parent_path: Some(var_path.clone()),
path: path.to_string(),
parent_path: Some(path.to_string()),
},
Some(format!(
"Variable '{}' deleted (linked resource deleted)",
var_path
path
)),
true,
None,
@@ -976,38 +948,13 @@ 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, path: path.to_owned() },
);
}
Ok(format!("resource {} deleted", path))
}
/// Recursively collect all `$var:path` references from a JSON value.
fn collect_var_refs(value: &serde_json::Value, out: &mut Vec<String>) {
match value {
serde_json::Value::String(s) => {
if let Some(var_path) = s.strip_prefix("$var:") {
out.push(var_path.to_string());
}
}
serde_json::Value::Object(m) => {
for v in m.values() {
collect_var_refs(v, out);
}
}
serde_json::Value::Array(arr) => {
for v in arr {
collect_var_refs(v, out);
}
}
_ => {}
}
}
async fn delete_resources_bulk(
authed: ApiAuthed,
Extension(db): Extension<DB>,
-1
View File
@@ -96,7 +96,6 @@ impl ScriptLang {
Python3 => "requirements.in",
// Go => "go.mod",
Php => "composer.json",
Powershell => "modules.json",
_ => return None,
}
.to_owned(),
-3
View File
@@ -143,8 +143,5 @@ hyper-tls = { workspace = true, optional = true }
hyper-util = { workspace = true, optional = true }
rcgen = { workspace = true, optional = true }
[dev-dependencies]
tempfile.workspace = true
[build-dependencies]
libffi-sys = { workspace = true, optional = true }
@@ -3,7 +3,6 @@ name: "ansible run script"
mode: ONCE
hostname: "ansible"
log_level: ERROR
time_limit: {TIMEOUT}
rlimit_as: 4096
rlimit_cpu: 1000
@@ -3,7 +3,6 @@ name: "bash run script"
mode: ONCE
hostname: "bash"
log_level: ERROR
time_limit: {TIMEOUT}
disable_rl: true
@@ -3,7 +3,6 @@ name: "{LANG} run script"
mode: ONCE
hostname: "{LANG}"
log_level: ERROR
time_limit: {TIMEOUT}
disable_rl: true
@@ -3,7 +3,6 @@ name: "csharp run script"
mode: ONCE
hostname: "csharp"
log_level: ERROR
time_limit: {TIMEOUT}
disable_rl: true
@@ -3,7 +3,6 @@ name: "go run script"
mode: ONCE
hostname: "go"
log_level: ERROR
time_limit: {TIMEOUT}
disable_rl: true
@@ -3,7 +3,6 @@ name: "java run script"
mode: ONCE
hostname: "java"
log_level: ERROR
time_limit: {TIMEOUT}
disable_rl: true
@@ -3,7 +3,6 @@ name: "nu run script"
mode: ONCE
hostname: "nu"
log_level: ERROR
time_limit: {TIMEOUT}
disable_rl: true
@@ -3,7 +3,6 @@ name: "php run script"
mode: ONCE
hostname: "php"
log_level: ERROR
time_limit: {TIMEOUT}
disable_rl: true
@@ -3,7 +3,6 @@ name: "powershell run script"
mode: ONCE
hostname: "powershell"
log_level: ERROR
time_limit: {TIMEOUT}
disable_rl: true
@@ -3,7 +3,6 @@ name: "python run script"
mode: ONCE
hostname: "python"
log_level: ERROR
time_limit: {TIMEOUT}
rlimit_as: 4096
rlimit_cpu: 1000
@@ -3,7 +3,6 @@ name: "ruby run script"
mode: ONCE
hostname: "ruby"
log_level: ERROR
time_limit: {TIMEOUT}
disable_rl: true
@@ -3,7 +3,6 @@ name: "rust run script"
mode: ONCE
hostname: "rust"
log_level: ERROR
time_limit: {TIMEOUT}
disable_rl: true
-7
View File
@@ -193,9 +193,6 @@ pub struct ProviderResource {
/// Enable 1M context window for Anthropic
#[serde(alias = "enable_1M_context", default)]
pub enable_1m_context: bool,
/// Custom HTTP headers to include in AI requests
#[serde(default)]
pub headers: HashMap<String, String>,
}
#[derive(Deserialize, Debug)]
@@ -247,10 +244,6 @@ impl ProviderWithResource {
pub fn get_enable_1m_context(&self) -> bool {
self.resource.enable_1m_context
}
pub fn get_headers(&self) -> &HashMap<String, String> {
&self.resource.headers
}
}
/// Token usage information from the AI provider
+4 -15
View File
@@ -762,10 +762,8 @@ 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 {
if used_structured_output_tool {
break;
}
@@ -832,8 +830,6 @@ pub async fn run_agent(
.await
.0;
let resource_headers = args.provider.get_headers();
// Helper to build HTTP request with headers
let build_http_request = |body: String| {
let mut req = HTTP_CLIENT
@@ -849,10 +845,6 @@ pub async fn run_agent(
req = req.header(header_name.as_str(), header_value.as_str());
}
for (header_name, header_value) in resource_headers {
req = req.header(header_name.as_str(), header_value.as_str());
}
req.body(body)
};
@@ -1038,13 +1030,10 @@ pub async fn run_agent(
if tool_calls.is_empty() {
break;
} else if i == max_iterations - 1 {
let partial_result = serde_json::to_string(&serde_json::json!({
"messages": messages,
})).unwrap_or_default();
return Err(Error::internal_err(format!(
"AI agent reached max iterations ({}), you can either increase max_iterations or enable the \"continue on error\" option from the advanced options of the step. Partial result:\n{}",
max_iterations, partial_result
)));
return Err(Error::internal_err(
"AI agent reached max iterations, but there are still tool calls"
.to_string(),
));
}
messages.push(OpenAIMessage {
@@ -30,8 +30,7 @@ use crate::{
bash_executor::BIN_BASH,
common::{
build_command_with_isolation, check_executor_binary_exists, get_reserved_variables,
read_and_check_result, resolve_nsjail_timeout, start_child_process, transform_json,
OccupancyMetrics,
read_and_check_result, start_child_process, transform_json, OccupancyMetrics,
},
handle_child::handle_child,
is_sandboxing_enabled,
@@ -1181,8 +1180,6 @@ mount {{
)
})
.join("\n");
let nsjail_timeout =
resolve_nsjail_timeout(conn, &job.workspace_id, job.id, job.timeout).await;
let _ = write_file(
job_dir,
"run.config.proto",
@@ -1196,8 +1193,7 @@ mount {{
.replace(
"{ADDITIONAL_PYTHON_PATHS}",
additional_python_paths_folders.as_str(),
)
.replace("{TIMEOUT}", &nsjail_timeout),
),
)?;
} else {
reserved_variables.insert("PYTHONPATH".to_string(), additional_python_paths_folders);
+2 -6
View File
@@ -41,8 +41,7 @@ use crate::handle_child::run_future_with_polling_update_job_poller;
use crate::{
common::{
build_args_map, build_command_with_isolation, get_reserved_variables, read_file,
read_file_content, resolve_nsjail_timeout, start_child_process, OccupancyMetrics,
DEV_CONF_NSJAIL,
read_file_content, start_child_process, OccupancyMetrics, DEV_CONF_NSJAIL,
},
get_proxy_envs_for_lang,
handle_child::handle_child,
@@ -193,8 +192,6 @@ exit $exit_status
// Use nsjail if globally enabled OR if script has #sandbox annotation
let nsjail = (is_sandboxing_enabled() || annotation.sandbox) && is_regular_job;
let child = if nsjail {
let nsjail_timeout =
resolve_nsjail_timeout(conn, &job.workspace_id, job.id, job.timeout).await;
let _ = write_file(
job_dir,
"run.config.proto",
@@ -203,8 +200,7 @@ exit $exit_status
.replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string())
.replace("{SHARED_MOUNT}", shared_mount)
.replace("{TRACING_PROXY_CA_CERT_PATH}", &*TRACING_PROXY_CA_CERT_PATH)
.replace("#{DEV}", DEV_CONF_NSJAIL)
.replace("{TIMEOUT}", &nsjail_timeout),
.replace("#{DEV}", DEV_CONF_NSJAIL),
)?;
let mut cmd_args = vec![
"--config",
+3 -7
View File
@@ -15,9 +15,8 @@ use windmill_queue::{append_logs, CanceledBy, MiniPulledJob, PrecomputedAgentInf
use crate::{
common::{
build_command_with_isolation, create_args_and_out_file, get_reserved_variables,
parse_npm_config, read_file, read_file_content, read_result, resolve_nsjail_timeout,
start_child_process, write_file_binary, MaybeLock, OccupancyMetrics, StreamNotifier,
DEV_CONF_NSJAIL,
parse_npm_config, read_file, read_file_content, read_result, start_child_process,
write_file_binary, MaybeLock, OccupancyMetrics, StreamNotifier, DEV_CONF_NSJAIL,
},
get_proxy_envs_for_lang,
handle_child::handle_child,
@@ -1775,8 +1774,6 @@ try {{
//do not cache local dependencies
let child = if is_sandboxing_enabled() || annotation.sandbox {
let nsjail_timeout =
resolve_nsjail_timeout(conn, &job.workspace_id, job.id, job.timeout).await;
let _ = write_file(
job_dir,
"run.config.proto",
@@ -1796,8 +1793,7 @@ try {{
),
)
.replace("{TRACING_PROXY_CA_CERT_PATH}", &*TRACING_PROXY_CA_CERT_PATH)
.replace("#{DEV}", DEV_CONF_NSJAIL)
.replace("{TIMEOUT}", &nsjail_timeout),
.replace("#{DEV}", DEV_CONF_NSJAIL),
)?;
let mut nsjail_cmd = Command::new(NSJAIL_PATH.as_str());
-11
View File
@@ -805,17 +805,6 @@ pub async fn resolve_job_timeout(
}
}
/// Compute the nsjail timeout (in seconds) with a 15s buffer so handle_child fires first.
pub async fn resolve_nsjail_timeout(
conn: &Connection,
w_id: &str,
job_id: Uuid,
custom_timeout: Option<i32>,
) -> String {
let (duration, _, _) = resolve_job_timeout(conn, w_id, job_id, custom_timeout).await;
(duration.as_secs() + 15).to_string()
}
async fn hash_args(
#[allow(unused)] db: &DB,
#[allow(unused)] client: &AuthedClient,
@@ -27,8 +27,7 @@ use windmill_queue::CanceledBy;
use crate::{
common::{
build_command_with_isolation, check_executor_binary_exists, create_args_and_out_file,
get_reserved_variables, read_result, resolve_nsjail_timeout, start_child_process,
DEV_CONF_NSJAIL,
get_reserved_variables, read_result, start_child_process, DEV_CONF_NSJAIL,
},
get_proxy_envs_for_lang,
handle_child::handle_child,
@@ -583,8 +582,6 @@ pub async fn handle_csharp_job(
get_reserved_variables(job, &client.token, conn, parent_runnable_path).await?;
let child = if is_sandboxing_enabled() {
let nsjail_timeout =
resolve_nsjail_timeout(conn, &job.workspace_id, job.id, job.timeout).await;
write_file(
job_dir,
"run.config.proto",
@@ -595,8 +592,7 @@ pub async fn handle_csharp_job(
.replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string())
.replace("{SHARED_MOUNT}", shared_mount)
.replace("{TRACING_PROXY_CA_CERT_PATH}", &*TRACING_PROXY_CA_CERT_PATH)
.replace("#{DEV}", DEV_CONF_NSJAIL)
.replace("{TIMEOUT}", &nsjail_timeout),
.replace("#{DEV}", DEV_CONF_NSJAIL),
)?;
let mut nsjail_cmd = Command::new(NSJAIL_PATH.as_str());
nsjail_cmd
+2 -6
View File
@@ -22,8 +22,7 @@ use windmill_queue::{append_logs, CanceledBy, MiniPulledJob};
use crate::{
common::{
build_command_with_isolation, capitalize, create_args_and_out_file, get_reserved_variables,
read_result, resolve_nsjail_timeout, start_child_process, OccupancyMetrics,
DEV_CONF_NSJAIL,
read_result, start_child_process, OccupancyMetrics, DEV_CONF_NSJAIL,
},
handle_child::handle_child,
is_sandboxing_enabled, read_ee_registry, DISABLE_NUSER, GOPRIVATE, GOPROXY, GO_BIN_CACHE_DIR,
@@ -339,8 +338,6 @@ func Run(req Req) (interface{{}}, error){{
get_reserved_variables(job, &client.token, conn, parent_runnable_path).await?;
let child = if is_sandboxing_enabled() {
let nsjail_timeout =
resolve_nsjail_timeout(conn, &job.workspace_id, job.id, job.timeout).await;
let _ = write_file(
job_dir,
"run.config.proto",
@@ -349,8 +346,7 @@ func Run(req Req) (interface{{}}, error){{
.replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string())
.replace("{SHARED_MOUNT}", shared_mount)
.replace("{TRACING_PROXY_CA_CERT_PATH}", &*TRACING_PROXY_CA_CERT_PATH)
.replace("#{DEV}", DEV_CONF_NSJAIL)
.replace("{TIMEOUT}", &nsjail_timeout),
.replace("#{DEV}", DEV_CONF_NSJAIL),
)?;
let mut nsjail_cmd = Command::new(NSJAIL_PATH.as_str());
nsjail_cmd
+2 -5
View File
@@ -23,7 +23,7 @@ use windmill_queue::{append_logs, CanceledBy, MiniPulledJob};
use crate::{
common::{
build_command_with_isolation, create_args_and_out_file, get_reserved_variables,
read_result, resolve_nsjail_timeout, start_child_process, OccupancyMetrics,
read_result, start_child_process, OccupancyMetrics,
},
handle_child, is_sandboxing_enabled, read_ee_registry,
universal_pkg_installer::{par_install_language_dependencies_all_at_once, RequiredDependency},
@@ -600,8 +600,6 @@ async fn run<'a>(
)
.await;
let nsjail_timeout =
resolve_nsjail_timeout(conn, &job.workspace_id, job.id, job.timeout).await;
write_file(
job_dir,
"run.config.proto",
@@ -610,8 +608,7 @@ async fn run<'a>(
.replace("{CACHE_DIR}", &*JAVA_CACHE_DIR)
.replace("{SHARED_MOUNT}", &shared_mount)
// .replace("{CACHED_TARGET}", &shared_mount)
.replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string())
.replace("{TIMEOUT}", &nsjail_timeout),
.replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string()),
)?;
let mut cmd = Command::new(NSJAIL_PATH.as_str());
cmd.env_clear()
+2 -6
View File
@@ -14,8 +14,7 @@ use windmill_queue::{append_logs, CanceledBy, MiniPulledJob};
use crate::{
common::{
build_command_with_isolation, create_args_and_out_file, get_reserved_variables,
read_result, resolve_nsjail_timeout, start_child_process, OccupancyMetrics,
DEV_CONF_NSJAIL,
read_result, start_child_process, OccupancyMetrics, DEV_CONF_NSJAIL,
},
get_proxy_envs_for_lang, handle_child, is_sandboxing_enabled, DISABLE_NUSER, NSJAIL_PATH,
PATH_ENV, TRACING_PROXY_CA_CERT_PATH,
@@ -246,8 +245,6 @@ async fn run<'a>(
)
.await;
let nsjail_timeout =
resolve_nsjail_timeout(conn, &job.workspace_id, job.id, job.timeout).await;
write_file(
job_dir,
"run.config.proto",
@@ -257,8 +254,7 @@ async fn run<'a>(
.replace("{SHARED_MOUNT}", &shared_mount)
.replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string())
.replace("{TRACING_PROXY_CA_CERT_PATH}", &*TRACING_PROXY_CA_CERT_PATH)
.replace("#{DEV}", DEV_CONF_NSJAIL)
.replace("{TIMEOUT}", &nsjail_timeout),
.replace("#{DEV}", DEV_CONF_NSJAIL),
)?;
let mut nsjail_cmd = Command::new(NSJAIL_PATH.as_str());
nsjail_cmd
+2 -6
View File
@@ -19,8 +19,7 @@ use windmill_queue::{append_logs, CanceledBy};
use crate::{
common::{
build_command_with_isolation, check_executor_binary_exists, create_args_and_out_file,
get_reserved_variables, read_result, resolve_nsjail_timeout, start_child_process,
MaybeLock, OccupancyMetrics,
get_reserved_variables, read_result, start_child_process, MaybeLock, OccupancyMetrics,
},
handle_child::handle_child,
is_sandboxing_enabled, COMPOSER_CACHE_DIR, COMPOSER_PATH, DISABLE_NUSER, NSJAIL_PATH, PHP_PATH,
@@ -295,16 +294,13 @@ try {{
let (reserved_variables, _) = tokio::try_join!(reserved_variables_args_out_f, write_wrapper_f)?;
let child = if is_sandboxing_enabled() {
let nsjail_timeout =
resolve_nsjail_timeout(conn, &job.workspace_id, job.id, job.timeout).await;
let _ = write_file(
job_dir,
"run.config.proto",
&NSJAIL_CONFIG_RUN_PHP_CONTENT
.replace("{JOB_DIR}", job_dir)
.replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string())
.replace("{SHARED_MOUNT}", shared_mount)
.replace("{TIMEOUT}", &nsjail_timeout),
.replace("{SHARED_MOUNT}", shared_mount),
)?;
let mut nsjail_cmd = Command::new(NSJAIL_PATH.as_str());
+26 -439
View File
@@ -18,14 +18,13 @@ const NSJAIL_CONFIG_RUN_POWERSHELL_CONTENT: &str =
include_str!("../nsjail/run.powershell.config.proto");
lazy_static::lazy_static! {
static ref RE_POWERSHELL_IMPORTS: Regex = Regex::new(r#"^\s*Import-Module\s+(?:-Name\s+)?"?([^\s"]+)"?(?:\s+-RequiredVersion\s+"?([^\s"]+)"?)?"#).unwrap();
static ref RE_POWERSHELL_IMPORTS: Regex = Regex::new(r#"^Import-Module\s+(?:-Name\s+)?"?([^\s"]+)"?(?:\s+-RequiredVersion\s+"?([^\s"]+)"?)?"#).unwrap();
}
use crate::{
common::{
build_args_map, build_command_with_isolation, get_reserved_variables, read_file,
read_file_content, resolve_nsjail_timeout, start_child_process, MaybeLock,
OccupancyMetrics,
read_file_content, start_child_process, OccupancyMetrics,
},
handle_child::handle_child,
is_sandboxing_enabled, read_ee_registry, DISABLE_NUSER, HOME_ENV, NSJAIL_PATH, PATH_ENV,
@@ -197,41 +196,17 @@ async fn get_module_versions(module_path: &str) -> Result<Vec<String>, Error> {
.to_string();
// Check if this looks like a version (contains dots and numbers)
// and verify a module manifest (.psd1) or script (.psm1) actually exists
if version.chars().any(|c| c.is_numeric()) && version.contains('.') {
let has_module_files = fs::read_dir(&version_path)
.map(|entries| {
entries.filter_map(|e| e.ok()).any(|e| {
let name = e.file_name();
let name = name.to_string_lossy();
name.ends_with(".psd1") || name.ends_with(".psm1")
})
})
.unwrap_or(false);
if has_module_files {
versions.push(version);
}
versions.push(version);
}
}
}
}
}
// If no version subdirectories found, check if module files exist directly
// in the module directory (flat/single-version installation)
// If no version subdirectories found, treat as single version installation
if versions.is_empty() {
let has_module_files = fs::read_dir(module_path)
.map(|entries| {
entries.filter_map(|e| e.ok()).any(|e| {
let name = e.file_name();
let name = name.to_string_lossy();
name.ends_with(".psd1") || name.ends_with(".psm1")
})
})
.unwrap_or(false);
if has_module_files {
versions.push("unknown".to_string());
}
versions.push("unknown".to_string());
}
Ok(versions)
@@ -262,67 +237,8 @@ struct ModuleRequest {
version: Option<String>,
}
/// Parse Import-Module statements from PowerShell code into module requests.
fn parse_script_imports(code: &str) -> Vec<ModuleRequest> {
let mut modules = Vec::new();
for line in code.lines() {
for cap in RE_POWERSHELL_IMPORTS.captures_iter(line) {
let name = cap.get(1).unwrap().as_str().to_string();
let version = cap.get(2).map(|m| m.as_str().to_string());
modules.push(ModuleRequest { name, version });
}
}
modules
}
/// Parse a modules.json workspace dependencies content into module requests.
/// Format: { "modules": { "ModuleName": "1.0.0", "Another": null } }
fn parse_modules_json(content: &str) -> Result<Vec<ModuleRequest>, Error> {
let parsed: serde_json::Value = serde_json::from_str(content).map_err(|e| {
Error::internal_err(format!("Failed to parse PowerShell modules.json: {e}"))
})?;
let modules = parsed
.get("modules")
.and_then(|m| m.as_object())
.ok_or_else(|| {
Error::internal_err(
"PowerShell modules.json must have a \"modules\" object".to_string(),
)
})?;
let mut result = Vec::new();
for (name, version) in modules {
let version = match version {
serde_json::Value::String(v) if v != "*" => Some(v.clone()),
_ => None,
};
result.push(ModuleRequest { name: name.clone(), version });
}
Ok(result)
}
/// Merge workspace dependency modules with script import modules.
/// Workspace dependency versions take precedence on overlap.
fn merge_module_requests(
workspace_modules: Vec<ModuleRequest>,
script_modules: Vec<ModuleRequest>,
) -> Vec<ModuleRequest> {
let mut seen: HashMap<String, ModuleRequest> = HashMap::new();
// Script imports first (lower priority)
for m in script_modules {
let key = m.name.to_lowercase();
seen.entry(key).or_insert(m);
}
// Workspace deps override
for m in workspace_modules {
let key = m.name.to_lowercase();
seen.insert(key, m);
}
seen.into_values().collect()
}
#[tracing::instrument(level = "trace", skip_all)]
pub async fn handle_powershell_job(
maybe_lock: MaybeLock,
mem_peak: &mut i32,
canceled_by: &mut Option<CanceledBy>,
job: &MiniPulledJob,
@@ -387,29 +303,18 @@ pub async fn handle_powershell_job(
.join(" ")
};
// Resolve modules from workspace dependencies and/or script imports
let all_modules = match &maybe_lock {
MaybeLock::Resolved { lock } if !lock.is_empty() => {
// Deployed script with lock: parse workspace deps from lock, merge with script imports
let ws_modules = parse_modules_json(lock)?;
let script_modules = parse_script_imports(content);
merge_module_requests(ws_modules, script_modules)
// First, collect all imported modules
let mut imported_modules: Vec<(String, Option<String>)> = Vec::new();
for line in content.lines() {
for cap in RE_POWERSHELL_IMPORTS.captures_iter(line) {
let module_name = cap.get(1).unwrap().as_str().to_string();
let required_version = cap.get(2).map(|m| m.as_str().to_string());
imported_modules.push((module_name, required_version));
}
MaybeLock::Unresolved { workspace_dependencies } => {
let script_modules = parse_script_imports(content);
match workspace_dependencies.get_powershell()? {
Some(modules_json) => {
let ws_modules = parse_modules_json(&modules_json)?;
merge_module_requests(ws_modules, script_modules)
}
None => script_modules,
}
}
_ => parse_script_imports(content),
};
}
// Only scan the top-level cache directory if there are modules to check
let module_dirs = if !all_modules.is_empty() {
let module_dirs = if !imported_modules.is_empty() {
scan_module_directories().await?
} else {
HashMap::new()
@@ -418,20 +323,19 @@ pub async fn handle_powershell_job(
let mut modules_to_install: Vec<ModuleRequest> = Vec::new();
let mut logs1 = String::new();
for module_req in all_modules {
for (module_name, required_version) in imported_modules {
// Check if this specific module is already installed, only scanning versions if needed
let (is_installed, installed_versions) = check_module_installed(
&module_dirs,
&module_req.name,
module_req.version.as_deref(),
)
.await?;
let (is_installed, installed_versions) =
check_module_installed(&module_dirs, &module_name, required_version.as_deref()).await?;
if !is_installed {
modules_to_install.push(module_req);
modules_to_install.push(ModuleRequest {
name: module_name.clone(),
version: required_version.clone(),
});
} else {
// Log what versions are actually installed
let version_info = if let Some(version) = &module_req.version {
let version_info = if let Some(version) = &required_version {
format!(" version {} found in cache", version)
} else if installed_versions.len() == 1 {
format!(" (version {}) found in cache", installed_versions[0])
@@ -443,7 +347,7 @@ pub async fn handle_powershell_job(
} else {
" found in cache".to_string()
};
logs1.push_str(&format!("\n{}{}", module_req.name, version_info));
logs1.push_str(&format!("\n{}{}", module_name, version_info));
}
}
@@ -562,8 +466,8 @@ $env:PSModulePath = \"{};$PSModulePathBackup\"",
let strict_termination_end = "\n\
} catch {\n\
Write-Output \"An error occurred:\"\n\
Write-Output $_\n\
Write-Output \"An error occurred:\n\"\
Write-Output $_
exit 1\n\
}\n";
@@ -614,8 +518,6 @@ $env:PSModulePath = \"{};$PSModulePathBackup\"",
let nsjail = is_sandboxing_enabled() && is_regular_job;
let child = if nsjail {
let nsjail_timeout =
resolve_nsjail_timeout(db, &job.workspace_id, job.id, job.timeout).await;
let _ = write_file(
job_dir,
"run.config.proto",
@@ -623,8 +525,7 @@ $env:PSModulePath = \"{};$PSModulePathBackup\"",
.replace("{JOB_DIR}", job_dir)
.replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string())
.replace("{SHARED_MOUNT}", shared_mount)
.replace("{CACHE_DIR}", &*POWERSHELL_CACHE_DIR)
.replace("{TIMEOUT}", &nsjail_timeout),
.replace("{CACHE_DIR}", &*POWERSHELL_CACHE_DIR),
)?;
let cmd_args = vec![
"--config",
@@ -771,317 +672,3 @@ $env:PSModulePath = \"{};$PSModulePathBackup\"",
"No result.out, result2.out or result.json found"
)))
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
// --- RE_POWERSHELL_IMPORTS regex tests ---
fn match_import(line: &str) -> Option<(String, Option<String>)> {
RE_POWERSHELL_IMPORTS.captures(line).map(|cap| {
let name = cap.get(1).unwrap().as_str().to_string();
let version = cap.get(2).map(|m| m.as_str().to_string());
(name, version)
})
}
#[test]
fn test_import_module_basic() {
let (name, version) = match_import("Import-Module WindmillClient").unwrap();
assert_eq!(name, "WindmillClient");
assert_eq!(version, None);
}
#[test]
fn test_import_module_with_leading_whitespace() {
let (name, _) = match_import(" Import-Module WindmillClient").unwrap();
assert_eq!(name, "WindmillClient");
}
#[test]
fn test_import_module_with_tab_indent() {
let (name, _) = match_import("\tImport-Module WindmillClient").unwrap();
assert_eq!(name, "WindmillClient");
}
#[test]
fn test_import_module_with_name_flag() {
let (name, _) = match_import("Import-Module -Name WindmillClient").unwrap();
assert_eq!(name, "WindmillClient");
}
#[test]
fn test_import_module_with_required_version() {
let (name, version) =
match_import(r#"Import-Module WindmillClient -RequiredVersion "1.655.0""#).unwrap();
assert_eq!(name, "WindmillClient");
assert_eq!(version, Some("1.655.0".to_string()));
}
#[test]
fn test_import_module_quoted_name() {
let (name, _) = match_import(r#"Import-Module "WindmillClient""#).unwrap();
assert_eq!(name, "WindmillClient");
}
#[test]
fn test_import_module_name_flag_quoted_with_version() {
let (name, version) =
match_import(r#"Import-Module -Name "WindmillClient" -RequiredVersion "2.0.0""#)
.unwrap();
assert_eq!(name, "WindmillClient");
assert_eq!(version, Some("2.0.0".to_string()));
}
#[test]
fn test_import_module_indented_with_version() {
let (name, version) =
match_import(r#" Import-Module WindmillClient -RequiredVersion 1.0.0"#).unwrap();
assert_eq!(name, "WindmillClient");
assert_eq!(version, Some("1.0.0".to_string()));
}
#[test]
fn test_commented_import_not_matched() {
assert!(match_import("# Import-Module WindmillClient").is_none());
}
// --- get_module_versions / check_module_installed tests ---
#[tokio::test]
async fn test_empty_module_dir_not_installed() {
let tmp = TempDir::new().unwrap();
let module_dir = tmp.path().join("WindmillClient");
fs::create_dir(&module_dir).unwrap();
let versions = get_module_versions(module_dir.to_str().unwrap())
.await
.unwrap();
assert!(versions.is_empty(), "empty dir should have no versions");
}
#[tokio::test]
async fn test_empty_version_subdir_not_installed() {
let tmp = TempDir::new().unwrap();
let module_dir = tmp.path().join("WindmillClient");
let version_dir = module_dir.join("1.655.0");
fs::create_dir_all(&version_dir).unwrap();
let versions = get_module_versions(module_dir.to_str().unwrap())
.await
.unwrap();
assert!(
versions.is_empty(),
"version dir without .psd1/.psm1 should not count"
);
}
#[tokio::test]
async fn test_valid_versioned_module_detected() {
let tmp = TempDir::new().unwrap();
let module_dir = tmp.path().join("WindmillClient");
let version_dir = module_dir.join("1.655.0");
fs::create_dir_all(&version_dir).unwrap();
fs::write(version_dir.join("WindmillClient.psd1"), "# manifest").unwrap();
fs::write(version_dir.join("WindmillClient.psm1"), "# module").unwrap();
let versions = get_module_versions(module_dir.to_str().unwrap())
.await
.unwrap();
assert_eq!(versions, vec!["1.655.0"]);
}
#[tokio::test]
async fn test_flat_module_with_files_detected() {
let tmp = TempDir::new().unwrap();
let module_dir = tmp.path().join("MyModule");
fs::create_dir(&module_dir).unwrap();
fs::write(module_dir.join("MyModule.psm1"), "# module").unwrap();
let versions = get_module_versions(module_dir.to_str().unwrap())
.await
.unwrap();
assert_eq!(versions, vec!["unknown"]);
}
#[tokio::test]
async fn test_flat_module_without_files_not_detected() {
let tmp = TempDir::new().unwrap();
let module_dir = tmp.path().join("MyModule");
fs::create_dir(&module_dir).unwrap();
fs::write(module_dir.join("readme.txt"), "not a module").unwrap();
let versions = get_module_versions(module_dir.to_str().unwrap())
.await
.unwrap();
assert!(versions.is_empty());
}
#[tokio::test]
async fn test_check_module_installed_empty_dir_returns_false() {
let tmp = TempDir::new().unwrap();
let module_dir = tmp.path().join("WindmillClient");
fs::create_dir(&module_dir).unwrap();
let mut dirs = HashMap::new();
dirs.insert(
"windmillclient".to_string(),
module_dir.to_str().unwrap().to_string(),
);
let (installed, _) = check_module_installed(&dirs, "WindmillClient", None)
.await
.unwrap();
assert!(
!installed,
"empty module dir should not be considered installed"
);
}
#[tokio::test]
async fn test_check_module_installed_valid_module_returns_true() {
let tmp = TempDir::new().unwrap();
let module_dir = tmp.path().join("WindmillClient");
let version_dir = module_dir.join("1.655.0");
fs::create_dir_all(&version_dir).unwrap();
fs::write(version_dir.join("WindmillClient.psd1"), "# manifest").unwrap();
let mut dirs = HashMap::new();
dirs.insert(
"windmillclient".to_string(),
module_dir.to_str().unwrap().to_string(),
);
let (installed, versions) = check_module_installed(&dirs, "WindmillClient", None)
.await
.unwrap();
assert!(installed);
assert_eq!(versions, vec!["1.655.0"]);
}
#[tokio::test]
async fn test_check_module_installed_wrong_version_returns_false() {
let tmp = TempDir::new().unwrap();
let module_dir = tmp.path().join("WindmillClient");
let version_dir = module_dir.join("1.0.0");
fs::create_dir_all(&version_dir).unwrap();
fs::write(version_dir.join("WindmillClient.psd1"), "# manifest").unwrap();
let mut dirs = HashMap::new();
dirs.insert(
"windmillclient".to_string(),
module_dir.to_str().unwrap().to_string(),
);
let (installed, _) = check_module_installed(&dirs, "WindmillClient", Some("2.0.0"))
.await
.unwrap();
assert!(!installed, "wrong version should not match");
}
// --- parse_modules_json tests ---
#[test]
fn test_parse_modules_json_basic() {
let json = r#"{"modules": {"PSWriteColor": "1.0.0", "ImportExcel": null}}"#;
let modules = parse_modules_json(json).unwrap();
assert_eq!(modules.len(), 2);
let by_name: HashMap<String, Option<String>> =
modules.into_iter().map(|m| (m.name, m.version)).collect();
assert_eq!(by_name["PSWriteColor"], Some("1.0.0".to_string()));
assert_eq!(by_name["ImportExcel"], None);
}
#[test]
fn test_parse_modules_json_wildcard_treated_as_none() {
let json = r#"{"modules": {"Mod": "*"}}"#;
let modules = parse_modules_json(json).unwrap();
assert_eq!(modules[0].version, None);
}
#[test]
fn test_parse_modules_json_empty() {
let json = r#"{"modules": {}}"#;
let modules = parse_modules_json(json).unwrap();
assert!(modules.is_empty());
}
#[test]
fn test_parse_modules_json_missing_modules_key() {
let json = r#"{"deps": {}}"#;
assert!(parse_modules_json(json).is_err());
}
#[test]
fn test_parse_modules_json_invalid_json() {
assert!(parse_modules_json("not json").is_err());
}
// --- parse_script_imports tests ---
#[test]
fn test_parse_script_imports() {
let code = r#"Import-Module PSWriteColor
Import-Module ImportExcel -RequiredVersion "7.8.6"
# Import-Module Commented
Write-Host "Hello""#;
let modules = parse_script_imports(code);
assert_eq!(modules.len(), 2);
assert_eq!(modules[0].name, "PSWriteColor");
assert_eq!(modules[0].version, None);
assert_eq!(modules[1].name, "ImportExcel");
assert_eq!(modules[1].version, Some("7.8.6".to_string()));
}
// --- merge_module_requests tests ---
#[test]
fn test_merge_workspace_overrides_script() {
let ws = vec![ModuleRequest { name: "Mod".to_string(), version: Some("2.0".to_string()) }];
let script =
vec![ModuleRequest { name: "Mod".to_string(), version: Some("1.0".to_string()) }];
let merged = merge_module_requests(ws, script);
assert_eq!(merged.len(), 1);
assert_eq!(merged[0].version, Some("2.0".to_string()));
}
#[test]
fn test_merge_combines_distinct_modules() {
let ws = vec![ModuleRequest { name: "WsMod".to_string(), version: None }];
let script = vec![ModuleRequest { name: "ScriptMod".to_string(), version: None }];
let merged = merge_module_requests(ws, script);
assert_eq!(merged.len(), 2);
}
#[test]
fn test_merge_case_insensitive() {
let ws =
vec![ModuleRequest { name: "MyModule".to_string(), version: Some("2.0".to_string()) }];
let script =
vec![ModuleRequest { name: "mymodule".to_string(), version: Some("1.0".to_string()) }];
let merged = merge_module_requests(ws, script);
assert_eq!(merged.len(), 1);
// Workspace version wins
assert_eq!(merged[0].version, Some("2.0".to_string()));
}
#[tokio::test]
async fn test_multiple_versions_detected() {
let tmp = TempDir::new().unwrap();
let module_dir = tmp.path().join("WindmillClient");
for ver in &["1.0.0", "1.655.0"] {
let version_dir = module_dir.join(ver);
fs::create_dir_all(&version_dir).unwrap();
fs::write(version_dir.join("WindmillClient.psd1"), "# manifest").unwrap();
}
let mut versions = get_module_versions(module_dir.to_str().unwrap())
.await
.unwrap();
versions.sort();
assert_eq!(versions, vec!["1.0.0", "1.655.0"]);
}
}
@@ -128,8 +128,7 @@ use windmill_object_store::OBJECT_STORE_SETTINGS;
use crate::{
common::{
build_command_with_isolation, create_args_and_out_file, get_reserved_variables, read_file,
read_result, resolve_nsjail_timeout, start_child_process, OccupancyMetrics, StreamNotifier,
DEV_CONF_NSJAIL,
read_result, start_child_process, OccupancyMetrics, StreamNotifier, DEV_CONF_NSJAIL,
},
get_proxy_envs_for_lang,
handle_child::handle_child,
@@ -876,8 +875,6 @@ mount {{
)
})
.join("\n");
let nsjail_timeout =
resolve_nsjail_timeout(conn, &job.workspace_id, job.id, job.timeout).await;
let _ = write_file(
job_dir,
"run.config.proto",
@@ -894,8 +891,7 @@ mount {{
additional_python_paths_folders.as_str(),
)
.replace("{TRACING_PROXY_CA_CERT_PATH}", &*TRACING_PROXY_CA_CERT_PATH)
.replace("#{DEV}", DEV_CONF_NSJAIL)
.replace("{TIMEOUT}", &nsjail_timeout),
.replace("#{DEV}", DEV_CONF_NSJAIL),
)?;
} else {
reserved_variables.insert("PYTHONPATH".to_string(), additional_python_paths_folders);
+2 -6
View File
@@ -23,8 +23,7 @@ use windmill_queue::{append_logs, CanceledBy, MiniPulledJob};
use crate::{
common::{
build_command_with_isolation, create_args_and_out_file, get_reserved_variables,
read_result, resolve_nsjail_timeout, start_child_process, OccupancyMetrics,
DEV_CONF_NSJAIL,
read_result, start_child_process, OccupancyMetrics, DEV_CONF_NSJAIL,
},
get_proxy_envs_for_lang,
handle_child::{self},
@@ -793,8 +792,6 @@ mount {{
})
.join("\n");
let nsjail_timeout =
resolve_nsjail_timeout(conn, &job.workspace_id, job.id, job.timeout).await;
write_file(
job_dir,
"run.config.proto",
@@ -804,8 +801,7 @@ mount {{
.replace("{SHARED_DEPENDENCIES}", &shared_deps)
.replace("{TRACING_PROXY_CA_CERT_PATH}", &*TRACING_PROXY_CA_CERT_PATH)
.replace("#{DEV}", DEV_CONF_NSJAIL)
.replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string())
.replace("{TIMEOUT}", &nsjail_timeout),
.replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string()),
)?;
let mut cmd = Command::new(NSJAIL_PATH.as_str());
cmd.env_clear()
+3 -6
View File
@@ -23,8 +23,8 @@ use windmill_queue::{append_logs, CanceledBy};
use crate::{
common::{
build_command_with_isolation, check_executor_binary_exists, create_args_and_out_file,
get_reserved_variables, read_result, resolve_nsjail_timeout, start_child_process,
OccupancyMetrics, DEV_CONF_NSJAIL,
get_reserved_variables, read_result, start_child_process, OccupancyMetrics,
DEV_CONF_NSJAIL,
},
get_proxy_envs_for_lang,
handle_child::handle_child,
@@ -682,8 +682,6 @@ pub async fn handle_rust_job(
append_logs(&job.id, &job.workspace_id, logs2, conn).await;
let child = if is_sandboxing_enabled() {
let nsjail_timeout =
resolve_nsjail_timeout(conn, &job.workspace_id, job.id, job.timeout).await;
let _ = write_file(
job_dir,
"run.config.proto",
@@ -694,8 +692,7 @@ pub async fn handle_rust_job(
.replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string())
.replace("{TRACING_PROXY_CA_CERT_PATH}", &*TRACING_PROXY_CA_CERT_PATH)
.replace("#{DEV}", DEV_CONF_NSJAIL)
.replace("{SHARED_MOUNT}", shared_mount)
.replace("{TIMEOUT}", &nsjail_timeout),
.replace("{SHARED_MOUNT}", shared_mount),
)?;
let mut nsjail_cmd = Command::new(NSJAIL_PATH.as_str());
nsjail_cmd
-10
View File
@@ -4562,17 +4562,7 @@ mount {{
"Inline execution is not yet supported for this language".to_string(),
));
}
let maybe_lock = resolve_maybe_lock(
&lock,
&code,
language,
&job.workspace_id,
job.runnable_path(),
conn.clone(),
)
.await?;
Box::pin(handle_powershell_job(
maybe_lock,
mem_peak,
canceled_by,
job,
+1 -1
View File
@@ -2,7 +2,7 @@ import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts";
import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts";
import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts";
export const VERSION = "v1.658.0";
export const VERSION = "v1.657.0";
export async function login(email: string, password: string): Promise<string> {
return await windmill.UserService.login({
-24
View File
@@ -252,16 +252,6 @@ async function initAction(opts: InitOptions) {
}
}
// Read nonDottedPaths from config to specialize generated skills
let nonDottedPaths = true; // default for new inits
try {
const { readConfigFile } = await import("../../core/conf.ts");
const config = await readConfigFile();
nonDottedPaths = config.nonDottedPaths ?? true;
} catch {
// If config can't be read, use default
}
// Create guidance files (AGENTS.md, CLAUDE.md, and Claude skills)
try {
// Generate skills reference section for AGENTS.md
@@ -300,20 +290,6 @@ async function initAction(opts: InitOptions) {
let skillContent = SKILL_CONTENT[skill.name];
if (skillContent) {
// Replace placeholders with actual suffixes based on nonDottedPaths
if (nonDottedPaths) {
skillContent = skillContent
.replaceAll("{{FLOW_SUFFIX}}", "__flow")
.replaceAll("{{APP_SUFFIX}}", "__app")
.replaceAll("{{RAW_APP_SUFFIX}}", "__raw_app")
.replaceAll("{{INLINE_SCRIPT_NAMING}}", "Inline script files should NOT include `.inline_script.` in their names (e.g. use `a.ts`, not `a.inline_script.ts`).");
} else {
skillContent = skillContent
.replaceAll("{{FLOW_SUFFIX}}", ".flow")
.replaceAll("{{APP_SUFFIX}}", ".app")
.replaceAll("{{RAW_APP_SUFFIX}}", ".raw_app")
.replaceAll("{{INLINE_SCRIPT_NAMING}}", "Inline script files use the `.inline_script.` naming convention (e.g. `a.inline_script.ts`).");
}
// Check if this skill has schemas that need to be appended
const schemaMappings = SCHEMA_MAPPINGS[skill.name];
if (schemaMappings && schemaMappings.length > 0) {
-4
View File
@@ -230,7 +230,6 @@ export async function pushNativeTrigger(
script_path: result.script_path,
is_flow: result.is_flow,
service_config: result.service_config,
summary: result.summary,
error: result.error,
};
log.debug(`Native trigger ${serviceName}/${externalId} exists on remote`);
@@ -244,7 +243,6 @@ export async function pushNativeTrigger(
script_path: localTrigger.script_path,
is_flow: localTrigger.is_flow,
service_config: localTrigger.service_config,
summary: localTrigger.summary || undefined,
};
if (remoteTrigger) {
@@ -253,13 +251,11 @@ export async function pushNativeTrigger(
script_path: localTrigger.script_path,
is_flow: localTrigger.is_flow,
service_config: localTrigger.service_config,
summary: localTrigger.summary,
};
const remoteCompare = {
script_path: remoteTrigger.script_path,
is_flow: remoteTrigger.is_flow,
service_config: remoteTrigger.service_config,
summary: remoteTrigger.summary,
};
if (isSuperset(localCompare, remoteCompare)) {
+4 -4
View File
@@ -4236,10 +4236,10 @@ description: MUST use when creating flows.
## CLI Commands
Create a folder ending with \`{{FLOW_SUFFIX}}\` and add a \`flow.yaml\` file with the flow definition.
For rawscript modules, use \`!inline path/to/script.ts\` for the content key. {{INLINE_SCRIPT_NAMING}}
Create a folder ending with \`.flow\` and add a YAML file with the flow definition.
For rawscript modules, use \`!inline path/to/script.ts\` for the content key.
After writing, tell the user they can run:
- \`wmill flow generate-locks <path_to_flow_folder> --yes\` - Generate lock files for the specific flow you modified (e.g. \`wmill flow generate-locks f/my_folder/my_flow{{FLOW_SUFFIX}} --yes\`)
- \`wmill flow generate-locks <path_to_flow_folder> --yes\` - Generate lock files for the specific flow you modified (e.g. \`wmill flow generate-locks f/my_folder/my_flow.flow --yes\`)
- \`wmill sync push\` - Deploy to Windmill
Do NOT run these commands yourself. Instead, inform the user that they should run them.
@@ -4375,7 +4375,7 @@ This interactive command creates a complete app structure with your choice of fr
## App Structure
\`\`\`
my_app{{RAW_APP_SUFFIX}}/
my_app.raw_app/
AGENTS.md # AI agent instructions (auto-generated)
DATATABLES.md # Database schemas (run 'wmill app generate-agents' to refresh)
raw_app.yaml # App configuration (summary, path, data settings)
+1 -1
View File
@@ -68,7 +68,7 @@ export {
workspaceAdd,
};
export const VERSION = "1.658.0";
export const VERSION = "1.657.0";
// Re-exported from constants.ts to maintain backwards compatibility
export { WM_FORK_PREFIX } from "./core/constants.ts";
+1 -3
View File
@@ -328,7 +328,6 @@ const LANG_ANNOTATION_CONFIG: Partial<
nativets: { comment: "//", keyword: "package_json" },
go: { comment: "//", keyword: "go_mod" },
php: { comment: "//", keyword: "composer_json" },
powershell: { comment: "#", keyword: "modules_json" },
};
export function extractWorkspaceDepsAnnotation(
@@ -506,8 +505,7 @@ async function updateScriptLock(
): Promise<void> {
if (
!(
(workspaceDependenciesLanguages.some((l) => l.language == language) &&
language !== "powershell") ||
workspaceDependenciesLanguages.some((l) => l.language == language) ||
language == "deno" ||
language == "rust" ||
language == "ansible"
+2 -5
View File
@@ -30,15 +30,13 @@ export type WorkspaceDependenciesLanguage =
| { language: "bun", filename /** (raw requirements filename) */: "package.json" }
| { language: "python3", filename: "requirements.in" }
| { language: "php", filename: "composer.json" }
| { language: "go", filename: "go.mod" }
| { language: "powershell", filename: "modules.json" };
| { language: "go", filename: "go.mod" };
export const workspaceDependenciesLanguages: WorkspaceDependenciesLanguage[] = [
{ language: "bun", filename: "package.json" },
{ language: "python3", filename: "requirements.in" },
{ language: "php", filename: "composer.json" },
{ language: "go", filename: "go.mod" },
{ language: "powershell", filename: "modules.json" },
] as const;
/**
@@ -47,8 +45,7 @@ export const workspaceDependenciesLanguages: WorkspaceDependenciesLanguage[] = [
*/
export function languageNeedsLock(language: ScriptLanguage | string): boolean {
return (
(workspaceDependenciesLanguages.some((l) => l.language === language) &&
language !== "powershell") ||
workspaceDependenciesLanguages.some((l) => l.language === language) ||
language === "deno" ||
language === "rust" ||
language === "ansible"
+3 -12
View File
@@ -229,20 +229,13 @@
# ---------------------------------------------------------------
devEnvVars = {
DATABASE_URL = "postgres://postgres:changeme@127.0.0.1:5432/windmill?sslmode=disable";
REMOTE = "http://127.0.0.1:8000";
REMOTE_LSP = "http://127.0.0.1:3001";
NODE_ENV = "development";
NODE_OPTIONS = "--max-old-space-size=16384";
};
# Connection-specific defaults — set via shellHook so they respect
# pre-existing values (e.g. from webmux runtime.env / .env.local).
# Nix attrs are injected unconditionally and would override per-worktree
# values set by webmux before the interactive shell starts.
devShellHook = ''
export DATABASE_URL="''${DATABASE_URL:-postgres://postgres:changeme@127.0.0.1:5432/windmill?sslmode=disable}"
export REMOTE="''${REMOTE:-http://127.0.0.1:8000}"
export REMOTE_LSP="''${REMOTE_LSP:-http://127.0.0.1:3001}"
'';
# ---------------------------------------------------------------
# Helper scripts — base set (default + full)
# ---------------------------------------------------------------
@@ -409,7 +402,6 @@
# =============================================================
devShells.default = pkgs.mkShell (buildEnvVars // commonRuntimeVars // devEnvVars // browserVars // {
shellHook = devShellHook;
buildInputs = coreBuildInputs;
packages = helperScriptsBase ++ [ playwrightWrapper ];
@@ -421,7 +413,6 @@
# =============================================================
devShells.full = pkgs.mkShell (buildEnvVars // commonRuntimeVars // extraRuntimeVars // devEnvVars // browserVars // {
shellHook = devShellHook;
buildInputs = coreBuildInputs ++ extraRuntimes ++ (with pkgs; [
# Python extras
poetry
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "windmill-components",
"version": "1.658.0",
"version": "1.657.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "windmill-components",
"version": "1.658.0",
"version": "1.657.0",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "windmill-components",
"version": "1.658.0",
"version": "1.657.0",
"scripts": {
"dev": "vite dev",
"build": "vite build",
@@ -21,7 +21,7 @@
resourceType: string
resourceTypeInfo: ResourceType | undefined
args?: Record<string, any> | any
linkedSecrets?: string[]
linkedSecret?: string | undefined
isValid?: boolean
linkedSecretCandidates?: string[] | undefined
description?: string | undefined
@@ -31,7 +31,7 @@
resourceType,
resourceTypeInfo,
args = $bindable({}),
linkedSecrets = $bindable([]),
linkedSecret = $bindable(undefined),
isValid = $bindable(true),
linkedSecretCandidates = undefined,
description = $bindable(undefined)
@@ -152,7 +152,7 @@
/>
<ResourceGen
bind:args
{resourceType}
resourceType={resourceType}
resourceSchema={notFound ? undefined : schema}
isFileset={resourceTypeInfo?.is_fileset ?? false}
/>
@@ -246,7 +246,9 @@
{/await}
</div>
{:else if resourceTypeInfo?.is_fileset}
<h5 class="mt-1 inline-flex items-center gap-4"> Fileset </h5>
<h5 class="mt-1 inline-flex items-center gap-4">
Fileset
</h5>
<FilesetEditor bind:args />
{:else if resourceTypeInfo?.format_extension}
<h5 class="mt-4 inline-flex items-center gap-4">
@@ -271,7 +273,7 @@
onlyMaskPassword
noDelete
{linkedSecretCandidates}
bind:linkedSecrets
bind:linkedSecret
isValid
{schema}
bind:args
@@ -84,22 +84,14 @@
)
}
let linkedSecrets: string[] = $state([])
let linkedSecret: string | undefined = $state(undefined)
let linkedSecretCandidates: string[] | undefined = $state(undefined)
function computeDefaultLinkedSecrets(
resourceType: string,
argsKeys: string[],
passwords: string[]
): string[] {
function computeLinkedSecret(resourceType: string, argsKeys: string[], passwords: string[]) {
linkedSecretCandidates = computeCandidates(resourceType, argsKeys, passwords)
const forced = forceSecretValue(resourceType)
if (forced) {
return [forced]
}
const best = linkedSecretCandidates?.sort(
(ua, ub) => linkedSecretValue(ub) - linkedSecretValue(ua)
)?.[0]
return best ? [best] : []
return (
forceSecretValue(resourceType) ??
linkedSecretCandidates?.sort((ua, ub) => linkedSecretValue(ub) - linkedSecretValue(ua))?.[0]
)
}
let scopes: string[] = $state([])
@@ -202,7 +194,7 @@
args['password'] == '' &&
args['api_key'] == '' &&
args['key'] == '' &&
linkedSecrets.length > 0
linkedSecret != undefined
: false)) ||
step == 3 ||
(step == 4 && pathError != '') ||
@@ -325,13 +317,13 @@
const passwords = newArgsKeys.filter((x) => {
return props?.[x]?.password
})
if (linkedSecrets.length === 0) {
linkedSecrets = computeDefaultLinkedSecrets(resourceType, newArgsKeys, passwords)
if (!linkedSecret) {
linkedSecret = computeLinkedSecret(resourceType, newArgsKeys, passwords)
}
}
export async function next() {
if (step == 1) {
linkedSecrets = []
linkedSecret = undefined
if (manual) {
getResourceTypeInfo()
args = {}
@@ -416,30 +408,14 @@
if (step == 2) return
throw Error('Path is not set')
}
// Check if variable paths already exist
if (!manual || linkedSecrets.length <= 1) {
const exists = await VariableService.existsVariable({
workspace: $workspaceStore!,
path
})
if (exists) {
throw Error(`Variable at path ${path} already exists. Delete it or pick another path`)
}
} else {
for (const secretField of linkedSecrets) {
const varPath = `${path}_${secretField}`
const exists = await VariableService.existsVariable({
workspace: $workspaceStore!,
path: varPath
})
if (exists) {
throw Error(
`Variable at path ${varPath} already exists. Delete it or pick another path`
)
}
}
let exists = await VariableService.existsVariable({
workspace: $workspaceStore!,
path
})
if (exists) {
throw Error(`Variable at path ${path} already exists. Delete it or pick another path`)
}
let exists = await ResourceService.existsResource({
exists = await ResourceService.existsResource({
workspace: $workspaceStore!,
path
})
@@ -486,65 +462,25 @@
const resourceValue = args
let savedVariableCount = 0
if (!manual) {
// OAuth flow: single secret variable for the token
if (typeof value == 'string' && value != '' && !value.startsWith('$var:')) {
savedVariableCount++
await VariableService.createVariable({
workspace: $workspaceStore!,
requestBody: {
path,
value: value,
is_secret: true,
description: emptyString(description)
? `OAuth token for ${resourceType}`
: description,
is_oauth: true,
account: account
}
})
resourceValue['token'] = `$var:${path}`
}
} else if (linkedSecrets.length === 1) {
// Single secret: use the resource path as variable name (original behavior)
const secretField = linkedSecrets[0]
const v = args[secretField]
let saveVariable = false
if (!manual || linkedSecret != undefined) {
let v = manual ? args[linkedSecret ?? ''] : value
if (typeof v == 'string' && v != '' && !v.startsWith('$var:')) {
savedVariableCount++
saveVariable = true
await VariableService.createVariable({
workspace: $workspaceStore!,
requestBody: {
path,
value: v,
is_secret: true,
description: emptyString(description) ? `Token for ${resourceType}` : description,
is_oauth: false
description: emptyString(description)
? `${manual ? 'Token' : 'OAuth token'} for ${resourceType}`
: description,
is_oauth: !manual,
account: account
}
})
resourceValue[secretField] = `$var:${path}`
}
} else if (linkedSecrets.length > 1) {
// Multiple secrets: append _field_name to each variable path
for (const secretField of linkedSecrets) {
const v = args[secretField]
if (typeof v == 'string' && v != '' && !v.startsWith('$var:')) {
const varPath = `${path}_${secretField}`
savedVariableCount++
await VariableService.createVariable({
workspace: $workspaceStore!,
requestBody: {
path: varPath,
value: v,
is_secret: true,
description: emptyString(description)
? `${secretField} for ${resourceType}`
: description,
is_oauth: false
}
})
resourceValue[secretField] = `$var:${varPath}`
}
resourceValue[linkedSecret ?? 'token'] = `$var:${path}`
}
}
@@ -559,9 +495,7 @@
})
dispatch('refresh', path)
dispatch('close')
sendUserToast(
`Saved resource${savedVariableCount > 0 ? ` and ${savedVariableCount} variable${savedVariableCount > 1 ? 's' : ''}` : ''} path: ${path}`
)
sendUserToast(`Saved resource${saveVariable ? ' and variable' : ''} path: ${path}`)
step = 1
resourceType = ''
}
@@ -804,7 +738,7 @@
{#key resourceTypeInfo}
<ApiConnectForm
bind:linkedSecrets
bind:linkedSecret
bind:description
{linkedSecretCandidates}
{resourceType}
@@ -652,13 +652,6 @@
onDone={async ({ job: completedJob }) => {
isRunning = false
$executionCount = $executionCount + 1
// Reset 'initial' flags for modules that were part of this flow test,
// so OutputPicker no longer shows "Run loaded from history"
for (const mod of completedJob.flow_status?.modules ?? []) {
if (mod.id) {
stepHistoryLoader?.resetInitial(mod.id)
}
}
if (flowRecording.active) {
lastRecording = flowRecording.stop()
setActiveRecording(undefined)
+13 -17
View File
@@ -34,7 +34,7 @@
defaultValues?: Record<string, any>
shouldHideNoInputs?: boolean
compact?: boolean
linkedSecrets?: string[]
linkedSecret?: string | undefined
linkedSecretCandidates?: string[] | undefined
noVariablePicker?: boolean
flexWrap?: boolean
@@ -86,7 +86,7 @@
defaultValues = {},
shouldHideNoInputs = false,
compact = false,
linkedSecrets = $bindable([]),
linkedSecret = $bindable(undefined),
linkedSecretCandidates = undefined,
noVariablePicker = false,
flexWrap = false,
@@ -333,7 +333,7 @@
{variableEditor}
{itemPicker}
{pickForField}
password={linkedSecrets.includes(argName)}
password={linkedSecret == argName}
extra={formerProperty}
{showSchemaExplorer}
simpleTooltip={schemaFieldTooltip[argName]}
@@ -398,24 +398,22 @@
customErrorMessage={prop?.customErrorMessage}
bind:properties={
() => prop?.properties,
(v) => {
if (prop) prop.properties = v
}
(v) => { if (prop) prop.properties = v }
}
bind:order={
() => prop?.order,
(v) => {
if (prop) prop.order = v
}
(v) => { if (prop) prop.order = v }
}
nestedRequired={prop?.required}
itemsType={prop?.items}
disabled={disabledArgs.includes(argName) || disabled || prop?.disabled}
disabled={disabledArgs.includes(argName) ||
disabled ||
prop?.disabled}
{compact}
{variableEditor}
{itemPicker}
bind:pickForField
password={linkedSecrets.includes(argName)}
password={linkedSecret == argName}
extra={prop}
{showSchemaExplorer}
simpleTooltip={schemaFieldTooltip[argName]}
@@ -442,14 +440,12 @@
{#if linkedSecretCandidates?.includes(argName)}
<div class="relative">
<ToggleButtonGroup
selected={linkedSecrets.includes(argName) ? 'secret' : 'inlined'}
selected={linkedSecret == argName ? 'secret' : 'inlined'}
on:selected={(e) => {
if (e.detail === 'secret') {
if (!linkedSecrets.includes(argName)) {
linkedSecrets = [...linkedSecrets, argName]
}
} else {
linkedSecrets = linkedSecrets.filter((s) => s !== argName)
linkedSecret = argName
} else if (linkedSecret == argName) {
linkedSecret = undefined
}
}}
>
@@ -40,19 +40,6 @@
return deps.name || `Default (${deps.language})`
}
function getEditorLang(language: ScriptLang): string {
switch (language) {
case 'bun':
case 'php':
case 'powershell':
return 'json'
case 'python3':
return 'plaintext'
default:
return 'markdown'
}
}
export function getFileExtension(language: ScriptLang): string | null {
switch (language) {
case 'python3':
@@ -63,8 +50,6 @@
// return 'go.mod'
case 'php':
return 'composer.json'
case 'powershell':
return 'modules.json'
default:
return null
}
@@ -128,8 +113,7 @@
{ value: 'python3', label: 'Python' },
{ value: 'bun', label: 'TypeScript (Bun/Bunnative)' },
// { value: 'go', label: 'Go' },
{ value: 'php', label: 'PHP' },
{ value: 'powershell', label: 'PowerShell' }
{ value: 'php', label: 'PHP' }
]
// Default templates for each language
@@ -173,13 +157,6 @@ numpy>=1.24.0
"vlucas/phpdotenv": "^5.6",
"symfony/console": "^6.4"
}
}`,
powershell: `{
"modules": {
"PSWriteColor": "*",
"ImportExcel": "7.8.6"
}
}`
}
@@ -520,7 +497,7 @@ numpy>=1.24.0
<Module.default
bind:this={editor}
autoHeight
lang={getEditorLang(workspaceDependencies.language)}
lang="markdown"
code={workspaceDependencies.content}
on:change={(e) => handleEditorChange(e.detail)}
fixedOverflowWidgets={false}
@@ -528,15 +505,6 @@ numpy>=1.24.0
/>
{/await}
</div>
{#if workspaceDependencies.language === 'powershell'}
<div class="text-sm text-tertiary mt-2">
JSON object with a <code>"modules"</code> key mapping module names to versions. Use
<code>"*"</code>
or <code>null</code> for latest version, or a specific version string to pin. These
modules are merged with script-level
<code>Import-Module</code> statements at runtime (workspace versions take precedence).
</div>
{/if}
</Section>
</div>
@@ -1111,8 +1111,8 @@
<Tabs bind:selected={advancedSelected} wrapperClass="shrink-0">
<Tab
value="retries"
active={flowModule.retry !== undefined || flowModule.continue_on_error}
label="Error handling"
active={flowModule.retry !== undefined}
label="Retries"
/>
{#if !selectedId.includes('failure')}
<Tab value="runtime" label="Runtime" />
@@ -1152,22 +1152,6 @@
{/if}
<div class="flex-1 overflow-auto p-4">
{#if advancedSelected === 'retries'}
<Section label="Continue on error">
{#snippet header()}
<Tooltip>
When enabled, the flow will continue to the next step even if this step fails (after exhausting all retries, if any). This enables to process the error in a branch one for instance.
</Tooltip>
{/snippet}
<Toggle
size="xs"
bind:checked={flowModule.continue_on_error}
options={{
left: 'Stop on error and propagate error up',
right: "Continue on error with error as step's return"
}}
/>
</Section>
<div class="mt-4"></div>
<Section label="Retries">
{#snippet header()}
<Tooltip
@@ -1177,6 +1161,19 @@
maximum number of attempts as defined below.
</Tooltip>
{/snippet}
<Label label="After all retries attempts have been exhausted:">
<Toggle
size="xs"
bind:checked={flowModule.continue_on_error}
options={{
left: 'Stop on error and propagate error up',
right: "Continue on error with error as step's return",
rightTooltip:
'When enabled, the flow will continue to the next step after going through all the retries (if any) even if this step fails. This enables to process the error in a branch one for instance.'
}}
/>
</Label>
<div class="my-8"></div>
<FlowRetries bind:flowModuleRetry={flowModule.retry} bind:flowModule />
</Section>
{:else if advancedSelected === 'runtime' && advancedRuntimeSelected === 'concurrency'}
@@ -110,7 +110,7 @@
const u32Max = 4294967295
</script>
<div class="flex flex-col gap-4">
<div class="h-full flex flex-col gap-4">
<ToggleButtonGroup
bind:selected={delayType}
class={`${disabled ? 'disabled' : ''}`}
@@ -216,7 +216,6 @@
</Section>
{/if}
{#if delayType === 'constant' || delayType === 'exponential'}
<div class="flex h-[calc(100%-22px)]">
<div class="w-1/2 h-full overflow-auto pr-2">
{#if delayType === 'constant'}
@@ -297,65 +296,66 @@
</div>
<div class="w-1/2 h-full overflow-auto pl-2">
{#if true}
{@const { attempts: cAttempts, seconds: cSeconds } = flowModuleRetry?.constant || {}}
{@const {
attempts: eAttempts,
seconds: eSeconds,
multiplier,
random_factor
} = flowModuleRetry?.exponential || {}}
{@const cArray = Array.from({ length: Math.min(cAttempts || 0, 100) }, () => cSeconds)}
{@const eArray = Array.from(
{ length: Math.min(eAttempts || 0, 100) },
(_, i) => (multiplier || 0) * (eSeconds || 0) ** (i + cArray.length + 1)
)}
{@const array = [...cArray, ...eArray]}
<div class="bg-surface-secondary border rounded px-4 py-2">
<div class="text-xs font-medium mb-2">Retry attempts</div>
{#if array.length > 0}
<table class="text-xs">
<thead>
<tr>
<td class="font-semibold pr-1 pb-1">1:</td>
<td class="pb-1"
>After {array[0]} second{array[0] === 1 ? '' : 's'}
{#if (random_factor ?? 0) > 0}(+/- {((array[0] ?? 0) * (random_factor ?? 0)) /
100}
seconds){/if}</td
>
</tr>
</thead>
<tbody>
{#each array.slice(1, 100) as delay, i}
{@const index = i + 2}
{@const { attempts: cAttempts, seconds: cSeconds } = flowModuleRetry?.constant || {}}
{@const {
attempts: eAttempts,
seconds: eSeconds,
multiplier,
random_factor
} = flowModuleRetry?.exponential || {}}
{@const cArray = Array.from({ length: Math.min(cAttempts || 0, 100) }, () => cSeconds)}
{@const eArray = Array.from(
{ length: Math.min(eAttempts || 0, 100) },
(_, i) => (multiplier || 0) * (eSeconds || 0) ** (i + cArray.length + 1)
)}
{@const array = [...cArray, ...eArray]}
<div class="bg-surface-secondary border rounded px-4 py-2">
<div class="text-xs font-medium mb-2">Retry attempts</div>
{#if array.length > 0}
<table class="text-xs">
<thead>
<tr>
<td class="font-semibold pr-1 align-top">{index}:</td>
<td class="pb-1 whitespace-nowrap">
{delay} second{delay === 1 ? '' : 's'}
{#if (random_factor ?? 0) > 0}(+/- {((delay ?? 0) * (random_factor ?? 0)) /
<td class="font-semibold pr-1 pb-1">1:</td>
<td class="pb-1"
>After {array[0]} second{array[0] === 1 ? '' : 's'}
{#if (random_factor ?? 0) > 0}(+/- {((array[0] ?? 0) * (random_factor ?? 0)) /
100}
seconds){/if}
after attempt #{index - 1}
{#if i > cArray.length - 2}
<span class="text-gray-400 pl-2">
({multiplier} * {eSeconds}<sup>{index}</sup>)
</span>
{/if}
</td>
seconds){/if}</td
>
</tr>
{/each}
{#if (cAttempts ?? 0) > 100 || (eAttempts ?? 0) > 100}
<tr>
<td class="font-semibold pr-1 align-top">...</td>
<td class="pb-1">...</td>
</tr>
{/if}
</tbody>
</table>
{/if}
</div>
</thead>
<tbody>
{#each array.slice(1, 100) as delay, i}
{@const index = i + 2}
<tr>
<td class="font-semibold pr-1 align-top">{index}:</td>
<td class="pb-1 whitespace-nowrap">
{delay} second{delay === 1 ? '' : 's'}
{#if (random_factor ?? 0) > 0}(+/- {((delay ?? 0) * (random_factor ?? 0)) /
100}
seconds){/if}
after attempt #{index - 1}
{#if i > cArray.length - 2}
<span class="text-gray-400 pl-2">
({multiplier} * {eSeconds}<sup>{index}</sup>)
</span>
{/if}
</td>
</tr>
{/each}
{#if (cAttempts ?? 0) > 100 || (eAttempts ?? 0) > 100}
<tr>
<td class="font-semibold pr-1 align-top">...</td>
<td class="pb-1">...</td>
</tr>
{/if}
</tbody>
</table>
{:else}
<div class="text-xs">No retries</div>
{/if}
</div>
{/if}
</div>
</div>
{/if}
</div>
@@ -204,8 +204,7 @@
}
function updateLastJob() {
// Prefer testJob only when actively running/streaming (individual step test in progress)
if (testJob && (testJob.result_stream || testJob.type === 'QueuedJob')) {
if (testJob) {
return testJob
}
if (
@@ -215,8 +214,6 @@
) {
return
}
// Use flowStateStore as source of truth — it's updated by both individual step tests
// (ModuleTest.jobDone) and flow tests (FlowStatusViewerInner.onJobsLoadedInner)
return {
id: flowStateStore.val[moduleId]?.previewJobId ?? '',
result: flowStateStore.val[moduleId]?.previewResult,
@@ -121,6 +121,7 @@
escapeBehavior: untrack(() => escapeBehavior),
openFocus: untrack(() => openFocus),
onOpenChange: ({ curr, next }) => {
console.log('Popover open state changed:', { curr, next })
if (curr != next) {
dispatch('openChange', next)
if (!next) {
@@ -16,7 +16,6 @@
import { Loader2, Save } from 'lucide-svelte'
import ScriptPicker from '$lib/components/ScriptPicker.svelte'
import Section from '$lib/components/Section.svelte'
import Label from '$lib/components/Label.svelte'
import Required from '$lib/components/Required.svelte'
import NextcloudTriggerForm from './services/nextcloud/NextcloudTriggerForm.svelte'
import GoogleTriggerForm from './services/google/GoogleTriggerForm.svelte'
@@ -97,7 +96,6 @@
let isFlow = $state(false)
let externalId = $state<string | null>(null)
let can_write = $state(true)
let summary = $state('')
let originalConfig = $state<Record<string, any> | undefined>(undefined)
let initialConfig = $state<Record<string, any> | undefined>(undefined)
@@ -123,7 +121,6 @@
loadingConfig = false
loadingForm = false
can_write = true
summary = ''
originalConfig = undefined
initialConfig = undefined
}
@@ -147,7 +144,6 @@
loadingConfig = false
loadingForm = false
can_write = true
summary = nativeTrigger.summary ?? ''
originalConfig = undefined
initialConfig = undefined
}
@@ -186,7 +182,6 @@
scriptPath = fullTrigger.script_path
initialScriptPath = fullTrigger.script_path
can_write = canWrite(fullTrigger.script_path, {}, $userStore)
summary = fullTrigger.summary ?? ''
externalData = fullTrigger.external_data
// Apply default values if provided (for draft triggers)
@@ -208,8 +203,7 @@
return {
script_path: scriptPath,
is_flow: isFlow,
service_config: serviceConfig,
summary: summary || undefined
service_config: serviceConfig
}
}
@@ -428,18 +422,6 @@
</Section>
{/if}
<Section label="Metadata">
<Label label="Summary">
<input
type="text"
placeholder="Short summary to be displayed when listed"
class="text-sm w-full"
bind:value={summary}
disabled={!can_write}
/>
</Label>
</Section>
{#if loadingConfig}
<Section label="{serviceInfo?.serviceDisplayName} configuration">
<div class="flex items-center gap-2 text-secondary text-xs">
@@ -199,8 +199,7 @@ export async function saveNativeTriggerFromCfg(
const requestBody: NativeTriggerData = {
script_path: triggerCfg.script_path,
is_flow: triggerCfg.is_flow,
service_config: triggerCfg.service_config,
summary: triggerCfg.summary || undefined
service_config: triggerCfg.service_config
}
const serviceName = NATIVE_TRIGGER_SERVICES[service].serviceDisplayName
@@ -481,15 +481,14 @@ export function getLightConfig(
} else if (triggerType === 'email') {
return { local_part: trigger.local_part }
} else if (triggerType === 'nextcloud') {
return { event: trigger.service_config?.event ?? trigger.event, summary: trigger.summary }
return { event: trigger.service_config?.event ?? trigger.event }
} else if (triggerType === 'google') {
return {
trigger_type: trigger.service_config?.triggerType ?? trigger.trigger_type,
resource_id: trigger.service_config?.resourceId ?? trigger.resource_id,
resource_name: trigger.service_config?.resourceName ?? trigger.resource_name,
calendar_id: trigger.service_config?.calendarId ?? trigger.calendar_id,
calendar_name: trigger.service_config?.calendarName ?? trigger.calendar_name,
summary: trigger.summary
calendar_name: trigger.service_config?.calendarName ?? trigger.calendar_name
}
} else {
return undefined
@@ -525,12 +524,8 @@ export function getTriggerLabel(trigger: Trigger): string {
return `${config?.url}`
} else if (type === 'email' && config?.local_part) {
return `${config?.local_part}`
} else if (type === 'nextcloud' && config?.summary) {
return `${config.summary}`
} else if (type === 'nextcloud' && path) {
return `${path}`
} else if (type === 'google' && config?.summary) {
return `${config.summary}`
} else if (type === 'google' && path) {
const triggerType = config?.trigger_type ?? config?.triggerType
if (triggerType === 'calendar') {
@@ -179,10 +179,11 @@
for (const serviceName of Object.keys(supportedServices)) {
try {
const available = await WorkspaceIntegrationService.checkInstanceSharingAvailable({
workspace: $workspaceStore,
serviceName: serviceName as NativeServiceName
})
const available =
await WorkspaceIntegrationService.checkInstanceSharingAvailable({
workspace: $workspaceStore,
serviceName: serviceName as NativeServiceName
})
instanceSharingAvailable[serviceName] = available
} catch {
instanceSharingAvailable[serviceName] = false
@@ -388,10 +389,7 @@
<span class="font-semibold">Connected</span>
</div>
<Button
onclick={() =>
integration?.oauth_data?.instance_shared
? connectWithInstanceCredentials(serviceName)
: connectService(serviceName, getRedirectUri(serviceName))}
onclick={() => connectService(serviceName, getRedirectUri(serviceName))}
disabled={isConnecting}
startIcon={{ icon: Plug }}
>
@@ -29,12 +29,6 @@ func main(x int) (interface{}, error) {
"python3": """
def main(x: int):
return x
""",
"php": """<?php
function main(int $x): int {
return $x;
}
""",
}
@@ -86,8 +80,3 @@ class TestIdentityScript(unittest.TestCase):
path = PATH_TEMPLATE.format(lang="python3")
result = self._client.run_sync(path, {"x": 5})
self.assertEqual(result, 5)
def test_php(self):
path = PATH_TEMPLATE.format(lang="php")
result = self._client.run_sync(path, {"x": 5})
self.assertEqual(result, 5)
+1 -1
View File
@@ -4,7 +4,7 @@ verify_ssl = true
name = "pypi"
[packages]
wmill = ">=1.658.0"
wmill = ">=1.657.0"
sendgrid = "*"
mysql-connector-python = "*"
pymongo = "*"
+1 -1
View File
@@ -1,7 +1,7 @@
openapi: '3.0.3'
info:
version: 1.658.0
version: 1.657.0
title: OpenFlow Spec
contact:
name: Ruben Fiszel
@@ -12,7 +12,7 @@
RootModule = 'WindmillClient.psm1'
# Version number of this module.
ModuleVersion = '1.658.0'
ModuleVersion = '1.657.0'
# Supported PSEditions
# CompatiblePSEditions = @()
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "wmill"
version = "1.658.0"
version = "1.657.0"
description = "A client library for accessing Windmill server wrapping the Windmill client API"
license = "Apache-2.0"
homepage = "https://windmill.dev"
+3 -3
View File
@@ -2,10 +2,10 @@
## CLI Commands
Create a folder ending with `__flow` and add a `flow.yaml` file with the flow definition.
For rawscript modules, use `!inline path/to/script.ts` for the content key. Inline script files should NOT include `.inline_script.` in their names (e.g. use `a.ts`, not `a.inline_script.ts`).
Create a folder ending with `.flow` and add a YAML file with the flow definition.
For rawscript modules, use `!inline path/to/script.ts` for the content key.
After writing, tell the user they can run:
- `wmill flow generate-locks <path_to_flow_folder> --yes` - Generate lock files for the specific flow you modified (e.g. `wmill flow generate-locks f/my_folder/my_flow__flow --yes`)
- `wmill flow generate-locks <path_to_flow_folder> --yes` - Generate lock files for the specific flow you modified (e.g. `wmill flow generate-locks f/my_folder/my_flow.flow --yes`)
- `wmill sync push` - Deploy to Windmill
Do NOT run these commands yourself. Instead, inform the user that they should run them.
+3 -3
View File
@@ -33,10 +33,10 @@ export const FLOW_BASE = `# Windmill Flow Building Guide
## CLI Commands
Create a folder ending with \`__flow\` and add a \`flow.yaml\` file with the flow definition.
For rawscript modules, use \`!inline path/to/script.ts\` for the content key. Inline script files should NOT include \`.inline_script.\` in their names (e.g. use \`a.ts\`, not \`a.inline_script.ts\`).
Create a folder ending with \`.flow\` and add a YAML file with the flow definition.
For rawscript modules, use \`!inline path/to/script.ts\` for the content key.
After writing, tell the user they can run:
- \`wmill flow generate-locks <path_to_flow_folder> --yes\` - Generate lock files for the specific flow you modified (e.g. \`wmill flow generate-locks f/my_folder/my_flow__flow --yes\`)
- \`wmill flow generate-locks <path_to_flow_folder> --yes\` - Generate lock files for the specific flow you modified (e.g. \`wmill flow generate-locks f/my_folder/my_flow.flow --yes\`)
- \`wmill sync push\` - Deploy to Windmill
Do NOT run these commands yourself. Instead, inform the user that they should run them.
@@ -18,7 +18,7 @@ This interactive command creates a complete app structure with your choice of fr
## App Structure
```
my_app__raw_app/
my_app.raw_app/
├── AGENTS.md # AI agent instructions (auto-generated)
├── DATATABLES.md # Database schemas (run 'wmill app generate-agents' to refresh)
├── raw_app.yaml # App configuration (summary, path, data settings)
@@ -7,10 +7,10 @@ description: MUST use when creating flows.
## CLI Commands
Create a folder ending with `__flow` and add a `flow.yaml` file with the flow definition.
For rawscript modules, use `!inline path/to/script.ts` for the content key. Inline script files should NOT include `.inline_script.` in their names (e.g. use `a.ts`, not `a.inline_script.ts`).
Create a folder ending with `.flow` and add a YAML file with the flow definition.
For rawscript modules, use `!inline path/to/script.ts` for the content key.
After writing, tell the user they can run:
- `wmill flow generate-locks <path_to_flow_folder> --yes` - Generate lock files for the specific flow you modified (e.g. `wmill flow generate-locks f/my_folder/my_flow__flow --yes`)
- `wmill flow generate-locks <path_to_flow_folder> --yes` - Generate lock files for the specific flow you modified (e.g. `wmill flow generate-locks f/my_folder/my_flow.flow --yes`)
- `wmill sync push` - Deploy to Windmill
Do NOT run these commands yourself. Instead, inform the user that they should run them.
+3 -3
View File
@@ -2,10 +2,10 @@
## CLI Commands
Create a folder ending with `__flow` and add a `flow.yaml` file with the flow definition.
For rawscript modules, use `!inline path/to/script.ts` for the content key. Inline script files should NOT include `.inline_script.` in their names (e.g. use `a.ts`, not `a.inline_script.ts`).
Create a folder ending with `.flow` and add a YAML file with the flow definition.
For rawscript modules, use `!inline path/to/script.ts` for the content key.
After writing, tell the user they can run:
- `wmill flow generate-locks <path_to_flow_folder> --yes` - Generate lock files for the specific flow you modified (e.g. `wmill flow generate-locks f/my_folder/my_flow__flow --yes`)
- `wmill flow generate-locks <path_to_flow_folder> --yes` - Generate lock files for the specific flow you modified (e.g. `wmill flow generate-locks f/my_folder/my_flow.flow --yes`)
- `wmill sync push` - Deploy to Windmill
Do NOT run these commands yourself. Instead, inform the user that they should run them.
+1 -1
View File
@@ -13,7 +13,7 @@ This interactive command creates a complete app structure with your choice of fr
## App Structure
```
my_app__raw_app/
my_app.raw_app/
├── AGENTS.md # AI agent instructions (auto-generated)
├── DATATABLES.md # Database schemas (run 'wmill app generate-agents' to refresh)
├── raw_app.yaml # App configuration (summary, path, data settings)
-14
View File
@@ -1094,20 +1094,6 @@ export function getFlowPrompt(): string {
# Generate skills TypeScript export for CLI
skills_ts = generate_skills_ts_export(skills, schema_yaml_content)
# Replace hardcoded path conventions with placeholders for CLI runtime resolution.
# init.ts resolves these based on the nonDottedPaths setting in wmill.yaml.
# (Frontend auto-generated files keep the default non-dotted conventions.)
skills_ts = (skills_ts
.replace("\\`__flow\\`", "\\`{{FLOW_SUFFIX}}\\`")
.replace(
"Inline script files should NOT include \\`.inline_script.\\`"
" in their names (e.g. use \\`a.ts\\`, not \\`a.inline_script.ts\\`).",
"{{INLINE_SCRIPT_NAMING}}"
)
.replace("my_flow__flow", "my_flow{{FLOW_SUFFIX}}")
.replace("my_app__raw_app/", "my_app{{RAW_APP_SUFFIX}}/")
)
(CLI_GUIDANCE_DIR / "skills.ts").write_text(skills_ts)
print(f"\nGenerated files:")
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@windmill/windmill",
"version": "1.658.0",
"version": "1.657.0",
"exports": "./src/index.ts",
"publish": {
"exclude": ["!src", "./s3Types.ts", "./sqlUtils.ts", "./client.ts"]
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "windmill-client",
"description": "Windmill SDK client for browsers and Node.js",
"version": "1.658.0",
"version": "1.657.0",
"author": "Ruben Fiszel",
"license": "Apache 2.0",
"sideEffects": false,
+1 -1
View File
@@ -1 +1 @@
1.658.0
1.657.0