Files
windmill/windmill-yaml-validator/scripts/gen-schemas.js
Ruben FiszelandClaude Opus 5 a372ae0c04 fix(cli): lint against the checkout's schema, not the published validator (#10418)
* fix(cli): lint against the checkout's schema, not the published validator

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

* fix(cli): mirror the permissioned_as exclusion into agent guidance

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

* chore: version windmill-yaml-validator with the release, publish by hand

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

* chore: generate schemas with the validator's own yaml parser

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

* fix(cli): declare ajv, no longer reaching tests via the validator

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

* fix: fail schema generation on a spec YAML syntax error

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:33:35 +02:00

123 lines
3.9 KiB
JavaScript

#!/usr/bin/env node
// Generates the AJV-ready JSON schemas in src/gen from the repo's OpenAPI specs.
// Kept in plain node (no shell) so it runs identically on Windows, where the CLI
// builds and tests this package as a local dependency.
const fs = require("fs");
const os = require("os");
const path = require("path");
// The package's own runtime YAML parser, so generating schemas needs no extra dependency.
const { parseWithPointers } = require("@stoplight/yaml");
const { generateSchemas } = require("./generate-resource-schemas.js");
// Severity 0 is DiagnosticSeverity.Error.
const YAML_ERROR = 0;
// parseWithPointers recovers from syntax errors rather than throwing, so an unchecked parse
// can hand back a spec silently missing whatever it failed to read — schemas that drop a
// definition and lint that quietly stops validating it.
function parseSpec(filePath) {
const { data, diagnostics } = parseWithPointers(
fs.readFileSync(filePath, "utf8")
);
const errors = diagnostics.filter((d) => d.severity === YAML_ERROR);
if (errors.length > 0) {
throw new Error(
`${filePath} is not valid YAML:\n` +
errors
.map((d) => ` line ${d.range.start.line + 1}: ${d.message}`)
.join("\n")
);
}
return data;
}
const packageDir = path.join(__dirname, "..");
const repoDir = path.join(packageDir, "..");
const outputDir = path.join(packageDir, "src", "gen");
function removeDiscriminatorMappings(schema) {
const removeMapping = (obj) => {
if (obj && typeof obj === "object") {
if (obj.discriminator?.mapping) delete obj.discriminator.mapping;
for (const v of Object.values(obj)) removeMapping(v);
}
};
removeMapping(schema);
// ToolValue's discriminator does not work with the allOf in FlowModuleTool
if (schema.components?.schemas?.ToolValue?.discriminator) {
delete schema.components.schemas.ToolValue.discriminator;
}
}
// AJV does not handle OpenAPI 3.0 `nullable: true` combined with `enum` — null must
// be explicitly listed in the enum for validation to accept null values.
function addNullToNullableEnums(obj) {
if (!obj || typeof obj !== "object") return;
if (Array.isArray(obj)) {
obj.forEach(addNullToNullableEnums);
return;
}
if (
obj.nullable === true &&
Array.isArray(obj.enum) &&
!obj.enum.includes(null)
) {
obj.enum.push(null);
}
for (const v of Object.values(obj)) addNullToNullableEnums(v);
}
function main() {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wmill-yaml-validator-"));
try {
fs.mkdirSync(path.join(outputDir, "triggers"), { recursive: true });
const openflowOutputPath = path.join(outputDir, "openflow.json");
const backendOutputPath = path.join(tmpDir, "backend-openapi.json");
const openflowSchema = parseSpec(
path.join(repoDir, "openflow.openapi.yaml")
);
const backendSchema = parseSpec(
path.join(repoDir, "backend", "windmill-api", "openapi.yaml")
);
removeDiscriminatorMappings(openflowSchema);
fs.writeFileSync(
openflowOutputPath,
JSON.stringify(openflowSchema, null, 2) + "\n"
);
fs.writeFileSync(
backendOutputPath,
JSON.stringify(backendSchema, null, 2) + "\n"
);
generateSchemas(backendOutputPath, openflowOutputPath, outputDir);
const files = [
openflowOutputPath,
path.join(outputDir, "schedule.json"),
...fs
.readdirSync(path.join(outputDir, "triggers"))
.map((f) => path.join(outputDir, "triggers", f)),
].filter((f) => f.endsWith(".json"));
for (const file of files) {
const schema = JSON.parse(fs.readFileSync(file, "utf8"));
addNullToNullableEnums(schema);
fs.writeFileSync(file, JSON.stringify(schema, null, 2) + "\n");
}
console.log(`Generated ${files.length} schemas in ${outputDir}`);
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
}
main();