From 8b9c2a52c2eb835aac9d144ca5f28cbe8ee8b6c6 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Mon, 15 Jun 2026 13:31:33 +0000 Subject: [PATCH] fix(cli): accept server-default retry/stop_after_if fields in lint The CLI linter rejected valid flow YAML that the server itself emits. A module with a constant retry and no early-stop error message serialises its unset defaults as `seconds: 0`, `random_factor: null` and `error_message: null`, which the OpenFlow-derived schema rejected: - `exponential.seconds` had `minimum: 1`, but the Rust default is 0 - `exponential.random_factor` (`Option`) was not nullable - typed nullable fields relied on AJV's non-standard `nullable` keyword Fixes: - openflow.openapi.yaml: `exponential.seconds` minimum 1 -> 0; add `nullable: true` to `exponential.random_factor`. - gen_openflow_schema.sh: rewrite typed (non-enum) `{ type, nullable: true }` fields to standard JSON Schema `{ type: ["", "null"] }` so the generated schemas accept null regardless of validator nullable support. - bump windmill-yaml-validator 1.1.1 -> 1.1.2 and the CLI dependency. Adds a regression test covering a flow with default retry and stop_after_if. Fixes WIN-2048 Co-Authored-By: Claude Opus 4.8 (1M context) --- cli/package-lock.json | 7 ++-- cli/package.json | 2 +- openflow.openapi.yaml | 3 +- .../gen_openflow_schema.sh | 32 +++++++++++++++++++ windmill-yaml-validator/package.json | 2 +- .../__tests__/flow-validator.test.ts | 10 ++++++ .../valid-default-retry-stop-after-if.yaml | 27 ++++++++++++++++ 7 files changed, 76 insertions(+), 7 deletions(-) create mode 100644 windmill-yaml-validator/src/validation/__tests__/test-samples/valid-default-retry-stop-after-if.yaml diff --git a/cli/package-lock.json b/cli/package-lock.json index c502b3b25e..3439988dc6 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -32,7 +32,7 @@ "windmill-parser-wasm-rust": "1.647.1", "windmill-parser-wasm-ts": "1.695.0", "windmill-parser-wasm-yaml": "1.593.0", - "windmill-yaml-validator": "1.1.1", + "windmill-yaml-validator": "1.1.2", "ws": "8.18.0", "yaml": "^2.7.0" }, @@ -1470,9 +1470,8 @@ "integrity": "sha512-Gyx4aR2jsJYuDrD3mCNTmz7LWOQQXPw5yKNCC1xRgUOPfjsD/tINAFfsBLwVOSmlQQcFZO+wHm4KtDtXOcnGVw==" }, "node_modules/windmill-yaml-validator": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/windmill-yaml-validator/-/windmill-yaml-validator-1.1.1.tgz", - "integrity": "sha512-CVgAwEoBdJhF39q2N012QffhlGPRIyIWd8gj7NnfG+/lMWgH2k5CBLtKIt6cPF8Bxz+6DGC3st1ARSsecDtbTg==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/windmill-yaml-validator/-/windmill-yaml-validator-1.1.2.tgz", "license": "Apache 2.0", "dependencies": { "@stoplight/yaml": "^4.3.0", diff --git a/cli/package.json b/cli/package.json index 2f46e3829b..35c77e4507 100644 --- a/cli/package.json +++ b/cli/package.json @@ -40,7 +40,7 @@ "windmill-parser-wasm-rust": "1.647.1", "windmill-parser-wasm-ts": "1.695.0", "windmill-parser-wasm-yaml": "1.593.0", - "windmill-yaml-validator": "1.1.1", + "windmill-yaml-validator": "1.1.2", "ws": "8.18.0", "yaml": "^2.7.0" }, diff --git a/openflow.openapi.yaml b/openflow.openapi.yaml index ada2724ff0..5321e0b917 100644 --- a/openflow.openapi.yaml +++ b/openflow.openapi.yaml @@ -152,10 +152,11 @@ components: description: Multiplier for exponential backoff seconds: type: integer - minimum: 1 + minimum: 0 description: Initial delay in seconds random_factor: type: integer + nullable: true minimum: 0 maximum: 100 description: Random jitter percentage (0-100) to avoid thundering herd diff --git a/windmill-yaml-validator/gen_openflow_schema.sh b/windmill-yaml-validator/gen_openflow_schema.sh index eb387b8d95..5fa07e1454 100755 --- a/windmill-yaml-validator/gen_openflow_schema.sh +++ b/windmill-yaml-validator/gen_openflow_schema.sh @@ -94,3 +94,35 @@ for (const file of files) { } console.log('Added null to nullable enums in generated schemas'); " + +# OpenAPI 3.0 expresses nullable typed fields as { type: \"\", nullable: true }, +# which is not standard JSON Schema. For typed (non-enum) fields, rewrite them to +# the standard { type: [\"\", \"null\"] } form so any JSON Schema validator accepts +# null. Enum-nullable fields are handled by the pass above and are left untouched. +node -e " +const fs = require('fs'); +const path = require('path'); + +function convertNullableTypesToArrays(obj) { + if (!obj || typeof obj !== 'object') return; + if (Array.isArray(obj)) { obj.forEach(convertNullableTypesToArrays); return; } + if (obj.nullable === true && typeof obj.type === 'string' && !Array.isArray(obj.enum)) { + obj.type = [obj.type, 'null']; + delete obj.nullable; + } + for (const v of Object.values(obj)) convertNullableTypesToArrays(v); +} + +const files = [ + '${output_dirpath}/openflow.json', + '${output_dirpath}/schedule.json', + ...fs.readdirSync('${output_dirpath}/triggers').map(f => path.join('${output_dirpath}/triggers', f)) +].filter(f => f.endsWith('.json')); + +for (const file of files) { + const schema = JSON.parse(fs.readFileSync(file, 'utf8')); + convertNullableTypesToArrays(schema); + fs.writeFileSync(file, JSON.stringify(schema, null, 2) + '\n'); +} +console.log('Converted nullable typed fields to type arrays in generated schemas'); +" diff --git a/windmill-yaml-validator/package.json b/windmill-yaml-validator/package.json index a43291c048..37202a347d 100644 --- a/windmill-yaml-validator/package.json +++ b/windmill-yaml-validator/package.json @@ -1,6 +1,6 @@ { "name": "windmill-yaml-validator", - "version": "1.1.1", + "version": "1.1.2", "description": "YAML validator for Windmill flow, schedule, and trigger files", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/windmill-yaml-validator/src/validation/__tests__/flow-validator.test.ts b/windmill-yaml-validator/src/validation/__tests__/flow-validator.test.ts index 392567f795..ffa2eaa4b7 100644 --- a/windmill-yaml-validator/src/validation/__tests__/flow-validator.test.ts +++ b/windmill-yaml-validator/src/validation/__tests__/flow-validator.test.ts @@ -88,6 +88,16 @@ describe("WindmillYamlValidator", () => { }, }); }); + it("should validate a flow with default retry and stop_after_if fields (WIN-2048)", () => { + // The server serialises unset retry/stop_after_if defaults as + // seconds: 0, random_factor: null and error_message: null. These must + // round-trip through the linter without errors. + const validFlow = readSample("valid-default-retry-stop-after-if.yaml"); + + const result = validator.validate(validFlow, { type: "flow" }); + + expect(result.errors).toHaveLength(0); + }); }); describe("invalid flows", () => { diff --git a/windmill-yaml-validator/src/validation/__tests__/test-samples/valid-default-retry-stop-after-if.yaml b/windmill-yaml-validator/src/validation/__tests__/test-samples/valid-default-retry-stop-after-if.yaml new file mode 100644 index 0000000000..8f47707ded --- /dev/null +++ b/windmill-yaml-validator/src/validation/__tests__/test-samples/valid-default-retry-stop-after-if.yaml @@ -0,0 +1,27 @@ +# Mirrors the YAML the server emits for a module with a constant retry and a +# stop_after_if without an error message. These defaults (seconds: 0, +# random_factor: null, error_message: null) used to be rejected by the linter. +summary: Flow with default retry and stop_after_if +value: + modules: + - id: script_step + value: + type: rawscript + input_transforms: {} + content: | + export async function main() { + return "ok"; + } + language: deno + retry: + constant: + attempts: 2 + seconds: 5 + exponential: + attempts: 0 + multiplier: 1 + seconds: 0 + random_factor: null + stop_after_if: + expr: "false" + error_message: null