116 lines
4.2 KiB
JavaScript
116 lines
4.2 KiB
JavaScript
#!/usr/bin/env node
|
|
import assert from "node:assert/strict";
|
|
import { readFile } from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
import { ensureNotRepoReportsPath } from "./src/report-paths.mjs";
|
|
|
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
|
|
function assertObject(value, label) {
|
|
assert.ok(value && typeof value === "object" && !Array.isArray(value), `${label} must be an object`);
|
|
}
|
|
|
|
function assertString(value, label) {
|
|
assert.equal(typeof value, "string", `${label} must be a string`);
|
|
assert.ok(value.trim().length > 0, `${label} must not be empty`);
|
|
}
|
|
|
|
function assertBoolean(value, label) {
|
|
assert.equal(typeof value, "boolean", `${label} must be a boolean`);
|
|
}
|
|
|
|
function repoRelative(inputPath) {
|
|
const absolute = path.isAbsolute(inputPath) ? inputPath : path.resolve(process.cwd(), inputPath);
|
|
return path.relative(repoRoot, absolute).split(path.sep).join("/");
|
|
}
|
|
|
|
async function readJson(inputPath) {
|
|
ensureNotRepoReportsPath(repoRoot, inputPath, "validation input");
|
|
const absolute = path.isAbsolute(inputPath) ? inputPath : path.resolve(process.cwd(), inputPath);
|
|
const raw = await readFile(absolute, "utf8");
|
|
return {
|
|
path: absolute,
|
|
relativePath: repoRelative(absolute),
|
|
report: JSON.parse(raw)
|
|
};
|
|
}
|
|
|
|
function validateReportShape(report, label) {
|
|
assertObject(report, label);
|
|
assertString(report.reportKind ?? report.taskId ?? report.kind ?? "temporary-json", `${label}.kind`);
|
|
if (Object.hasOwn(report, "devOnly")) assertBoolean(report.devOnly, `${label}.devOnly`);
|
|
if (Object.hasOwn(report, "prodDisabled")) assertBoolean(report.prodDisabled, `${label}.prodDisabled`);
|
|
if (Array.isArray(report.blockers)) {
|
|
for (const [index, blocker] of report.blockers.entries()) {
|
|
assertObject(blocker, `${label}.blockers[${index}]`);
|
|
assertString(blocker.scope ?? blocker.code ?? blocker.type ?? "blocker", `${label}.blockers[${index}].scope`);
|
|
}
|
|
}
|
|
if (Array.isArray(report.validationCommands)) {
|
|
for (const [index, command] of report.validationCommands.entries()) {
|
|
assertString(command, `${label}.validationCommands[${index}]`);
|
|
}
|
|
}
|
|
if (Object.hasOwn(report, "dryRunPlan")) {
|
|
assertObject(report.dryRunPlan, `${label}.dryRunPlan`);
|
|
if (Object.hasOwn(report.dryRunPlan, "dryRunCallsLiveEndpoints")) {
|
|
assert.equal(report.dryRunPlan.dryRunCallsLiveEndpoints, false, `${label}.dryRunPlan.dryRunCallsLiveEndpoints`);
|
|
}
|
|
if (Object.hasOwn(report.dryRunPlan, "liveWriteWillRun")) {
|
|
assert.equal(report.dryRunPlan.liveWriteWillRun, false, `${label}.dryRunPlan.liveWriteWillRun`);
|
|
}
|
|
if (Object.hasOwn(report.dryRunPlan, "evidenceLevel")) {
|
|
assert.equal(report.dryRunPlan.evidenceLevel, "DRY-RUN", `${label}.dryRunPlan.evidenceLevel`);
|
|
}
|
|
assertObject(report.liveOperation, `${label}.liveOperation`);
|
|
assert.notEqual(
|
|
report.liveOperation.status,
|
|
"pass",
|
|
`${label}.liveOperation.status cannot be pass when only a DRY-RUN plan is present`
|
|
);
|
|
for (const field of ["operationId", "traceId", "auditId", "evidenceId"]) {
|
|
if (Object.hasOwn(report.liveOperation, field)) {
|
|
assert.equal(report.liveOperation[field], "not_observed", `${label}.liveOperation.${field} must remain not_observed for DRY-RUN evidence`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
async function main(argv) {
|
|
if (argv.length === 0) {
|
|
process.stdout.write(`${JSON.stringify({
|
|
ok: true,
|
|
status: "skipped",
|
|
reason: "repository report files are disabled; pass explicit temporary JSON paths to validate them"
|
|
})}\n`);
|
|
return;
|
|
}
|
|
|
|
const results = [];
|
|
for (const inputPath of argv) {
|
|
const { relativePath, report } = await readJson(inputPath);
|
|
validateReportShape(report, relativePath);
|
|
results.push(relativePath);
|
|
}
|
|
|
|
process.stdout.write(`${JSON.stringify({
|
|
ok: true,
|
|
status: "pass",
|
|
validated: results.length,
|
|
files: results
|
|
})}\n`);
|
|
}
|
|
|
|
try {
|
|
await main(process.argv.slice(2));
|
|
} catch (error) {
|
|
process.stderr.write(`${JSON.stringify({
|
|
ok: false,
|
|
status: "blocked",
|
|
error: error instanceof Error ? error.message : String(error)
|
|
})}\n`);
|
|
process.exitCode = 1;
|
|
}
|