fix: derive workbench smoke report identity
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user