chore: add dev gate preflight
This commit is contained in:
@@ -9,10 +9,16 @@ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."
|
||||
const reportsDir = path.join(repoRoot, "reports/dev-gate");
|
||||
|
||||
const allowedIssues = new Set(["pikasTech/HWLAB#31", "pikasTech/HWLAB#33"]);
|
||||
const requiredPreflightIssue = "pikasTech/HWLAB#34";
|
||||
const requiredValidationCommands = [
|
||||
"node --check scripts/validate-dev-gate-report.mjs",
|
||||
"node scripts/validate-dev-gate-report.mjs"
|
||||
];
|
||||
const requiredPreflightValidationCommands = [
|
||||
"node --check scripts/dev-gate-preflight.mjs",
|
||||
"node --check scripts/src/dev-gate-preflight.mjs",
|
||||
"node scripts/dev-gate-preflight.mjs"
|
||||
];
|
||||
const requiredSmokeCommand = "node scripts/m1-contract-smoke.mjs";
|
||||
const requiredDryRunCommand =
|
||||
"node tools/hwlab-cli/bin/hwlab-cli.mjs test e2e --env dev --mvp --dry-run";
|
||||
@@ -41,6 +47,16 @@ const blockerTypes = new Set([
|
||||
"safety_blocker"
|
||||
]);
|
||||
const blockerStates = new Set(["open", "acknowledged", "closed"]);
|
||||
const preflightConclusions = new Set(["ready", "blocked"]);
|
||||
const requiredPreflightSupports = [
|
||||
"pikasTech/HWLAB#7",
|
||||
"pikasTech/HWLAB#12",
|
||||
"pikasTech/HWLAB#22",
|
||||
"pikasTech/HWLAB#23",
|
||||
"pikasTech/HWLAB#29",
|
||||
"pikasTech/HWLAB#30",
|
||||
"pikasTech/HWLAB#31"
|
||||
];
|
||||
|
||||
function assertObject(value, label) {
|
||||
assert.ok(value && typeof value === "object" && !Array.isArray(value), `${label} must be an object`);
|
||||
@@ -107,6 +123,11 @@ async function collectReportFiles() {
|
||||
|
||||
async function validateReport(relativePath) {
|
||||
const report = await readJsonFile(relativePath);
|
||||
if (report.reportKind === "dev-gate-preflight") {
|
||||
await validatePreflightReport(relativePath, report);
|
||||
return;
|
||||
}
|
||||
|
||||
const label = relativePath;
|
||||
|
||||
assertObject(report, label);
|
||||
@@ -388,6 +409,110 @@ async function validateDevM3Report(report, label) {
|
||||
assertString(report.summary.result, `${label}.summary.result`);
|
||||
}
|
||||
|
||||
async function validatePreflightReport(relativePath, report) {
|
||||
const label = relativePath;
|
||||
|
||||
assertObject(report, label);
|
||||
for (const field of [
|
||||
"$schema",
|
||||
"$id",
|
||||
"reportVersion",
|
||||
"reportKind",
|
||||
"issue",
|
||||
"supports",
|
||||
"target",
|
||||
"generatedAt",
|
||||
"mode",
|
||||
"devOnly",
|
||||
"prodDisabled",
|
||||
"forbiddenActions",
|
||||
"validationCommands",
|
||||
"conclusion",
|
||||
"checks",
|
||||
"blockers"
|
||||
]) {
|
||||
assert.ok(Object.hasOwn(report, field), `${label} missing ${field}`);
|
||||
}
|
||||
|
||||
assertString(report.$schema, `${label}.$schema`);
|
||||
assertString(report.$id, `${label}.$id`);
|
||||
assert.equal(report.reportVersion, "v1", `${label}.reportVersion`);
|
||||
assert.equal(report.reportKind, "dev-gate-preflight", `${label}.reportKind`);
|
||||
assert.equal(report.issue, requiredPreflightIssue, `${label}.issue`);
|
||||
assert.equal(report.mode, "read-only", `${label}.mode`);
|
||||
assert.equal(report.devOnly, true, `${label}.devOnly`);
|
||||
assert.equal(report.prodDisabled, true, `${label}.prodDisabled`);
|
||||
assert.ok(preflightConclusions.has(report.conclusion), `${label}.conclusion must be ready or blocked`);
|
||||
assert.match(report.generatedAt, /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/, `${label}.generatedAt`);
|
||||
|
||||
assertStringArray(report.supports, `${label}.supports`, { minLength: requiredPreflightSupports.length });
|
||||
for (const supportedIssue of requiredPreflightSupports) {
|
||||
assert.ok(report.supports.includes(supportedIssue), `${label}.supports missing ${supportedIssue}`);
|
||||
}
|
||||
|
||||
assertObject(report.target, `${label}.target`);
|
||||
assert.equal(report.target.ref, "origin/main", `${label}.target.ref`);
|
||||
assert.match(report.target.commitId, /^[a-f0-9]{40}$/, `${label}.target.commitId`);
|
||||
assert.equal(report.target.shortCommitId, report.target.commitId.slice(0, 7), `${label}.target.shortCommitId`);
|
||||
|
||||
assertStringArray(report.forbiddenActions, `${label}.forbiddenActions`, { minLength: 1 });
|
||||
for (const forbiddenAction of ["prod-deploy", "secret-material-read", "unidesk-runtime-substitute", "force-push"]) {
|
||||
assert.ok(report.forbiddenActions.includes(forbiddenAction), `${label}.forbiddenActions missing ${forbiddenAction}`);
|
||||
}
|
||||
|
||||
assertStringArray(report.validationCommands, `${label}.validationCommands`, { minLength: 2 });
|
||||
for (const requiredCommand of requiredPreflightValidationCommands) {
|
||||
assert.ok(
|
||||
report.validationCommands.includes(requiredCommand),
|
||||
`${label}.validationCommands missing ${requiredCommand}`
|
||||
);
|
||||
}
|
||||
|
||||
assertArray(report.checks, `${label}.checks`);
|
||||
assert.ok(report.checks.length >= 1, `${label}.checks must not be empty`);
|
||||
assertUnique(report.checks.map((check) => check.id), `${label}.checks`);
|
||||
for (const [index, check] of report.checks.entries()) {
|
||||
const checkLabel = `${label}.checks[${index}]`;
|
||||
assertObject(check, checkLabel);
|
||||
for (const field of ["id", "category", "status", "summary", "evidence"]) {
|
||||
assert.ok(Object.hasOwn(check, field), `${checkLabel} missing ${field}`);
|
||||
}
|
||||
assert.match(check.id, /^[a-z][a-z0-9-]*$/, `${checkLabel}.id`);
|
||||
assertString(check.category, `${checkLabel}.category`);
|
||||
assertStatus(check.status, `${checkLabel}.status`);
|
||||
assertString(check.summary, `${checkLabel}.summary`);
|
||||
assertArray(check.evidence, `${checkLabel}.evidence`);
|
||||
}
|
||||
|
||||
assertArray(report.blockers, `${label}.blockers`);
|
||||
if (report.conclusion === "blocked") {
|
||||
assert.ok(report.blockers.length >= 1, `${label}.blockers required when conclusion is blocked`);
|
||||
}
|
||||
assertUnique(
|
||||
report.blockers.map((blocker) => `${blocker.type}::${blocker.scope}`),
|
||||
`${label}.blockers`
|
||||
);
|
||||
for (const [index, blocker] of report.blockers.entries()) {
|
||||
const blockerLabel = `${label}.blockers[${index}]`;
|
||||
assertObject(blocker, blockerLabel);
|
||||
for (const field of ["type", "scope", "status", "summary", "nextTask"]) {
|
||||
assert.ok(Object.hasOwn(blocker, field), `${blockerLabel} missing ${field}`);
|
||||
}
|
||||
assert.ok(blockerTypes.has(blocker.type), `${blockerLabel}.type must be a known blocker type`);
|
||||
assertString(blocker.scope, `${blockerLabel}.scope`);
|
||||
assert.ok(blockerStates.has(blocker.status), `${blockerLabel}.status must be open, acknowledged, or closed`);
|
||||
assertString(blocker.summary, `${blockerLabel}.summary`);
|
||||
assertString(blocker.nextTask, `${blockerLabel}.nextTask`);
|
||||
}
|
||||
|
||||
if (report.conclusion === "ready") {
|
||||
assert.equal(report.blockers.length, 0, `${label}.blockers must be empty when ready`);
|
||||
}
|
||||
if (Object.hasOwn(report, "notes")) {
|
||||
assertString(report.notes, `${label}.notes`);
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const reportFiles = await collectReportFiles();
|
||||
assert.ok(reportFiles.length >= 1, "expected at least one dev-gate report JSON file");
|
||||
|
||||
Reference in New Issue
Block a user