From b6d5ef900fe647808a4899ca0a109be13c8b608e Mon Sep 17 00:00:00 2001 From: Code Queue Review Date: Fri, 22 May 2026 18:23:21 +0000 Subject: [PATCH] fix: derive workbench smoke report identity --- .../dev-gate/dev-cloud-workbench-live.json | 34 +++- scripts/src/dev-cloud-workbench-smoke-lib.mjs | 145 +++++++++++++++++- scripts/validate-dev-gate-report.mjs | 58 +++++++ 3 files changed, 230 insertions(+), 7 deletions(-) diff --git a/reports/dev-gate/dev-cloud-workbench-live.json b/reports/dev-gate/dev-cloud-workbench-live.json index cc15e2e1..40578dbb 100644 --- a/reports/dev-gate/dev-cloud-workbench-live.json +++ b/reports/dev-gate/dev-cloud-workbench-live.json @@ -5,7 +5,7 @@ "status": "pass", "issue": "pikasTech/HWLAB#7", "taskId": "dev-cloud-workbench-live", - "commitId": "ab252f5", + "commitId": "unknown", "acceptanceLevel": "dev_cloud_workbench_live", "devOnly": true, "prodDisabled": true, @@ -32,9 +32,33 @@ ], "mode": "live", "url": "http://74.48.78.17:16666/", - "generatedAt": "2026-05-22T17:52:04.341Z", + "generatedAt": "2026-05-22T18:11:35.955Z", "evidenceLevel": "DEV-LIVE-BROWSER", "devLive": true, + "sourceIdentity": { + "status": "observed", + "source": "git-head", + "commitId": "ee9d60f00b1bc47c1d99b7edb8746623333fe544", + "shortCommitId": "ee9d60f00b1b", + "ref": "test/deployed-workbench-journey-evidence", + "worktreeState": "dirty", + "dirty": true, + "reportCommitId": "unknown", + "summary": "Source HEAD was observed, but the worktree was not cleanly attributable when the report was generated; top-level commitId is unknown rather than pretending to identify uncommitted source." + }, + "runtimeIdentity": { + "status": "observed", + "source": "health-live", + "endpoint": "http://74.48.78.17:16667/health/live", + "serviceId": "hwlab-cloud-api", + "environment": "dev", + "healthStatus": "degraded", + "commitId": "c7de474", + "commitSource": "runtime-env", + "imageTag": "c7de474", + "observedAt": "2026-05-22T18:11:24.844Z", + "summary": "Live runtime identity was observed through the existing read-only health endpoint and is not inferred from source git HEAD." + }, "sourceContract": { "status": "pass", "documents": [ @@ -139,7 +163,7 @@ "status=completed", "provider=openai-responses", "model=gpt-5.5", - "traceId=trc_cloud-web-workbench-1779472320347-11", + "traceId=trc_cloud-web-workbench-1779473490624-11", "uiStatus=已回复" ], "observations": { @@ -164,7 +188,7 @@ "provider": "openai-responses", "model": "gpt-5.5", "backend": "hwlab-cloud-api/openai-responses", - "traceId": "trc_cloud-web-workbench-1779472320347-11", + "traceId": "trc_cloud-web-workbench-1779473490624-11", "hasReply": true, "error": null }, @@ -190,7 +214,7 @@ "provider": "openai-responses", "model": "gpt-5.5", "backend": "hwlab-cloud-api/openai-responses", - "traceId": "trc_cloud-web-workbench-1779472320347-11", + "traceId": "trc_cloud-web-workbench-1779473490624-11", "hasReply": true, "error": null } diff --git a/scripts/src/dev-cloud-workbench-smoke-lib.mjs b/scripts/src/dev-cloud-workbench-smoke-lib.mjs index 8ac13aff..f176d730 100644 --- a/scripts/src/dev-cloud-workbench-smoke-lib.mjs +++ b/scripts/src/dev-cloud-workbench-smoke-lib.mjs @@ -1,4 +1,5 @@ import fs from "node:fs"; +import { execFileSync } from "node:child_process"; import http from "node:http"; import path from "node:path"; import { fileURLToPath } from "node:url"; @@ -328,6 +329,7 @@ function runStaticSmoke() { async function runLiveSmoke(args) { const checks = []; const blockers = []; + const runtimeIdentity = await observeLiveRuntimeIdentity(runtime.endpoints.api); const http = await fetchText(args.url); addCheck(checks, blockers, "live-http-html", http.ok && http.status === 200 && /text\/html/u.test(http.contentType) && liveHtmlLooksLikeWorkbench(http.body), "Live URL serves the Cloud Workbench HTML.", { blocker: "runtime_blocker", @@ -375,6 +377,7 @@ async function runLiveSmoke(args) { blockers, evidenceLevel: status === "pass" ? "DEV-LIVE-BROWSER" : "BLOCKED", devLive: status === "pass", + runtimeIdentity, safety: { prodTouched: false, servicesRestarted: false, @@ -388,7 +391,8 @@ async function runLiveSmoke(args) { }); } -function baseReport({ mode, status, checks, blockers, evidenceLevel, devLive, help = undefined, safety, url = null }) { +function baseReport({ mode, status, checks, blockers, evidenceLevel, devLive, help = undefined, safety, url = null, runtimeIdentity = null }) { + const sourceIdentity = observeSourceIdentity(); return { $schema: "https://hwlab.pikastech.local/schemas/dev-gate-report.schema.json", $id: "https://hwlab.pikastech.local/reports/dev-gate/dev-cloud-workbench-live.json", @@ -396,7 +400,7 @@ function baseReport({ mode, status, checks, blockers, evidenceLevel, devLive, he status, issue: "pikasTech/HWLAB#7", taskId: "dev-cloud-workbench-live", - commitId: "ab252f5", + commitId: sourceIdentity.reportCommitId, acceptanceLevel: "dev_cloud_workbench_live", devOnly: true, prodDisabled: true, @@ -426,6 +430,8 @@ function baseReport({ mode, status, checks, blockers, evidenceLevel, devLive, he generatedAt: new Date().toISOString(), evidenceLevel, devLive, + sourceIdentity, + runtimeIdentity: runtimeIdentity ?? notObservedRuntimeIdentity("live runtime identity was not observed in this smoke mode"), sourceContract: { status: "pass", documents: [ @@ -518,6 +524,65 @@ function readText(relativePath) { return fs.readFileSync(path.join(repoRoot, relativePath), "utf8"); } +function observeSourceIdentity() { + const head = runGit(["rev-parse", "--verify", "HEAD"]); + const shortHead = head ? runGit(["rev-parse", "--short=12", "HEAD"]) : null; + const ref = runGit(["rev-parse", "--abbrev-ref", "HEAD"]) ?? process.env.GITHUB_HEAD_REF ?? process.env.GITHUB_REF_NAME ?? "unknown"; + const statusOutput = runGit(["status", "--porcelain"]); + const envIdentity = head ? null : environmentCommitIdentity(); + const commitId = head ?? envIdentity?.commitId ?? "unknown"; + const shortCommitId = shortHead ?? envIdentity?.commitId.slice(0, 12) ?? "unknown"; + const dirty = statusOutput === null ? null : statusOutput.length > 0; + const worktreeState = dirty === null ? "unknown" : dirty ? "dirty" : "clean"; + const source = head ? "git-head" : envIdentity ? "environment" : "unknown"; + const reportCommitId = commitId !== "unknown" && worktreeState === "clean" ? shortCommitId : "unknown"; + + return { + status: commitId === "unknown" ? "unknown" : "observed", + source, + ...(envIdentity ? { environmentKey: envIdentity.key } : {}), + commitId, + shortCommitId, + ref, + worktreeState, + dirty, + reportCommitId, + summary: worktreeState !== "clean" + ? "Source HEAD was observed, but the worktree was not cleanly attributable when the report was generated; top-level commitId is unknown rather than pretending to identify uncommitted source." + : "Source identity was derived from the current worktree or source environment and is separate from the live runtime identity." + }; +} + +function environmentCommitIdentity() { + for (const key of [ + "GITHUB_SHA", + "CI_COMMIT_SHA", + "BUILDKITE_COMMIT", + "CODEBUILD_RESOLVED_SOURCE_VERSION", + "SOURCE_COMMIT", + "HWLAB_SOURCE_COMMIT_ID" + ]) { + const value = process.env[key]; + if (typeof value === "string" && /^[a-f0-9]{7,40}$/iu.test(value.trim())) { + return { key, commitId: value.trim().toLowerCase() }; + } + } + return null; +} + +function runGit(args) { + try { + return execFileSync("git", args, { + cwd: repoRoot, + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], + timeout: 5000 + }).trim(); + } catch { + return null; + } +} + function assetsExist() { return requiredWebAssets.every((file) => fs.existsSync(path.join(webRoot, file))); } @@ -907,6 +972,82 @@ function staticSafety() { }; } +async function observeLiveRuntimeIdentity(apiEndpoint) { + const endpoint = new URL("/health/live", apiEndpoint).toString(); + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), 6000); + try { + const response = await fetch(endpoint, { + signal: controller.signal, + headers: { Accept: "application/json" } + }); + let body = null; + try { + body = await response.json(); + } catch { + body = null; + } + if (!response.ok || !body || typeof body !== "object") { + return notObservedRuntimeIdentity(`health-live returned HTTP ${response.status}`, endpoint); + } + return { + status: "observed", + source: "health-live", + endpoint, + serviceId: sanitizeRuntimeString(body.serviceId), + environment: sanitizeRuntimeString(body.environment), + healthStatus: sanitizeRuntimeString(body.status), + commitId: sanitizeRevision(body.commit?.id ?? body.commitId), + commitSource: sanitizeRuntimeString(body.commit?.source), + imageTag: sanitizeRuntimeString(body.image?.tag), + observedAt: sanitizeTimestamp(body.observedAt), + summary: "Live runtime identity was observed through the existing read-only health endpoint and is not inferred from source git HEAD." + }; + } catch (error) { + return notObservedRuntimeIdentity( + error.name === "AbortError" ? "health-live request timed out" : `health-live request failed: ${oneLine(error.message)}`, + endpoint + ); + } finally { + clearTimeout(timer); + } +} + +function notObservedRuntimeIdentity(reason, endpoint = new URL("/health/live", runtime.endpoints.api).toString()) { + return { + status: "not_observed", + source: "health-live", + endpoint, + serviceId: "not_observed", + environment: "not_observed", + healthStatus: "not_observed", + commitId: "not_observed", + commitSource: "not_observed", + imageTag: "not_observed", + observedAt: new Date().toISOString(), + reason: oneLine(reason), + summary: "Live runtime identity is recorded as not_observed; source identity must not be treated as the deployed runtime revision." + }; +} + +function sanitizeRevision(value) { + if (typeof value !== "string") return "unknown"; + const trimmed = value.trim().toLowerCase(); + return /^[a-f0-9]{7,40}$/u.test(trimmed) ? trimmed : "unknown"; +} + +function sanitizeTimestamp(value) { + return typeof value === "string" && !Number.isNaN(Date.parse(value)) ? value : new Date().toISOString(); +} + +function sanitizeRuntimeString(value) { + return typeof value === "string" && value.trim().length > 0 ? oneLine(value) : "unknown"; +} + +function oneLine(value) { + return String(value ?? "unknown").replace(/\s+/gu, " ").trim().slice(0, 240) || "unknown"; +} + async function fetchText(url) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), 6000); diff --git a/scripts/validate-dev-gate-report.mjs b/scripts/validate-dev-gate-report.mjs index a6c57cc3..a8b1cb04 100644 --- a/scripts/validate-dev-gate-report.mjs +++ b/scripts/validate-dev-gate-report.mjs @@ -1311,6 +1311,8 @@ async function validateDevCloudWorkbenchLiveReport(report, label) { "url", "evidenceLevel", "devLive", + "sourceIdentity", + "runtimeIdentity", "sourceContract", "validationCommands", "localSmoke", @@ -1339,6 +1341,7 @@ async function validateDevCloudWorkbenchLiveReport(report, label) { assert.equal(report.url, "http://74.48.78.17:16666/", `${label}.url`); assert.equal(report.evidenceLevel, report.status === "pass" ? "DEV-LIVE-BROWSER" : "BLOCKED", `${label}.evidenceLevel`); assert.equal(report.devLive, report.status === "pass", `${label}.devLive`); + assertDevCloudWorkbenchIdentity(report, label); assertObject(report.sourceContract, `${label}.sourceContract`); assertStatus(report.sourceContract.status, `${label}.sourceContract.status`); @@ -1455,6 +1458,61 @@ async function validateDevCloudWorkbenchLiveReport(report, label) { assertString(report.safety.statement, `${label}.safety.statement`); } +function assertDevCloudWorkbenchIdentity(report, label) { + assertObject(report.sourceIdentity, `${label}.sourceIdentity`); + for (const field of ["status", "source", "commitId", "shortCommitId", "ref", "worktreeState", "reportCommitId", "summary"]) { + assert.ok(Object.hasOwn(report.sourceIdentity, field), `${label}.sourceIdentity missing ${field}`); + assertString(report.sourceIdentity[field], `${label}.sourceIdentity.${field}`); + } + assert.ok(["observed", "unknown"].includes(report.sourceIdentity.status), `${label}.sourceIdentity.status`); + assert.ok(["git-head", "environment", "unknown"].includes(report.sourceIdentity.source), `${label}.sourceIdentity.source`); + assert.match(report.sourceIdentity.commitId, /^([a-f0-9]{7,40}|unknown)$/, `${label}.sourceIdentity.commitId`); + assert.match(report.sourceIdentity.shortCommitId, /^([a-f0-9]{7,40}|unknown)$/, `${label}.sourceIdentity.shortCommitId`); + assert.ok(["clean", "dirty", "unknown"].includes(report.sourceIdentity.worktreeState), `${label}.sourceIdentity.worktreeState`); + if (report.sourceIdentity.source === "environment") { + assertString(report.sourceIdentity.environmentKey, `${label}.sourceIdentity.environmentKey`); + } + if (report.sourceIdentity.worktreeState === "clean" && report.sourceIdentity.commitId !== "unknown") { + assert.match(report.sourceIdentity.reportCommitId, /^[a-f0-9]{7,40}$/, `${label}.sourceIdentity.reportCommitId`); + assert.equal(report.commitId, report.sourceIdentity.reportCommitId, `${label}.commitId must match clean source reportCommitId`); + } else { + assert.equal(report.sourceIdentity.reportCommitId, "unknown", `${label}.sourceIdentity.reportCommitId`); + assert.equal(report.commitId, "unknown", `${label}.commitId must be unknown when source worktree identity is dirty or unknown`); + } + if (report.sourceIdentity.dirty !== null) { + assertBoolean(report.sourceIdentity.dirty, `${label}.sourceIdentity.dirty`); + assert.equal(report.sourceIdentity.dirty, report.sourceIdentity.worktreeState === "dirty", `${label}.sourceIdentity.dirty`); + } + + assertObject(report.runtimeIdentity, `${label}.runtimeIdentity`); + for (const field of ["status", "source", "endpoint", "serviceId", "environment", "healthStatus", "commitId", "commitSource", "imageTag", "observedAt", "summary"]) { + assert.ok(Object.hasOwn(report.runtimeIdentity, field), `${label}.runtimeIdentity missing ${field}`); + assertString(report.runtimeIdentity[field], `${label}.runtimeIdentity.${field}`); + } + assert.ok(["observed", "not_observed"].includes(report.runtimeIdentity.status), `${label}.runtimeIdentity.status`); + assert.equal(report.runtimeIdentity.source, "health-live", `${label}.runtimeIdentity.source`); + assert.equal(report.runtimeIdentity.endpoint, "http://74.48.78.17:16667/health/live", `${label}.runtimeIdentity.endpoint`); + assertTimestamp(report.runtimeIdentity.observedAt, `${label}.runtimeIdentity.observedAt`); + assert.match(report.runtimeIdentity.commitId, /^([a-f0-9]{7,40}|unknown|not_observed)$/, `${label}.runtimeIdentity.commitId`); + if (report.runtimeIdentity.status === "not_observed") { + assert.equal(report.runtimeIdentity.commitId, "not_observed", `${label}.runtimeIdentity.commitId`); + assertString(report.runtimeIdentity.reason, `${label}.runtimeIdentity.reason`); + } else { + assert.ok( + ["hwlab-cloud-api", "unknown"].includes(report.runtimeIdentity.serviceId), + `${label}.runtimeIdentity.serviceId` + ); + assert.ok(["dev", "unknown"].includes(report.runtimeIdentity.environment), `${label}.runtimeIdentity.environment`); + } + assert.ok( + report.sourceIdentity.commitId === "unknown" || + report.runtimeIdentity.commitId === "unknown" || + report.runtimeIdentity.commitId === "not_observed" || + report.runtimeIdentity.commitSource !== "source-git-head", + `${label}.runtimeIdentity must not present source git HEAD as the deployed live revision` + ); +} + async function validateDevM3Report(report, label) { for (const field of [ "$schema",