822 lines
42 KiB
TypeScript
822 lines
42 KiB
TypeScript
import { activeV02PipelineRuns, g14ObservabilityQueryAssertion, gitMirrorFlushJobManifest, gitMirrorStatusSummary, gitMirrorSyncJobManifest, gitMirrorV02SyncRequirement, hwlabG14Help, hwlabG14MonitorStateFileName, parseGitMirrorStatusRefs, parseK8sCpuMillicores, parseK8sMemoryMiB, parsePipelineTaskRunMetrics, parseV02TriggerSnapshot, rolloutRecordBody, semanticChangelogBullets, summarizeV02CdStatus, v02CloseoutVerdict, v02CommitAlignment, v02ControlPlaneRefreshScriptHash, v02ControlPlaneRenderScript, v02ExistingPipelineRunReuseDecision, v02FalseGreenGuard, v02GitMirrorPreSyncWaitMs, v02LatestOnlyTargetValidation, v02PipelineServiceIds, v02PrAutomationCommentBody, v02ReusableGitMirrorPreSyncMarker, v02ReusableRefreshMarker, v02StatusHistoryPolicy, v02TaskRunPerformanceSummary } from "./src/hwlab-g14";
|
|
import { runCommand } from "./src/command";
|
|
|
|
function assertCondition(condition: unknown, message: string, detail: unknown = {}): void {
|
|
if (!condition) throw new Error(`${message}: ${JSON.stringify(detail)}`);
|
|
}
|
|
|
|
function record(value: unknown): Record<string, unknown> {
|
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
|
}
|
|
|
|
assertCondition(
|
|
hwlabG14MonitorStateFileName({ once: false, dryRun: false }) === "latest-monitor-job.json",
|
|
"long-running monitor should own latest-monitor-job.json",
|
|
);
|
|
assertCondition(
|
|
hwlabG14MonitorStateFileName({ once: true, dryRun: false }) === "latest-once-job.json",
|
|
"once jobs must not overwrite the long-running monitor pointer",
|
|
);
|
|
assertCondition(
|
|
hwlabG14MonitorStateFileName({ once: false, dryRun: true }) === "latest-dry-run-job.json",
|
|
"dry-run monitor jobs must not overwrite the live monitor pointer",
|
|
);
|
|
assertCondition(
|
|
hwlabG14MonitorStateFileName({ once: true, dryRun: true }) === "latest-once-dry-run-job.json",
|
|
"once dry-runs need a distinct pointer for low-noise diagnostics",
|
|
);
|
|
assertCondition(
|
|
hwlabG14MonitorStateFileName({ lane: "v02", once: false, dryRun: false }) === "latest-v02-monitor-job.json"
|
|
&& hwlabG14MonitorStateFileName({ lane: "v02", once: true, dryRun: false }) === "latest-v02-once-job.json"
|
|
&& hwlabG14MonitorStateFileName({ lane: "v02", once: false, dryRun: true }) === "latest-v02-dry-run-job.json"
|
|
&& hwlabG14MonitorStateFileName({ lane: "v02", once: true, dryRun: true }) === "latest-v02-once-dry-run-job.json",
|
|
"v0.2 PR monitor state pointers must not overwrite the legacy G14 monitor pointers",
|
|
);
|
|
const hwlabHelpUsage = Array.isArray(hwlabG14Help().usage) ? hwlabG14Help().usage.map(String) : [];
|
|
assertCondition(
|
|
hwlabHelpUsage.some((line) => line.includes("control-plane status --lane v02 --pipeline-run"))
|
|
&& hwlabHelpUsage.some((line) => line.includes("control-plane status --lane v02 --source-commit"))
|
|
&& hwlabHelpUsage.some((line) => line.includes("control-plane status --lane v02 --history"))
|
|
&& hwlabHelpUsage.some((line) => line.includes("control-plane closeout --lane v02 --pipeline-run"))
|
|
&& hwlabHelpUsage.some((line) => line.includes("control-plane closeout --lane v02 --source-commit"))
|
|
&& hwlabHelpUsage.some((line) => line.includes("control-plane cleanup-runs --lane v02 --pipeline-run"))
|
|
&& hwlabHelpUsage.some((line) => line.includes("control-plane cleanup-runs --lane v02 --source-commit")),
|
|
"v0.2 control-plane help must expose targeted PipelineRun/source-commit status, closeout inspection, and cleanup",
|
|
hwlabHelpUsage,
|
|
);
|
|
assertCondition(
|
|
hwlabHelpUsage.some((line) => line.includes("monitor-prs --lane v02"))
|
|
&& hwlabHelpUsage.some((line) => line.includes("monitor-prs --lane v02 --status"))
|
|
&& JSON.stringify(hwlabG14Help()).includes("v02-pr-comment-signatures.json")
|
|
&& JSON.stringify(hwlabG14Help()).includes("latest-only"),
|
|
"v0.2 PR monitor help must expose the auto CI/CD lane, status query, latest-only CD, and dedupe comment state",
|
|
hwlabG14Help(),
|
|
);
|
|
assertCondition(
|
|
hwlabHelpUsage.some((line) => line.includes("secret status --lane v02 --name hwlab-v02-openfga"))
|
|
&& hwlabHelpUsage.some((line) => line.includes("secret ensure --lane v02 --name hwlab-v02-openfga --confirm"))
|
|
&& hwlabHelpUsage.some((line) => line.includes("secret status --lane v02 --name hwlab-v02-master-server-admin-api-key"))
|
|
&& hwlabHelpUsage.some((line) => line.includes("secret ensure --lane v02 --name hwlab-v02-master-server-admin-api-key --confirm")),
|
|
"v0.2 secret help must expose controlled OpenFGA and master-server admin API key SecretRef bootstrap paths",
|
|
hwlabHelpUsage,
|
|
);
|
|
assertCondition(
|
|
hwlabHelpUsage.some((line) => line.includes("upstream-image status --name openfga --tag v1.17.0"))
|
|
&& hwlabHelpUsage.some((line) => line.includes("upstream-image ensure --name openfga --tag v1.17.0 --confirm")),
|
|
"v0.2 help must expose the controlled OpenFGA upstream image mirroring path",
|
|
hwlabHelpUsage,
|
|
);
|
|
assertCondition(
|
|
hwlabHelpUsage.some((line) => line.includes("observability query --promql 'up{namespace=\"hwlab-v02\"}' --expect-count 5 --expect-value 1"))
|
|
&& hwlabHelpUsage.some((line) => line.includes("observability targets --lane v02"))
|
|
&& hwlabHelpUsage.some((line) => line.includes("observability boundary --lane v02"))
|
|
&& hwlabHelpUsage.some((line) => line.includes("observability closeout --lane v02")),
|
|
"observability help must expose assertion, target, boundary, and closeout entrypoints",
|
|
hwlabHelpUsage,
|
|
);
|
|
|
|
const observabilityAssertionPass = g14ObservabilityQueryAssertion({
|
|
status: "success",
|
|
data: {
|
|
resultType: "vector",
|
|
result: [
|
|
{ metric: { job: "hwlab-cloud-api", namespace: "hwlab-v02", pod: "api-pod", hwlab_pikastech_local_service_id: "hwlab-cloud-api" }, value: [1, "1"] },
|
|
{ metric: { job: "hwlab-cloud-web", namespace: "hwlab-v02", pod: "web-pod", hwlab_pikastech_local_service_id: "hwlab-cloud-web" }, value: [1, "1.0"] },
|
|
],
|
|
},
|
|
}, 2, "1");
|
|
const observabilityAssertionFail = g14ObservabilityQueryAssertion({
|
|
status: "success",
|
|
data: {
|
|
resultType: "vector",
|
|
result: [
|
|
{ metric: { job: "hwlab-cloud-api", namespace: "hwlab-v02", pod: "api-pod", hwlab_pikastech_local_service_id: "hwlab-cloud-api" }, value: [1, "0"] },
|
|
],
|
|
},
|
|
}, 2, "1");
|
|
assertCondition(
|
|
record(observabilityAssertionPass).ok === true
|
|
&& record(observabilityAssertionPass).actualCount === 2
|
|
&& record(observabilityAssertionPass).valueOk === true,
|
|
"observability query assertion must pass matching vector count and numeric string values",
|
|
observabilityAssertionPass,
|
|
);
|
|
assertCondition(
|
|
record(observabilityAssertionFail).ok === false
|
|
&& record(observabilityAssertionFail).actualCount === 1
|
|
&& Array.isArray(record(observabilityAssertionFail).badValues)
|
|
&& Array.isArray(record(observabilityAssertionFail).missingSeries)
|
|
&& record(observabilityAssertionFail).badValueCount === 1,
|
|
"observability query assertion must report bad values and missing series instead of requiring manual vector inspection",
|
|
observabilityAssertionFail,
|
|
);
|
|
const unsupportedObservabilityOption = runCommand(["bun", "scripts/cli.ts", "hwlab", "g14", "observability", "query", "--not-a-real-option"], process.cwd(), { timeoutMs: 30_000 });
|
|
const unsupportedObservabilityJson = JSON.parse(unsupportedObservabilityOption.stdout) as unknown;
|
|
assertCondition(
|
|
unsupportedObservabilityOption.exitCode !== 0
|
|
&& record(unsupportedObservabilityJson).ok === false
|
|
&& String(record(record(unsupportedObservabilityJson).error).message ?? "").includes("unsupported observability option"),
|
|
"observability CLI must fail visibly on unsupported options instead of silently ignoring friction-prone flags",
|
|
unsupportedObservabilityJson,
|
|
);
|
|
assertCondition(
|
|
parseK8sCpuMillicores("46095136n") !== null
|
|
&& Math.abs((parseK8sCpuMillicores("46095136n") ?? 0) - 46.095136) < 0.000001
|
|
&& parseK8sCpuMillicores("47m") === 47
|
|
&& parseK8sCpuMillicores("1") === 1000,
|
|
"observability resource snapshot must convert metrics.k8s.io CPU quantities to millicores",
|
|
);
|
|
assertCondition(
|
|
parseK8sMemoryMiB("99860Ki") !== null
|
|
&& Math.abs((parseK8sMemoryMiB("99860Ki") ?? 0) - 97.51953125) < 0.000001
|
|
&& parseK8sMemoryMiB("97Mi") === 97
|
|
&& parseK8sMemoryMiB("1048576") === 1,
|
|
"observability resource snapshot must convert metrics.k8s.io memory quantities to MiB",
|
|
);
|
|
|
|
const v02CommentBody = v02PrAutomationCommentBody({
|
|
pr: {
|
|
number: 848,
|
|
title: "迁移:v0.2 PR 合并后自动触发 CD",
|
|
url: "https://github.com/pikasTech/HWLAB/pull/848",
|
|
baseRefName: "v0.2",
|
|
headRefName: "fix/v02-auto-cd",
|
|
},
|
|
phase: "cd-passed",
|
|
state: "cd-succeeded",
|
|
startedAt: "2026-06-04T00:00:00.000Z",
|
|
observedAt: "2026-06-04T00:07:30.000Z",
|
|
elapsedSeconds: 450,
|
|
preflight: {
|
|
conclusion: "ready",
|
|
readyForCommanderMerge: true,
|
|
mergeable: "MERGEABLE",
|
|
mergeStateStatus: "CLEAN",
|
|
blockers: [],
|
|
pending: [],
|
|
},
|
|
sourceCommit: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
pipelineRun: "hwlab-v02-ci-poll-aaaaaaaaaaaa",
|
|
cd: {
|
|
pipelineStatus: "True",
|
|
pipelineReason: "Succeeded",
|
|
targetValidationState: "passed",
|
|
argoSync: "Synced",
|
|
argoHealth: "Healthy",
|
|
webAssetsOk: true,
|
|
pendingFlush: false,
|
|
githubInSync: true,
|
|
rolloutServices: ["hwlab-cloud-api"],
|
|
buildServices: [],
|
|
reusedServices: ["hwlab-cloud-web"],
|
|
},
|
|
flush: { ok: true },
|
|
});
|
|
assertCondition(
|
|
v02CommentBody.includes("elapsed: 7m30s")
|
|
&& v02CommentBody.includes("conflict: `clear-or-unknown`")
|
|
&& v02CommentBody.includes("hwlab-v02-ci-poll-aaaaaaaaaaaa")
|
|
&& v02CommentBody.includes("pendingFlush=`false`")
|
|
&& v02CommentBody.includes("targetValidation: `passed`"),
|
|
"v0.2 PR automation comments must include duration, conflict state, PipelineRun, target validation, and git mirror flush status",
|
|
v02CommentBody,
|
|
);
|
|
|
|
const gitMirrorJob = gitMirrorSyncJobManifest("git-mirror-hwlab-sync-manual-test");
|
|
assertCondition(gitMirrorJob.kind === "Job", "git mirror sync must be a manual Job, not a CronJob", gitMirrorJob);
|
|
assertCondition(record(gitMirrorJob.metadata).namespace === "devops-infra", "git mirror sync Job must target devops-infra", gitMirrorJob);
|
|
assertCondition(record(record(gitMirrorJob.metadata).labels)["hwlab.pikastech.local/trigger"] === "manual-cli", "git mirror sync Job must be labeled as manual CLI triggered", gitMirrorJob);
|
|
assertCondition(record(gitMirrorJob.spec).backoffLimit === 0, "git mirror sync Job should fail visibly instead of retrying in the background", gitMirrorJob);
|
|
|
|
const gitMirrorFlushJob = gitMirrorFlushJobManifest("git-mirror-hwlab-flush-manual-test");
|
|
assertCondition(gitMirrorFlushJob.kind === "Job", "git mirror flush must be a manual Job", gitMirrorFlushJob);
|
|
assertCondition(record(record(gitMirrorFlushJob.metadata).labels)["app.kubernetes.io/component"] === "flush-controller", "git mirror flush Job must be labeled separately from sync", gitMirrorFlushJob);
|
|
assertCondition(record(record(gitMirrorFlushJob.metadata).labels)["hwlab.pikastech.local/trigger"] === "manual-cli", "git mirror flush Job must be labeled as manual CLI triggered", gitMirrorFlushJob);
|
|
const flushTemplate = record(record(gitMirrorFlushJob.spec).template);
|
|
const flushPodSpec = record(flushTemplate.spec);
|
|
const flushContainer = record(Array.isArray(flushPodSpec.containers) ? flushPodSpec.containers[0] : null);
|
|
assertCondition(JSON.stringify(flushContainer.command) === JSON.stringify(["/script/flush.sh"]), "git mirror flush Job must run the flush script", gitMirrorFlushJob);
|
|
|
|
const gitMirrorStatusRaw = [
|
|
'lastSync={"ok":true}',
|
|
"lastWrite=",
|
|
"lastFlush=",
|
|
'refs={"refs":{"localV02":"0cf79ecc9d8a784b7712b0b3a58d5e39025ba0dc","githubV02":"0cf79ecc9d8a784b7712b0b3a58d5e39025ba0dc","localG14":"1111111111111111111111111111111111111111","githubG14":"1111111111111111111111111111111111111111","localGitops":"2222222222222222222222222222222222222222","githubGitops":"3333333333333333333333333333333333333333"},"pendingFlush":true}',
|
|
].join("\n");
|
|
const parsedGitMirrorRefs = parseGitMirrorStatusRefs(gitMirrorStatusRaw);
|
|
assertCondition(
|
|
parsedGitMirrorRefs.refs.localV02 === "0cf79ecc9d8a784b7712b0b3a58d5e39025ba0dc",
|
|
"git mirror status parser must expose local v0.2 ref for trigger pre-sync",
|
|
parsedGitMirrorRefs,
|
|
);
|
|
assertCondition(parsedGitMirrorRefs.pendingFlush === true, "git mirror status parser must preserve pending flush signal", parsedGitMirrorRefs);
|
|
assertCondition(
|
|
parsedGitMirrorRefs.refs.githubV02 === "0cf79ecc9d8a784b7712b0b3a58d5e39025ba0dc",
|
|
"git mirror status parser must expose GitHub source branch staging ref",
|
|
parsedGitMirrorRefs,
|
|
);
|
|
assertCondition(
|
|
gitMirrorV02SyncRequirement("0cf79ecc9d8a784b7712b0b3a58d5e39025ba0dc", gitMirrorStatusRaw).required === false,
|
|
"trigger-current must not sync mirror when local v0.2 already matches source commit",
|
|
);
|
|
assertCondition(
|
|
gitMirrorV02SyncRequirement("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", gitMirrorStatusRaw).required === true,
|
|
"trigger-current must sync mirror before creating PipelineRun when local v0.2 is stale",
|
|
);
|
|
const reusableGitMirrorPreSyncMarker = v02ReusableGitMirrorPreSyncMarker({
|
|
ok: true,
|
|
sourceCommit: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
syncedAt: "2026-06-04T16:00:10.000Z",
|
|
localV02: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
githubV02: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
}, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", Date.parse("2026-06-04T16:01:00.000Z"), Date.parse("2026-06-04T16:00:00.000Z"));
|
|
const preObservationGitMirrorMarker = v02ReusableGitMirrorPreSyncMarker({
|
|
ok: true,
|
|
sourceCommit: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
syncedAt: "2026-06-04T16:00:10.000Z",
|
|
}, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", Date.parse("2026-06-04T16:01:00.000Z"), Date.parse("2026-06-04T16:00:20.000Z"));
|
|
const staleGitMirrorPreSyncMarker = v02ReusableGitMirrorPreSyncMarker({
|
|
ok: true,
|
|
sourceCommit: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
syncedAt: "2026-06-04T16:00:10.000Z",
|
|
}, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", Date.parse("2026-06-04T16:03:00.000Z"), Date.parse("2026-06-04T16:00:00.000Z"));
|
|
assertCondition(
|
|
reusableGitMirrorPreSyncMarker !== null && preObservationGitMirrorMarker === null && staleGitMirrorPreSyncMarker === null,
|
|
"v0.2 git mirror pre-sync marker must only reuse fresh same-commit markers written after the stale status observation",
|
|
{ reusableGitMirrorPreSyncMarker, preObservationGitMirrorMarker, staleGitMirrorPreSyncMarker },
|
|
);
|
|
assertCondition(
|
|
v02GitMirrorPreSyncWaitMs(10) === 10_000
|
|
&& v02GitMirrorPreSyncWaitMs(180) === 120_000
|
|
&& v02GitMirrorPreSyncWaitMs(0) === 120_000,
|
|
"v0.2 git mirror pre-sync wait must not impose a hidden 30s minimum latency",
|
|
{ tenSeconds: v02GitMirrorPreSyncWaitMs(10), capped: v02GitMirrorPreSyncWaitMs(180), default: v02GitMirrorPreSyncWaitMs(0) },
|
|
);
|
|
const gitMirrorSummary = gitMirrorStatusSummary(gitMirrorStatusRaw);
|
|
assertCondition(
|
|
gitMirrorSummary.flushNeeded === true && gitMirrorSummary.flushCommand === "bun scripts/cli.ts hwlab g14 git-mirror flush --confirm",
|
|
"git mirror status summary must expose pending flush and the exact controlled flush command",
|
|
gitMirrorSummary,
|
|
);
|
|
assertCondition(gitMirrorSummary.githubInSync === false, "git mirror status summary must expose GitHub GitOps drift", gitMirrorSummary);
|
|
assertCondition(gitMirrorSummary.sourceInSync === true && gitMirrorSummary.gitopsInSync === false, "git mirror status must split source and gitops GitHub sync state", gitMirrorSummary);
|
|
const renderScript = v02ControlPlaneRenderScript("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
|
|
const renderScriptHash = v02ControlPlaneRefreshScriptHash("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
|
|
const sourceText = await Bun.file(new URL("./src/hwlab-g14.ts", import.meta.url)).text();
|
|
assertCondition(
|
|
renderScript.includes("git clone --shared --no-checkout \"$cicd_repo\" \"$worktree_dir\"")
|
|
&& renderScript.includes("git -C \"$worktree_dir\" checkout --detach \"$source_commit\"")
|
|
&& renderScript.includes("/tmp/hwlab-v02-control-plane-source-aaaaaaaaaaaa-"),
|
|
"v0.2 control-plane render must use an isolated temp clone from the CI/CD repo so same-commit concurrent triggers do not share worktree metadata",
|
|
renderScript,
|
|
);
|
|
assertCondition(
|
|
renderScript.includes("flock -w 120 9") && renderScript.includes("v0.2 CI/CD repo refresh failed status="),
|
|
"v0.2 CI/CD repo refresh must serialize only the shared bare repo fetch and expose lock/fetch failures",
|
|
renderScript,
|
|
);
|
|
const reusableRefreshMarker = v02ReusableRefreshMarker({
|
|
ok: true,
|
|
sourceCommit: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
refreshedAt: "2026-06-04T16:00:00.000Z",
|
|
renderScriptHash,
|
|
renderDir: "/tmp/hwlab-v02-control-plane-aaaaaaaaaaaa-contract",
|
|
}, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", renderScriptHash, Date.parse("2026-06-04T16:01:00.000Z"));
|
|
const staleRefreshMarker = v02ReusableRefreshMarker({
|
|
ok: true,
|
|
sourceCommit: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
refreshedAt: "2026-06-04T16:00:00.000Z",
|
|
renderScriptHash,
|
|
}, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", renderScriptHash, Date.parse("2026-06-04T16:10:01.000Z"));
|
|
const wrongScriptMarker = v02ReusableRefreshMarker({
|
|
ok: true,
|
|
sourceCommit: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
refreshedAt: "2026-06-04T16:00:00.000Z",
|
|
renderScriptHash: "old-script",
|
|
}, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", renderScriptHash, Date.parse("2026-06-04T16:01:00.000Z"));
|
|
assertCondition(
|
|
reusableRefreshMarker !== null && staleRefreshMarker === null && wrongScriptMarker === null,
|
|
"v0.2 control-plane refresh marker must only reuse recent markers for the same render script contract",
|
|
{ reusableRefreshMarker, staleRefreshMarker, wrongScriptMarker },
|
|
);
|
|
assertCondition(
|
|
renderScript.includes("cicd_repo='/root/hwlab-v02-cicd.git'") && !renderScript.includes("git -C /root/hwlab-v02") && !renderScript.includes("git checkout v0.2"),
|
|
"v0.2 control-plane render must not use the fixed workspace checkout or its clean status",
|
|
renderScript,
|
|
);
|
|
assertCondition(
|
|
sourceText.includes("function runG14K3sRemoteAsync")
|
|
&& sourceText.includes("label: \"v02-control-plane-apply\"")
|
|
&& sourceText.includes("remote async command timed out after")
|
|
&& sourceText.includes("return runG14K3sRemoteAsync({"),
|
|
"v0.2 control-plane apply must use short start/poll remote-async semantics instead of one long G14:k3s apply call",
|
|
);
|
|
const existingPipelineRunReuse = v02ExistingPipelineRunReuseDecision({
|
|
sourceCommit: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
before: { exists: true, status: "True" },
|
|
latestSourceCommit: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
});
|
|
const existingPipelineRunFailed = v02ExistingPipelineRunReuseDecision({
|
|
sourceCommit: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
before: { exists: true, status: "False" },
|
|
latestSourceCommit: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
});
|
|
const existingPipelineRunHeadAdvanced = v02ExistingPipelineRunReuseDecision({
|
|
sourceCommit: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
before: { exists: true, status: "True" },
|
|
latestSourceCommit: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
|
});
|
|
const existingPipelineRunHeadUnresolved = v02ExistingPipelineRunReuseDecision({
|
|
sourceCommit: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
before: { exists: true, status: "True" },
|
|
latestSourceCommit: null,
|
|
});
|
|
const triggerSnapshot = parseV02TriggerSnapshot({
|
|
ok: true,
|
|
command: ["test"],
|
|
exitCode: 0,
|
|
stdout: [
|
|
"sourceCommit\taaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
"pipelineRun\thwlab-v02-ci-poll-aaaaaaaaaaaa",
|
|
"pipelineRunExitCode\t0",
|
|
"status\tTrue",
|
|
"reason\tCompleted",
|
|
"message\tTasks Completed: 9",
|
|
"pipelineRunStderr\t",
|
|
].join("\n"),
|
|
stderr: "",
|
|
parsed: null,
|
|
}, Date.parse("2026-06-04T16:00:00.000Z"));
|
|
assertCondition(
|
|
existingPipelineRunReuse.reusable === true
|
|
&& existingPipelineRunReuse.alreadyUsable === true
|
|
&& existingPipelineRunFailed.reusable === true
|
|
&& existingPipelineRunFailed.alreadyUsable === false
|
|
&& existingPipelineRunHeadAdvanced.reusable === false
|
|
&& existingPipelineRunHeadAdvanced.reason === "source-head-advanced-before-existing-pipelinerun-reuse"
|
|
&& existingPipelineRunHeadUnresolved.reusable === false
|
|
&& existingPipelineRunHeadUnresolved.reason === "source-head-recheck-unresolved-before-existing-pipelinerun-reuse",
|
|
"trigger-current must recheck latest v0.2 head before reusing an existing PipelineRun",
|
|
{ existingPipelineRunReuse, existingPipelineRunFailed, existingPipelineRunHeadAdvanced, existingPipelineRunHeadUnresolved },
|
|
);
|
|
assertCondition(
|
|
triggerSnapshot.sourceCommit === "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
|
&& triggerSnapshot.pipelineRun === "hwlab-v02-ci-poll-aaaaaaaaaaaa"
|
|
&& record(triggerSnapshot.before).status === "True",
|
|
"trigger-current fast snapshot must parse source head and PipelineRun status in one G14:k3s route while latest head is checked locally",
|
|
triggerSnapshot,
|
|
);
|
|
assertCondition(
|
|
!v02PipelineServiceIds().includes("hwlab-cli")
|
|
&& !v02PipelineServiceIds().includes("hwlab-agent-mgr")
|
|
&& !v02PipelineServiceIds().includes("hwlab-agent-worker")
|
|
&& !v02PipelineServiceIds().includes("hwlab-device-pod"),
|
|
"v0.2 PipelineRun service matrix must exclude short-connection cli, removed HWLAB-owned code-agent control-plane services, and retired device-pod service",
|
|
v02PipelineServiceIds(),
|
|
);
|
|
assertCondition(
|
|
sourceText.includes("curl -fsS --connect-timeout 2 --max-time 10")
|
|
&& !sourceText.includes("curl -fsS --connect-timeout 2 --max-time 5"),
|
|
"v0.2 web asset probes must not use a 5s app.js timeout that creates false deployment failures",
|
|
);
|
|
assertCondition(
|
|
sourceText.includes("printf 'probeElapsedMs\\\\t%s\\\\n'")
|
|
&& sourceText.includes("probeElapsedMs: numericField(fields.probeElapsedMs)"),
|
|
"v0.2 web asset probes must expose probeElapsedMs for timeout diagnostics",
|
|
);
|
|
assertCondition(
|
|
sourceText.includes("probe_start_s=$(date +%s")
|
|
&& !sourceText.includes("date +%s%3N"),
|
|
"v0.2 web asset elapsed timing must avoid non-portable date +%s%3N in k3s probe shells",
|
|
);
|
|
assertCondition(
|
|
sourceText.includes("if (args.includes(\"--status\")) return monitorStatus(options);")
|
|
&& sourceText.indexOf("if (args.includes(\"--status\")) return monitorStatus(options);") < sourceText.indexOf("const command = [\"bun\", \"scripts/cli.ts\", \"hwlab\", \"g14\", \"monitor-prs\""),
|
|
"monitor-prs --status must be a read-only query before async monitor startJob",
|
|
);
|
|
assertCondition(
|
|
sourceText.includes("protectedLatestByPrefix")
|
|
&& sourceText.includes("protected-latest-pipelinerun"),
|
|
"control-plane cleanup-runs must protect the latest PipelineRun per lane by default",
|
|
);
|
|
|
|
const staleSuccessAlignment = v02CommitAlignment({
|
|
expectedSourceHead: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
sourceHeads: {
|
|
cicdRepo: "/root/hwlab-v02-cicd.git",
|
|
cicdSourceHead: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
originHead: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
workspace: { path: "/root/hwlab-v02", head: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", originHead: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", dirty: true, dirtyCount: 1 },
|
|
},
|
|
gitMirrorSummary: { localV02: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", githubV02: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", sourceInSync: false, gitopsInSync: true },
|
|
pipelineRun: { pipelineRun: "hwlab-v02-ci-poll-aaaaaaaaaaaa", status: null },
|
|
recentPipelineRuns: { items: [{ name: "hwlab-v02-ci-poll-bbbbbbbbbbbb", sourceCommit: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", status: "True", reason: "Completed" }] },
|
|
runtimeWorkloads: { items: [] },
|
|
webAssets: { apiRevision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" },
|
|
});
|
|
assertCondition(
|
|
staleSuccessAlignment.aligned === false
|
|
&& staleSuccessAlignment.state === "stale-success"
|
|
&& JSON.stringify(staleSuccessAlignment.staleReasons).includes("mirror-source-stale")
|
|
&& JSON.stringify(staleSuccessAlignment.workspaceWarnings).includes("workspace-dirty-but-isolated-from-cicd"),
|
|
"v0.2 commit alignment must call out stale-success while keeping dirty workspace isolated from CI source selection",
|
|
staleSuccessAlignment,
|
|
);
|
|
|
|
const latestOnlySuperseded = v02LatestOnlyTargetValidation({
|
|
targetMode: "source-commit",
|
|
sourceCommit: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
pipelineRun: { exists: true, status: "True", pipelineRun: "hwlab-v02-ci-poll-aaaaaaaaaaaa" },
|
|
commitAlignment: { staleReasons: ["origin-head-mismatch"], originHead: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" },
|
|
targetValidation: {
|
|
ok: false,
|
|
state: "failed",
|
|
failures: [{ reason: "runtime-service-source-mismatch", serviceId: "hwlab-cloud-api" }],
|
|
},
|
|
});
|
|
assertCondition(
|
|
latestOnlySuperseded.ok === true
|
|
&& latestOnlySuperseded.state === "superseded"
|
|
&& latestOnlySuperseded.latestOnlySuperseded === true
|
|
&& Array.isArray(latestOnlySuperseded.failures)
|
|
&& latestOnlySuperseded.failures.length === 0
|
|
&& JSON.stringify(latestOnlySuperseded.supersededFailures).includes("runtime-service-source-mismatch"),
|
|
"v0.2 latest-only target validation must close a succeeded stale source commit as superseded instead of failing runtime mismatch",
|
|
latestOnlySuperseded,
|
|
);
|
|
|
|
const latestOnlyStalePassed = v02LatestOnlyTargetValidation({
|
|
targetMode: "pipeline-run",
|
|
sourceCommit: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
pipelineRun: { exists: true, status: "True", pipelineRun: "hwlab-v02-ci-poll-aaaaaaaaaaaa" },
|
|
commitAlignment: { staleReasons: ["latest-pipelinerun-not-current"], latestPipelineSourceCommit: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" },
|
|
targetValidation: {
|
|
ok: true,
|
|
state: "passed",
|
|
failures: [],
|
|
},
|
|
});
|
|
assertCondition(
|
|
latestOnlyStalePassed.ok === true
|
|
&& latestOnlyStalePassed.state === "superseded"
|
|
&& latestOnlyStalePassed.latestOnlySuperseded === true
|
|
&& latestOnlyStalePassed.originalState === "passed"
|
|
&& Array.isArray(latestOnlyStalePassed.failures)
|
|
&& latestOnlyStalePassed.failures.length === 0,
|
|
"v0.2 latest-only target validation must mark stale successful historical runs as superseded instead of plain passed",
|
|
latestOnlyStalePassed,
|
|
);
|
|
|
|
const supersededCloseoutStatus = {
|
|
command: "hwlab g14 control-plane status --lane v02 --source-commit aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
sourceCommit: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
statusTarget: { mode: "source-commit", sourceCommit: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", pipelineRun: "hwlab-v02-ci-poll-aaaaaaaaaaaa" },
|
|
pipelineRun: { exists: true, pipelineRun: "hwlab-v02-ci-poll-aaaaaaaaaaaa", status: "True", reason: "Completed" },
|
|
targetValidation: { ok: true, state: "superseded", latestOnlySuperseded: true, latestOnlyReasons: ["origin-head-mismatch"], failures: [] },
|
|
sourceHeads: {
|
|
originHead: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
|
target: {
|
|
sourceCommit: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
objectExists: true,
|
|
ancestorOfOriginHead: true,
|
|
ancestorExitCode: 0,
|
|
},
|
|
},
|
|
commitAlignment: { originHead: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", staleReasons: ["origin-head-mismatch"] },
|
|
gitMirror: {
|
|
summary: {
|
|
pendingFlush: false,
|
|
githubInSync: true,
|
|
flushNeeded: false,
|
|
flushCommand: null,
|
|
lastFlush: { status: "flushed", at: "2026-06-04T00:00:00Z" },
|
|
},
|
|
},
|
|
argo: { fields: { syncStatus: "Synced", health: "Healthy", syncRevision: "gitopsbbbb", targetRevision: "v0.2-gitops", path: "deploy/gitops/g14/runtime-v02" } },
|
|
webAssets: { ok: true, summary: "19666/19667 React/Vite asset probes passed", apiRevision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" },
|
|
activePipelineRuns: [],
|
|
};
|
|
const supersededCloseout = v02CloseoutVerdict(supersededCloseoutStatus);
|
|
assertCondition(
|
|
supersededCloseout.ok === true
|
|
&& supersededCloseout.closeable === true
|
|
&& supersededCloseout.state === "superseded"
|
|
&& record(record(supersededCloseout.latestOnly).ancestorCheck).ancestorOfLatest === true
|
|
&& String(supersededCloseout.issueCommentMarkdown).includes("ancestorOfLatest=`true`")
|
|
&& String(record(supersededCloseout.recommendedNext).action) === "comment-and-close-issue",
|
|
"v0.2 closeout verdict must close historical successful runs only after ancestor, Argo, and git mirror checks pass",
|
|
supersededCloseout,
|
|
);
|
|
|
|
const unresolvedAncestorCloseout = v02CloseoutVerdict({
|
|
...supersededCloseoutStatus,
|
|
sourceHeads: { originHead: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", target: { sourceCommit: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", objectExists: true, ancestorOfOriginHead: null } },
|
|
});
|
|
assertCondition(
|
|
unresolvedAncestorCloseout.closeable === false
|
|
&& unresolvedAncestorCloseout.state === "pending"
|
|
&& JSON.stringify(unresolvedAncestorCloseout.pendingReasons).includes("ancestor-check-unresolved"),
|
|
"v0.2 closeout verdict must not close superseded targets when ancestor evidence is missing",
|
|
unresolvedAncestorCloseout,
|
|
);
|
|
|
|
const pendingFlushCloseout = v02CloseoutVerdict({
|
|
...supersededCloseoutStatus,
|
|
targetValidation: {
|
|
ok: false,
|
|
state: "failed",
|
|
failures: [{ reason: "git-mirror-not-flushed", pendingFlush: true, githubInSync: false }],
|
|
},
|
|
gitMirror: {
|
|
summary: {
|
|
pendingFlush: true,
|
|
githubInSync: false,
|
|
flushNeeded: true,
|
|
flushCommand: "bun scripts/cli.ts hwlab g14 git-mirror flush --confirm",
|
|
},
|
|
},
|
|
});
|
|
assertCondition(
|
|
pendingFlushCloseout.closeable === false
|
|
&& pendingFlushCloseout.state === "pending"
|
|
&& String(record(pendingFlushCloseout.recommendedNext).action) === "flush-git-mirror"
|
|
&& String(record(pendingFlushCloseout.recommendedNext).command).includes("git-mirror flush --confirm --wait"),
|
|
"v0.2 closeout verdict must turn pendingFlush into an explicit controlled flush command",
|
|
pendingFlushCloseout,
|
|
);
|
|
|
|
const cdSummaryWithFields = summarizeV02CdStatus({
|
|
pipelineRun: { pipelineRun: "hwlab-v02-ci-poll-aaaaaaaaaaaa", status: "True", reason: "Completed" },
|
|
targetValidation: { state: "passed", ok: true, failures: [] },
|
|
gitMirror: { summary: { pendingFlush: false, githubInSync: true } },
|
|
argo: { fields: { syncStatus: "Synced", health: "Healthy" } },
|
|
webAssets: { ok: true },
|
|
planArtifacts: { buildServices: ["hwlab-cloud-api"], reusedServices: ["hwlab-cloud-web"], rolloutServices: ["hwlab-cloud-api"] },
|
|
});
|
|
assertCondition(
|
|
cdSummaryWithFields.argoSync === "Synced"
|
|
&& cdSummaryWithFields.argoHealth === "Healthy",
|
|
"v0.2 CD summary must read Argo sync/health from status.argo.fields",
|
|
cdSummaryWithFields,
|
|
);
|
|
const activeRunSummary = activeV02PipelineRuns({
|
|
activePipelineRuns: [
|
|
{ name: "hwlab-v02-ci-poll-aaaaaaaaaaaa", status: "Unknown" },
|
|
{ name: "hwlab-v02-ci-poll-bbbbbbbbbbbb", status: "True" },
|
|
],
|
|
});
|
|
assertCondition(
|
|
activeRunSummary.length === 1 && activeRunSummary[0]?.name === "hwlab-v02-ci-poll-aaaaaaaaaaaa",
|
|
"v0.2 active PipelineRun helper must accept the status.activePipelineRuns array shape",
|
|
activeRunSummary,
|
|
);
|
|
const hiddenHistoryPolicy = v02StatusHistoryPolicy({
|
|
activePipelineRuns: [],
|
|
history: {
|
|
included: false,
|
|
note: "recent PipelineRun history is hidden by default so old manifest/service-id failures do not pollute the current-target verdict; rerun with --history to inspect it",
|
|
command: "hwlab g14 control-plane status --lane v02 --history",
|
|
},
|
|
});
|
|
const explicitHistoryPolicy = v02StatusHistoryPolicy({
|
|
activePipelineRuns: [],
|
|
recentPipelineRuns: { items: [{ name: "hwlab-v02-ci-poll-aaaaaaaaaaaa", status: "False" }] },
|
|
history: { included: true },
|
|
});
|
|
assertCondition(
|
|
hiddenHistoryPolicy.included === false
|
|
&& hiddenHistoryPolicy.recentPipelineRunsVisible === false
|
|
&& hiddenHistoryPolicy.activePipelineRunsVisible === true
|
|
&& String(hiddenHistoryPolicy.command).includes("--history")
|
|
&& explicitHistoryPolicy.included === true
|
|
&& explicitHistoryPolicy.recentPipelineRunsVisible === true,
|
|
"v0.2 status must hide historical PipelineRun lists by default and expose them only via --history",
|
|
{ hiddenHistoryPolicy, explicitHistoryPolicy },
|
|
);
|
|
|
|
const falseGreenPassed = v02FalseGreenGuard({
|
|
sourceCommit: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
pipelineRun: { exists: true, status: "True" },
|
|
taskRuns: {
|
|
items: [
|
|
{ name: "hwlab-v02-ci-poll-aaaaaaaaaaaa-build-hwlab-cloud-api", status: "True" },
|
|
{ name: "hwlab-v02-ci-poll-aaaaaaaaaaaa-build-hwlab-cloud-web", status: "True" },
|
|
],
|
|
},
|
|
planArtifacts: {
|
|
ok: true,
|
|
buildServices: ["hwlab-cloud-api"],
|
|
reusedServices: ["hwlab-cloud-web"],
|
|
artifactProvenanceAudit: {
|
|
ok: true,
|
|
unsafeReuseServices: [],
|
|
},
|
|
},
|
|
runtimeWorkloads: {
|
|
ok: true,
|
|
items: [
|
|
{ serviceId: "hwlab-cloud-api", artifactSourceCommit: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" },
|
|
{ serviceId: "hwlab-cloud-web", artifactSourceCommit: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" },
|
|
],
|
|
},
|
|
});
|
|
assertCondition(
|
|
falseGreenPassed.ok === true && falseGreenPassed.state === "passed",
|
|
"v0.2 false-green guard should pass when built runtime services and reuse provenance are proven",
|
|
falseGreenPassed,
|
|
);
|
|
|
|
const falseGreenReuseMissingAudit = v02FalseGreenGuard({
|
|
sourceCommit: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
pipelineRun: { exists: true, status: "True" },
|
|
taskRuns: { items: [{ name: "hwlab-v02-ci-poll-aaaaaaaaaaaa-build-hwlab-cloud-api", status: "True" }] },
|
|
planArtifacts: {
|
|
ok: true,
|
|
buildServices: ["hwlab-cloud-api"],
|
|
reusedServices: ["hwlab-cloud-web"],
|
|
artifactProvenanceAudit: null,
|
|
},
|
|
runtimeWorkloads: { ok: true, items: [{ serviceId: "hwlab-cloud-api", artifactSourceCommit: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" }] },
|
|
});
|
|
assertCondition(
|
|
falseGreenReuseMissingAudit.ok === true
|
|
&& JSON.stringify(falseGreenReuseMissingAudit.provenanceWarnings).includes("artifact-provenance-audit-missing-for-reuse"),
|
|
"v0.2 false-green guard should not fail a completed rollout only because optional reuse provenance is missing",
|
|
falseGreenReuseMissingAudit,
|
|
);
|
|
|
|
const falseGreenRuntimeMismatch = v02FalseGreenGuard({
|
|
sourceCommit: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
pipelineRun: { exists: true, status: "True" },
|
|
taskRuns: { items: [{ name: "hwlab-v02-ci-poll-aaaaaaaaaaaa-build-hwlab-cloud-api", status: "True" }] },
|
|
planArtifacts: {
|
|
ok: true,
|
|
buildServices: ["hwlab-cloud-api"],
|
|
reusedServices: [],
|
|
artifactProvenanceAudit: null,
|
|
},
|
|
runtimeWorkloads: { ok: true, items: [{ serviceId: "hwlab-cloud-api", artifactSourceCommit: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" }] },
|
|
});
|
|
assertCondition(
|
|
falseGreenRuntimeMismatch.ok === false
|
|
&& JSON.stringify(falseGreenRuntimeMismatch.failures).includes("artifact-source-commit-mismatch"),
|
|
"v0.2 false-green guard must fail when a built service still runs an old artifact",
|
|
falseGreenRuntimeMismatch,
|
|
);
|
|
|
|
const slowBuildSummary = v02TaskRunPerformanceSummary([
|
|
{
|
|
name: "hwlab-v02-ci-poll-f8a090b66616-build-hwlab-agent-worker",
|
|
status: "True",
|
|
reason: "Succeeded",
|
|
durationSeconds: 234,
|
|
},
|
|
{
|
|
name: "hwlab-v02-ci-poll-f8a090b66616-build-hwlab-cloud-web",
|
|
status: "True",
|
|
reason: "Succeeded",
|
|
durationSeconds: 37,
|
|
},
|
|
]);
|
|
const slowBuildItems = Array.isArray(record(slowBuildSummary).slowTaskRuns) ? record(slowBuildSummary).slowTaskRuns as Record<string, unknown>[] : [];
|
|
assertCondition(
|
|
slowBuildSummary.ok === false
|
|
&& record(record(slowBuildSummary).thresholds).buildTaskRunWarningSeconds === 120
|
|
&& slowBuildItems.length === 1
|
|
&& slowBuildItems[0]?.serviceId === "hwlab-agent-worker"
|
|
&& slowBuildItems[0]?.severity === "critical",
|
|
"v0.2 status must warn on slow build TaskRuns like issue #659",
|
|
slowBuildSummary,
|
|
);
|
|
|
|
const prBody = [
|
|
"## 背景",
|
|
"",
|
|
"G14 DEV Cloud Workbench 中,内部 `message-trace-body` 的 `pre` 会在 trace 更新时被替换,导致用户滚到中间后被重置到顶部。",
|
|
"",
|
|
"## 修改",
|
|
"",
|
|
"- trace row patch 从 index/replace 改为 stable `data-trace-row-id` keyed reconciliation。",
|
|
"- 已存在 row 只移动和原地更新 header/body,不替换 `li/pre`。",
|
|
"- `message-trace-body` 内部滚动状态按 `traceUiKey + rowId` 记忆。",
|
|
].join("\n");
|
|
|
|
const semanticBullets = semanticChangelogBullets("fix: preserve inner trace scroll", prBody, {
|
|
keyFiles: [
|
|
"modified web/hwlab-cloud-web/app.mjs",
|
|
"modified web/hwlab-cloud-web/scripts/trace-scroll.test.mjs",
|
|
],
|
|
});
|
|
|
|
assertCondition(
|
|
semanticBullets.some((line) => line.includes("修复目标") && line.includes("滚到中间后被重置到顶部")),
|
|
"semantic changelog should explain the user-visible bug being fixed",
|
|
semanticBullets,
|
|
);
|
|
assertCondition(
|
|
semanticBullets.some((line) => line.includes("data-trace-row-id")),
|
|
"semantic changelog should include PR body change bullets instead of only file stats",
|
|
semanticBullets,
|
|
);
|
|
|
|
const summaryBullets = semanticChangelogBullets("feat: preload gateway tran helper", [
|
|
"## 摘要",
|
|
"- 新增 `/app/tools/hwlab-gateway-tran.mjs`,支持 `cmd`、`ps`、`upload`、`download`。",
|
|
].join("\n"), {});
|
|
|
|
assertCondition(
|
|
summaryBullets.some((line) => line.includes("hwlab-gateway-tran.mjs") && line.includes("upload")),
|
|
"semantic changelog should also extract Chinese summary sections",
|
|
summaryBullets,
|
|
);
|
|
|
|
const rolloutBody = rolloutRecordBody({
|
|
pr: { number: 506, title: "fix: preserve inner trace scroll", url: "https://github.com/pikasTech/HWLAB/pull/506" },
|
|
prData: { json: { title: "fix: preserve inner trace scroll", body: prBody, url: "https://github.com/pikasTech/HWLAB/pull/506" } },
|
|
fileSummary: {
|
|
summary: { files: 2, additions: 268, deletions: 22, commits: 1 },
|
|
keyFiles: [
|
|
"modified web/hwlab-cloud-web/app.mjs",
|
|
"modified web/hwlab-cloud-web/scripts/trace-scroll.test.mjs",
|
|
],
|
|
},
|
|
sourceCommit: "1a3fa9e6fbc987142463ecff2cbcef240a6278f2",
|
|
pipelineRun: "hwlab-g14-ci-poll-1a3fa9e6fbc9",
|
|
gitopsRevision: "21462b78ce4e7dba4ea374398f60db690e290147",
|
|
mergedAt: "2026-05-27T06:52:22Z",
|
|
pipelineSucceededAt: "2026-05-27T06:55:38Z",
|
|
finishedAt: "2026-05-27T06:55:47Z",
|
|
rollout: { pipelineText: "True\nSucceeded", argoText: "21462\nSynced\nHealthy\nSucceeded\n21462", healthOk: true },
|
|
ciMetrics: parsePipelineTaskRunMetrics("hwlab-g14-ci-poll-test", [
|
|
"taskrun-a\tbuild-hwlab-cloud-api\t2026-05-27T06:54:43Z\t2026-05-27T06:54:52Z\tservice-id=hwlab-cloud-api;status=reused;build-backend=reused-catalog;",
|
|
"taskrun-b\tbuild-hwlab-cloud-web\t2026-05-27T06:54:43Z\t2026-05-27T06:55:08Z\tservice-id=hwlab-cloud-web;status=published;build-backend=buildkit;",
|
|
].join("\n")),
|
|
});
|
|
|
|
assertCondition(
|
|
rolloutBody.includes("- 上线 changelog(语义化):"),
|
|
"rollout record must label natural-language changelog separately",
|
|
);
|
|
assertCondition(
|
|
rolloutBody.includes("- 自动 diff 摘要:"),
|
|
"rollout record must keep automatic diff summary separately",
|
|
);
|
|
assertCondition(
|
|
rolloutBody.indexOf("- 上线 changelog(语义化):") < rolloutBody.indexOf("- 自动 diff 摘要:"),
|
|
"semantic changelog should appear before automatic diff summary",
|
|
);
|
|
assertCondition(
|
|
rolloutBody.includes("changed files: 2; +268 / -22; commits: 1"),
|
|
"automatic diff summary should preserve file/stat evidence",
|
|
);
|
|
assertCondition(
|
|
rolloutBody.includes("lazy build reused: 1/2"),
|
|
"rollout record should include lazy-build reused ratio",
|
|
rolloutBody,
|
|
);
|
|
assertCondition(
|
|
rolloutBody.includes("reused services: hwlab-cloud-api") && rolloutBody.includes("rebuild services: hwlab-cloud-web"),
|
|
"rollout record should list reused and rebuild services",
|
|
rolloutBody,
|
|
);
|
|
assertCondition(
|
|
rolloutBody.includes("hwlab-cloud-api: reused, 9s") && rolloutBody.includes("hwlab-cloud-web: published, 25s"),
|
|
"rollout record should include each service duration",
|
|
rolloutBody,
|
|
);
|
|
|
|
console.log(JSON.stringify({
|
|
ok: true,
|
|
checks: [
|
|
"long-running monitor owns latest-monitor-job.json",
|
|
"once jobs do not overwrite long-running monitor state",
|
|
"dry-run jobs do not overwrite live monitor state",
|
|
"once dry-run jobs have a distinct diagnostic state file",
|
|
"v0.2 PR monitor state pointers do not overwrite legacy G14 monitor pointers",
|
|
"observability help exposes assertion, target, boundary, and closeout entrypoints",
|
|
"observability query assertions report count and terminal value pass/fail",
|
|
"observability CLI rejects unsupported options with visible JSON errors",
|
|
"observability resource snapshot converts metrics.k8s.io CPU quantities to millicores",
|
|
"observability resource snapshot converts metrics.k8s.io memory quantities to MiB",
|
|
"git mirror sync is a manual devops-infra Job, not a CronJob",
|
|
"git mirror flush is a manual devops-infra Job, not a CronJob",
|
|
"trigger-current can decide whether v0.2 git mirror pre-sync is required",
|
|
"v0.2 git mirror pre-sync marker only reuses fresh same-commit post-observation markers",
|
|
"git mirror status exposes source and gitops GitHub sync state plus controlled flush command",
|
|
"v0.2 control-plane status and closeout expose targeted PipelineRun/source-commit inspection",
|
|
"v0.2 control-plane render uses an isolated temp clone from a CI/CD dedicated bare repo",
|
|
"v0.2 control-plane refresh marker only reuses recent same-contract refreshes",
|
|
"trigger-current rechecks latest v0.2 head before reusing an existing PipelineRun",
|
|
"trigger-current fast snapshot parses source head and PipelineRun status in one G14:k3s route while latest head is checked locally",
|
|
"v0.2 status alignment reports stale-success without coupling CI to dirty workspace state",
|
|
"v0.2 closeout verdicts distinguish passed, superseded, pending flush, and unresolved ancestor states",
|
|
"v0.2 CD summary and active run helpers expose Argo fields plus concurrent PipelineRun visibility",
|
|
"v0.2 PipelineRun service matrix excludes hwlab-cli",
|
|
"v0.2 false-green guard checks build TaskRuns, runtime artifact source commits, and reuse provenance",
|
|
"v0.2 status warns on slow build TaskRuns",
|
|
"rollout brief includes natural-language changelog before automatic diff summary",
|
|
"semantic changelog extracts Chinese summary sections",
|
|
"rollout brief includes lazy-build reused/rebuild metrics and service durations",
|
|
],
|
|
}, null, 2));
|