Merge pull request #125 from pikasTech/fix/dev-report-port-contract
fix: enforce dev report port lifecycle
This commit is contained in:
@@ -25,6 +25,11 @@ Each committed DEV gate report must carry `reportLifecycle`:
|
|||||||
- Active reports may preserve a legacy public endpoint only inside an explicit
|
- Active reports may preserve a legacy public endpoint only inside an explicit
|
||||||
`runtimeSmoke.legacyPublicEndpoint` object with `status: "deprecated"` and
|
`runtimeSmoke.legacyPublicEndpoint` object with `status: "deprecated"` and
|
||||||
`activeGreenEligible: false`.
|
`activeGreenEligible: false`.
|
||||||
|
- Active reports must not present `:6666` or `:6667` as a current public DEV
|
||||||
|
endpoint in endpoint, route, health, ingress, API, frontend, or browser
|
||||||
|
evidence. Internal k3s service/listen ports may still be `6667` when the
|
||||||
|
surrounding field names them as cluster-local service ports rather than
|
||||||
|
public endpoints.
|
||||||
- M3 DEV-LIVE remains narrower than report freshness: it requires
|
- M3 DEV-LIVE remains narrower than report freshness: it requires
|
||||||
`res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1` in real DEV
|
`res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1` in real DEV
|
||||||
plus operation, audit, and evidence identifiers. SOURCE, LOCAL, DRY-RUN,
|
plus operation, audit, and evidence identifiers. SOURCE, LOCAL, DRY-RUN,
|
||||||
@@ -40,6 +45,9 @@ Each committed DEV gate report must carry `reportLifecycle`:
|
|||||||
- `reports/dev-gate/dev-gate-report.example.json`: machine-readable example.
|
- `reports/dev-gate/dev-gate-report.example.json`: machine-readable example.
|
||||||
- `reports/dev-gate/dev-deploy-report.json`: DEV deploy apply/preflight result.
|
- `reports/dev-gate/dev-deploy-report.json`: DEV deploy apply/preflight result.
|
||||||
- `scripts/validate-dev-gate-report.mjs`: local validator for the report shape.
|
- `scripts/validate-dev-gate-report.mjs`: local validator for the report shape.
|
||||||
|
- `scripts/report-lifecycle.mjs`: lightweight lifecycle helper for validating
|
||||||
|
one report or marking a replaced report historical/deprecated after endpoint
|
||||||
|
changes.
|
||||||
|
|
||||||
## Required Fields
|
## Required Fields
|
||||||
|
|
||||||
@@ -105,3 +113,24 @@ node scripts/validate-dev-gate-report.mjs
|
|||||||
The validator scans `reports/dev-gate/*.json` by default. It only checks the
|
The validator scans `reports/dev-gate/*.json` by default. It only checks the
|
||||||
contract shape and the frozen report metadata; it does not run any DEV or PROD
|
contract shape and the frozen report metadata; it does not run any DEV or PROD
|
||||||
deployment.
|
deployment.
|
||||||
|
|
||||||
|
## Invalidate/Rebuild Workflow
|
||||||
|
|
||||||
|
When the public DEV endpoint mapping changes, do not rewrite historical
|
||||||
|
evidence strings. Preserve the old facts and mark any report that can no longer
|
||||||
|
represent the active gate as historical:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
node --check scripts/report-lifecycle.mjs
|
||||||
|
node scripts/report-lifecycle.mjs invalidate reports/dev-gate/<old-report>.json \
|
||||||
|
--deprecated-endpoint http://74.48.78.17:6667 \
|
||||||
|
--reason "Public DEV endpoint moved to frontend :16666 and API/edge :16667" \
|
||||||
|
--out reports/dev-gate/historical/<old-report>-legacy-6667.json
|
||||||
|
node scripts/validate-dev-gate-report.mjs reports/dev-gate/historical/<old-report>-legacy-6667.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Then regenerate the active report through its owning script so the active file
|
||||||
|
uses `http://74.48.78.17:16666` for browser/frontend evidence and
|
||||||
|
`http://74.48.78.17:16667` for API/edge/live evidence. Keep PR notes explicit:
|
||||||
|
which legacy reports were moved or marked historical, which active reports were
|
||||||
|
regenerated, and which current reports still need real DEV reruns.
|
||||||
|
|||||||
@@ -4,15 +4,55 @@ 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 DEPRECATED_RESERVED_PROD_ENDPOINT = "http://74.48.78.17:6666";
|
||||||
export const REPORT_LIFECYCLE_VERSION = "v1";
|
export const REPORT_LIFECYCLE_VERSION = "v1";
|
||||||
|
|
||||||
|
export const deprecatedPublicPorts = Object.freeze([6666, 6667]);
|
||||||
export const deprecatedPublicEndpoints = Object.freeze([
|
export const deprecatedPublicEndpoints = Object.freeze([
|
||||||
DEPRECATED_DEV_PUBLIC_ENDPOINT,
|
DEPRECATED_DEV_PUBLIC_ENDPOINT,
|
||||||
DEPRECATED_RESERVED_PROD_ENDPOINT
|
DEPRECATED_RESERVED_PROD_ENDPOINT
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const deprecatedPublicEndpointPattern = /http:\/\/74\.48\.78\.17:666[67]\b/gu;
|
const deprecatedPublicEndpointPattern = /\bhttps?:\/\/74\.48\.78\.17:(666[67])(?=[/?#\s"'`),\]}]|$)/giu;
|
||||||
|
const deprecatedPublicHostPortPattern = /(?<!\/\/)\b74\.48\.78\.17:(666[67])(?=[/?#\s"'`),\]}]|$)/giu;
|
||||||
|
const endpointContextWords = [
|
||||||
|
"active",
|
||||||
|
"api",
|
||||||
|
"browser",
|
||||||
|
"current",
|
||||||
|
"deprecated",
|
||||||
|
"edge",
|
||||||
|
"endpoint",
|
||||||
|
"frontend",
|
||||||
|
"health",
|
||||||
|
"historical",
|
||||||
|
"ingress",
|
||||||
|
"legacy",
|
||||||
|
"public",
|
||||||
|
"route"
|
||||||
|
].join("|");
|
||||||
|
const deprecatedPublicPortContextPattern = new RegExp(
|
||||||
|
`\\b(?:${endpointContextWords})\\b[^.\\n]{0,80}(?<![A-Za-z0-9.-]):(666[67])\\b|(?<![A-Za-z0-9.-]):(666[67])\\b[^.\\n]{0,80}\\b(?:${endpointContextWords})\\b`,
|
||||||
|
"giu"
|
||||||
|
);
|
||||||
|
|
||||||
|
function endpointForPort(port) {
|
||||||
|
if (Number(port) === 6666) return DEPRECATED_RESERVED_PROD_ENDPOINT;
|
||||||
|
if (Number(port) === 6667) return DEPRECATED_DEV_PUBLIC_ENDPOINT;
|
||||||
|
throw new Error(`unsupported deprecated public port ${port}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addMatches(matches, pattern, text) {
|
||||||
|
pattern.lastIndex = 0;
|
||||||
|
for (const match of String(text).matchAll(pattern)) {
|
||||||
|
const port = match[1] ?? match[2];
|
||||||
|
matches.add(endpointForPort(port));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function findDeprecatedPublicEndpoints(text) {
|
export function findDeprecatedPublicEndpoints(text) {
|
||||||
return [...new Set(String(text).match(deprecatedPublicEndpointPattern) ?? [])].sort();
|
const matches = new Set();
|
||||||
|
addMatches(matches, deprecatedPublicEndpointPattern, text);
|
||||||
|
addMatches(matches, deprecatedPublicHostPortPattern, text);
|
||||||
|
addMatches(matches, deprecatedPublicPortContextPattern, text);
|
||||||
|
return [...matches].sort();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isDeprecatedEndpointAllowedInActiveReport(report, endpoint) {
|
export function isDeprecatedEndpointAllowedInActiveReport(report, endpoint) {
|
||||||
|
|||||||
+4
-2
@@ -5,13 +5,14 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"validate": "node scripts/validate-contract.mjs && node scripts/deploy-contract-plan.mjs --check && node scripts/deploy-desired-state-plan.mjs --check",
|
"validate": "node scripts/validate-contract.mjs && node scripts/deploy-contract-plan.mjs --check && node scripts/deploy-desired-state-plan.mjs --check",
|
||||||
"check": "node --check internal/protocol/index.mjs && node --check internal/agent/index.mjs && node --check internal/agent/runtime.mjs && node --check internal/audit/index.mjs && node --check internal/db/runtime-store.mjs && node --check internal/mvp-gate/summary.mjs && node --check internal/cloud/db-contract.mjs && node --check internal/cloud/json-rpc.mjs && node --check internal/cloud/server.mjs && node --check internal/sim/model.mjs && node --check internal/sim/http.mjs && node --check internal/sim/l2-runtime.mjs && node --check internal/patchpanel/model.mjs && node --check internal/patchpanel/runtime.mjs && node --check cmd/hwlab-cloud-api/main.mjs && node --check cmd/hwlab-agent-mgr/main.mjs && node --check cmd/hwlab-agent-worker/main.mjs && node --check cmd/hwlab-box-simu/main.mjs && node --check cmd/hwlab-gateway-simu/main.mjs && node --check scripts/cloud-api-runtime-smoke.mjs && node --check scripts/dev-edge-health-smoke.mjs && node --check scripts/src/dev-edge-health-smoke-lib.mjs && node --check scripts/validate-contract.mjs && node --check scripts/deploy-desired-state-plan.mjs && node --check scripts/src/deploy-desired-state-plan.mjs && node --check scripts/validate-dev-gate-report.mjs && node --check scripts/validate-dev-m3-cardinality.mjs && node --check scripts/validate-artifact-catalog.mjs && node --check scripts/refresh-artifact-catalog.mjs && node --check scripts/dev-artifact-publish.mjs && node --check scripts/src/dev-artifact-services.mjs && node --check scripts/src/registry-capabilities.mjs && node --check scripts/preflight-dev-base-image.mjs && node --check scripts/src/dev-base-image-preflight.mjs && node --check scripts/dev-evidence-blocker-aggregator.mjs && node --check scripts/src/dev-evidence-blocker-aggregator.mjs && node --check scripts/d601-k3s-readonly-observability.mjs && node --check scripts/src/d601-k3s-readonly-observability.mjs && node --check scripts/l2-runtime-contract-smoke.mjs && node --check scripts/patch-panel-runtime-smoke.mjs && node --check scripts/export-web-gate-summary.mjs && node --check scripts/l6-cli-web-smoke.mjs && node --check tools/hwlab-cli/bin/hwlab-cli.mjs && node --check tools/hwlab-cli/lib/cli.mjs && node --check web/hwlab-cloud-web/app.mjs && node --check web/hwlab-cloud-web/gate-summary.mjs && node --check web/hwlab-cloud-web/scripts/check.mjs && node --check web/hwlab-cloud-web/scripts/build.mjs && node scripts/validate-contract.mjs && node scripts/validate-dev-gate-report.mjs && node scripts/validate-dev-m3-cardinality.mjs && node scripts/validate-artifact-catalog.mjs && node scripts/deploy-desired-state-plan.mjs --check && node scripts/dev-evidence-blocker-aggregator.mjs --check && node scripts/l2-runtime-contract-smoke.mjs && node scripts/l6-cli-web-smoke.mjs && node web/hwlab-cloud-web/scripts/check.mjs && node --test scripts/deploy-desired-state-plan.test.mjs scripts/src/dev-deploy-apply.test.mjs internal/agent/index.test.mjs internal/audit/index.test.mjs internal/db/schema.test.mjs internal/cloud/json-rpc.test.mjs internal/cloud/server.test.mjs internal/patchpanel/model.test.mjs internal/patchpanel/runtime.test.mjs && node scripts/cloud-api-runtime-smoke.mjs && sh -n scripts/bootstrap-skills.sh scripts/worker-entrypoint.sh",
|
"check": "node --check internal/protocol/index.mjs && node --check internal/agent/index.mjs && node --check internal/agent/runtime.mjs && node --check internal/audit/index.mjs && node --check internal/db/runtime-store.mjs && node --check internal/mvp-gate/summary.mjs && node --check internal/cloud/db-contract.mjs && node --check internal/cloud/json-rpc.mjs && node --check internal/cloud/server.mjs && node --check internal/sim/model.mjs && node --check internal/sim/http.mjs && node --check internal/sim/l2-runtime.mjs && node --check internal/patchpanel/model.mjs && node --check internal/patchpanel/runtime.mjs && node --check cmd/hwlab-cloud-api/main.mjs && node --check cmd/hwlab-agent-mgr/main.mjs && node --check cmd/hwlab-agent-worker/main.mjs && node --check cmd/hwlab-box-simu/main.mjs && node --check cmd/hwlab-gateway-simu/main.mjs && node --check scripts/cloud-api-runtime-smoke.mjs && node --check scripts/dev-edge-health-smoke.mjs && node --check scripts/src/dev-edge-health-smoke-lib.mjs && node --check scripts/validate-contract.mjs && node --check scripts/deploy-desired-state-plan.mjs && node --check scripts/src/deploy-desired-state-plan.mjs && node --check scripts/report-lifecycle.mjs && node --check scripts/report-lifecycle.test.mjs && node --check scripts/validate-dev-gate-report.mjs && node --check scripts/validate-dev-m3-cardinality.mjs && node --check scripts/validate-artifact-catalog.mjs && node --check scripts/refresh-artifact-catalog.mjs && node --check scripts/dev-artifact-publish.mjs && node --check scripts/src/dev-artifact-services.mjs && node --check scripts/src/registry-capabilities.mjs && node --check scripts/preflight-dev-base-image.mjs && node --check scripts/src/dev-base-image-preflight.mjs && node --check scripts/dev-evidence-blocker-aggregator.mjs && node --check scripts/src/dev-evidence-blocker-aggregator.mjs && node --check scripts/d601-k3s-readonly-observability.mjs && node --check scripts/src/d601-k3s-readonly-observability.mjs && node --check scripts/l2-runtime-contract-smoke.mjs && node --check scripts/patch-panel-runtime-smoke.mjs && node --check scripts/export-web-gate-summary.mjs && node --check scripts/l6-cli-web-smoke.mjs && node --check tools/hwlab-cli/bin/hwlab-cli.mjs && node --check tools/hwlab-cli/lib/cli.mjs && node --check web/hwlab-cloud-web/app.mjs && node --check web/hwlab-cloud-web/gate-summary.mjs && node --check web/hwlab-cloud-web/scripts/check.mjs && node --check web/hwlab-cloud-web/scripts/build.mjs && node scripts/validate-contract.mjs && node scripts/validate-dev-gate-report.mjs && node scripts/report-lifecycle.test.mjs && node scripts/validate-dev-m3-cardinality.mjs && node scripts/validate-artifact-catalog.mjs && node scripts/deploy-desired-state-plan.mjs --check && node scripts/dev-evidence-blocker-aggregator.mjs --check && node scripts/l2-runtime-contract-smoke.mjs && node scripts/l6-cli-web-smoke.mjs && node web/hwlab-cloud-web/scripts/check.mjs && node --test scripts/deploy-desired-state-plan.test.mjs scripts/src/dev-deploy-apply.test.mjs internal/agent/index.test.mjs internal/audit/index.test.mjs internal/db/schema.test.mjs internal/cloud/json-rpc.test.mjs internal/cloud/server.test.mjs internal/patchpanel/model.test.mjs internal/patchpanel/runtime.test.mjs && node scripts/cloud-api-runtime-smoke.mjs && sh -n scripts/bootstrap-skills.sh scripts/worker-entrypoint.sh",
|
||||||
"dev-base-image:preflight": "node scripts/preflight-dev-base-image.mjs",
|
"dev-base-image:preflight": "node scripts/preflight-dev-base-image.mjs",
|
||||||
"cloud-api:smoke": "node scripts/cloud-api-runtime-smoke.mjs",
|
"cloud-api:smoke": "node scripts/cloud-api-runtime-smoke.mjs",
|
||||||
"m1:smoke": "node scripts/m1-contract-smoke.mjs",
|
"m1:smoke": "node scripts/m1-contract-smoke.mjs",
|
||||||
"l2:smoke": "node scripts/l2-runtime-contract-smoke.mjs",
|
"l2:smoke": "node scripts/l2-runtime-contract-smoke.mjs",
|
||||||
"dev:evidence": "node scripts/dev-evidence-blocker-aggregator.mjs --pretty",
|
"dev:evidence": "node scripts/dev-evidence-blocker-aggregator.mjs --pretty",
|
||||||
"dev:evidence:check": "node scripts/dev-evidence-blocker-aggregator.mjs --check",
|
"dev:evidence:check": "node scripts/dev-evidence-blocker-aggregator.mjs --check",
|
||||||
|
"report:lifecycle": "node scripts/report-lifecycle.mjs",
|
||||||
"dev-edge:smoke": "node scripts/dev-edge-health-smoke.mjs",
|
"dev-edge:smoke": "node scripts/dev-edge-health-smoke.mjs",
|
||||||
"deploy:contract:plan": "node scripts/deploy-contract-plan.mjs --pretty",
|
"deploy:contract:plan": "node scripts/deploy-contract-plan.mjs --pretty",
|
||||||
"deploy:contract:check": "node scripts/deploy-contract-plan.mjs --check",
|
"deploy:contract:check": "node scripts/deploy-contract-plan.mjs --check",
|
||||||
@@ -30,6 +31,7 @@
|
|||||||
"dev-artifact:publish": "node scripts/dev-artifact-publish.mjs --publish",
|
"dev-artifact:publish": "node scripts/dev-artifact-publish.mjs --publish",
|
||||||
"docs:validate:m3-rollout": "node scripts/validate-m3-rollout-runbook.mjs",
|
"docs:validate:m3-rollout": "node scripts/validate-m3-rollout-runbook.mjs",
|
||||||
"d601:k3s:readonly": "node scripts/d601-k3s-readonly-observability.mjs",
|
"d601:k3s:readonly": "node scripts/d601-k3s-readonly-observability.mjs",
|
||||||
"runner:issue-visibility:preflight": "node scripts/runner-issue-visibility-preflight.mjs"
|
"runner:issue-visibility:preflight": "node scripts/runner-issue-visibility-preflight.mjs",
|
||||||
|
"report:lifecycle:test": "node scripts/report-lifecycle.test.mjs"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,235 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
import {
|
||||||
|
ACTIVE_DEV_BROWSER_ENDPOINT,
|
||||||
|
ACTIVE_DEV_PUBLIC_ENDPOINT,
|
||||||
|
REPORT_LIFECYCLE_VERSION,
|
||||||
|
deprecatedPublicEndpoints,
|
||||||
|
findForbiddenActiveDeprecatedEndpoints,
|
||||||
|
historicalReportLifecycle
|
||||||
|
} from "../internal/dev-report-lifecycle.mjs";
|
||||||
|
|
||||||
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||||
|
const defaultDeprecatedEndpoint = "http://74.48.78.17:6667";
|
||||||
|
const statusFields = ["status", "gateStatus", "conclusion"];
|
||||||
|
|
||||||
|
function usage() {
|
||||||
|
return [
|
||||||
|
"Usage:",
|
||||||
|
" node scripts/report-lifecycle.mjs validate <report.json>",
|
||||||
|
" node scripts/report-lifecycle.mjs invalidate <report.json> --reason <text> [--deprecated-endpoint <url>] [--replaced-by <path>] [--out <path>]",
|
||||||
|
"",
|
||||||
|
"Notes:",
|
||||||
|
" validate checks the active/historical endpoint lifecycle contract for one report.",
|
||||||
|
" invalidate marks one report historical/deprecated without changing evidence payloads.",
|
||||||
|
" --out defaults to in-place; use a historical/ path when keeping an old active filename reserved for regenerated evidence."
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(argv) {
|
||||||
|
if (argv.length === 0 || argv.includes("--help")) {
|
||||||
|
return { help: true };
|
||||||
|
}
|
||||||
|
const [command, reportPath, ...rest] = argv;
|
||||||
|
const args = {
|
||||||
|
command,
|
||||||
|
reportPath,
|
||||||
|
deprecatedEndpoint: defaultDeprecatedEndpoint,
|
||||||
|
reason: null,
|
||||||
|
replacedBy: null,
|
||||||
|
out: null
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let index = 0; index < rest.length; index += 1) {
|
||||||
|
const arg = rest[index];
|
||||||
|
if (arg === "--deprecated-endpoint") {
|
||||||
|
args.deprecatedEndpoint = rest[++index];
|
||||||
|
} else if (arg === "--reason") {
|
||||||
|
args.reason = rest[++index];
|
||||||
|
} else if (arg === "--replaced-by") {
|
||||||
|
args.replacedBy = rest[++index];
|
||||||
|
} else if (arg === "--out") {
|
||||||
|
args.out = rest[++index];
|
||||||
|
} else if (arg === "--help") {
|
||||||
|
args.help = true;
|
||||||
|
} else {
|
||||||
|
throw new Error(`unknown argument: ${arg}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.help || !args.command) return args;
|
||||||
|
if (!["validate", "invalidate"].includes(args.command)) {
|
||||||
|
throw new Error(`unknown command: ${args.command}`);
|
||||||
|
}
|
||||||
|
assert.ok(args.reportPath, "report path is required");
|
||||||
|
if (args.command === "invalidate") {
|
||||||
|
assert.ok(args.reason, "--reason is required for invalidate");
|
||||||
|
assert.ok(
|
||||||
|
deprecatedPublicEndpoints.includes(args.deprecatedEndpoint),
|
||||||
|
`--deprecated-endpoint must be one of ${deprecatedPublicEndpoints.join(", ")}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
function repoPath(inputPath) {
|
||||||
|
const absolutePath = path.resolve(process.cwd(), inputPath);
|
||||||
|
assert.ok(
|
||||||
|
absolutePath === repoRoot || absolutePath.startsWith(`${repoRoot}${path.sep}`),
|
||||||
|
`${inputPath} must stay inside the repository`
|
||||||
|
);
|
||||||
|
return absolutePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
function relativeRepoPath(inputPath) {
|
||||||
|
return path.relative(repoRoot, repoPath(inputPath)).split(path.sep).join("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertObject(value, label) {
|
||||||
|
assert.ok(value && typeof value === "object" && !Array.isArray(value), `${label} must be an object`);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 validateLifecycle(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 present :6666/:6667 as current public endpoint evidence`
|
||||||
|
);
|
||||||
|
return "active";
|
||||||
|
}
|
||||||
|
|
||||||
|
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`
|
||||||
|
);
|
||||||
|
return "historical";
|
||||||
|
}
|
||||||
|
|
||||||
|
function primaryStatus(report) {
|
||||||
|
return report.status ?? report.gateStatus ?? report.conclusion ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function markNonGreen(report) {
|
||||||
|
let hasTopLevelStatus = false;
|
||||||
|
for (const field of statusFields) {
|
||||||
|
if (Object.hasOwn(report, field)) {
|
||||||
|
hasTopLevelStatus = true;
|
||||||
|
if (["pass", "ready", "green", "published"].includes(report[field])) {
|
||||||
|
report[field] = "blocked";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!hasTopLevelStatus) {
|
||||||
|
report.status = "blocked";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendInvalidationBlocker(report, reason, deprecatedEndpoint) {
|
||||||
|
if (!Array.isArray(report.blockers)) {
|
||||||
|
report.blockers = [];
|
||||||
|
}
|
||||||
|
const scope = "deprecated-public-endpoint";
|
||||||
|
const exists = report.blockers.some((blocker) => blocker?.scope === scope);
|
||||||
|
if (!exists) {
|
||||||
|
report.blockers.push({
|
||||||
|
type: "environment_blocker",
|
||||||
|
scope,
|
||||||
|
status: "open",
|
||||||
|
summary: `${reason} Deprecated endpoint ${deprecatedEndpoint} cannot be used as active DEV evidence.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readReport(reportPath) {
|
||||||
|
const absolutePath = repoPath(reportPath);
|
||||||
|
const raw = await readFile(absolutePath, "utf8");
|
||||||
|
return { absolutePath, raw, report: JSON.parse(raw) };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function validateCommand(args) {
|
||||||
|
const relativePath = relativeRepoPath(args.reportPath);
|
||||||
|
const { raw, report } = await readReport(args.reportPath);
|
||||||
|
const state = validateLifecycle(report, raw, relativePath);
|
||||||
|
process.stdout.write(`${JSON.stringify({
|
||||||
|
ok: true,
|
||||||
|
path: relativePath,
|
||||||
|
state,
|
||||||
|
primaryStatus: primaryStatus(report)
|
||||||
|
})}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function invalidateCommand(args) {
|
||||||
|
const inputRelativePath = relativeRepoPath(args.reportPath);
|
||||||
|
const { report } = await readReport(args.reportPath);
|
||||||
|
assertObject(report, inputRelativePath);
|
||||||
|
|
||||||
|
report.reportLifecycle = historicalReportLifecycle({
|
||||||
|
deprecatedEndpoint: args.deprecatedEndpoint,
|
||||||
|
summary: `Historical/deprecated after DEV public endpoint change: ${args.reason}`
|
||||||
|
});
|
||||||
|
if (args.replacedBy) {
|
||||||
|
report.reportLifecycle.replacedBy = relativeRepoPath(args.replacedBy);
|
||||||
|
}
|
||||||
|
markNonGreen(report);
|
||||||
|
appendInvalidationBlocker(report, args.reason, args.deprecatedEndpoint);
|
||||||
|
|
||||||
|
const outPath = args.out ? repoPath(args.out) : repoPath(args.reportPath);
|
||||||
|
await mkdir(path.dirname(outPath), { recursive: true });
|
||||||
|
const nextRaw = `${JSON.stringify(report, null, 2)}\n`;
|
||||||
|
validateLifecycle(report, nextRaw, inputRelativePath);
|
||||||
|
await writeFile(outPath, nextRaw);
|
||||||
|
process.stdout.write(`${JSON.stringify({
|
||||||
|
ok: true,
|
||||||
|
input: inputRelativePath,
|
||||||
|
output: path.relative(repoRoot, outPath).split(path.sep).join("/"),
|
||||||
|
state: "historical",
|
||||||
|
deprecatedEndpoint: args.deprecatedEndpoint,
|
||||||
|
replacedBy: report.reportLifecycle.replacedBy ?? null
|
||||||
|
})}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const args = parseArgs(process.argv.slice(2));
|
||||||
|
if (args.help || !args.command) {
|
||||||
|
process.stdout.write(`${usage()}\n`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (args.command === "validate") {
|
||||||
|
await validateCommand(args);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (args.command === "invalidate") {
|
||||||
|
await invalidateCommand(args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await main();
|
||||||
|
} catch (error) {
|
||||||
|
process.stderr.write(`[report-lifecycle] ${error instanceof Error ? error.message : String(error)}\n`);
|
||||||
|
process.exitCode = 1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
|
||||||
|
import {
|
||||||
|
DEPRECATED_DEV_PUBLIC_ENDPOINT,
|
||||||
|
DEPRECATED_RESERVED_PROD_ENDPOINT,
|
||||||
|
findDeprecatedPublicEndpoints,
|
||||||
|
findForbiddenActiveDeprecatedEndpoints
|
||||||
|
} from "../internal/dev-report-lifecycle.mjs";
|
||||||
|
|
||||||
|
const activeReport = {
|
||||||
|
reportLifecycle: {
|
||||||
|
state: "active"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const activeReportWithLegacyException = {
|
||||||
|
reportLifecycle: {
|
||||||
|
state: "active"
|
||||||
|
},
|
||||||
|
runtimeSmoke: {
|
||||||
|
legacyPublicEndpoint: {
|
||||||
|
endpoint: DEPRECATED_DEV_PUBLIC_ENDPOINT,
|
||||||
|
status: "deprecated",
|
||||||
|
activeGreenEligible: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
findDeprecatedPublicEndpoints("current public endpoint http://74.48.78.17:6667/health"),
|
||||||
|
[DEPRECATED_DEV_PUBLIC_ENDPOINT]
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
findDeprecatedPublicEndpoints("old public :6666 evidence"),
|
||||||
|
[DEPRECATED_RESERVED_PROD_ENDPOINT]
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
findDeprecatedPublicEndpoints("http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667 is an internal k3s service URL"),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
findDeprecatedPublicEndpoints('"port": 6667, "note": "Internal k3s service port only"'),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
findForbiddenActiveDeprecatedEndpoints(activeReport, "current public :6667 endpoint"),
|
||||||
|
[DEPRECATED_DEV_PUBLIC_ENDPOINT]
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
findForbiddenActiveDeprecatedEndpoints(activeReportWithLegacyException, "legacy public :6667 endpoint is deprecated"),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
findForbiddenActiveDeprecatedEndpoints({ reportLifecycle: { state: "historical" } }, "current public :6667 endpoint"),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("validated report lifecycle endpoint scanner");
|
||||||
@@ -4,6 +4,11 @@ import { readFile } from "node:fs/promises";
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
import {
|
||||||
|
findForbiddenActiveDeprecatedEndpoints,
|
||||||
|
REPORT_LIFECYCLE_VERSION
|
||||||
|
} from "../internal/dev-report-lifecycle.mjs";
|
||||||
|
|
||||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.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");
|
const reportPath = path.join(repoRoot, "reports/dev-gate/dev-m2-deploy-smoke-active.json");
|
||||||
|
|
||||||
@@ -26,10 +31,22 @@ function assertTimestamp(value, label) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const report = JSON.parse(await readFile(reportPath, "utf8"));
|
const raw = await readFile(reportPath, "utf8");
|
||||||
|
const report = JSON.parse(raw);
|
||||||
assertObject(report, "report");
|
assertObject(report, "report");
|
||||||
assert.equal(report.issue, "pikasTech/HWLAB#23");
|
assert.equal(report.issue, "pikasTech/HWLAB#23");
|
||||||
assert.equal(report.taskId, "m2-dev-deploy-smoke");
|
assert.equal(report.taskId, "m2-dev-deploy-smoke");
|
||||||
|
assertObject(report.reportLifecycle, "reportLifecycle");
|
||||||
|
assert.equal(report.reportLifecycle.version, REPORT_LIFECYCLE_VERSION);
|
||||||
|
assert.equal(report.reportLifecycle.state, "active");
|
||||||
|
assert.equal(report.reportLifecycle.activeEndpoint, "http://74.48.78.17:16667");
|
||||||
|
assert.equal(report.reportLifecycle.activeBrowserEndpoint, "http://74.48.78.17:16666");
|
||||||
|
assert.equal(report.reportLifecycle.deprecatedEndpoint, null);
|
||||||
|
assert.deepEqual(
|
||||||
|
findForbiddenActiveDeprecatedEndpoints(report, raw),
|
||||||
|
[],
|
||||||
|
"active M2 report must not present deprecated public ports as current endpoint evidence"
|
||||||
|
);
|
||||||
assert.equal(report.status, "pass");
|
assert.equal(report.status, "pass");
|
||||||
assert.equal(report.endpoint, "http://74.48.78.17:16667");
|
assert.equal(report.endpoint, "http://74.48.78.17:16667");
|
||||||
assert.equal(report.frontendEndpoint, "http://74.48.78.17:16666");
|
assert.equal(report.frontendEndpoint, "http://74.48.78.17:16666");
|
||||||
|
|||||||
Reference in New Issue
Block a user