236 lines
7.9 KiB
JavaScript
236 lines
7.9 KiB
JavaScript
#!/usr/bin/env node
|
|
import assert from "node:assert/strict";
|
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
import {
|
|
ACTIVE_DEV_BROWSER_ENDPOINT,
|
|
ACTIVE_DEV_PUBLIC_ENDPOINT,
|
|
REPORT_LIFECYCLE_VERSION,
|
|
deprecatedPublicEndpoints,
|
|
findForbiddenActiveDeprecatedEndpoints,
|
|
historicalReportLifecycle
|
|
} from "../internal/dev-report-lifecycle.mjs";
|
|
|
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
const defaultDeprecatedEndpoint = "http://74.48.78.17:6667";
|
|
const statusFields = ["status", "gateStatus", "conclusion"];
|
|
|
|
function usage() {
|
|
return [
|
|
"Usage:",
|
|
" node scripts/report-lifecycle.mjs validate <report.json>",
|
|
" node scripts/report-lifecycle.mjs invalidate <report.json> --reason <text> [--deprecated-endpoint <url>] [--replaced-by <path>] [--out <path>]",
|
|
"",
|
|
"Notes:",
|
|
" validate checks the active/historical endpoint lifecycle contract for one report.",
|
|
" invalidate marks one report historical/deprecated without changing evidence payloads.",
|
|
" --out defaults to in-place; use a historical/ path when keeping an old active filename reserved for regenerated evidence."
|
|
].join("\n");
|
|
}
|
|
|
|
function parseArgs(argv) {
|
|
if (argv.length === 0 || argv.includes("--help")) {
|
|
return { help: true };
|
|
}
|
|
const [command, reportPath, ...rest] = argv;
|
|
const args = {
|
|
command,
|
|
reportPath,
|
|
deprecatedEndpoint: defaultDeprecatedEndpoint,
|
|
reason: null,
|
|
replacedBy: null,
|
|
out: null
|
|
};
|
|
|
|
for (let index = 0; index < rest.length; index += 1) {
|
|
const arg = rest[index];
|
|
if (arg === "--deprecated-endpoint") {
|
|
args.deprecatedEndpoint = rest[++index];
|
|
} else if (arg === "--reason") {
|
|
args.reason = rest[++index];
|
|
} else if (arg === "--replaced-by") {
|
|
args.replacedBy = rest[++index];
|
|
} else if (arg === "--out") {
|
|
args.out = rest[++index];
|
|
} else if (arg === "--help") {
|
|
args.help = true;
|
|
} else {
|
|
throw new Error(`unknown argument: ${arg}`);
|
|
}
|
|
}
|
|
|
|
if (args.help || !args.command) return args;
|
|
if (!["validate", "invalidate"].includes(args.command)) {
|
|
throw new Error(`unknown command: ${args.command}`);
|
|
}
|
|
assert.ok(args.reportPath, "report path is required");
|
|
if (args.command === "invalidate") {
|
|
assert.ok(args.reason, "--reason is required for invalidate");
|
|
assert.ok(
|
|
deprecatedPublicEndpoints.includes(args.deprecatedEndpoint),
|
|
`--deprecated-endpoint must be one of ${deprecatedPublicEndpoints.join(", ")}`
|
|
);
|
|
}
|
|
return args;
|
|
}
|
|
|
|
function repoPath(inputPath) {
|
|
const absolutePath = path.resolve(process.cwd(), inputPath);
|
|
assert.ok(
|
|
absolutePath === repoRoot || absolutePath.startsWith(`${repoRoot}${path.sep}`),
|
|
`${inputPath} must stay inside the repository`
|
|
);
|
|
return absolutePath;
|
|
}
|
|
|
|
function relativeRepoPath(inputPath) {
|
|
return path.relative(repoRoot, repoPath(inputPath)).split(path.sep).join("/");
|
|
}
|
|
|
|
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 validateLifecycle(report, raw, label) {
|
|
assertObject(report.reportLifecycle, `${label}.reportLifecycle`);
|
|
const lifecycle = report.reportLifecycle;
|
|
assert.equal(lifecycle.version, REPORT_LIFECYCLE_VERSION, `${label}.reportLifecycle.version`);
|
|
assert.ok(["active", "historical"].includes(lifecycle.state), `${label}.reportLifecycle.state`);
|
|
assert.equal(lifecycle.activeEndpoint, ACTIVE_DEV_PUBLIC_ENDPOINT, `${label}.reportLifecycle.activeEndpoint`);
|
|
assert.equal(lifecycle.activeBrowserEndpoint, ACTIVE_DEV_BROWSER_ENDPOINT, `${label}.reportLifecycle.activeBrowserEndpoint`);
|
|
assertString(lifecycle.summary, `${label}.reportLifecycle.summary`);
|
|
|
|
if (lifecycle.state === "active") {
|
|
assert.equal(lifecycle.deprecatedEndpoint, null, `${label}.reportLifecycle.deprecatedEndpoint`);
|
|
assert.deepEqual(
|
|
findForbiddenActiveDeprecatedEndpoints(report, raw),
|
|
[],
|
|
`${label} active report must not present :6666/:6667 as current public endpoint evidence`
|
|
);
|
|
return "active";
|
|
}
|
|
|
|
assert.ok(
|
|
deprecatedPublicEndpoints.includes(lifecycle.deprecatedEndpoint),
|
|
`${label}.reportLifecycle.deprecatedEndpoint must name a deprecated public endpoint`
|
|
);
|
|
assert.ok(
|
|
lifecycle.summary.toLowerCase().includes("historical") ||
|
|
lifecycle.summary.toLowerCase().includes("deprecated"),
|
|
`${label}.reportLifecycle.summary must explain the historical/deprecated state`
|
|
);
|
|
return "historical";
|
|
}
|
|
|
|
function primaryStatus(report) {
|
|
return report.status ?? report.gateStatus ?? report.conclusion ?? null;
|
|
}
|
|
|
|
function markNonGreen(report) {
|
|
let hasTopLevelStatus = false;
|
|
for (const field of statusFields) {
|
|
if (Object.hasOwn(report, field)) {
|
|
hasTopLevelStatus = true;
|
|
if (["pass", "ready", "green", "published"].includes(report[field])) {
|
|
report[field] = "blocked";
|
|
}
|
|
}
|
|
}
|
|
if (!hasTopLevelStatus) {
|
|
report.status = "blocked";
|
|
}
|
|
}
|
|
|
|
function appendInvalidationBlocker(report, reason, deprecatedEndpoint) {
|
|
if (!Array.isArray(report.blockers)) {
|
|
report.blockers = [];
|
|
}
|
|
const scope = "deprecated-public-endpoint";
|
|
const exists = report.blockers.some((blocker) => blocker?.scope === scope);
|
|
if (!exists) {
|
|
report.blockers.push({
|
|
type: "environment_blocker",
|
|
scope,
|
|
status: "open",
|
|
summary: `${reason} Deprecated endpoint ${deprecatedEndpoint} cannot be used as active DEV evidence.`
|
|
});
|
|
}
|
|
}
|
|
|
|
async function readReport(reportPath) {
|
|
const absolutePath = repoPath(reportPath);
|
|
const raw = await readFile(absolutePath, "utf8");
|
|
return { absolutePath, raw, report: JSON.parse(raw) };
|
|
}
|
|
|
|
async function validateCommand(args) {
|
|
const relativePath = relativeRepoPath(args.reportPath);
|
|
const { raw, report } = await readReport(args.reportPath);
|
|
const state = validateLifecycle(report, raw, relativePath);
|
|
process.stdout.write(`${JSON.stringify({
|
|
ok: true,
|
|
path: relativePath,
|
|
state,
|
|
primaryStatus: primaryStatus(report)
|
|
})}\n`);
|
|
}
|
|
|
|
async function invalidateCommand(args) {
|
|
const inputRelativePath = relativeRepoPath(args.reportPath);
|
|
const { report } = await readReport(args.reportPath);
|
|
assertObject(report, inputRelativePath);
|
|
|
|
report.reportLifecycle = historicalReportLifecycle({
|
|
deprecatedEndpoint: args.deprecatedEndpoint,
|
|
summary: `Historical/deprecated after DEV public endpoint change: ${args.reason}`
|
|
});
|
|
if (args.replacedBy) {
|
|
report.reportLifecycle.replacedBy = relativeRepoPath(args.replacedBy);
|
|
}
|
|
markNonGreen(report);
|
|
appendInvalidationBlocker(report, args.reason, args.deprecatedEndpoint);
|
|
|
|
const outPath = args.out ? repoPath(args.out) : repoPath(args.reportPath);
|
|
await mkdir(path.dirname(outPath), { recursive: true });
|
|
const nextRaw = `${JSON.stringify(report, null, 2)}\n`;
|
|
validateLifecycle(report, nextRaw, inputRelativePath);
|
|
await writeFile(outPath, nextRaw);
|
|
process.stdout.write(`${JSON.stringify({
|
|
ok: true,
|
|
input: inputRelativePath,
|
|
output: path.relative(repoRoot, outPath).split(path.sep).join("/"),
|
|
state: "historical",
|
|
deprecatedEndpoint: args.deprecatedEndpoint,
|
|
replacedBy: report.reportLifecycle.replacedBy ?? null
|
|
})}\n`);
|
|
}
|
|
|
|
async function main() {
|
|
const args = parseArgs(process.argv.slice(2));
|
|
if (args.help || !args.command) {
|
|
process.stdout.write(`${usage()}\n`);
|
|
return;
|
|
}
|
|
if (args.command === "validate") {
|
|
await validateCommand(args);
|
|
return;
|
|
}
|
|
if (args.command === "invalidate") {
|
|
await invalidateCommand(args);
|
|
}
|
|
}
|
|
|
|
try {
|
|
await main();
|
|
} catch (error) {
|
|
process.stderr.write(`[report-lifecycle] ${error instanceof Error ? error.message : String(error)}\n`);
|
|
process.exitCode = 1;
|
|
}
|