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:
@@ -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,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."
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user