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<i8>`) 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: ["<T>", "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) <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-06-15 13:31:33 +00:00
co-authored by Claude Opus 4.8
parent 3cf4083960
commit 8b9c2a52c2
7 changed files with 76 additions and 7 deletions
+3 -4
View File
@@ -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",
+1 -1
View File
@@ -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"
},
+2 -1
View File
@@ -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
@@ -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: \"<T>\", nullable: true },
# which is not standard JSON Schema. For typed (non-enum) fields, rewrite them to
# the standard { type: [\"<T>\", \"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');
"
+1 -1
View File
@@ -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",
@@ -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", () => {
@@ -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