fix: add dev report lifecycle contract (#85)
Co-authored-by: root <root@lyon.remote>
This commit is contained in:
@@ -10,6 +10,26 @@ blockers stay in one place.
|
||||
This contract is report-only. It does not authorize a real DEV or PROD
|
||||
deployment.
|
||||
|
||||
## Report Lifecycle
|
||||
|
||||
Each committed DEV gate report must carry `reportLifecycle`:
|
||||
|
||||
- `state: "active"` means the report belongs to the current DEV gate surface.
|
||||
Its `activeEndpoint` must be `http://74.48.78.17:16667`, its
|
||||
`activeBrowserEndpoint` must be `http://74.48.78.17:16666`, and
|
||||
`deprecatedEndpoint` must be `null`.
|
||||
- `state: "historical"` means the report is retained only for evidence
|
||||
provenance. It may name a deprecated endpoint such as
|
||||
`http://74.48.78.17:6667`, but it must stay blocked/non-green and cannot be
|
||||
consumed as active acceptance evidence.
|
||||
- Active reports may preserve a legacy public endpoint only inside an explicit
|
||||
`runtimeSmoke.legacyPublicEndpoint` object with `status: "deprecated"` and
|
||||
`activeGreenEligible: false`.
|
||||
- M3 DEV-LIVE remains narrower than report freshness: it requires
|
||||
`res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1` in real DEV
|
||||
plus operation, audit, and evidence identifiers. SOURCE, LOCAL, DRY-RUN,
|
||||
fixture, and historical report evidence are support/diagnostics only.
|
||||
|
||||
## Files
|
||||
|
||||
- `reports/dev-gate/dev-gate-report.example.json`: machine-readable example.
|
||||
@@ -29,6 +49,7 @@ Every report must include:
|
||||
- `acceptanceLevel`
|
||||
- `devOnly`
|
||||
- `prodDisabled`
|
||||
- `reportLifecycle`
|
||||
- `sourceContract`
|
||||
- `validationCommands`
|
||||
- `localSmoke`
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
export const ACTIVE_DEV_PUBLIC_ENDPOINT = "http://74.48.78.17:16667";
|
||||
export const ACTIVE_DEV_BROWSER_ENDPOINT = "http://74.48.78.17:16666";
|
||||
export const DEPRECATED_DEV_PUBLIC_ENDPOINT = "http://74.48.78.17:6667";
|
||||
export const DEPRECATED_RESERVED_PROD_ENDPOINT = "http://74.48.78.17:6666";
|
||||
export const REPORT_LIFECYCLE_VERSION = "v1";
|
||||
|
||||
export const deprecatedPublicEndpoints = Object.freeze([
|
||||
DEPRECATED_DEV_PUBLIC_ENDPOINT,
|
||||
DEPRECATED_RESERVED_PROD_ENDPOINT
|
||||
]);
|
||||
|
||||
const deprecatedPublicEndpointPattern = /http:\/\/74\.48\.78\.17:666[67]\b/gu;
|
||||
|
||||
export function findDeprecatedPublicEndpoints(text) {
|
||||
return [...new Set(String(text).match(deprecatedPublicEndpointPattern) ?? [])].sort();
|
||||
}
|
||||
|
||||
export function isDeprecatedEndpointAllowedInActiveReport(report, endpoint) {
|
||||
const legacyEndpoint = report?.runtimeSmoke?.legacyPublicEndpoint;
|
||||
return Boolean(
|
||||
legacyEndpoint &&
|
||||
legacyEndpoint.endpoint === endpoint &&
|
||||
legacyEndpoint.status === "deprecated" &&
|
||||
legacyEndpoint.activeGreenEligible === false
|
||||
);
|
||||
}
|
||||
|
||||
export function findForbiddenActiveDeprecatedEndpoints(report, raw) {
|
||||
if (reportIsHistorical(report)) return [];
|
||||
return findDeprecatedPublicEndpoints(raw).filter(
|
||||
(endpoint) => !isDeprecatedEndpointAllowedInActiveReport(report, endpoint)
|
||||
);
|
||||
}
|
||||
|
||||
export function reportLifecycleState(report) {
|
||||
return report?.reportLifecycle?.state ?? "active";
|
||||
}
|
||||
|
||||
export function reportIsHistorical(report) {
|
||||
return reportLifecycleState(report) === "historical";
|
||||
}
|
||||
|
||||
export function activeReportLifecycle(summary) {
|
||||
return {
|
||||
version: REPORT_LIFECYCLE_VERSION,
|
||||
state: "active",
|
||||
activeEndpoint: ACTIVE_DEV_PUBLIC_ENDPOINT,
|
||||
activeBrowserEndpoint: ACTIVE_DEV_BROWSER_ENDPOINT,
|
||||
deprecatedEndpoint: null,
|
||||
summary
|
||||
};
|
||||
}
|
||||
|
||||
export function historicalReportLifecycle({ activeEndpoint = ACTIVE_DEV_PUBLIC_ENDPOINT, deprecatedEndpoint, summary }) {
|
||||
return {
|
||||
version: REPORT_LIFECYCLE_VERSION,
|
||||
state: "historical",
|
||||
activeEndpoint,
|
||||
activeBrowserEndpoint: ACTIVE_DEV_BROWSER_ENDPOINT,
|
||||
deprecatedEndpoint,
|
||||
summary
|
||||
};
|
||||
}
|
||||
@@ -1114,5 +1114,13 @@
|
||||
],
|
||||
"blockers": []
|
||||
},
|
||||
"notes": "DEV-only artifact report. Do not treat runtime placeholder images as real service implementations."
|
||||
"notes": "DEV-only artifact report. Do not treat runtime placeholder images as real service implementations.",
|
||||
"reportLifecycle": {
|
||||
"version": "v1",
|
||||
"state": "active",
|
||||
"activeEndpoint": "http://74.48.78.17:16667",
|
||||
"activeBrowserEndpoint": "http://74.48.78.17:16666",
|
||||
"deprecatedEndpoint": null,
|
||||
"summary": "Current DEV artifact publish report; it is not a DEV-LIVE runtime substitute."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1124,5 +1124,13 @@
|
||||
}
|
||||
},
|
||||
"blockers": [],
|
||||
"notes": "DEV-only result. No PROD resources, secret values, force push, heavy e2e, browser e2e, or UniDesk runtime substitute were used."
|
||||
"notes": "DEV-only result. No PROD resources, secret values, force push, heavy e2e, browser e2e, or UniDesk runtime substitute were used.",
|
||||
"reportLifecycle": {
|
||||
"version": "v1",
|
||||
"state": "active",
|
||||
"activeEndpoint": "http://74.48.78.17:16667",
|
||||
"activeBrowserEndpoint": "http://74.48.78.17:16666",
|
||||
"deprecatedEndpoint": null,
|
||||
"summary": "Current DEV deploy apply report; active public API/health endpoint is 16667 and browser endpoint is 16666."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -752,5 +752,13 @@
|
||||
],
|
||||
"nextTask": "Deploy cloud-api DB runtime readiness probe and/or repair DEV DB connectivity, then rerun the read-only health smoke without reading or printing the DB secret value."
|
||||
}
|
||||
},
|
||||
"reportLifecycle": {
|
||||
"version": "v1",
|
||||
"state": "active",
|
||||
"activeEndpoint": "http://74.48.78.17:16667",
|
||||
"activeBrowserEndpoint": "http://74.48.78.17:16666",
|
||||
"deprecatedEndpoint": null,
|
||||
"summary": "Current DEV edge-health report; deprecated public 6666/6667 endpoints are not valid active evidence."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,5 +69,13 @@
|
||||
"summary": "No live DEV precondition evidence is attached."
|
||||
}
|
||||
],
|
||||
"notes": "Contract-only example. Do not treat this file as a real DEV gate report."
|
||||
"notes": "Contract-only example. Do not treat this file as a real DEV gate report.",
|
||||
"reportLifecycle": {
|
||||
"version": "v1",
|
||||
"state": "active",
|
||||
"activeEndpoint": "http://74.48.78.17:16667",
|
||||
"activeBrowserEndpoint": "http://74.48.78.17:16666",
|
||||
"deprecatedEndpoint": null,
|
||||
"summary": "Active example report for the current DEV gate contract."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,5 +252,13 @@
|
||||
"summary": "Legacy public :6667 observations are retained only as historical blockers and are not accepted as active green evidence."
|
||||
}
|
||||
},
|
||||
"notes": "M2 active report uses read-only GET probes only; PROD, service restarts, deployments, and script-driven apply flows were not touched."
|
||||
"notes": "M2 active report uses read-only GET probes only; PROD, service restarts, deployments, and script-driven apply flows were not touched.",
|
||||
"reportLifecycle": {
|
||||
"version": "v1",
|
||||
"state": "active",
|
||||
"activeEndpoint": "http://74.48.78.17:16667",
|
||||
"activeBrowserEndpoint": "http://74.48.78.17:16666",
|
||||
"deprecatedEndpoint": null,
|
||||
"summary": "Current M2 DEV deploy smoke report; legacy public endpoints are deprecated and not active green evidence."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,5 +195,13 @@
|
||||
"classification": "hardware loop",
|
||||
"observedAt": "2026-05-22T04:30:50.820Z",
|
||||
"result": "Live DEV M3 runtime was reachable but did not satisfy the M3 hardware-loop contract."
|
||||
},
|
||||
"reportLifecycle": {
|
||||
"version": "v1",
|
||||
"state": "active",
|
||||
"activeEndpoint": "http://74.48.78.17:16667",
|
||||
"activeBrowserEndpoint": "http://74.48.78.17:16666",
|
||||
"deprecatedEndpoint": null,
|
||||
"summary": "Current M3 DEV hardware trusted-loop report; DEV-LIVE requires res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1 with operation, audit, and evidence ids."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,5 +75,13 @@
|
||||
],
|
||||
"summary": "Blocked at DB live readiness before scheduling a DEV agent task."
|
||||
},
|
||||
"notes": "DEV M4 agent loop live preflight report. No secret material is read."
|
||||
"notes": "DEV M4 agent loop live preflight report. No secret material is read.",
|
||||
"reportLifecycle": {
|
||||
"version": "v1",
|
||||
"state": "active",
|
||||
"activeEndpoint": "http://74.48.78.17:16667",
|
||||
"activeBrowserEndpoint": "http://74.48.78.17:16666",
|
||||
"deprecatedEndpoint": null,
|
||||
"summary": "Current DEV M4 agent-loop preflight report; it does not substitute for M3 DEV-LIVE acceptance."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1109,5 +1109,13 @@
|
||||
"node scripts/dev-evidence-blocker-aggregator.mjs --markdown",
|
||||
"node --check scripts/validate-dev-gate-report.mjs",
|
||||
"node scripts/validate-dev-gate-report.mjs"
|
||||
]
|
||||
],
|
||||
"reportLifecycle": {
|
||||
"version": "v1",
|
||||
"state": "active",
|
||||
"activeEndpoint": "http://74.48.78.17:16667",
|
||||
"activeBrowserEndpoint": "http://74.48.78.17:16666",
|
||||
"deprecatedEndpoint": null,
|
||||
"summary": "Current M5 evidence aggregator report; source, local, and dry-run evidence are not DEV-LIVE acceptance."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,5 +256,13 @@
|
||||
"status": "not_observed",
|
||||
"evidence": "No additional blocker class observed in the short DEV-only retest"
|
||||
}
|
||||
]
|
||||
],
|
||||
"reportLifecycle": {
|
||||
"version": "v1",
|
||||
"state": "active",
|
||||
"activeEndpoint": "http://74.48.78.17:16667",
|
||||
"activeBrowserEndpoint": "http://74.48.78.17:16666",
|
||||
"deprecatedEndpoint": null,
|
||||
"summary": "Current MVP gate report; M3 DEV-LIVE remains blocked until the patch-panel trusted loop is proven."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1553,5 +1553,13 @@
|
||||
"nextTask": "Deploy cloud-api DB runtime readiness probe and/or repair DEV DB connectivity, then rerun the read-only health smoke without reading or printing the DB secret value."
|
||||
}
|
||||
],
|
||||
"notes": "No PROD action, secret read, UniDesk runtime substitution, heavy e2e, browser e2e, runtime restart, or force push was performed."
|
||||
"notes": "No PROD action, secret read, UniDesk runtime substitution, heavy e2e, browser e2e, runtime restart, or force push was performed.",
|
||||
"reportLifecycle": {
|
||||
"version": "v1",
|
||||
"state": "active",
|
||||
"activeEndpoint": "http://74.48.78.17:16667",
|
||||
"activeBrowserEndpoint": "http://74.48.78.17:16666",
|
||||
"deprecatedEndpoint": null,
|
||||
"summary": "Current read-only DEV preflight report; source, local, and dry-run checks are not DEV-LIVE acceptance."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"$schema": "https://hwlab.pikastech.local/schemas/dev-m3-hardware-loop-report.schema.json",
|
||||
"$id": "https://hwlab.pikastech.local/reports/dev-gate/historical/dev-m3-hardware-loop-legacy-6667.json",
|
||||
"reportVersion": "v1",
|
||||
"issue": "pikasTech/HWLAB#38",
|
||||
"taskId": "dev-m3-hardware-loop",
|
||||
"commitId": "historical",
|
||||
"acceptanceLevel": "dev_m3_hardware_loop",
|
||||
"devOnly": true,
|
||||
"prodDisabled": true,
|
||||
"reportLifecycle": {
|
||||
"version": "v1",
|
||||
"state": "historical",
|
||||
"activeEndpoint": "http://74.48.78.17:16667",
|
||||
"activeBrowserEndpoint": "http://74.48.78.17:16666",
|
||||
"deprecatedEndpoint": "http://74.48.78.17:6667",
|
||||
"summary": "Historical deprecated endpoint snapshot retained only for evidence provenance; not active DEV-LIVE evidence."
|
||||
},
|
||||
"status": "blocked",
|
||||
"generatedAt": "2026-05-22T00:00:00.000Z",
|
||||
"runtimeTarget": {
|
||||
"environment": "dev",
|
||||
"namespace": "hwlab-dev",
|
||||
"endpoint": "http://74.48.78.17:6667",
|
||||
"requiredBoxSimulators": 2,
|
||||
"requiredGatewaySimulators": 2,
|
||||
"requiredPatchPanels": 1
|
||||
},
|
||||
"blockers": [
|
||||
{
|
||||
"type": "observability_blocker",
|
||||
"scope": "deprecated-public-endpoint",
|
||||
"status": "open",
|
||||
"summary": "Legacy public :6667 evidence is historical/deprecated and cannot support current M3 DEV-LIVE acceptance."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { activeReportLifecycle } from "../internal/dev-report-lifecycle.mjs";
|
||||
import { ENVIRONMENT_DEV, SERVICE_IDS } from "../internal/protocol/index.mjs";
|
||||
import {
|
||||
BUILDABLE_IMPLEMENTATION_STATES,
|
||||
@@ -824,6 +825,7 @@ function createReport({ args, repo, commitId, shortCommit, mode, services, artif
|
||||
acceptanceLevel: "dev_artifact_publish",
|
||||
devOnly: true,
|
||||
prodDisabled: true,
|
||||
reportLifecycle: activeReportLifecycle("Current DEV artifact publish report; it is not a DEV-LIVE runtime substitute."),
|
||||
sourceContract: {
|
||||
status: fatalBlocked ? "blocked" : "pass",
|
||||
documents: [
|
||||
|
||||
@@ -8,6 +8,7 @@ import { request as httpsRequest } from "node:https";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { activeReportLifecycle } from "../internal/dev-report-lifecycle.mjs";
|
||||
import { DEV_ENDPOINT, ENVIRONMENT_DEV } from "../internal/protocol/index.mjs";
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
@@ -566,6 +567,7 @@ function baseReport({ commitId, observedAt }) {
|
||||
acceptanceLevel: "dev_m3_hardware_loop",
|
||||
devOnly: true,
|
||||
prodDisabled: true,
|
||||
reportLifecycle: activeReportLifecycle("Current M3 DEV hardware trusted-loop report; DEV-LIVE requires res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1 with operation, audit, and evidence ids."),
|
||||
sourceContract: {
|
||||
status: "pass",
|
||||
documents: [
|
||||
|
||||
@@ -7,6 +7,7 @@ import { readFile, writeFile, mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { activeReportLifecycle } from "../internal/dev-report-lifecycle.mjs";
|
||||
import { DEV_ENDPOINT, ENVIRONMENT_DEV } from "../internal/protocol/index.mjs";
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
@@ -151,6 +152,7 @@ function buildReport(fixture, evidence, result) {
|
||||
acceptanceLevel: "dev_m4_agent_loop",
|
||||
devOnly: true,
|
||||
prodDisabled: true,
|
||||
reportLifecycle: activeReportLifecycle("Current DEV M4 agent-loop preflight report; it does not substitute for M3 DEV-LIVE acceptance."),
|
||||
sourceContract,
|
||||
validationCommands,
|
||||
localSmoke: {
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
DEV_DB_ENV_CONTRACT,
|
||||
summarizeDbContract
|
||||
} from "../../internal/cloud/db-contract.mjs";
|
||||
import { activeReportLifecycle } from "../../internal/dev-report-lifecycle.mjs";
|
||||
import { DEV_ENDPOINT, ENVIRONMENT_DEV, SERVICE_IDS } from "../../internal/protocol/index.mjs";
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
@@ -970,6 +971,7 @@ export async function runDevDeployApply(argv, io = {}) {
|
||||
acceptanceLevel: "dev_deploy_apply",
|
||||
devOnly: true,
|
||||
prodDisabled: true,
|
||||
reportLifecycle: activeReportLifecycle("Current DEV deploy apply report; active public API/health endpoint is 16667 and browser endpoint is 16666."),
|
||||
status,
|
||||
generatedAt: new Date().toISOString(),
|
||||
namespace,
|
||||
|
||||
@@ -6,6 +6,7 @@ import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { DEV_DB_ENV_CONTRACT, summarizeDbContract } from "../../internal/cloud/db-contract.mjs";
|
||||
import { activeReportLifecycle } from "../../internal/dev-report-lifecycle.mjs";
|
||||
import { DEV_ENDPOINT, ENVIRONMENT_DEV } from "../../internal/protocol/index.mjs";
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
@@ -118,6 +119,7 @@ async function createDevGateReport(edgeHealth) {
|
||||
acceptanceLevel: "dev_edge_health",
|
||||
devOnly: true,
|
||||
prodDisabled: true,
|
||||
reportLifecycle: activeReportLifecycle("Current DEV edge-health report; deprecated public 6666/6667 endpoints are not valid active evidence."),
|
||||
status,
|
||||
generatedAt: edgeHealth.generatedAt,
|
||||
namespace,
|
||||
|
||||
@@ -4,6 +4,7 @@ import { readFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { activeReportLifecycle, reportIsHistorical, reportLifecycleState } from "../../internal/dev-report-lifecycle.mjs";
|
||||
import { DEV_ENDPOINT, ENVIRONMENT_DEV, SERVICE_IDS } from "../../internal/protocol/index.mjs";
|
||||
|
||||
export const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
@@ -33,6 +34,8 @@ const blockerTypes = new Set([
|
||||
"observability_blocker",
|
||||
"safety_blocker"
|
||||
]);
|
||||
const historicalLifecycleSummary =
|
||||
"Historical report retained for evidence provenance; it is not active green DEV evidence.";
|
||||
|
||||
const reportPaths = Object.freeze({
|
||||
devPreflight: "reports/dev-gate/dev-preflight-report.json",
|
||||
@@ -275,6 +278,7 @@ function sourceEntry(entry) {
|
||||
level: entry.level,
|
||||
status: entry.status,
|
||||
category: entry.category,
|
||||
lifecycleState: entry.lifecycleState ?? "active",
|
||||
sources: entry.sources,
|
||||
commands: entry.commands,
|
||||
summary: entry.summary
|
||||
@@ -282,18 +286,21 @@ function sourceEntry(entry) {
|
||||
}
|
||||
|
||||
function reportEvidence({ milestone, level, report, status, category, summary, evidence = [], commands = [] }) {
|
||||
const lifecycleState = reportLifecycleState(report);
|
||||
const historical = lifecycleState === "historical";
|
||||
return {
|
||||
milestone,
|
||||
issue: report.issue,
|
||||
taskId: report.taskId ?? report.reportKind,
|
||||
reportPath: report.path,
|
||||
commitId: short(report.commitId ?? report.target?.commitId),
|
||||
level,
|
||||
status,
|
||||
lifecycleState,
|
||||
level: historical ? "BLOCKED" : level,
|
||||
status: historical && status === "pass" ? "blocked" : status,
|
||||
category,
|
||||
commands,
|
||||
evidence,
|
||||
summary
|
||||
summary: historical ? `${summary} ${historicalLifecycleSummary}` : summary
|
||||
};
|
||||
}
|
||||
|
||||
@@ -485,6 +492,19 @@ function collectM5Evidence(reports) {
|
||||
function collectBlockers(reports) {
|
||||
const blockers = [];
|
||||
for (const report of Object.values(reports)) {
|
||||
if (reportIsHistorical(report)) {
|
||||
blockers.push({
|
||||
id: `${report.taskId ?? report.reportKind ?? "report"}-historical:${report.path}`,
|
||||
priority: "P2",
|
||||
type: "observability_blocker",
|
||||
scope: `${report.taskId ?? report.reportKind ?? "report"}-historical`,
|
||||
status: "open",
|
||||
source: report.path,
|
||||
sourceIssue: report.issue,
|
||||
summary: `${report.taskId ?? report.reportKind ?? "Report"} is historical/deprecated and must be refreshed before it can support active DEV evidence. ${historicalLifecycleSummary}`,
|
||||
nextTask: "Regenerate the report as active DEV evidence or keep the historical snapshot in an archival path."
|
||||
});
|
||||
}
|
||||
for (const blocker of report.blockers ?? []) {
|
||||
if (isStaleLegacyIngressBlocker(blocker, report, reports)) {
|
||||
continue;
|
||||
@@ -593,6 +613,7 @@ function groupByLevel(evidence, blockers) {
|
||||
milestone: item.milestone,
|
||||
issue: item.issue,
|
||||
taskId: item.taskId,
|
||||
lifecycleState: item.lifecycleState,
|
||||
status: item.status,
|
||||
category: item.category,
|
||||
reportPath: item.reportPath,
|
||||
@@ -780,6 +801,9 @@ export async function buildReport() {
|
||||
endpoint: DEV_ENDPOINT,
|
||||
devOnly: true,
|
||||
prodDisabled: true,
|
||||
reportLifecycle: activeReportLifecycle(
|
||||
"Current M5 evidence aggregator report; source, local, and dry-run evidence are not DEV-LIVE acceptance."
|
||||
),
|
||||
safety: {
|
||||
reportOnly: true,
|
||||
noDeploy: true,
|
||||
@@ -797,6 +821,7 @@ export async function buildReport() {
|
||||
path: report.path,
|
||||
issue: report.issue,
|
||||
taskId: report.taskId ?? report.reportKind,
|
||||
lifecycleState: reportLifecycleState(report),
|
||||
status: primaryStatus(report),
|
||||
commitId: short(report.commitId ?? report.target?.commitId)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
buildDbHealthContract,
|
||||
summarizeDbContract
|
||||
} from "../../internal/cloud/db-contract.mjs";
|
||||
import { activeReportLifecycle } from "../../internal/dev-report-lifecycle.mjs";
|
||||
import { DEV_ENDPOINT, ENVIRONMENT_DEV, SERVICE_IDS } from "../../internal/protocol/index.mjs";
|
||||
import { probeRegistryCapabilities } from "./registry-capabilities.mjs";
|
||||
|
||||
@@ -1095,6 +1096,7 @@ function makeReport(args, targetCommit, targetShortCommit, reporter, artifactIde
|
||||
mode: "read-only",
|
||||
devOnly: true,
|
||||
prodDisabled: true,
|
||||
reportLifecycle: activeReportLifecycle("Current read-only DEV preflight report; source, local, and dry-run checks are not DEV-LIVE acceptance."),
|
||||
forbiddenActions,
|
||||
validationCommands: [
|
||||
"node --check scripts/dev-gate-preflight.mjs",
|
||||
|
||||
@@ -5,6 +5,16 @@ import { constants as fsConstants } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import {
|
||||
ACTIVE_DEV_BROWSER_ENDPOINT,
|
||||
ACTIVE_DEV_PUBLIC_ENDPOINT,
|
||||
REPORT_LIFECYCLE_VERSION,
|
||||
deprecatedPublicEndpoints,
|
||||
findDeprecatedPublicEndpoints,
|
||||
findForbiddenActiveDeprecatedEndpoints,
|
||||
reportIsHistorical
|
||||
} from "../internal/dev-report-lifecycle.mjs";
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const reportsDir = path.join(repoRoot, "reports/dev-gate");
|
||||
|
||||
@@ -330,24 +340,101 @@ function assertUnique(values, label) {
|
||||
assert.equal(new Set(values).size, values.length, `${label} must be unique`);
|
||||
}
|
||||
|
||||
async function readJsonFile(relativePath) {
|
||||
function assertReportLifecycle(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 use deprecated public endpoints as active evidence`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
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`
|
||||
);
|
||||
const deprecatedMatches = findDeprecatedPublicEndpoints(raw);
|
||||
assert.ok(
|
||||
deprecatedMatches.length === 0 || deprecatedMatches.includes(lifecycle.deprecatedEndpoint),
|
||||
`${label}.reportLifecycle.deprecatedEndpoint must cover deprecated public endpoint evidence`
|
||||
);
|
||||
}
|
||||
|
||||
function primaryReportStatus(report) {
|
||||
return report.status ??
|
||||
report.gateStatus ??
|
||||
report.conclusion ??
|
||||
report.artifactPublish?.status ??
|
||||
report.summary?.status ??
|
||||
(Array.isArray(report.blockers) && report.blockers.length > 0 ? "blocked" : "not_run");
|
||||
}
|
||||
|
||||
function validateHistoricalReport(report, label) {
|
||||
assert.equal(report.devOnly, true, `${label}.devOnly`);
|
||||
assert.equal(report.prodDisabled, true, `${label}.prodDisabled`);
|
||||
assertString(report.issue, `${label}.issue`);
|
||||
assert.ok(allowedIssues.has(report.issue), `${label}.issue must be a known HWLAB DEV gate issue`);
|
||||
assertString(report.taskId ?? report.reportKind, `${label}.taskId`);
|
||||
assert.ok(
|
||||
["blocked", "not_run", "not_applicable", "failed", "degraded"].includes(primaryReportStatus(report)),
|
||||
`${label} historical report must not present active green status`
|
||||
);
|
||||
assert.ok(
|
||||
Array.isArray(report.blockers) && report.blockers.length >= 1,
|
||||
`${label}.blockers must explain why the report is historical`
|
||||
);
|
||||
assertBlockers(report.blockers, `${label}.blockers`);
|
||||
}
|
||||
|
||||
async function readJsonReport(relativePath) {
|
||||
const raw = await readFile(path.join(repoRoot, relativePath), "utf8");
|
||||
return JSON.parse(raw);
|
||||
return { report: JSON.parse(raw), raw };
|
||||
}
|
||||
|
||||
async function collectReportFiles() {
|
||||
if (process.argv.length > 2) {
|
||||
return process.argv.slice(2).map((entry) => path.relative(repoRoot, path.resolve(process.cwd(), entry)));
|
||||
}
|
||||
const entries = await readdir(reportsDir, { withFileTypes: true });
|
||||
return entries
|
||||
.filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
|
||||
.map((entry) => path.join("reports/dev-gate", entry.name))
|
||||
.sort();
|
||||
return collectReportJsonFiles(reportsDir, "reports/dev-gate");
|
||||
}
|
||||
|
||||
async function collectReportJsonFiles(directory, relativeDirectory) {
|
||||
const entries = await readdir(directory, { withFileTypes: true });
|
||||
const nested = await Promise.all(entries.map(async (entry) => {
|
||||
const relativePath = path.join(relativeDirectory, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
return collectReportJsonFiles(path.join(directory, entry.name), relativePath);
|
||||
}
|
||||
return entry.isFile() && entry.name.endsWith(".json") ? [relativePath] : [];
|
||||
}));
|
||||
return nested.flat().sort();
|
||||
}
|
||||
|
||||
async function validateReport(relativePath) {
|
||||
const report = await readJsonFile(relativePath);
|
||||
const { report, raw } = await readJsonReport(relativePath);
|
||||
const label = relativePath;
|
||||
assertObject(report, label);
|
||||
assertReportLifecycle(report, raw, label);
|
||||
if (reportIsHistorical(report)) {
|
||||
validateHistoricalReport(report, label);
|
||||
return;
|
||||
}
|
||||
|
||||
if (report.reportKind === "dev-gate-preflight") {
|
||||
await validatePreflightReport(relativePath, report);
|
||||
return;
|
||||
@@ -357,8 +444,6 @@ async function validateReport(relativePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
const label = relativePath;
|
||||
assertObject(report, label);
|
||||
if (report.issue === requiredDevM3Issue || report.taskId === "dev-m3-hardware-loop") {
|
||||
await validateDevM3Report(report, label);
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user