From 4f93bc07bf79194813ea7205d38a357a2e52137b Mon Sep 17 00:00:00 2001 From: Lyon <88232613+pikasTech@users.noreply.github.com> Date: Fri, 22 May 2026 14:20:07 +0800 Subject: [PATCH] docs: add M3 rollout support runbook (#81) Co-authored-by: HWLAB Code Queue --- README.md | 4 +- docs/reference/m3-loop-rollout-runbook.md | 85 +++++++++++++++++++++++ package.json | 1 + scripts/validate-m3-rollout-runbook.mjs | 59 ++++++++++++++++ 4 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 docs/reference/m3-loop-rollout-runbook.md create mode 100644 scripts/validate-m3-rollout-runbook.mjs diff --git a/README.md b/README.md index f60e1338..ae7275f1 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,8 @@ entry points that later service implementations must follow. - MVP scope is DEV only. - DEV browser/frontend endpoint is fixed at `http://74.48.78.17:16666/`. - DEV API/edge/live endpoint is fixed at `http://74.48.78.17:16667`. -- PROD is not an MVP acceptance target. +- Legacy `:6666` and `:6667` are historical/deprecated only and are not MVP + acceptance targets. - UniDesk may be used only as an external scheduling, CI, or CD base. - HWLAB runtime services must not be replaced by UniDesk backend, provider-gateway, or microservice proxy implementations. @@ -60,6 +61,7 @@ The MVP branches may add implementation files under these paths: - MVP e2e contract: [protocol/mvp-e2e-contract.md](protocol/mvp-e2e-contract.md) - JSON Schemas: [protocol/schemas](protocol/schemas) - Deploy schema: [deploy/deploy.schema.json](deploy/deploy.schema.json) +- M3 rollout support runbook: [docs/reference/m3-loop-rollout-runbook.md](docs/reference/m3-loop-rollout-runbook.md) ## Validation diff --git a/docs/reference/m3-loop-rollout-runbook.md b/docs/reference/m3-loop-rollout-runbook.md new file mode 100644 index 00000000..adb59f3c --- /dev/null +++ b/docs/reference/m3-loop-rollout-runbook.md @@ -0,0 +1,85 @@ +# M3 Loop Rollout Runbook + +This reference is support / diagnostics / contract for the HWLAB P0 M3 virtual +hardware trusted loop only. It is not P0 live evidence and it must not relabel +SOURCE / LOCAL / DRY-RUN / fixture output as `DEV-LIVE`. + +## M3 Loop Boundary + +- P0 scope: `2 x hwlab-box-simu`, `2 x hwlab-gateway-simu`, `1 x hwlab-patch-panel`. +- Accepted chain for live M3 proof: `DO1 -> patch-panel -> DI1`. +- Current public endpoints: browser/frontend `http://74.48.78.17:16666/` and + API/edge/live `http://74.48.78.17:16667/`. +- Legacy public ports `:6666` and `:6667` are historical / deprecated only. +- `pikasTech/HWLAB#63` remains a known CD blocker until suspended Job immutable + apply is fixed. + +## Source Of Truth + +`deploy/deploy.json` is the deploy intent source. `deploy/artifact-catalog.dev.json` +is the artifact identity source. No shell scrollback, issue comment, or report +fixture is authoritative over those two files. + +| Fact | Read From | Notes | +| --- | --- | --- | +| source commit | `deploy/deploy.json.commitId` and `deploy/artifact-catalog.dev.json.commitId` | They must match. | +| image tag | `deploy/deploy.json.services[].image` and `deploy/artifact-catalog.dev.json.services[].imageTag` | The tag must stay tied to the source commit. | +| digest | `deploy/artifact-catalog.dev.json.services[].digest` | Real digests come only from publish evidence; otherwise keep `not_published`. | +| public endpoint | `deploy/deploy.json.publicEndpoints.*.url` and `deploy/deploy.json.endpoint` | Current endpoints are `16666/16667`; do not backslide to legacy ports. | +| frp | `deploy/deploy.json.frp.proxies[]` | Derive proxy name, local service, local port, and remote port from the manifest. | +| k3s service | `deploy/deploy.json.k3s.serviceMappings[]` | Derive service name, namespace, port, and targetPort from the manifest. | +| health path | `deploy/deploy.json.health.path` and `deploy/deploy.json.services[].healthPath` | Must stay on `/health/live`. | + +## Rollout Support + +Use this order for M3 loop support work: + +1. Render the manifest contract and compare it with the artifact catalog. +2. Verify the M3 inventory cardinality for the two boxes, two gateways, and + one patch-panel. +3. Check that the current public endpoints stay on `16666/16667`. +4. Confirm the health path is `/health/live`. +5. Record any manual hotfix facts back into the issue, PR, and runbook before + closing the task. + +Recommended checks: + +```sh +node scripts/deploy-contract-plan.mjs --pretty +node scripts/deploy-contract-plan.mjs --check +node scripts/validate-dev-m3-cardinality.mjs +npm run docs:validate:m3-rollout +``` + +These checks are support only. They do not apply to DEV, do not touch PROD, do +not restart services, and do not prove live M3 acceptance. + +## Rollback Support + +If a manual hotfix or rollback is required, record the following before the +task closes: + +- source commit and branch or PR; +- image reference, image tag, and digest; +- public endpoint and whether it was read-only or routed; +- FRP proxy name and ownership; +- k3s service mapping and ownership; +- health path used for verification; +- any field that diverged from `deploy/deploy.json` or the artifact catalog. + +Rollback support must keep the live chain honest. If the chain was not actually +`DO1 -> patch-panel -> DI1`, do not claim live M3. Only write support, +diagnostics, or contract evidence. + +## Prohibited Actions + +- Do not treat this runbook as a general console or PROD flow. +- Do not deploy or smoke PROD. +- Do not read, print, or infer secrets, tokens, kubeconfig, or tunnel + credentials. +- Do not restart Code Queue, `backend-core`, or any runtime service from the + runner. +- Do not run heavyweight e2e or destructive hardware tests. +- Do not bypass `pikasTech/HWLAB#63` and then claim CD is green. +- Do not promote SOURCE / LOCAL / DRY-RUN / fixture output to `DEV-LIVE`. + diff --git a/package.json b/package.json index 65785ff5..26c4966d 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "artifact-catalog:refresh-blocked": "node scripts/refresh-artifact-catalog.mjs --target-ref origin/main --blocked", "dev-artifact:preflight": "node scripts/dev-artifact-publish.mjs --preflight", "dev-artifact:publish": "node scripts/dev-artifact-publish.mjs --publish", + "docs:validate:m3-rollout": "node scripts/validate-m3-rollout-runbook.mjs", "d601:k3s:readonly": "node scripts/d601-k3s-readonly-observability.mjs", "runner:issue-visibility:preflight": "node scripts/runner-issue-visibility-preflight.mjs" } diff --git a/scripts/validate-m3-rollout-runbook.mjs b/scripts/validate-m3-rollout-runbook.mjs new file mode 100644 index 00000000..718ee0eb --- /dev/null +++ b/scripts/validate-m3-rollout-runbook.mjs @@ -0,0 +1,59 @@ +#!/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 docPath = "docs/reference/m3-loop-rollout-runbook.md"; + +async function readText(relativePath) { + return readFile(path.join(repoRoot, relativePath), "utf8"); +} + +function extractSection(doc, heading) { + const marker = `## ${heading}\n`; + const start = doc.indexOf(marker); + assert.ok(start >= 0, `missing section ${heading}`); + const bodyStart = start + marker.length; + const nextHeading = doc.indexOf("\n## ", bodyStart); + return doc.slice(bodyStart, nextHeading >= 0 ? nextHeading + 1 : doc.length); +} + +function assertIncludes(text, snippet, label) { + assert.ok(text.includes(snippet), `${label} must include ${snippet}`); +} + +function assertNotIncludes(text, snippet, label) { + assert.equal(text.includes(snippet), false, `${label} must not include ${snippet}`); +} + +async function main() { + const doc = await readText(docPath); + const boundary = extractSection(doc, "M3 Loop Boundary"); + + assertIncludes(doc, "support / diagnostics / contract", "m3 runbook"); + assertIncludes(doc, "2 x hwlab-box-simu", "m3 runbook"); + assertIncludes(doc, "2 x hwlab-gateway-simu", "m3 runbook"); + assertIncludes(doc, "1 x hwlab-patch-panel", "m3 runbook"); + assertIncludes(doc, "DO1 -> patch-panel -> DI1", "m3 runbook"); + assertIncludes(doc, "deploy/deploy.json", "m3 runbook"); + assertIncludes(doc, "deploy/artifact-catalog.dev.json", "m3 runbook"); + assertIncludes(doc, "pikasTech/HWLAB#63", "m3 runbook"); + assertIncludes(doc, "Do not promote SOURCE / LOCAL / DRY-RUN / fixture output to `DEV-LIVE`.", "m3 runbook"); + assertNotIncludes(boundary, "http://74.48.78.17:6666/", "m3 current boundary"); + assertNotIncludes(boundary, "http://74.48.78.17:6667/", "m3 current boundary"); + assertIncludes(boundary, "http://74.48.78.17:16666/", "m3 current boundary"); + assertIncludes(boundary, "http://74.48.78.17:16667/", "m3 current boundary"); + assertIncludes(boundary, ":6666", "m3 historical boundary"); + assertIncludes(boundary, ":6667", "m3 historical boundary"); + + console.log(`validated ${docPath}`); +} + +try { + await main(); +} catch (error) { + process.stderr.write(`[validate-m3-rollout-runbook] ${error instanceof Error ? error.message : String(error)}\n`); + process.exitCode = 1; +}