fix: pre-sync hwlab v02 git mirror

This commit is contained in:
Codex
2026-05-30 06:36:57 +00:00
parent 231d17ebab
commit 86616e226b
4 changed files with 174 additions and 3 deletions
+30 -1
View File
@@ -1,4 +1,4 @@
import { gitMirrorFlushJobManifest, gitMirrorSyncJobManifest, hwlabG14MonitorStateFileName, parsePipelineTaskRunMetrics, rolloutRecordBody, semanticChangelogBullets } from "./src/hwlab-g14";
import { gitMirrorFlushJobManifest, gitMirrorSyncJobManifest, gitMirrorV02SyncRequirement, hwlabG14MonitorStateFileName, parseGitMirrorStatusRefs, parsePipelineTaskRunMetrics, rolloutRecordBody, semanticChangelogBullets, v02PipelineServiceIds } from "./src/hwlab-g14";
function assertCondition(condition: unknown, message: string, detail: unknown = {}): void {
if (!condition) throw new Error(`${message}: ${JSON.stringify(detail)}`);
@@ -40,6 +40,33 @@ 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","localG14":"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(
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",
);
assertCondition(
!v02PipelineServiceIds().includes("hwlab-cli"),
"v0.2 PipelineRun service matrix must not build hwlab-cli because cli is short-connection source tool",
v02PipelineServiceIds(),
);
const prBody = [
"## 背景",
"",
@@ -145,6 +172,8 @@ console.log(JSON.stringify({
"once dry-run jobs have a distinct diagnostic state file",
"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 PipelineRun service matrix excludes hwlab-cli",
"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",
+141 -1
View File
@@ -40,9 +40,13 @@ const V02_SERVICE_IDS = [
"hwlab-device-pod",
"hwlab-gateway",
"hwlab-edge-proxy",
"hwlab-cli",
"hwlab-agent-skills",
];
export function v02PipelineServiceIds(): string[] {
return [...V02_SERVICE_IDS];
}
const G14_CI_TOOLS_IMAGE_REPO = "127.0.0.1:5000/hwlab/hwlab-ci-node-tools";
const G14_CI_TOOLS_BASE_TAG = "node22-alpine-v1";
const DEFAULT_INTERVAL_SECONDS = 600;
@@ -313,6 +317,10 @@ function record(value: unknown): Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : {};
}
function stringOrNull(value: unknown): string | null {
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
}
function nested(value: unknown, keys: string[]): unknown {
let current = value;
for (const key of keys) current = record(current)[key];
@@ -438,6 +446,11 @@ function commandErrorSummary(result: CommandJsonResult): string {
return text.slice(0, 4000);
}
function tailText(value: unknown, maxBytes = 2000): string {
const text = String(value ?? "").trim();
return text.length <= maxBytes ? text : text.slice(-maxBytes);
}
function listCleanupPipelineRuns(options: G14ControlPlaneOptions): Record<string, unknown>[] {
const result = g14K3s([
"kubectl",
@@ -932,6 +945,7 @@ function runV02ControlPlane(options: G14ControlPlaneOptions): Record<string, unk
}
const before = getPipelineRunCompact(v02PipelineRunName(sourceCommit));
if (options.dryRun) {
const gitMirrorPreSync = preSyncV02GitMirror(sourceCommit, options);
return {
ok: true,
command: "hwlab g14 control-plane trigger-current --lane v02",
@@ -940,6 +954,7 @@ function runV02ControlPlane(options: G14ControlPlaneOptions): Record<string, unk
sourceCommit,
pipelineRun: v02PipelineRunName(sourceCommit),
before,
gitMirrorPreSync,
manifest: v02PipelineRunManifest(sourceCommit),
next: { triggerCurrent: "bun scripts/cli.ts hwlab g14 control-plane trigger-current --lane v02 --confirm" },
};
@@ -956,6 +971,21 @@ function runV02ControlPlane(options: G14ControlPlaneOptions): Record<string, unk
degradedReason: "refuse-active-or-successful-pipelinerun",
};
}
const gitMirrorPreSync = preSyncV02GitMirror(sourceCommit, options);
if (gitMirrorPreSync.ok !== true) {
return {
ok: false,
command: "hwlab g14 control-plane trigger-current --lane v02",
lane: "v02",
mode: "confirmed-trigger",
phase: "git-mirror-pre-sync",
sourceCommit,
pipelineRun: v02PipelineRunName(sourceCommit),
before,
gitMirrorPreSync,
degradedReason: "git-mirror-pre-sync-failed",
};
}
const deletePipelineRun = deleteV02PipelineRun(v02PipelineRunName(sourceCommit));
const createPipelineRun = isCommandSuccess(deletePipelineRun)
? createV02PipelineRun(sourceCommit, options.timeoutSeconds)
@@ -968,6 +998,7 @@ function runV02ControlPlane(options: G14ControlPlaneOptions): Record<string, unk
sourceCommit,
pipelineRun: v02PipelineRunName(sourceCommit),
before,
gitMirrorPreSync,
deletePipelineRun,
createPipelineRun,
after: getPipelineRunCompact(v02PipelineRunName(sourceCommit)),
@@ -1076,6 +1107,115 @@ export function gitMirrorFlushJobManifest(name: string): Record<string, unknown>
return manifest;
}
export function parseGitMirrorStatusRefs(raw: string): { refs: Record<string, string | null>; pendingFlush: boolean | null } {
const line = raw.split(/\r?\n/u).find((item) => item.startsWith("refs="));
if (line === undefined) return { refs: {}, pendingFlush: null };
try {
const parsed = record(JSON.parse(line.slice("refs=".length)) as unknown);
const refs = record(parsed.refs);
return {
refs: {
localV02: stringOrNull(refs.localV02),
localG14: stringOrNull(refs.localG14),
localGitops: stringOrNull(refs.localGitops),
githubGitops: stringOrNull(refs.githubGitops),
},
pendingFlush: typeof parsed.pendingFlush === "boolean" ? parsed.pendingFlush : null,
};
} catch {
return { refs: {}, pendingFlush: null };
}
}
export function gitMirrorV02SyncRequirement(sourceCommit: string, rawStatus: string): Record<string, unknown> {
const parsed = parseGitMirrorStatusRefs(rawStatus);
const localV02 = parsed.refs.localV02 ?? null;
return {
required: localV02 !== sourceCommit,
sourceCommit,
localV02,
pendingFlush: parsed.pendingFlush,
refs: parsed.refs,
reason: localV02 === sourceCommit ? "local-v02-current" : localV02 === null ? "local-v02-unresolved" : "local-v02-stale",
};
}
function gitMirrorStatusCacheRaw(status: Record<string, unknown>): string {
return String(nested(status, ["cache", "raw"]) ?? "");
}
function compactGitMirrorStatus(status: Record<string, unknown>, sourceCommit: string): Record<string, unknown> {
const requirement = gitMirrorV02SyncRequirement(sourceCommit, gitMirrorStatusCacheRaw(status));
return {
ok: status.ok === true,
readUrl: status.readUrl ?? V02_GIT_READ_URL,
writeUrl: status.writeUrl ?? V02_GIT_WRITE_URL,
legacyCronJobExists: nested(status, ["legacyCronJob", "exists"]) === true,
required: requirement.required,
sourceCommit,
localV02: requirement.localV02 ?? null,
pendingFlush: requirement.pendingFlush ?? null,
reason: requirement.reason,
cacheOk: nested(status, ["cache", "ok"]) === true,
cacheStderr: tailText(nested(status, ["cache", "stderr"]), 1000),
resourcesOk: nested(status, ["resources", "ok"]) === true,
};
}
function compactGitMirrorSync(sync: Record<string, unknown>): Record<string, unknown> {
return {
ok: sync.ok === true,
command: sync.command ?? "hwlab g14 git-mirror sync",
mode: sync.mode ?? null,
namespace: sync.namespace ?? GIT_MIRROR_NAMESPACE,
jobName: sync.jobName ?? null,
exitCode: nested(sync, ["result", "exitCode"]) ?? null,
stdoutTail: tailText(nested(sync, ["result", "stdout"]), 3000),
stderrTail: tailText(nested(sync, ["result", "stderr"]), 3000),
};
}
function preSyncV02GitMirror(sourceCommit: string, options: Pick<G14ControlPlaneOptions, "dryRun" | "timeoutSeconds">): Record<string, unknown> {
const before = runGitMirrorStatus();
const beforeSummary = compactGitMirrorStatus(before, sourceCommit);
if (options.dryRun) {
return {
ok: true,
mode: "dry-run",
sourceCommit,
before: beforeSummary,
action: beforeSummary.required === true ? "would-sync-before-trigger" : "already-current",
next: beforeSummary.required === true ? { syncAndTrigger: "bun scripts/cli.ts hwlab g14 control-plane trigger-current --lane v02 --confirm" } : undefined,
};
}
if (beforeSummary.required !== true) {
return {
ok: true,
mode: "already-current",
sourceCommit,
before: beforeSummary,
};
}
const sync = runGitMirrorSync({
action: "sync",
confirm: true,
dryRun: false,
timeoutSeconds: options.timeoutSeconds,
});
const after = runGitMirrorStatus();
const afterSummary = compactGitMirrorStatus(after, sourceCommit);
const ok = sync.ok === true && afterSummary.required === false;
return {
ok,
mode: "auto-sync-before-trigger",
sourceCommit,
before: beforeSummary,
sync: compactGitMirrorSync(sync),
after: afterSummary,
degradedReason: ok ? undefined : "git-mirror-local-v02-not-current-after-sync",
};
}
function runGitMirrorStatus(): Record<string, unknown> {
const resources = g14K3s([
"kubectl",