fix: rebaseline M2 DEV smoke endpoints (#77)

* feat: add hwlab dev console homepage

* fix: rebaseline M2 DEV smoke endpoints

---------

Co-authored-by: HWLAB Code Queue <code-queue@pikastech.local>
This commit is contained in:
Lyon
2026-05-22 13:56:16 +08:00
committed by GitHub
parent 82ad96c5c6
commit c1c9e72538
7 changed files with 773 additions and 10 deletions
+22 -5
View File
@@ -1,4 +1,4 @@
# M2 DEV Deployment Smoke Dry-Run
# M2 DEV Deployment Smoke
`m2-dev-deploy-smoke` is a local dry-run smoke harness for the HWLAB DEV
deployment shape. It validates the frozen route phases, service identities,
@@ -32,18 +32,24 @@ node scripts/m2-dev-deploy-smoke.mjs --dry-run
## Live Gate
The live gate is intentionally strict. It is only for a future real DEV smoke
observation, not for deployment, and this harness stays fixture-only by
default.
observation, not for deployment, and this harness stays fixture-only unless the
operator supplies the live flags. The public DEV endpoint split is:
- browser/frontend: `http://74.48.78.17:16666/`
- API/edge health: `http://74.48.78.17:16667/health` and
`http://74.48.78.17:16667/health/live`
- legacy public `:6667`: deprecated and never accepted as active green evidence
Required flags:
```sh
node scripts/m2-dev-deploy-smoke.mjs --live --confirm-dev --confirmed-non-production
node scripts/m2-dev-deploy-smoke.mjs --live --confirm-dev --confirmed-non-production --write-report
```
Before using `--live`, confirm all of the following:
- the target remains `http://74.48.78.17:16667`;
- the API/edge target remains `http://74.48.78.17:16667`;
- the frontend target remains `http://74.48.78.17:16666`;
- the workspace is not a PROD or shared deployment context;
- no prohibited action is being attempted;
- the request is observational only and does not mutate runtime state.
@@ -57,6 +63,17 @@ The script rejects live execution if any of these are missing:
- `--read-secret`
- `--force-push`
When `--write-report` is provided, the script writes
`reports/dev-gate/dev-m2-deploy-smoke-active.json`. The report contains only
read-only GET evidence and marks legacy `:6667` observations as ineligible for
active green.
Validate the written report with:
```sh
node scripts/validate-m2-deploy-smoke-active.mjs
```
## Boundary
This harness does not:
@@ -3,6 +3,12 @@
"issue": "pikasTech/HWLAB#23",
"environment": "dev",
"endpoint": "http://74.48.78.17:16667",
"frontendEndpoint": "http://74.48.78.17:16666",
"legacyPublicEndpoint": {
"endpoint": "http://74.48.78.17:6667",
"status": "deprecated",
"activeGreenEligible": false
},
"defaultMode": "dry-run",
"safetyGates": {
"dryRunOnly": true,
+1
View File
@@ -1,5 +1,6 @@
export const ENVIRONMENT_DEV = "dev";
export const DEV_ENDPOINT = "http://74.48.78.17:16667";
export const DEV_FRONTEND_ENDPOINT = "http://74.48.78.17:16666";
export const JSON_RPC_VERSION = "2.0";
export const SERVICE_IDS = Object.freeze([
@@ -0,0 +1,256 @@
{
"$schema": "https://hwlab.pikastech.local/schemas/dev-gate-report.schema.json",
"$id": "https://hwlab.pikastech.local/reports/dev-gate/dev-m2-deploy-smoke-active.json",
"reportVersion": "v1",
"issue": "pikasTech/HWLAB#23",
"taskId": "m2-dev-deploy-smoke",
"commitId": "54f927b02bc1",
"acceptanceLevel": "dev_m2_deploy_smoke",
"devOnly": true,
"prodDisabled": true,
"status": "pass",
"generatedAt": "2026-05-22T04:53:00.375Z",
"endpoint": "http://74.48.78.17:16667",
"frontendEndpoint": "http://74.48.78.17:16666",
"sourceContract": {
"status": "pass",
"documents": [
"docs/m2-dev-deploy-smoke.md",
"docs/dev-acceptance-matrix.md",
"fixtures/dev-deploy-smoke/dev-deploy-smoke.json"
],
"summary": "M2 DEV deploy smoke is rebaselined to frontend :16666 and API/edge :16667."
},
"validationCommands": [
"node --check scripts/m2-dev-deploy-smoke.mjs",
"node scripts/m2-dev-deploy-smoke.mjs --dry-run",
"node scripts/m2-dev-deploy-smoke.mjs --live --confirm-dev --confirmed-non-production --write-report",
"node --check scripts/validate-dev-gate-report.mjs",
"node scripts/validate-dev-gate-report.mjs"
],
"localSmoke": {
"status": "pass",
"commands": [
"node scripts/m2-dev-deploy-smoke.mjs --dry-run"
],
"evidence": [
"[m2-smoke] validated 11 service contracts",
"[m2-smoke] mode=dry-run endpoint=http://74.48.78.17:16667",
"[m2-smoke] no real DEV/PROD request was made"
],
"summary": "The fixture smoke validates the M2 route and artifact contract without contacting DEV."
},
"dryRun": {
"status": "pass",
"commands": [
"node scripts/m2-dev-deploy-smoke.mjs --dry-run"
],
"evidence": [
"fixture endpoint=http://74.48.78.17:16667",
"fixture frontendEndpoint=http://74.48.78.17:16666",
"legacy :6667 public evidence is marked ineligible for active green"
],
"summary": "Dry-run evidence is contract-only and pinned to the current DEV public endpoints."
},
"devPreconditions": {
"status": "pass",
"requirements": [
"GET http://74.48.78.17:16667/health returns HWLAB DEV JSON",
"GET http://74.48.78.17:16667/health/live returns HWLAB DEV JSON",
"GET http://74.48.78.17:16666/ returns the HWLAB DEV frontend",
"No PROD, secret read, runtime restart, deployment, or heavyweight e2e action is performed"
],
"commands": [
"curl -fsS --max-time 10 http://74.48.78.17:16667/health",
"curl -fsS --max-time 10 http://74.48.78.17:16667/health/live",
"curl -fsS --max-time 10 http://74.48.78.17:16666/"
],
"evidence": [
"api-health: HTTP 200 accepted=yes identity=hwlab-edge-proxy url=http://74.48.78.17:16667/health",
"api-live: HTTP 200 accepted=yes identity=hwlab-cloud-api url=http://74.48.78.17:16667/health/live",
"frontend-root: HTTP 200 accepted=yes identity=HWLAB DEV MVP Gate url=http://74.48.78.17:16666/"
],
"summary": "Read-only DEV public endpoint probes passed on :16666/:16667."
},
"blockers": [],
"runtimeSmoke": {
"mode": "live-read-only",
"status": "pass",
"generatedAt": "2026-05-22T04:53:00.375Z",
"apiEndpoint": "http://74.48.78.17:16667",
"frontendEndpoint": "http://74.48.78.17:16666",
"safety": {
"environment": "dev",
"prodTouched": false,
"secretsRead": false,
"restarts": false,
"deployAttempted": false,
"heavyE2E": false,
"unideskRuntimeSubstitute": false
},
"probes": [
{
"id": "api-health",
"url": "http://74.48.78.17:16667/health",
"ok": true,
"status": 200,
"statusText": "OK",
"durationMs": 939,
"contentType": "application/json; charset=utf-8",
"headers": {
"content-type": "application/json; charset=utf-8",
"content-length": "661",
"date": "Fri, 22 May 2026 04:53:00 GMT"
},
"json": {
"serviceId": "hwlab-edge-proxy",
"environment": "dev",
"status": "ok",
"service": {
"id": "hwlab-edge-proxy",
"role": "public-dev-ingress",
"healthPath": "/health",
"livePath": "/health/live"
},
"commit": {
"id": "cb35ada6860653b27269b0a57991184118cbf4b1",
"source": "runtime-env"
},
"image": {
"reference": "ghcr.io/pikastech/hwlab-edge-proxy:cb35ada",
"tag": "cb35ada",
"digest": "unknown"
},
"endpoint": "http://74.48.78.17:16667",
"observedAt": "2026-05-22T04:53:00.143Z",
"details": {
"upstream": "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667",
"mode": "dev-edge-proxy"
}
},
"title": null,
"bodyPreview": "{\n \"serviceId\": \"hwlab-edge-proxy\",\n \"environment\": \"dev\",\n \"status\": \"ok\",\n \"service\": {\n \"id\": \"hwlab-edge-proxy\",\n \"role\": \"public-dev-ingress\",\n \"healthPath\": \"/health\",\n \"livePath\": \"/health/live\"\n },\n \"commit\": {\n \"id\": \"cb35ada6860653b27269b0a57991184118cbf4b1\",\n \"source\": \"runtime-env\"\n },\n \"image\": {\n \"reference\": \"ghcr.io/pikastech/hwlab-edge-proxy:cb35ada\",\n \"tag\": \"cb35ada\",\n \"digest\": \"unknown\"\n },\n \"endpoint\": \"http://74.48.78.17:16667\",\n \"observedAt\": \"20"
},
{
"id": "api-live",
"url": "http://74.48.78.17:16667/health/live",
"ok": true,
"status": 200,
"statusText": "OK",
"durationMs": 936,
"contentType": "application/json; charset=utf-8",
"headers": {
"content-type": "application/json; charset=utf-8",
"cache-control": "no-store",
"date": "Fri, 22 May 2026 04:53:00 GMT"
},
"json": {
"serviceId": "hwlab-cloud-api",
"environment": "dev",
"status": "degraded",
"service": {
"id": "hwlab-cloud-api",
"role": "cloud-api",
"healthPath": "/health",
"livePath": "/health/live"
},
"commit": {
"id": "cb35ada",
"source": "runtime-env"
},
"image": {
"reference": "127.0.0.1:5000/hwlab/hwlab-cloud-api:cb35ada",
"tag": "cb35ada",
"digest": "unknown"
},
"endpoint": "http://74.48.78.17:16667",
"observedAt": "2026-05-22T04:53:00.145Z",
"db": {
"contractVersion": "v1",
"environment": "dev",
"connected": false,
"connectionChecked": false,
"configReady": true,
"ready": false,
"status": "degraded",
"mode": "configured_without_live_connection",
"fields": [
{
"name": "HWLAB_CLOUD_DB_URL",
"present": true,
"redacted": true,
"source": "k8s-secret-ref",
"required": true
},
{
"name": "HWLAB_CLOUD_DB_SSL_MODE",
"present": true,
"redacted": false,
"source": "runtime-env",
"required": true
}
],
"missingEnv": [],
"secretRefs": [
{
"env": "HWLAB_CLOUD_DB_URL",
"secretName": "hwlab-cloud-api-dev-db",
"secretKey": "database-url",
"present": true,
"redacted": true
}
],
"safety": {
"devOnly": true,
"prodAllowed": false,
"secretsRead": false,
"valuesRedacted": true,
"liveDbEvidence": false
},
"evidence": "env_presence_only_no_live_db"
},
"runtime": {
"adapter": "memory",
"durable": false,
"status": "degraded",
"reason": "live DB persistence is not connected; L1 runtime writes are process-local only",
"counts": {
"gatewaySessions": 0,
"boxResources": 0,
"boxCapabilities": 0,
"hardwareOperations": 0,
"auditEvents": 0,
"evidenceRecords": 0
}
}
},
"title": null,
"bodyPreview": "{\"serviceId\":\"hwlab-cloud-api\",\"environment\":\"dev\",\"status\":\"degraded\",\"service\":{\"id\":\"hwlab-cloud-api\",\"role\":\"cloud-api\",\"healthPath\":\"/health\",\"livePath\":\"/health/live\"},\"commit\":{\"id\":\"cb35ada\",\"source\":\"runtime-env\"},\"image\":{\"reference\":\"127.0.0.1:5000/hwlab/hwlab-cloud-api:cb35ada\",\"tag\":\"cb35ada\",\"digest\":\"unknown\"},\"endpoint\":\"http://74.48.78.17:16667\",\"observedAt\":\"2026-05-22T04:53:00.145Z\",\"db\":{\"contractVersion\":\"v1\",\"environment\":\"dev\",\"connected\":false,\"connectionChecked\":false,\"configReady\":"
},
{
"id": "frontend-root",
"url": "http://74.48.78.17:16666/",
"ok": true,
"status": 200,
"statusText": "OK",
"durationMs": 923,
"contentType": "text/html; charset=utf-8",
"headers": {
"content-type": "text/html; charset=utf-8",
"content-length": "4230",
"date": "Fri, 22 May 2026 04:53:00 GMT"
},
"json": null,
"title": "HWLAB DEV MVP Gate",
"bodyPreview": "<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>HWLAB DEV MVP Gate</title>\n <link rel=\"stylesheet\" href=\"./styles.css\" />\n <script type=\"module\" src=\"./app.mjs\"></script>\n </head>\n <body>\n <main class=\"shell\">\n <header class=\"topbar\">\n <div>\n <p class=\"eyebrow\">HWLAB L6 Frontend / CLI MVP</p>\n <h1>DEV MVP Gate</h1>\n </div>\n <div class=\"status\" id"
}
],
"legacyPublicEndpoint": {
"endpoint": "http://74.48.78.17:6667",
"status": "deprecated",
"activeGreenEligible": false,
"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."
}
+273 -5
View File
@@ -1,17 +1,23 @@
#!/usr/bin/env node
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { execFile } from "node:child_process";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import path from "node:path";
import { promisify } from "node:util";
import { fileURLToPath } from "node:url";
import {
DEV_ENDPOINT,
DEV_FRONTEND_ENDPOINT,
ENVIRONMENT_DEV,
SERVICE_IDS
} from "../internal/protocol/index.mjs";
const execFileAsync = promisify(execFile);
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const fixturePath = path.join(repoRoot, "fixtures/dev-deploy-smoke/dev-deploy-smoke.json");
const defaultActiveReportPath = path.join(repoRoot, "reports/dev-gate/dev-m2-deploy-smoke-active.json");
const legacyPublicEndpoint = "http://74.48.78.17:6667";
const timestampPattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
const digestPattern = /^(sha256:[a-f0-9]{64}|not_applicable)$/;
const tagPattern = /^m2-dev-deploy-smoke-[a-f0-9]{7}$/;
@@ -25,16 +31,30 @@ const allowedFailureClassifications = new Set([
function parseArgs(argv) {
const flags = new Set();
const options = {
reportPath: defaultActiveReportPath,
timeoutMs: 5000
};
const rest = [];
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (arg.startsWith("--")) {
if (arg === "--report") {
flags.add(arg);
index += 1;
assert.ok(argv[index], "--report requires a path");
options.reportPath = path.resolve(process.cwd(), argv[index]);
} else if (arg === "--timeout-ms") {
flags.add(arg);
index += 1;
options.timeoutMs = Number.parseInt(argv[index], 10);
assert.ok(Number.isInteger(options.timeoutMs) && options.timeoutMs > 0, "--timeout-ms must be positive");
} else if (arg.startsWith("--")) {
flags.add(arg);
} else {
rest.push(arg);
}
}
return { flags, rest };
return { flags, options, rest };
}
function loadFixture() {
@@ -129,6 +149,9 @@ function assertFixture(fixture) {
assert.equal(fixture.issue, "pikasTech/HWLAB#23");
assert.equal(fixture.environment, ENVIRONMENT_DEV);
assert.equal(fixture.endpoint, DEV_ENDPOINT);
assert.equal(fixture.frontendEndpoint, DEV_FRONTEND_ENDPOINT);
assert.equal(fixture.legacyPublicEndpoint?.endpoint, legacyPublicEndpoint);
assert.equal(fixture.legacyPublicEndpoint?.activeGreenEligible, false);
assert.equal(fixture.defaultMode, "dry-run");
assert.equal(fixture.safetyGates.dryRunOnly, true, "dry-run must be the default");
assert.ok(Array.isArray(fixture.safetyGates.realRequestsRequire), "real request gates must exist");
@@ -168,8 +191,240 @@ function assertFixture(fixture) {
assertTimestamp(observations.healthTimestamp, "observations.healthTimestamp");
}
function redactedHeaders(headers) {
return Object.fromEntries(
["content-type", "content-length", "cache-control", "date"].flatMap((name) => {
const value = headers.get(name);
return value ? [[name, value]] : [];
})
);
}
async function probeHttp({ id, url, expect }) {
const startedAt = Date.now();
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), expect.timeoutMs);
try {
const response = await fetch(url, { signal: controller.signal });
const body = await response.text();
const contentType = response.headers.get("content-type") ?? "";
let json = null;
if (contentType.includes("application/json")) {
try {
json = JSON.parse(body);
} catch {
json = null;
}
}
return {
id,
url,
ok: response.ok,
status: response.status,
statusText: response.statusText,
durationMs: Date.now() - startedAt,
contentType,
headers: redactedHeaders(response.headers),
json,
title: body.match(/<title>([^<]+)<\/title>/iu)?.[1] ?? null,
bodyPreview: body.slice(0, 512)
};
} catch (error) {
return {
id,
url,
ok: false,
status: null,
statusText: null,
durationMs: Date.now() - startedAt,
contentType: null,
headers: {},
json: null,
title: null,
bodyPreview: "",
error: error?.name === "AbortError" ? `timeout after ${expect.timeoutMs}ms` : String(error?.message ?? error)
};
} finally {
clearTimeout(timeout);
}
}
function acceptedProbe(probe) {
if (!probe.ok) return false;
if (probe.id === "frontend-root") {
return probe.contentType?.includes("text/html") && probe.title === "HWLAB DEV MVP Gate";
}
return Boolean(
probe.json &&
SERVICE_IDS.includes(probe.json.serviceId) &&
probe.json.environment === ENVIRONMENT_DEV &&
(probe.json.endpoint === undefined || probe.json.endpoint === DEV_ENDPOINT)
);
}
function probeEvidenceLine(probe) {
const accepted = acceptedProbe(probe) ? "yes" : "no";
const identity = probe.json?.serviceId ?? probe.title ?? "unknown";
return `${probe.id}: HTTP ${probe.status ?? "none"} accepted=${accepted} identity=${identity} url=${probe.url}`;
}
async function gitCommitId() {
try {
const result = await execFileAsync("git", ["rev-parse", "--short=12", "HEAD"], {
cwd: repoRoot,
timeout: 5000
});
return result.stdout.trim();
} catch {
return "unknown";
}
}
function buildActiveReport({ fixture, probes, generatedAt, commitId }) {
const passed = probes.every(acceptedProbe);
const status = passed ? "pass" : "blocked";
const commands = [
"curl -fsS --max-time 10 http://74.48.78.17:16667/health",
"curl -fsS --max-time 10 http://74.48.78.17:16667/health/live",
"curl -fsS --max-time 10 http://74.48.78.17:16666/"
];
return {
$schema: "https://hwlab.pikastech.local/schemas/dev-gate-report.schema.json",
$id: "https://hwlab.pikastech.local/reports/dev-gate/dev-m2-deploy-smoke-active.json",
reportVersion: "v1",
issue: "pikasTech/HWLAB#23",
taskId: "m2-dev-deploy-smoke",
commitId,
acceptanceLevel: "dev_m2_deploy_smoke",
devOnly: true,
prodDisabled: true,
status,
generatedAt,
endpoint: DEV_ENDPOINT,
frontendEndpoint: DEV_FRONTEND_ENDPOINT,
sourceContract: {
status: "pass",
documents: [
"docs/m2-dev-deploy-smoke.md",
"docs/dev-acceptance-matrix.md",
"fixtures/dev-deploy-smoke/dev-deploy-smoke.json"
],
summary: "M2 DEV deploy smoke is rebaselined to frontend :16666 and API/edge :16667."
},
validationCommands: [
"node --check scripts/m2-dev-deploy-smoke.mjs",
"node scripts/m2-dev-deploy-smoke.mjs --dry-run",
"node scripts/m2-dev-deploy-smoke.mjs --live --confirm-dev --confirmed-non-production --write-report",
"node --check scripts/validate-dev-gate-report.mjs",
"node scripts/validate-dev-gate-report.mjs"
],
localSmoke: {
status: "pass",
commands: ["node scripts/m2-dev-deploy-smoke.mjs --dry-run"],
evidence: [
`[m2-smoke] validated ${fixture.serviceContracts.length} service contracts`,
`[m2-smoke] mode=dry-run endpoint=${DEV_ENDPOINT}`,
"[m2-smoke] no real DEV/PROD request was made"
],
summary: "The fixture smoke validates the M2 route and artifact contract without contacting DEV."
},
dryRun: {
status: "pass",
commands: ["node scripts/m2-dev-deploy-smoke.mjs --dry-run"],
evidence: [
`fixture endpoint=${DEV_ENDPOINT}`,
`fixture frontendEndpoint=${DEV_FRONTEND_ENDPOINT}`,
"legacy :6667 public evidence is marked ineligible for active green"
],
summary: "Dry-run evidence is contract-only and pinned to the current DEV public endpoints."
},
devPreconditions: {
status,
requirements: [
"GET http://74.48.78.17:16667/health returns HWLAB DEV JSON",
"GET http://74.48.78.17:16667/health/live returns HWLAB DEV JSON",
"GET http://74.48.78.17:16666/ returns the HWLAB DEV frontend",
"No PROD, secret read, runtime restart, deployment, or heavyweight e2e action is performed"
],
commands,
evidence: probes.map(probeEvidenceLine),
summary: passed
? "Read-only DEV public endpoint probes passed on :16666/:16667."
: "One or more read-only DEV public endpoint probes failed; no active green is claimed."
},
blockers: passed
? []
: [
{
type: "network_blocker",
scope: "m2-public-endpoints",
status: "open",
summary: "One or more M2 DEV public endpoint probes failed on :16666/:16667."
}
],
runtimeSmoke: {
mode: "live-read-only",
status,
generatedAt,
apiEndpoint: DEV_ENDPOINT,
frontendEndpoint: DEV_FRONTEND_ENDPOINT,
safety: {
environment: ENVIRONMENT_DEV,
prodTouched: false,
secretsRead: false,
restarts: false,
deployAttempted: false,
heavyE2E: false,
unideskRuntimeSubstitute: false
},
probes,
legacyPublicEndpoint: {
endpoint: legacyPublicEndpoint,
status: "deprecated",
activeGreenEligible: false,
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."
};
}
async function writeActiveReport(report, reportPath) {
await mkdir(path.dirname(reportPath), { recursive: true });
await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`);
}
async function runLiveSmoke(fixture, options) {
const probes = await Promise.all([
probeHttp({
id: "api-health",
url: new URL("/health", DEV_ENDPOINT).href,
expect: { timeoutMs: options.timeoutMs }
}),
probeHttp({
id: "api-live",
url: new URL("/health/live", DEV_ENDPOINT).href,
expect: { timeoutMs: options.timeoutMs }
}),
probeHttp({
id: "frontend-root",
url: new URL("/", DEV_FRONTEND_ENDPOINT).href,
expect: { timeoutMs: options.timeoutMs }
})
]);
const report = buildActiveReport({
fixture,
probes,
generatedAt: new Date().toISOString(),
commitId: await gitCommitId()
});
return report;
}
async function main() {
const { flags } = parseArgs(process.argv.slice(2));
const { flags, options } = parseArgs(process.argv.slice(2));
const isLive = flags.has("--live") || flags.has("--allow-live");
if (isLive) {
@@ -188,7 +443,20 @@ async function main() {
process.stdout.write(`[m2-smoke] validated ${fixture.serviceContracts.length} service contracts\n`);
process.stdout.write(`[m2-smoke] mode=${isLive ? "live" : "dry-run"} endpoint=${fixture.endpoint}\n`);
process.stdout.write("[m2-smoke] no real DEV/PROD request was made\n");
if (isLive) {
const report = await runLiveSmoke(fixture, options);
if (flags.has("--write-report")) {
await writeActiveReport(report, options.reportPath);
process.stdout.write(`[m2-smoke] wrote ${path.relative(repoRoot, options.reportPath)}\n`);
}
process.stdout.write(`[m2-smoke] active-report=${report.status} probes=${report.runtimeSmoke.probes.length}\n`);
process.stdout.write("[m2-smoke] no DEV/PROD mutation was made\n");
if (report.status !== "pass") {
process.exitCode = 2;
}
} else {
process.stdout.write("[m2-smoke] no real DEV/PROD request was made\n");
}
}
try {
+139
View File
@@ -91,6 +91,19 @@ const requiredDevEdgeDocs = [
"docs/m0-contract-audit.md",
"docs/dev-edge-health.md"
];
const requiredDevM2Issue = "pikasTech/HWLAB#23";
const requiredDevM2ValidationCommands = [
"node --check scripts/m2-dev-deploy-smoke.mjs",
"node scripts/m2-dev-deploy-smoke.mjs --dry-run",
"node scripts/m2-dev-deploy-smoke.mjs --live --confirm-dev --confirmed-non-production --write-report",
"node --check scripts/validate-dev-gate-report.mjs",
"node scripts/validate-dev-gate-report.mjs"
];
const requiredDevM2Docs = [
"docs/m2-dev-deploy-smoke.md",
"docs/dev-acceptance-matrix.md",
"fixtures/dev-deploy-smoke/dev-deploy-smoke.json"
];
const reportFamilyTemplates = new Map([
[
"dev-gate-report-contract",
@@ -353,6 +366,10 @@ async function validateReport(relativePath) {
await validateDevEdgeReport(report, label);
return;
}
if (report.issue === requiredDevM2Issue || report.taskId === "m2-dev-deploy-smoke") {
await validateDevM2Report(report, label);
return;
}
const template = reportFamilyTemplates.get(report.taskId);
assert.ok(
@@ -949,6 +966,128 @@ async function validateDevEdgeReport(report, label) {
assertObject(report.edgeHealth.kubernetes ?? {}, `${label}.edgeHealth.kubernetes`);
}
async function validateDevM2Report(report, label) {
for (const field of [
"$schema",
"$id",
"reportVersion",
"issue",
"taskId",
"commitId",
"acceptanceLevel",
"devOnly",
"prodDisabled",
"status",
"generatedAt",
"endpoint",
"frontendEndpoint",
"sourceContract",
"validationCommands",
"localSmoke",
"dryRun",
"devPreconditions",
"blockers",
"runtimeSmoke"
]) {
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.issue, requiredDevM2Issue, `${label}.issue`);
assert.equal(report.taskId, "m2-dev-deploy-smoke", `${label}.taskId`);
assert.match(report.commitId, /^([a-f0-9]{7,40}|unknown)$/, `${label}.commitId`);
assert.equal(report.acceptanceLevel, "dev_m2_deploy_smoke", `${label}.acceptanceLevel`);
assert.equal(report.devOnly, true, `${label}.devOnly`);
assert.equal(report.prodDisabled, true, `${label}.prodDisabled`);
assert.equal(report.status, "pass", `${label}.status`);
assertTimestamp(report.generatedAt, `${label}.generatedAt`);
assert.equal(report.endpoint, "http://74.48.78.17:16667", `${label}.endpoint`);
assert.equal(report.frontendEndpoint, "http://74.48.78.17:16666", `${label}.frontendEndpoint`);
assertObject(report.sourceContract, `${label}.sourceContract`);
assertStatus(report.sourceContract.status, `${label}.sourceContract.status`);
await assertDocumentSet(
report.sourceContract.documents,
`${label}.sourceContract.documents`,
requiredDevM2Docs
);
assertString(report.sourceContract.summary, `${label}.sourceContract.summary`);
assertStringArray(report.validationCommands, `${label}.validationCommands`, {
minLength: requiredDevM2ValidationCommands.length
});
assertUnique(report.validationCommands, `${label}.validationCommands`);
for (const requiredCommand of requiredDevM2ValidationCommands) {
assert.ok(
report.validationCommands.includes(requiredCommand),
`${label}.validationCommands missing ${requiredCommand}`
);
}
for (const section of ["localSmoke", "dryRun", "devPreconditions"]) {
assertObject(report[section], `${label}.${section}`);
assertStatus(report[section].status, `${label}.${section}.status`);
assertStringArray(report[section].commands, `${label}.${section}.commands`, { minLength: 1 });
assertStringArray(report[section].evidence, `${label}.${section}.evidence`, { minLength: 1 });
assertString(report[section].summary, `${label}.${section}.summary`);
}
assertStringArray(report.devPreconditions.requirements, `${label}.devPreconditions.requirements`, {
minLength: 1
});
for (const command of [
"curl -fsS --max-time 10 http://74.48.78.17:16667/health",
"curl -fsS --max-time 10 http://74.48.78.17:16667/health/live",
"curl -fsS --max-time 10 http://74.48.78.17:16666/"
]) {
assert.ok(
report.devPreconditions.commands.includes(command),
`${label}.devPreconditions.commands missing ${command}`
);
}
assertBlockers(report.blockers, `${label}.blockers`);
assertObject(report.runtimeSmoke, `${label}.runtimeSmoke`);
assert.equal(report.runtimeSmoke.mode, "live-read-only", `${label}.runtimeSmoke.mode`);
assert.equal(report.runtimeSmoke.status, "pass", `${label}.runtimeSmoke.status`);
assertTimestamp(report.runtimeSmoke.generatedAt, `${label}.runtimeSmoke.generatedAt`);
assert.equal(report.runtimeSmoke.apiEndpoint, "http://74.48.78.17:16667", `${label}.runtimeSmoke.apiEndpoint`);
assert.equal(report.runtimeSmoke.frontendEndpoint, "http://74.48.78.17:16666", `${label}.runtimeSmoke.frontendEndpoint`);
assertObject(report.runtimeSmoke.safety, `${label}.runtimeSmoke.safety`);
assert.equal(report.runtimeSmoke.safety.environment, "dev", `${label}.runtimeSmoke.safety.environment`);
for (const field of [
"prodTouched",
"secretsRead",
"restarts",
"deployAttempted",
"heavyE2E",
"unideskRuntimeSubstitute"
]) {
assert.equal(report.runtimeSmoke.safety[field], false, `${label}.runtimeSmoke.safety.${field}`);
}
assertArray(report.runtimeSmoke.probes, `${label}.runtimeSmoke.probes`);
assert.ok(report.runtimeSmoke.probes.length >= 3, `${label}.runtimeSmoke.probes must include api and frontend probes`);
assertObject(report.runtimeSmoke.legacyPublicEndpoint, `${label}.runtimeSmoke.legacyPublicEndpoint`);
assert.equal(
report.runtimeSmoke.legacyPublicEndpoint.endpoint,
"http://74.48.78.17:6667",
`${label}.runtimeSmoke.legacyPublicEndpoint.endpoint`
);
assert.equal(
report.runtimeSmoke.legacyPublicEndpoint.status,
"deprecated",
`${label}.runtimeSmoke.legacyPublicEndpoint.status`
);
assert.equal(
report.runtimeSmoke.legacyPublicEndpoint.activeGreenEligible,
false,
`${label}.runtimeSmoke.legacyPublicEndpoint.activeGreenEligible`
);
}
async function validateDevM3Report(report, label) {
for (const field of [
"$schema",
@@ -0,0 +1,76 @@
#!/usr/bin/env node
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const reportPath = path.join(repoRoot, "reports/dev-gate/dev-m2-deploy-smoke-active.json");
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 assertObject(value, label) {
assert.ok(value && typeof value === "object" && !Array.isArray(value), `${label} must be an object`);
}
function assertArray(value, label) {
assert.ok(Array.isArray(value), `${label} must be an array`);
}
function assertTimestamp(value, label) {
assertString(value, label);
assert.ok(!Number.isNaN(Date.parse(value)), `${label} must be a timestamp`);
}
async function main() {
const report = JSON.parse(await readFile(reportPath, "utf8"));
assertObject(report, "report");
assert.equal(report.issue, "pikasTech/HWLAB#23");
assert.equal(report.taskId, "m2-dev-deploy-smoke");
assert.equal(report.status, "pass");
assert.equal(report.endpoint, "http://74.48.78.17:16667");
assert.equal(report.frontendEndpoint, "http://74.48.78.17:16666");
assertTimestamp(report.generatedAt, "generatedAt");
assertObject(report.runtimeSmoke, "runtimeSmoke");
assert.equal(report.runtimeSmoke.mode, "live-read-only");
assert.equal(report.runtimeSmoke.status, "pass");
assert.equal(report.runtimeSmoke.apiEndpoint, "http://74.48.78.17:16667");
assert.equal(report.runtimeSmoke.frontendEndpoint, "http://74.48.78.17:16666");
assertObject(report.runtimeSmoke.safety, "runtimeSmoke.safety");
for (const field of ["prodTouched", "secretsRead", "restarts", "deployAttempted", "heavyE2E", "unideskRuntimeSubstitute"]) {
assert.equal(report.runtimeSmoke.safety[field], false, `runtimeSmoke.safety.${field}`);
}
assertObject(report.runtimeSmoke.legacyPublicEndpoint, "runtimeSmoke.legacyPublicEndpoint");
assert.equal(report.runtimeSmoke.legacyPublicEndpoint.endpoint, "http://74.48.78.17:6667");
assert.equal(report.runtimeSmoke.legacyPublicEndpoint.activeGreenEligible, false);
assertObject(report.devPreconditions, "devPreconditions");
assert.equal(report.devPreconditions.status, "pass");
assertArray(report.devPreconditions.commands, "devPreconditions.commands");
assertArray(report.devPreconditions.evidence, "devPreconditions.evidence");
assert.ok(
report.devPreconditions.commands.includes("curl -fsS --max-time 10 http://74.48.78.17:16667/health"),
"devPreconditions.commands missing /health probe"
);
assert.ok(
report.devPreconditions.commands.includes("curl -fsS --max-time 10 http://74.48.78.17:16667/health/live"),
"devPreconditions.commands missing /health/live probe"
);
assert.ok(
report.devPreconditions.commands.includes("curl -fsS --max-time 10 http://74.48.78.17:16666/"),
"devPreconditions.commands missing frontend probe"
);
console.log("validated m2-dev-deploy-smoke-active.json");
}
try {
await main();
} catch (error) {
process.stderr.write(`[validate-m2-dev-deploy-smoke-active] ${error instanceof Error ? error.message : String(error)}\n`);
process.exitCode = 1;
}