Files
rea/scripts/ci/static-check-report.mjs
T
Ysopwnable d96550f2f6 ci: summarize static checks and retain complete reports (#2242)
Keep static feedback concise while retaining complete diagnostic reports,
visible lint errors, module-boundary checks, and the 3% duplication gate.
Upload complete reports after static failures and restore their outputs
from Turbo caches. Preserve unambiguous, checkout-relative clone locations.

Bind both lint caches to the website, documentation configuration and
experiment sources actually scanned by oxlint, including ignore policy.
Guard this ownership by comparing live checker discovery with resolved
Turbo inputs. A real website syntax error now invalidates a cached pass.

Validation: full current-head CI and the separate Go lane passed. Seven
real-checker regressions passed; failure-status and stale-report mutations
were rejected, and restored source passed. Actual Turbo restored both
reports byte-for-byte; full static/docs/workflow checks passed.

Closes #2238.
Refs #2190.

Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing)
2026-10-13 01:02:35 +08:00

150 lines
4.8 KiB
JavaScript

import { spawnSync } from "node:child_process";
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
import { createRequire } from "node:module";
import { dirname, join, relative, sep } from "node:path";
import { z } from "zod";
const require = createRequire(import.meta.url);
const mode = process.argv[2];
const reportRoot = join(".cache", "static-checks");
const extraArguments = process.argv.slice(3);
const lintReport = z.object({
number_of_files: z.number(),
diagnostics: z.array(
z.object({
message: z.string(),
code: z.string().optional(),
severity: z.enum(["error", "warning", "advice"]),
filename: z.string(),
labels: z.array(
z.object({
span: z.object({ line: z.number(), column: z.number() }),
}),
),
}),
),
});
const duplicateReport = z
.object({
duplicates: z.array(
z
.object({
firstFile: z.object({ name: z.string() }).passthrough(),
secondFile: z.object({ name: z.string() }).passthrough(),
})
.passthrough(),
),
statistics: z
.object({
total: z
.object({
sources: z.number(),
clones: z.number(),
percentage: z.number(),
})
.passthrough(),
})
.passthrough(),
})
.passthrough();
const run = (packageName, entrypoint, arguments_) => {
const executable = join(
dirname(require.resolve(`${packageName}/package.json`)),
entrypoint,
);
const result = spawnSync(process.execPath, [executable, ...arguments_], {
encoding: "utf8",
maxBuffer: 32 * 1024 * 1024,
});
if (result.stderr) process.stderr.write(result.stderr);
if (result.error !== undefined) throw result.error;
if (result.status === null)
throw new Error(`${packageName} terminated by ${result.signal}`);
return result;
};
try {
await mkdir(reportRoot, { recursive: true });
if (mode === "lint") {
const reportPath = join(reportRoot, "oxlint.json");
await rm(reportPath, { force: true });
const result = run("oxlint", "bin/oxlint", [
...extraArguments,
"--format",
"json",
]);
let report;
try {
report = lintReport.parse(JSON.parse(result.stdout));
} catch (cause) {
process.stderr.write(result.stdout);
throw cause;
}
await writeFile(reportPath, result.stdout);
const warnings = report.diagnostics.filter(
({ severity }) => severity === "warning",
);
const errors = report.diagnostics.filter(
({ severity }) => severity === "error",
);
console.log(
`Lint: ${report.number_of_files} files, ${errors.length} errors, ${warnings.length} warnings.`,
);
for (const diagnostic of errors) {
const location = diagnostic.labels[0]?.span;
console.error(
`${diagnostic.filename}:${location?.line ?? 1}:${location?.column ?? 1}: ${diagnostic.code ?? "lint"}: ${diagnostic.message}`,
);
}
const counts = new Map();
for (const { code } of warnings)
counts.set(code ?? "warning", (counts.get(code ?? "warning") ?? 0) + 1);
for (const [rule, count] of [...counts]
.sort(
(left, right) => right[1] - left[1] || left[0].localeCompare(right[0]),
)
.slice(0, 8))
console.log(` ${rule}: ${count} warnings`);
console.log(`Full diagnostics: ${reportPath}; npm run lint:details`);
process.exitCode = result.status;
} else if (mode === "duplicates") {
const output = join(reportRoot, "jscpd");
const reportPath = join(output, "jscpd-report.json");
await rm(reportPath, { force: true });
const result = run("jscpd", "run-jscpd.js", [
"src/",
"tests/",
...extraArguments,
"--reporters",
"json",
"--output",
output,
"--no-colors",
"--no-tips",
"--absolute",
]);
if (result.stdout) process.stdout.write(result.stdout);
const report = duplicateReport.parse(
JSON.parse(await readFile(reportPath, "utf8")),
);
// jscpd otherwise strips each scan root, making src/ and tests/ names
// ambiguous. Normalize absolute producer paths once for portable reports.
for (const clone of report.duplicates)
for (const file of [clone.firstFile, clone.secondFile])
file.name = relative(process.cwd(), file.name).split(sep).join("/");
await writeFile(reportPath, JSON.stringify(report, null, 2));
const { sources, clones, percentage } = report.statistics.total;
console.log(
`Duplication: ${sources} files, ${clones} clones, ${percentage.toFixed(2)}% duplicated lines.`,
);
console.log(`Full diagnostics: ${reportPath}; npm run jscpd:details`);
process.exitCode = result.status;
} else {
throw new Error("Expected lint or duplicates");
}
} catch (cause) {
console.error(cause instanceof Error ? cause.message : String(cause));
process.exitCode = 1;
}