mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 17:53:39 +00:00
605 lines
21 KiB
JavaScript
605 lines
21 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import { readFileSync } from "node:fs";
|
|
import { isDirectRunUrl } from "../lib/direct-run.mjs";
|
|
import { isRecord as isObject } from "../lib/record-shared.mjs";
|
|
|
|
const REVIEW_ARTIFACT_ENUMS = Object.freeze({
|
|
recommendation: Object.freeze([
|
|
"READY FOR /prepare-pr",
|
|
"NEEDS WORK",
|
|
"NEEDS DISCUSSION",
|
|
"NOT USEFUL (CLOSE)",
|
|
]),
|
|
findingSeverity: Object.freeze(["BLOCKER", "IMPORTANT", "NIT"]),
|
|
nitSweepStatus: Object.freeze(["none", "has_nits"]),
|
|
issueValidationSource: Object.freeze(["linked_issue", "pr_body", "both"]),
|
|
issueValidationStatus: Object.freeze(["valid", "unclear", "invalid", "already_fixed_on_main"]),
|
|
behavioralSweepStatus: Object.freeze(["pass", "needs_work", "not_applicable"]),
|
|
behavioralSweepRisk: Object.freeze(["none", "present", "unknown"]),
|
|
testsResult: Object.freeze(["pass", "fail", "not_run"]),
|
|
docs: Object.freeze(["up_to_date", "missing", "not_applicable"]),
|
|
changelog: Object.freeze(["required", "not_required"]),
|
|
});
|
|
|
|
function reviewIdentityLine({ number, headSha }) {
|
|
return `Review artifact for PR #${number} at ${headSha}`;
|
|
}
|
|
|
|
export function renderReviewMarkdown(review) {
|
|
const lines = [reviewIdentityLine(review.pr), "", review.recommendation, ""];
|
|
for (const finding of review.findings) {
|
|
lines.push(`- ${finding.severity}: ${finding.title} (${finding.area})`, ` ${finding.fix}`);
|
|
}
|
|
lines.push(
|
|
"",
|
|
`Issue: ${review.issueValidation.status} (performed=${review.issueValidation.performed}). ${review.issueValidation.summary}`,
|
|
`Behavior: ${review.behavioralSweep.status} (performed=${review.behavioralSweep.performed}, silentDropRisk=${review.behavioralSweep.silentDropRisk}). ${review.behavioralSweep.summary}`,
|
|
);
|
|
for (const branch of review.behavioralSweep.branches) {
|
|
lines.push(`- ${branch.path}: ${branch.decision} → ${branch.outcome}`);
|
|
}
|
|
lines.push("", `Tests: ${review.tests.result}`);
|
|
if (review.tests.preExistingCi) {
|
|
const ci = review.tests.preExistingCi;
|
|
lines.push(
|
|
`Pre-existing CI attribution: head=${ci.head}, run=${ci.runId}, attempt=${ci.runAttempt}. ${ci.reason}`,
|
|
);
|
|
}
|
|
for (const test of review.tests.ran) {
|
|
lines.push(`- ${test}`);
|
|
}
|
|
for (const gap of review.tests.gaps) {
|
|
lines.push(`- Gap: ${gap}`);
|
|
}
|
|
lines.push(`Docs: ${review.docs}`, `Changelog: ${review.changelog}`);
|
|
if (review.nitSweep) {
|
|
lines.push(`Optional nits: ${review.nitSweep.summary}`);
|
|
}
|
|
return `${lines.join("\n")}\n`;
|
|
}
|
|
|
|
export function createReviewArtifactTemplate({ number, headSha }) {
|
|
return {
|
|
// Identity stamp, not reviewer input: validation refuses artifacts whose pr
|
|
// disagrees with .local/pr-meta.json, so a review written for another PR (or
|
|
// an already-superseded head) can never be landed as this one's verdict.
|
|
pr: { number, headSha },
|
|
recommendation: "NEEDS WORK",
|
|
findings: [],
|
|
behavioralSweep: {
|
|
performed: false,
|
|
status: "needs_work",
|
|
summary: "Review not completed yet.",
|
|
silentDropRisk: "unknown",
|
|
branches: [],
|
|
},
|
|
issueValidation: {
|
|
performed: false,
|
|
source: "pr_body",
|
|
status: "unclear",
|
|
summary: "Review not completed yet.",
|
|
},
|
|
tests: {
|
|
ran: [],
|
|
gaps: [],
|
|
result: "not_run",
|
|
},
|
|
docs: "not_applicable",
|
|
changelog: "not_required",
|
|
};
|
|
}
|
|
|
|
function isNonEmptyString(value) {
|
|
return typeof value === "string" && value.trim().length > 0;
|
|
}
|
|
|
|
function jsonValue(value) {
|
|
return JSON.stringify(value === undefined ? null : value);
|
|
}
|
|
|
|
export function validateReviewArtifacts({ review, prMeta }) {
|
|
const violations = [];
|
|
const add = (message) => {
|
|
if (!violations.includes(message)) {
|
|
violations.push(message);
|
|
}
|
|
};
|
|
const requireType = (valid, message) => {
|
|
if (!valid) {
|
|
add(message);
|
|
}
|
|
return valid;
|
|
};
|
|
const requireEnum = (value, enumName, field, messagePrefix) => {
|
|
const allowed = REVIEW_ARTIFACT_ENUMS[enumName];
|
|
if (!allowed.includes(value)) {
|
|
add(`${messagePrefix}: ${field}=${jsonValue(value)} (allowed: ${allowed.join("|")})`);
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
|
|
const reviewIsObject = requireType(
|
|
isObject(review),
|
|
"Invalid .local/review.json: top-level value must be an object",
|
|
);
|
|
const value = reviewIsObject ? review : {};
|
|
|
|
const prMetaIsValid =
|
|
isObject(prMeta) &&
|
|
Array.isArray(prMeta.files) &&
|
|
prMeta.files.every((file) => isObject(file) && typeof file.path === "string");
|
|
if (!prMetaIsValid) {
|
|
add("Invalid .local/pr-meta.json: files must be an array of objects with string path");
|
|
}
|
|
const prMetaIdentifiesHead =
|
|
isObject(prMeta) && Number.isInteger(prMeta.number) && typeof prMeta.headRefOid === "string";
|
|
if (!prMetaIdentifiesHead) {
|
|
add(
|
|
"Invalid .local/pr-meta.json: number and headRefOid must identify the reviewed PR head; re-run scripts/pr review-init",
|
|
);
|
|
}
|
|
const stamp = isObject(value.pr) ? value.pr : undefined;
|
|
const stampIsValid =
|
|
stamp !== undefined && Number.isInteger(stamp.number) && typeof stamp.headSha === "string";
|
|
if (!stampIsValid) {
|
|
add(
|
|
"Invalid PR identity in .local/review.json: pr.number must be an integer and pr.headSha must be a string; re-run scripts/pr review-artifacts-init",
|
|
);
|
|
}
|
|
if (
|
|
stampIsValid &&
|
|
prMetaIdentifiesHead &&
|
|
(stamp.number !== prMeta.number || stamp.headSha !== prMeta.headRefOid)
|
|
) {
|
|
add(
|
|
`Review artifact identity mismatch in .local/review.json: authored for PR #${String(stamp.number)} at ${String(stamp.headSha)}, but .local/pr-meta.json describes PR #${String(prMeta.number)} at ${String(prMeta.headRefOid)}; re-run scripts/pr review-artifacts-init`,
|
|
);
|
|
}
|
|
const recommendationIsString = requireType(
|
|
typeof value.recommendation === "string",
|
|
"Invalid recommendation in .local/review.json: recommendation must be a string",
|
|
);
|
|
const findingsAreArray = requireType(
|
|
Array.isArray(value.findings),
|
|
"Invalid findings in .local/review.json: findings must be an array",
|
|
);
|
|
const findings = findingsAreArray ? value.findings : [];
|
|
requireType(
|
|
findings.every(isObject),
|
|
"Invalid finding entry in .local/review.json: each finding must be an object",
|
|
);
|
|
const issueValidationIsObject = requireType(
|
|
isObject(value.issueValidation),
|
|
"Invalid issue validation in .local/review.json: issueValidation must be an object",
|
|
);
|
|
const behavioralSweepIsObject = requireType(
|
|
isObject(value.behavioralSweep),
|
|
"Invalid behavioral sweep in .local/review.json: behavioralSweep must be an object",
|
|
);
|
|
const testsIsObject = requireType(
|
|
isObject(value.tests),
|
|
"Invalid tests in .local/review.json: tests must be an object",
|
|
);
|
|
|
|
if (recommendationIsString) {
|
|
requireEnum(
|
|
value.recommendation,
|
|
"recommendation",
|
|
"recommendation",
|
|
"Invalid recommendation in .local/review.json",
|
|
);
|
|
}
|
|
|
|
const invalidSeverityIndex = findings.findIndex(
|
|
(finding) =>
|
|
isObject(finding) && !REVIEW_ARTIFACT_ENUMS.findingSeverity.includes(finding.severity),
|
|
);
|
|
if (invalidSeverityIndex !== -1) {
|
|
add(
|
|
`Invalid finding severity in .local/review.json: findings[${invalidSeverityIndex}].severity=${jsonValue(findings[invalidSeverityIndex].severity)} (allowed: ${REVIEW_ARTIFACT_ENUMS.findingSeverity.join("|")})`,
|
|
);
|
|
}
|
|
if (
|
|
findings.some(
|
|
(finding) =>
|
|
!isObject(finding) ||
|
|
typeof finding.id !== "string" ||
|
|
typeof finding.title !== "string" ||
|
|
typeof finding.area !== "string" ||
|
|
typeof finding.fix !== "string",
|
|
)
|
|
) {
|
|
add("Invalid finding shape in .local/review.json (id/title/area/fix must be strings)");
|
|
}
|
|
const nitFindingsCount = findings.filter(
|
|
(finding) => isObject(finding) && finding.severity === "NIT",
|
|
).length;
|
|
if (
|
|
value.recommendation === "READY FOR /prepare-pr" &&
|
|
findings.some(
|
|
(finding) =>
|
|
isObject(finding) && (finding.severity === "BLOCKER" || finding.severity === "IMPORTANT"),
|
|
)
|
|
) {
|
|
add(
|
|
"Invalid recommendation in .local/review.json: READY FOR /prepare-pr cannot include BLOCKER or IMPORTANT findings",
|
|
);
|
|
}
|
|
|
|
if (value.nitSweep !== undefined) {
|
|
const nitSweepIsObject = requireType(
|
|
isObject(value.nitSweep),
|
|
"Invalid nit sweep in .local/review.json: nitSweep must be an object",
|
|
);
|
|
const nitSweep = nitSweepIsObject ? value.nitSweep : {};
|
|
const nitSweepPerformedIsBoolean = requireType(
|
|
typeof nitSweep.performed === "boolean",
|
|
"Invalid nit sweep in .local/review.json: nitSweep.performed must be a boolean",
|
|
);
|
|
if (nitSweepPerformedIsBoolean && nitSweep.performed !== true) {
|
|
add("Invalid nit sweep in .local/review.json: nitSweep.performed must be true");
|
|
}
|
|
const nitSweepStatusIsString = requireType(
|
|
typeof nitSweep.status === "string",
|
|
"Invalid nit sweep status in .local/review.json: nitSweep.status must be a string",
|
|
);
|
|
if (nitSweepStatusIsString) {
|
|
const validStatus = requireEnum(
|
|
nitSweep.status,
|
|
"nitSweepStatus",
|
|
"nitSweep.status",
|
|
"Invalid nit sweep status in .local/review.json",
|
|
);
|
|
if (validStatus && nitSweep.status === "none" && nitFindingsCount > 0) {
|
|
add(
|
|
"Invalid nit sweep in .local/review.json: nitSweep.status is none but NIT findings exist",
|
|
);
|
|
}
|
|
if (validStatus && nitSweep.status === "has_nits" && nitFindingsCount < 1) {
|
|
add(
|
|
"Invalid nit sweep in .local/review.json: nitSweep.status is has_nits but no NIT findings exist",
|
|
);
|
|
}
|
|
}
|
|
requireType(
|
|
typeof nitSweep.summary === "string",
|
|
"Invalid nit sweep summary in .local/review.json: nitSweep.summary must be a string",
|
|
);
|
|
if (typeof nitSweep.summary === "string" && !isNonEmptyString(nitSweep.summary)) {
|
|
add(
|
|
"Invalid nit sweep summary in .local/review.json: nitSweep.summary must be a non-empty string",
|
|
);
|
|
}
|
|
}
|
|
|
|
const issueValidation = issueValidationIsObject ? value.issueValidation : {};
|
|
const issuePerformedIsBoolean = requireType(
|
|
typeof issueValidation.performed === "boolean",
|
|
"Invalid issue validation in .local/review.json: issueValidation.performed must be a boolean",
|
|
);
|
|
if (
|
|
issuePerformedIsBoolean &&
|
|
value.recommendation === "READY FOR /prepare-pr" &&
|
|
issueValidation.performed !== true
|
|
) {
|
|
add("Invalid issue validation in .local/review.json: issueValidation.performed must be true");
|
|
}
|
|
const issueSourceIsString = requireType(
|
|
typeof issueValidation.source === "string",
|
|
"Invalid issue validation source in .local/review.json: issueValidation.source must be a string",
|
|
);
|
|
if (issueSourceIsString) {
|
|
requireEnum(
|
|
issueValidation.source,
|
|
"issueValidationSource",
|
|
"issueValidation.source",
|
|
"Invalid issue validation source in .local/review.json",
|
|
);
|
|
}
|
|
const issueStatusIsString = requireType(
|
|
typeof issueValidation.status === "string",
|
|
"Invalid issue validation status in .local/review.json: issueValidation.status must be a string",
|
|
);
|
|
if (issueStatusIsString) {
|
|
requireEnum(
|
|
issueValidation.status,
|
|
"issueValidationStatus",
|
|
"issueValidation.status",
|
|
"Invalid issue validation status in .local/review.json",
|
|
);
|
|
}
|
|
requireType(
|
|
typeof issueValidation.summary === "string",
|
|
"Invalid issue validation summary in .local/review.json: issueValidation.summary must be a string",
|
|
);
|
|
if (typeof issueValidation.summary === "string" && !isNonEmptyString(issueValidation.summary)) {
|
|
add(
|
|
"Invalid issue validation summary in .local/review.json: issueValidation.summary must be a non-empty string",
|
|
);
|
|
}
|
|
|
|
const runtimeFileCount = prMetaIsValid
|
|
? prMeta.files.filter(
|
|
({ path }) =>
|
|
/^(src|extensions|apps|packages|ui)\//u.test(path) &&
|
|
!/(^|\/)__tests__\/|\.test\.|\.spec\./u.test(path) &&
|
|
!/\.(md|mdx)$/u.test(path),
|
|
).length
|
|
: 0;
|
|
const runtimeReviewRequired = runtimeFileCount > 0;
|
|
|
|
const behavioralSweep = behavioralSweepIsObject ? value.behavioralSweep : {};
|
|
const behavioralPerformedIsBoolean = requireType(
|
|
typeof behavioralSweep.performed === "boolean",
|
|
"Invalid behavioral sweep in .local/review.json: behavioralSweep.performed must be a boolean",
|
|
);
|
|
if (
|
|
behavioralPerformedIsBoolean &&
|
|
value.recommendation === "READY FOR /prepare-pr" &&
|
|
behavioralSweep.performed !== true
|
|
) {
|
|
add("Invalid behavioral sweep in .local/review.json: behavioralSweep.performed must be true");
|
|
}
|
|
const behavioralStatusIsString = requireType(
|
|
typeof behavioralSweep.status === "string",
|
|
"Invalid behavioral sweep status in .local/review.json: behavioralSweep.status must be a string",
|
|
);
|
|
const behavioralStatusIsValid =
|
|
behavioralStatusIsString &&
|
|
requireEnum(
|
|
behavioralSweep.status,
|
|
"behavioralSweepStatus",
|
|
"behavioralSweep.status",
|
|
"Invalid behavioral sweep status in .local/review.json",
|
|
);
|
|
const behavioralRiskIsString = requireType(
|
|
typeof behavioralSweep.silentDropRisk === "string",
|
|
"Invalid behavioral sweep risk in .local/review.json: behavioralSweep.silentDropRisk must be a string",
|
|
);
|
|
const behavioralRiskIsValid =
|
|
behavioralRiskIsString &&
|
|
requireEnum(
|
|
behavioralSweep.silentDropRisk,
|
|
"behavioralSweepRisk",
|
|
"behavioralSweep.silentDropRisk",
|
|
"Invalid behavioral sweep risk in .local/review.json",
|
|
);
|
|
requireType(
|
|
typeof behavioralSweep.summary === "string",
|
|
"Invalid behavioral sweep summary in .local/review.json: behavioralSweep.summary must be a string",
|
|
);
|
|
if (typeof behavioralSweep.summary === "string" && !isNonEmptyString(behavioralSweep.summary)) {
|
|
add(
|
|
"Invalid behavioral sweep summary in .local/review.json: behavioralSweep.summary must be a non-empty string",
|
|
);
|
|
}
|
|
const branchesAreArray = Array.isArray(behavioralSweep.branches);
|
|
if (!branchesAreArray) {
|
|
add(
|
|
"Invalid behavioral sweep in .local/review.json: behavioralSweep.branches must be an array",
|
|
);
|
|
}
|
|
const branches = branchesAreArray ? behavioralSweep.branches : [];
|
|
if (
|
|
branches.some(
|
|
(branch) =>
|
|
!isObject(branch) ||
|
|
typeof branch.path !== "string" ||
|
|
typeof branch.decision !== "string" ||
|
|
typeof branch.outcome !== "string",
|
|
)
|
|
) {
|
|
add(
|
|
"Invalid behavioral sweep branch entry in .local/review.json: each entry must be an object with string path/decision/outcome",
|
|
);
|
|
}
|
|
|
|
if (
|
|
behavioralStatusIsValid &&
|
|
runtimeReviewRequired &&
|
|
behavioralSweep.status === "not_applicable"
|
|
) {
|
|
add(
|
|
"Invalid behavioral sweep in .local/review.json: runtime file changes require behavioralSweep.status=pass|needs_work",
|
|
);
|
|
}
|
|
if (runtimeReviewRequired && behavioralSweep.performed === true && branches.length < 1) {
|
|
add(
|
|
"Invalid behavioral sweep in .local/review.json: runtime file changes require at least one branch entry",
|
|
);
|
|
}
|
|
if (
|
|
behavioralStatusIsValid &&
|
|
behavioralSweep.status === "not_applicable" &&
|
|
branches.length > 0
|
|
) {
|
|
add(
|
|
"Invalid behavioral sweep in .local/review.json: not_applicable cannot include branch entries",
|
|
);
|
|
}
|
|
if (
|
|
behavioralStatusIsValid &&
|
|
behavioralRiskIsValid &&
|
|
behavioralSweep.status === "pass" &&
|
|
behavioralSweep.silentDropRisk !== "none"
|
|
) {
|
|
add("Invalid behavioral sweep in .local/review.json: status=pass requires silentDropRisk=none");
|
|
}
|
|
|
|
if (value.recommendation === "READY FOR /prepare-pr" && issueValidation.status !== "valid") {
|
|
add(
|
|
"Invalid recommendation in .local/review.json: READY FOR /prepare-pr requires issueValidation.status=valid",
|
|
);
|
|
}
|
|
if (value.recommendation === "READY FOR /prepare-pr" && behavioralSweep.status === "needs_work") {
|
|
add(
|
|
"Invalid recommendation in .local/review.json: READY FOR /prepare-pr requires behavioralSweep.status!=needs_work",
|
|
);
|
|
}
|
|
if (
|
|
value.recommendation === "READY FOR /prepare-pr" &&
|
|
runtimeReviewRequired &&
|
|
behavioralSweep.status !== "pass"
|
|
) {
|
|
add(
|
|
"Invalid recommendation in .local/review.json: READY FOR /prepare-pr on runtime changes requires behavioralSweep.status=pass",
|
|
);
|
|
}
|
|
if (
|
|
value.recommendation === "READY FOR /prepare-pr" &&
|
|
behavioralSweep.silentDropRisk === "present"
|
|
) {
|
|
add(
|
|
"Invalid recommendation in .local/review.json: READY FOR /prepare-pr is not allowed when behavioralSweep.silentDropRisk=present",
|
|
);
|
|
}
|
|
|
|
const tests = testsIsObject ? value.tests : {};
|
|
const testsRanAreArray = requireType(
|
|
Array.isArray(tests.ran),
|
|
"Invalid tests in .local/review.json: tests.ran must be an array of strings",
|
|
);
|
|
if (testsRanAreArray && !tests.ran.every((entry) => typeof entry === "string")) {
|
|
add("Invalid tests in .local/review.json: tests.ran must be an array of strings");
|
|
}
|
|
const testsGapsAreArray = requireType(
|
|
Array.isArray(tests.gaps),
|
|
"Invalid tests in .local/review.json: tests.gaps must be an array of strings",
|
|
);
|
|
if (testsGapsAreArray && !tests.gaps.every((entry) => typeof entry === "string")) {
|
|
add("Invalid tests in .local/review.json: tests.gaps must be an array of strings");
|
|
}
|
|
const testsResultIsString = requireType(
|
|
typeof tests.result === "string",
|
|
"Invalid tests result in .local/review.json: tests.result must be a string",
|
|
);
|
|
if (testsResultIsString) {
|
|
requireEnum(
|
|
tests.result,
|
|
"testsResult",
|
|
"tests.result",
|
|
"Invalid tests result in .local/review.json",
|
|
);
|
|
}
|
|
const ci = tests.preExistingCi;
|
|
const attributedCiFailure =
|
|
tests.result === "fail" &&
|
|
isObject(ci) &&
|
|
ci.head === value.pr?.headSha &&
|
|
Number.isSafeInteger(ci.runId) &&
|
|
ci.runId > 0 &&
|
|
Number.isSafeInteger(ci.runAttempt) &&
|
|
ci.runAttempt > 0 &&
|
|
isNonEmptyString(ci.reason);
|
|
if (ci !== undefined && !attributedCiFailure) {
|
|
add(
|
|
"Invalid pre-existing CI attribution: keep tests.result=fail and bind this review head, run, attempt, and reason",
|
|
);
|
|
}
|
|
if (
|
|
value.recommendation === "READY FOR /prepare-pr" &&
|
|
tests.result === "fail" &&
|
|
!attributedCiFailure
|
|
) {
|
|
add(
|
|
"Invalid recommendation in .local/review.json: READY FOR /prepare-pr cannot include failing tests",
|
|
);
|
|
}
|
|
if (
|
|
value.recommendation === "READY FOR /prepare-pr" &&
|
|
runtimeReviewRequired &&
|
|
tests.result !== "pass" &&
|
|
!attributedCiFailure
|
|
) {
|
|
add(
|
|
"Invalid recommendation in .local/review.json: READY FOR /prepare-pr on runtime changes requires passing tests",
|
|
);
|
|
}
|
|
|
|
const docsIsString = requireType(
|
|
typeof value.docs === "string",
|
|
"Invalid docs status in .local/review.json: docs must be a string",
|
|
);
|
|
if (docsIsString) {
|
|
requireEnum(value.docs, "docs", "docs", "Invalid docs status in .local/review.json");
|
|
}
|
|
const changelogIsString = requireType(
|
|
typeof value.changelog === "string",
|
|
"Invalid changelog status in .local/review.json: changelog must be a string",
|
|
);
|
|
if (changelogIsString) {
|
|
requireEnum(
|
|
value.changelog,
|
|
"changelog",
|
|
"changelog",
|
|
"Invalid changelog status in .local/review.json",
|
|
);
|
|
}
|
|
|
|
return violations;
|
|
}
|
|
|
|
function readJson(filePath) {
|
|
try {
|
|
return JSON.parse(readFileSync(filePath, "utf8"));
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
throw new Error(`Invalid JSON in ${filePath}: ${message}`, { cause: error });
|
|
}
|
|
}
|
|
|
|
function main(argv = process.argv.slice(2)) {
|
|
const [command, ...args] = argv;
|
|
if ((command === "template" || command === "markdown") && args.length === 2) {
|
|
const [prNumber, headSha] = args;
|
|
if (!/^[1-9][0-9]*$/u.test(prNumber) || !/^[0-9a-f]{40}$/u.test(headSha)) {
|
|
console.error(
|
|
`Usage: review-artifacts.mjs ${command} <pr-number> <head-sha>; pr-number must be a positive integer and head-sha a 40-character lowercase hex commit id.`,
|
|
);
|
|
process.exitCode = 2;
|
|
return;
|
|
}
|
|
const identity = { number: Number(prNumber), headSha };
|
|
process.stdout.write(
|
|
command === "markdown"
|
|
? renderReviewMarkdown(createReviewArtifactTemplate(identity))
|
|
: `${JSON.stringify(createReviewArtifactTemplate(identity), null, 2)}\n`,
|
|
);
|
|
return;
|
|
}
|
|
if (command === "render" && args.length === 1) {
|
|
process.stdout.write(renderReviewMarkdown(readJson(args[0])));
|
|
return;
|
|
}
|
|
if (command === "validate" && args.length === 2) {
|
|
const [reviewPath, prMetaPath] = args;
|
|
const violations = validateReviewArtifacts({
|
|
review: readJson(reviewPath),
|
|
prMeta: readJson(prMetaPath),
|
|
});
|
|
if (violations.length > 0) {
|
|
for (const violation of violations) {
|
|
console.log(violation);
|
|
}
|
|
console.log(`${violations.length} artifact violations`);
|
|
process.exitCode = 1;
|
|
}
|
|
return;
|
|
}
|
|
console.error(
|
|
"Usage: review-artifacts.mjs template|markdown <pr-number> <head-sha> | render <review.json> | validate <review.json> <pr-meta.json>",
|
|
);
|
|
process.exitCode = 2;
|
|
}
|
|
|
|
if (isDirectRunUrl(process.argv[1], import.meta.url)) {
|
|
try {
|
|
main();
|
|
} catch (error) {
|
|
console.log(error instanceof Error ? error.message : String(error));
|
|
console.log("1 artifact violations");
|
|
process.exitCode = 1;
|
|
}
|
|
}
|