fix: enforce dev report port lifecycle
This commit is contained in:
@@ -0,0 +1,235 @@
|
||||
#!/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;
|
||||
}
|
||||
Reference in New Issue
Block a user