snapshot of https://github.com/zulip/zulip.git @ 48c69911c7dd4e03b20552eb3bd77ad8151c1357 (2026-09-15)
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled
This commit is contained in:
194
tools/check-openapi.ts
Executable file
194
tools/check-openapi.ts
Executable file
@@ -0,0 +1,194 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
import * as fs from "node:fs";
|
||||
import {parseArgs} from "node:util";
|
||||
|
||||
import SwaggerParser from "@apidevtools/swagger-parser";
|
||||
import * as Diff from "diff";
|
||||
import * as ExampleValidator from "openapi-examples-validator";
|
||||
import {format as prettierFormat} from "prettier";
|
||||
import {CST, Composer, LineCounter, Parser, Scalar, YAMLMap, YAMLSeq, visit} from "yaml";
|
||||
|
||||
const usage = "Usage: check-openapi.ts [--fix] <file>...";
|
||||
const {
|
||||
values: {fix, help},
|
||||
positionals: files,
|
||||
} = parseArgs({options: {fix: {type: "boolean"}, help: {type: "boolean"}}, allowPositionals: true});
|
||||
|
||||
if (help) {
|
||||
console.log(usage);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
async function checkFile(file: string): Promise<void> {
|
||||
const yaml = await fs.promises.readFile(file, "utf8");
|
||||
const lineCounter = new LineCounter();
|
||||
const tokens = [...new Parser(lineCounter.addNewLine).parse(yaml)];
|
||||
const docs = [...new Composer().compose(tokens)];
|
||||
if (docs.length !== 1) {
|
||||
return;
|
||||
}
|
||||
const [doc] = docs;
|
||||
assert.ok(doc !== undefined);
|
||||
if (doc.errors.length > 0) {
|
||||
for (const error of doc.errors) {
|
||||
console.error("%s: %s", file, error.message);
|
||||
}
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
const root = doc.contents;
|
||||
if (!(root instanceof YAMLMap && root.has("openapi"))) {
|
||||
return;
|
||||
}
|
||||
|
||||
let ok = true;
|
||||
const reformats = new Map<
|
||||
number,
|
||||
{value: string; context: Parameters<typeof CST.setScalarValue>[2]}
|
||||
>();
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
visit(doc, {
|
||||
Map(_key, node) {
|
||||
if (node.has("$ref") && node.items.length !== 1) {
|
||||
assert.ok(node.range);
|
||||
const {line, col} = lineCounter.linePos(node.range[0]);
|
||||
console.error("%s:%d:%d: Siblings of $ref have no effect", file, line, col);
|
||||
ok = false;
|
||||
}
|
||||
|
||||
const combinator = ["allOf", "anyOf", "oneOf"].find((combinator) =>
|
||||
node.has(combinator),
|
||||
);
|
||||
if (node.has("nullable") && combinator !== undefined) {
|
||||
assert.ok(node.range);
|
||||
const {line, col} = lineCounter.linePos(node.range[0]);
|
||||
console.error(
|
||||
`%s:%d:%d: nullable has no effect as a sibling of ${combinator}`,
|
||||
file,
|
||||
line,
|
||||
col,
|
||||
);
|
||||
ok = false;
|
||||
}
|
||||
},
|
||||
|
||||
Pair(_key, node) {
|
||||
if (
|
||||
node.key instanceof Scalar &&
|
||||
node.key.value === "allOf" &&
|
||||
node.value instanceof YAMLSeq &&
|
||||
node.value.items.filter(
|
||||
(subschema) => !(subschema instanceof YAMLMap && subschema.has("$ref")),
|
||||
).length > 1
|
||||
) {
|
||||
assert.ok(node.value.range);
|
||||
const {line, col} = lineCounter.linePos(node.value.range[0]);
|
||||
console.error("%s:%d:%d: Too many inline allOf subschemas", file, line, col);
|
||||
ok = false;
|
||||
}
|
||||
|
||||
if (
|
||||
node.key instanceof Scalar &&
|
||||
node.key.value === "description" &&
|
||||
node.value instanceof Scalar &&
|
||||
typeof node.value.value === "string"
|
||||
) {
|
||||
const value = node.value;
|
||||
const description = node.value.value;
|
||||
promises.push(
|
||||
(async () => {
|
||||
let formatted = await prettierFormat(description, {
|
||||
parser: "markdown",
|
||||
});
|
||||
if (
|
||||
value.type !== Scalar.BLOCK_FOLDED &&
|
||||
value.type !== Scalar.BLOCK_LITERAL
|
||||
) {
|
||||
formatted = formatted.replace(/\n$/, "");
|
||||
}
|
||||
if (formatted !== description) {
|
||||
assert.ok(value.range);
|
||||
if (fix) {
|
||||
reformats.set(value.range[0], {
|
||||
value: formatted,
|
||||
context: {afterKey: true},
|
||||
});
|
||||
} else {
|
||||
ok = false;
|
||||
const {line, col} = lineCounter.linePos(value.range[0]);
|
||||
console.error(
|
||||
"%s:%d:%d: Format description with Prettier:",
|
||||
file,
|
||||
line,
|
||||
col,
|
||||
);
|
||||
let diff = "";
|
||||
for (const part of Diff.diffLines(description, formatted)) {
|
||||
const prefix = part.added
|
||||
? "\u{1B}[32m+"
|
||||
: part.removed
|
||||
? "\u{1B}[31m-"
|
||||
: "\u{1B}[34m ";
|
||||
diff += prefix;
|
||||
diff += part.value
|
||||
.replace(/\n$/, "")
|
||||
.replaceAll("\n", () => "\n" + prefix);
|
||||
diff += "\n";
|
||||
}
|
||||
diff += "\u{1B}[0m";
|
||||
console.error(diff);
|
||||
}
|
||||
}
|
||||
})(),
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
await Promise.all(promises);
|
||||
|
||||
if (!ok) {
|
||||
process.exitCode = 1;
|
||||
}
|
||||
if (reformats.size > 0) {
|
||||
console.log("%s: Fixing problems", file);
|
||||
for (const token of tokens) {
|
||||
if (token.type === "document") {
|
||||
CST.visit(token, ({value}) => {
|
||||
let reformat;
|
||||
if (
|
||||
CST.isScalar(value) &&
|
||||
(reformat = reformats.get(value.offset)) !== undefined
|
||||
) {
|
||||
CST.setScalarValue(value, reformat.value, reformat.context);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
await fs.promises.writeFile(file, tokens.map((token) => CST.stringify(token)).join(""));
|
||||
}
|
||||
|
||||
try {
|
||||
await SwaggerParser.validate(file);
|
||||
} catch (error) {
|
||||
if (!(error instanceof SyntaxError)) {
|
||||
throw error;
|
||||
}
|
||||
console.error("%s: %s", file, error.message);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
const res = await ExampleValidator.validateFile(file);
|
||||
if (!res.valid) {
|
||||
for (const error of res.errors) {
|
||||
console.error(error);
|
||||
}
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
await checkFile(file);
|
||||
}
|
||||
Reference in New Issue
Block a user