953 lines
40 KiB
TypeScript
953 lines
40 KiB
TypeScript
// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. git-mirror module for scripts/src/hwlab-g14.ts.
|
|
|
|
// Moved mechanically from scripts/src/hwlab-g14.ts:5846-6777 for #903.
|
|
|
|
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
import { dirname, join } from "node:path";
|
|
import { createHash, randomBytes } from "node:crypto";
|
|
import { repoRoot, rootPath, type Config } from "../config";
|
|
import { runCommand } from "../command";
|
|
import { cancelJob, listJobs, readJob, startJob } from "../jobs";
|
|
import { hwlabRequiredNoProxyEntries, hwlabRuntimeLaneConfigPath, hwlabRuntimeLaneIds, hwlabRuntimeLaneSpec, isHwlabRuntimeLane, type HwlabRuntimeLane, type HwlabRuntimeLaneSpec } from "../hwlab-node-lanes";
|
|
|
|
import type { CommandJsonResult, G14ControlPlaneOptions, G14GitMirrorOptions } from "./types";
|
|
import { tailText } from "./cleanup";
|
|
import { statusText } from "./pr-monitor";
|
|
import { compactCommandResult, g14K3s, isCommandSuccess, nested, printProgressEvent, record, shellQuote, shortSha, stringOrNull } from "./remote";
|
|
import { cleanupRuntimeLaneRenderDir, runRuntimeLaneRenderToTemp } from "./render";
|
|
import { resolveRuntimeLaneHead } from "./source";
|
|
import { GIT_MIRROR_LEGACY_CRONJOB, GIT_MIRROR_MANIFEST_FIELD_MANAGER, GIT_MIRROR_NAMESPACE, GIT_MIRROR_SYNC_JOB_PREFIX, V02_GIT_MIRROR_PRESYNC_LOCK_STALE_MS, V02_GIT_MIRROR_PRESYNC_MAX_WAIT_MS, V02_GIT_MIRROR_PRESYNC_POLL_MS, V02_GIT_READ_URL, V02_GIT_WRITE_URL } from "./types";
|
|
import { parseShellSections, shellSectionOk, timestampMs } from "./v02-status";
|
|
|
|
export function deleteLegacyGitMirrorCronJob(dryRun: boolean): CommandJsonResult {
|
|
return g14K3s([
|
|
"kubectl",
|
|
"delete",
|
|
"cronjob",
|
|
"-n",
|
|
GIT_MIRROR_NAMESPACE,
|
|
GIT_MIRROR_LEGACY_CRONJOB,
|
|
"--ignore-not-found=true",
|
|
...(dryRun ? ["--dry-run=server", "-o", "name"] : []),
|
|
], 60_000);
|
|
}
|
|
|
|
export function applyGitMirrorManifestFile(renderDir: string, dryRun: boolean, timeoutSeconds: number): CommandJsonResult {
|
|
return g14K3s([
|
|
"kubectl",
|
|
"apply",
|
|
"--server-side",
|
|
"--force-conflicts",
|
|
`--field-manager=${GIT_MIRROR_MANIFEST_FIELD_MANAGER}`,
|
|
...(dryRun ? ["--dry-run=server"] : []),
|
|
"-f",
|
|
`${renderDir}/devops-infra/git-mirror.yaml`,
|
|
], timeoutSeconds * 1000);
|
|
}
|
|
|
|
export function gitMirrorSyncJobName(): string {
|
|
return `${GIT_MIRROR_SYNC_JOB_PREFIX}-${Date.now().toString(36)}`.slice(0, 63);
|
|
}
|
|
|
|
export function gitMirrorFlushJobName(): string {
|
|
return `git-mirror-hwlab-flush-manual-${Date.now().toString(36)}`.slice(0, 63);
|
|
}
|
|
|
|
export function gitMirrorSyncJobManifest(name: string): Record<string, unknown> {
|
|
return {
|
|
apiVersion: "batch/v1",
|
|
kind: "Job",
|
|
metadata: {
|
|
name,
|
|
namespace: GIT_MIRROR_NAMESPACE,
|
|
labels: {
|
|
"app.kubernetes.io/name": "git-mirror",
|
|
"app.kubernetes.io/part-of": "devops-infra",
|
|
"app.kubernetes.io/component": "sync-controller",
|
|
"hwlab.pikastech.local/trigger": "manual-cli",
|
|
},
|
|
},
|
|
spec: {
|
|
backoffLimit: 0,
|
|
activeDeadlineSeconds: 300,
|
|
ttlSecondsAfterFinished: 3600,
|
|
template: {
|
|
metadata: {
|
|
labels: {
|
|
"app.kubernetes.io/name": "git-mirror",
|
|
"app.kubernetes.io/part-of": "devops-infra",
|
|
"app.kubernetes.io/component": "sync-controller",
|
|
"hwlab.pikastech.local/trigger": "manual-cli",
|
|
},
|
|
},
|
|
spec: {
|
|
restartPolicy: "Never",
|
|
hostNetwork: true,
|
|
dnsPolicy: "ClusterFirstWithHostNet",
|
|
volumes: [
|
|
{ name: "cache", persistentVolumeClaim: { claimName: "git-mirror-cache" } },
|
|
{ name: "git-ssh", secret: { secretName: "git-mirror-github-ssh", defaultMode: 0o400 } },
|
|
{ name: "script", configMap: { name: "git-mirror-sync-script", defaultMode: 0o755 } },
|
|
],
|
|
containers: [{
|
|
name: "sync",
|
|
image: "127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1",
|
|
imagePullPolicy: "IfNotPresent",
|
|
command: ["/script/sync.sh"],
|
|
volumeMounts: [
|
|
{ name: "cache", mountPath: "/cache" },
|
|
{ name: "git-ssh", mountPath: "/git-ssh", readOnly: true },
|
|
{ name: "script", mountPath: "/script", readOnly: true },
|
|
],
|
|
}],
|
|
},
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
export function gitMirrorFlushJobManifest(name: string): Record<string, unknown> {
|
|
const manifest = gitMirrorSyncJobManifest(name);
|
|
record(record(manifest.metadata).labels)["app.kubernetes.io/component"] = "flush-controller";
|
|
const template = record(record(manifest.spec).template);
|
|
record(record(template.metadata).labels)["app.kubernetes.io/component"] = "flush-controller";
|
|
const podSpec = record(template.spec);
|
|
const containers = Array.isArray(podSpec.containers) ? podSpec.containers : [];
|
|
const first = record(containers[0]);
|
|
first.name = "flush";
|
|
first.command = ["/script/flush.sh"];
|
|
podSpec.containers = [first];
|
|
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);
|
|
const parsedRefs: Record<string, string | null> = {};
|
|
for (const [key, value] of Object.entries(refs)) parsedRefs[key] = stringOrNull(value);
|
|
return {
|
|
refs: {
|
|
...parsedRefs,
|
|
localV02: stringOrNull(refs.localV02),
|
|
githubV02: stringOrNull(refs.githubV02),
|
|
localG14: stringOrNull(refs.localG14),
|
|
githubG14: stringOrNull(refs.githubG14),
|
|
localGitops: stringOrNull(refs.localGitops),
|
|
githubGitops: stringOrNull(refs.githubGitops),
|
|
localV03: stringOrNull(refs.localV03),
|
|
githubV03: stringOrNull(refs.githubV03),
|
|
localV03Gitops: stringOrNull(refs.localV03Gitops),
|
|
githubV03Gitops: stringOrNull(refs.githubV03Gitops),
|
|
},
|
|
pendingFlush: typeof parsed.pendingFlush === "boolean" ? parsed.pendingFlush : null,
|
|
};
|
|
} catch {
|
|
return { refs: {}, pendingFlush: null };
|
|
}
|
|
}
|
|
|
|
export function parseLabeledJsonLine(raw: string, label: string): Record<string, unknown> {
|
|
const line = raw.split(/\r?\n/u).find((item) => item.startsWith(`${label}=`));
|
|
if (line === undefined) return {};
|
|
const value = line.slice(label.length + 1).trim();
|
|
if (value.length === 0) return {};
|
|
try {
|
|
return record(JSON.parse(value) as unknown);
|
|
} catch {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
export function gitMirrorStatusSummary(raw: string): Record<string, unknown> {
|
|
const refs = parseGitMirrorStatusRefs(raw);
|
|
const lastSync = parseLabeledJsonLine(raw, "lastSync");
|
|
const lastWrite = parseLabeledJsonLine(raw, "lastWrite");
|
|
const lastFlush = parseLabeledJsonLine(raw, "lastFlush");
|
|
const localV02 = refs.refs.localV02 ?? null;
|
|
const githubV02 = refs.refs.githubV02 ?? null;
|
|
const localGitops = refs.refs.localGitops ?? null;
|
|
const githubGitops = refs.refs.githubGitops ?? null;
|
|
const localV03 = refs.refs.localV03 ?? null;
|
|
const githubV03 = refs.refs.githubV03 ?? null;
|
|
const localV03Gitops = refs.refs.localV03Gitops ?? null;
|
|
const githubV03Gitops = refs.refs.githubV03Gitops ?? null;
|
|
const sourceInSync = Boolean(localV02 && githubV02 && localV02 === githubV02);
|
|
const gitopsInSync = Boolean(localGitops && githubGitops && localGitops === githubGitops);
|
|
const runtimeLanes = Object.fromEntries(hwlabRuntimeLaneIds().map((lane) => {
|
|
const normalized = lane.slice(0, 1).toUpperCase() + lane.slice(1).toLowerCase();
|
|
const localSource = refs.refs[`local${normalized}`] ?? null;
|
|
const githubSource = refs.refs[`github${normalized}`] ?? null;
|
|
const localLaneGitops = refs.refs[`local${normalized}Gitops`] ?? null;
|
|
const githubLaneGitops = refs.refs[`github${normalized}Gitops`] ?? null;
|
|
return [lane, {
|
|
localSource,
|
|
githubSource,
|
|
localGitops: localLaneGitops,
|
|
githubGitops: githubLaneGitops,
|
|
sourceInSync: Boolean(localSource && githubSource && localSource === githubSource),
|
|
gitopsInSync: Boolean(localLaneGitops && githubLaneGitops && localLaneGitops === githubLaneGitops),
|
|
}];
|
|
}));
|
|
const allRuntimeLanesInSync = Object.values(runtimeLanes).every((item) => {
|
|
const laneSummary = record(item);
|
|
return laneSummary.sourceInSync === true && laneSummary.gitopsInSync === true;
|
|
});
|
|
return {
|
|
localV02,
|
|
githubV02,
|
|
localV03,
|
|
githubV03,
|
|
localG14: refs.refs.localG14 ?? null,
|
|
githubG14: refs.refs.githubG14 ?? null,
|
|
localGitops,
|
|
githubGitops,
|
|
localV03Gitops,
|
|
githubV03Gitops,
|
|
pendingFlush: refs.pendingFlush,
|
|
lastSync: {
|
|
status: stringOrNull(lastSync.status) ?? null,
|
|
at: stringOrNull(lastSync.publishedAt) ?? stringOrNull(lastSync.syncedAt) ?? null,
|
|
pendingFlush: typeof lastSync.pendingFlush === "boolean" ? lastSync.pendingFlush : null,
|
|
},
|
|
lastWrite: {
|
|
status: stringOrNull(lastWrite.status) ?? null,
|
|
at: stringOrNull(lastWrite.writtenAt) ?? null,
|
|
pendingFlush: typeof lastWrite.pendingFlush === "boolean" ? lastWrite.pendingFlush : null,
|
|
},
|
|
lastFlush: {
|
|
status: stringOrNull(lastFlush.status) ?? null,
|
|
at: stringOrNull(lastFlush.flushedAt) ?? null,
|
|
pendingFlush: typeof lastFlush.pendingFlush === "boolean" ? lastFlush.pendingFlush : null,
|
|
},
|
|
flushNeeded: refs.pendingFlush === true,
|
|
flushCommand: refs.pendingFlush === true ? "bun scripts/cli.ts hwlab g14 git-mirror flush --confirm" : null,
|
|
sourceInSync,
|
|
gitopsInSync,
|
|
v03SourceInSync: Boolean(localV03 && githubV03 && localV03 === githubV03),
|
|
v03GitopsInSync: Boolean(localV03Gitops && githubV03Gitops && localV03Gitops === githubV03Gitops),
|
|
runtimeLanes,
|
|
allRuntimeLanesInSync,
|
|
githubInSync: sourceInSync && gitopsInSync,
|
|
};
|
|
}
|
|
|
|
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",
|
|
};
|
|
}
|
|
|
|
export function runtimeLaneGitMirrorSourceInSyncForTest(lane: string, sourceCommit: string, summary: Record<string, unknown>): boolean {
|
|
const localKey = lane === "v02" ? "localV02" : lane === "v03" ? "localV03" : null;
|
|
return localKey !== null && typeof summary[localKey] === "string" && summary[localKey] === sourceCommit;
|
|
}
|
|
|
|
export function gitMirrorStatusCacheRaw(status: Record<string, unknown>): string {
|
|
return String(nested(status, ["cache", "raw"]) ?? "");
|
|
}
|
|
|
|
export function runtimeLaneGitMirrorSourceInSync(spec: HwlabRuntimeLaneSpec, sourceCommit: string, status: Record<string, unknown>): boolean {
|
|
return runtimeLaneGitMirrorSourceInSyncForTest(spec.lane, sourceCommit, record(nested(status, ["cache", "summary"])));
|
|
}
|
|
|
|
export 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,
|
|
elapsedMs: status.elapsedMs ?? null,
|
|
legacyCronJobExists: nested(status, ["legacyCronJob", "exists"]) === true,
|
|
required: requirement.required,
|
|
sourceCommit,
|
|
localV02: requirement.localV02 ?? null,
|
|
githubV02: requirement.refs.githubV02 ?? null,
|
|
pendingFlush: requirement.pendingFlush ?? null,
|
|
sourceInSync: Boolean(requirement.refs.localV02 && requirement.refs.githubV02 && requirement.refs.localV02 === requirement.refs.githubV02),
|
|
reason: requirement.reason,
|
|
cacheOk: nested(status, ["cache", "ok"]) === true,
|
|
cacheStderr: tailText(nested(status, ["cache", "stderr"]), 1000),
|
|
resourcesOk: nested(status, ["resources", "ok"]) === true,
|
|
};
|
|
}
|
|
|
|
export 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,
|
|
skipped: sync.skipped === true,
|
|
reason: sync.reason ?? null,
|
|
namespace: sync.namespace ?? GIT_MIRROR_NAMESPACE,
|
|
jobName: sync.jobName ?? null,
|
|
elapsedMs: sync.elapsedMs ?? null,
|
|
exitCode: nested(sync, ["result", "exitCode"]) ?? null,
|
|
stdoutTail: tailText(nested(sync, ["result", "stdout"]), 1600),
|
|
stderrTail: tailText(nested(sync, ["result", "stderr"]), 1200),
|
|
stdoutBytes: nested(sync, ["result", "stdoutBytes"]) ?? null,
|
|
stderrBytes: nested(sync, ["result", "stderrBytes"]) ?? null,
|
|
};
|
|
}
|
|
|
|
export interface V02GitMirrorPreSyncMarker {
|
|
ok: boolean;
|
|
sourceCommit: string;
|
|
syncedAt: string;
|
|
localV02?: string | null;
|
|
githubV02?: string | null;
|
|
reason?: string | null;
|
|
}
|
|
|
|
export function v02GitMirrorPreSyncStateDir(): string {
|
|
return rootPath(".state", "hwlab-g14", "v02-git-mirror-presync");
|
|
}
|
|
|
|
export function v02GitMirrorPreSyncMarkerPath(sourceCommit: string): string {
|
|
return join(v02GitMirrorPreSyncStateDir(), `${shortSha(sourceCommit)}.json`);
|
|
}
|
|
|
|
export function v02GitMirrorPreSyncLockDir(sourceCommit: string): string {
|
|
return join(v02GitMirrorPreSyncStateDir(), `${shortSha(sourceCommit)}.lock`);
|
|
}
|
|
|
|
export function v02GitMirrorPreSyncWaitMs(timeoutSeconds: number): number {
|
|
const requestedMs = Math.max(0, Math.floor(timeoutSeconds * 1000));
|
|
if (requestedMs === 0) return V02_GIT_MIRROR_PRESYNC_MAX_WAIT_MS;
|
|
return Math.min(V02_GIT_MIRROR_PRESYNC_MAX_WAIT_MS, requestedMs);
|
|
}
|
|
|
|
export function v02ReusableGitMirrorPreSyncMarker(marker: unknown, sourceCommit: string, nowMs = Date.now(), minSyncedAtMs = 0): V02GitMirrorPreSyncMarker | null {
|
|
const candidate = record(marker) as V02GitMirrorPreSyncMarker;
|
|
const syncedAtMs = timestampMs(candidate.syncedAt);
|
|
if (candidate.ok !== true || candidate.sourceCommit !== sourceCommit) return null;
|
|
if (syncedAtMs === null || nowMs - syncedAtMs > V02_GIT_MIRROR_PRESYNC_MAX_WAIT_MS) return null;
|
|
if (syncedAtMs < minSyncedAtMs) return null;
|
|
return candidate;
|
|
}
|
|
|
|
export function readV02GitMirrorPreSyncMarker(sourceCommit: string, nowMs = Date.now(), minSyncedAtMs = 0): V02GitMirrorPreSyncMarker | null {
|
|
const path = v02GitMirrorPreSyncMarkerPath(sourceCommit);
|
|
if (!existsSync(path)) return null;
|
|
try {
|
|
return v02ReusableGitMirrorPreSyncMarker(JSON.parse(readFileSync(path, "utf8")) as unknown, sourceCommit, nowMs, minSyncedAtMs);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function writeV02GitMirrorPreSyncMarker(sourceCommit: string, summary: Record<string, unknown>): V02GitMirrorPreSyncMarker {
|
|
const marker = {
|
|
ok: true,
|
|
sourceCommit,
|
|
syncedAt: new Date().toISOString(),
|
|
localV02: stringOrNull(summary.localV02),
|
|
githubV02: stringOrNull(summary.githubV02),
|
|
reason: stringOrNull(summary.reason),
|
|
};
|
|
const dir = v02GitMirrorPreSyncStateDir();
|
|
mkdirSync(dir, { recursive: true });
|
|
writeFileSync(v02GitMirrorPreSyncMarkerPath(sourceCommit), `${JSON.stringify(marker, null, 2)}\n`, "utf8");
|
|
return marker;
|
|
}
|
|
|
|
export function acquireV02GitMirrorPreSyncLock(sourceCommit: string, waitMs: number, minSyncedAtMs = 0): { acquired: boolean; lockDir: string; waitedMs: number; marker?: V02GitMirrorPreSyncMarker } {
|
|
const stateDir = v02GitMirrorPreSyncStateDir();
|
|
mkdirSync(stateDir, { recursive: true });
|
|
const lockDir = v02GitMirrorPreSyncLockDir(sourceCommit);
|
|
const startedAtMs = Date.now();
|
|
for (;;) {
|
|
const marker = readV02GitMirrorPreSyncMarker(sourceCommit, Date.now(), minSyncedAtMs);
|
|
if (marker !== null) return { acquired: false, lockDir, waitedMs: Date.now() - startedAtMs, marker };
|
|
try {
|
|
mkdirSync(lockDir);
|
|
writeFileSync(join(lockDir, "owner.json"), `${JSON.stringify({ pid: process.pid, sourceCommit, acquiredAt: new Date().toISOString() }, null, 2)}\n`, "utf8");
|
|
return { acquired: true, lockDir, waitedMs: Date.now() - startedAtMs };
|
|
} catch {
|
|
const ageMs = (() => {
|
|
try {
|
|
return Date.now() - statSync(lockDir).mtimeMs;
|
|
} catch {
|
|
return 0;
|
|
}
|
|
})();
|
|
if (ageMs > V02_GIT_MIRROR_PRESYNC_LOCK_STALE_MS) {
|
|
try {
|
|
rmSync(lockDir, { recursive: true, force: true });
|
|
continue;
|
|
} catch {
|
|
return { acquired: false, lockDir, waitedMs: Date.now() - startedAtMs };
|
|
}
|
|
}
|
|
if (Date.now() - startedAtMs >= waitMs) return { acquired: false, lockDir, waitedMs: Date.now() - startedAtMs };
|
|
const wait = runCommand(["sleep", "1"], repoRoot, { timeoutMs: 2_000 });
|
|
if (wait.exitCode !== 0 && wait.timedOut) return { acquired: false, lockDir, waitedMs: Date.now() - startedAtMs };
|
|
}
|
|
}
|
|
}
|
|
|
|
export function releaseV02GitMirrorPreSyncLock(lockDir: string): void {
|
|
try {
|
|
rmSync(lockDir, { recursive: true, force: true });
|
|
} catch {
|
|
// Best-effort cleanup; stale lock expiry handles interrupted workers.
|
|
}
|
|
}
|
|
|
|
export function waitForV02GitMirrorPreSync(sourceCommit: string, waitMs: number): Record<string, unknown> {
|
|
const startedAtMs = Date.now();
|
|
const observations: Record<string, unknown>[] = [];
|
|
let attempt = 0;
|
|
for (;;) {
|
|
attempt += 1;
|
|
const statusStartMs = Date.now();
|
|
printProgressEvent("hwlab.v02.trigger.progress", {
|
|
stage: "git-mirror-pre-sync-wait",
|
|
status: "polling",
|
|
sourceCommit,
|
|
attempt,
|
|
elapsedMs: statusStartMs - startedAtMs,
|
|
});
|
|
const status = runGitMirrorStatus();
|
|
const summary = compactGitMirrorStatus(status, sourceCommit);
|
|
observations.push({
|
|
attempt,
|
|
elapsedMs: Date.now() - startedAtMs,
|
|
statusElapsedMs: Date.now() - statusStartMs,
|
|
ok: summary.ok,
|
|
required: summary.required,
|
|
localV02: summary.localV02,
|
|
githubV02: summary.githubV02,
|
|
pendingFlush: summary.pendingFlush,
|
|
reason: summary.reason,
|
|
});
|
|
printProgressEvent("hwlab.v02.trigger.progress", {
|
|
stage: "git-mirror-pre-sync-wait",
|
|
status: summary.required === false ? "succeeded" : "waiting",
|
|
sourceCommit,
|
|
attempt,
|
|
elapsedMs: Date.now() - startedAtMs,
|
|
statusElapsedMs: Date.now() - statusStartMs,
|
|
required: summary.required,
|
|
localV02: summary.localV02,
|
|
githubV02: summary.githubV02,
|
|
pendingFlush: summary.pendingFlush,
|
|
reason: summary.reason,
|
|
});
|
|
if (summary.ok === true && summary.required === false) {
|
|
return {
|
|
ok: true,
|
|
sourceCommit,
|
|
attempts: attempt,
|
|
elapsedMs: Date.now() - startedAtMs,
|
|
final: summary,
|
|
observations: observations.slice(-6),
|
|
};
|
|
}
|
|
if (Date.now() - startedAtMs >= waitMs) {
|
|
return {
|
|
ok: false,
|
|
sourceCommit,
|
|
attempts: attempt,
|
|
elapsedMs: Date.now() - startedAtMs,
|
|
final: summary,
|
|
observations: observations.slice(-6),
|
|
degradedReason: "git-mirror-local-v02-not-current-after-wait",
|
|
};
|
|
}
|
|
const remainingMs = waitMs - (Date.now() - startedAtMs);
|
|
const sleepMs = Math.max(500, Math.min(V02_GIT_MIRROR_PRESYNC_POLL_MS, remainingMs));
|
|
const wait = runCommand(["sleep", String(Math.ceil(sleepMs / 1000))], repoRoot, { timeoutMs: sleepMs + 2_000 });
|
|
if (wait.exitCode !== 0 && wait.timedOut) {
|
|
return {
|
|
ok: false,
|
|
sourceCommit,
|
|
attempts: attempt,
|
|
elapsedMs: Date.now() - startedAtMs,
|
|
final: summary,
|
|
observations: observations.slice(-6),
|
|
degradedReason: "git-mirror-pre-sync-wait-sleep-timeout",
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
export function gitMirrorCacheProbeScript(): string {
|
|
const runtimeLanesJson = JSON.stringify(hwlabRuntimeLaneIds().map((lane) => {
|
|
const spec = hwlabRuntimeLaneSpec(lane);
|
|
return { lane, sourceBranch: spec.sourceBranch, gitopsBranch: spec.gitopsBranch };
|
|
}));
|
|
return [
|
|
"printf 'lastSync='; cat /cache/HWLAB.last-sync.json 2>/dev/null || true; printf '\\n'",
|
|
"printf 'lastWrite='; cat /cache/HWLAB.last-write.json 2>/dev/null || true; printf '\\n'",
|
|
"printf 'lastFlush='; cat /cache/HWLAB.last-flush.json 2>/dev/null || true; printf '\\n'",
|
|
"printf 'refs='",
|
|
"node - <<'NODE' 2>/dev/null || true",
|
|
"const { execFileSync } = require('node:child_process');",
|
|
"const repo = '/cache/pikasTech/HWLAB.git';",
|
|
"function rev(ref) {",
|
|
" try {",
|
|
" return execFileSync('git', ['--git-dir=' + repo, 'rev-parse', ref], { encoding: 'utf8' }).trim();",
|
|
" } catch {",
|
|
" return null;",
|
|
" }",
|
|
"}",
|
|
`const runtimeLanes = ${runtimeLanesJson};`,
|
|
"const refs = {};",
|
|
"for (const spec of runtimeLanes) {",
|
|
" const key = spec.lane.toUpperCase();",
|
|
" const normalized = key.slice(0, 1) + key.slice(1).toLowerCase();",
|
|
" const localSource = rev('refs/heads/' + spec.sourceBranch);",
|
|
" const githubSource = rev('refs/mirror-stage/heads/' + spec.sourceBranch);",
|
|
" const localGitops = rev('refs/heads/' + spec.gitopsBranch);",
|
|
" const githubGitops = rev('refs/mirror-stage/heads/' + spec.gitopsBranch);",
|
|
" refs['local' + normalized] = localSource;",
|
|
" refs['github' + normalized] = githubSource;",
|
|
" refs['local' + normalized + 'Gitops'] = localGitops;",
|
|
" refs['github' + normalized + 'Gitops'] = githubGitops;",
|
|
" if (spec.lane === 'v02') {",
|
|
" refs.localV02 = localSource;",
|
|
" refs.githubV02 = githubSource;",
|
|
" refs.localGitops = localGitops;",
|
|
" refs.githubGitops = githubGitops;",
|
|
" }",
|
|
"}",
|
|
"refs.localG14 = rev('refs/heads/G14');",
|
|
"refs.githubG14 = rev('refs/mirror-stage/heads/G14');",
|
|
"const pendingFlush = runtimeLanes.some((spec) => {",
|
|
" const localGitops = rev('refs/heads/' + spec.gitopsBranch);",
|
|
" const githubGitops = rev('refs/mirror-stage/heads/' + spec.gitopsBranch);",
|
|
" return Boolean(localGitops && githubGitops && localGitops !== githubGitops);",
|
|
"});",
|
|
"console.log(JSON.stringify({ refs, pendingFlush }));",
|
|
"NODE",
|
|
].join("\n");
|
|
}
|
|
|
|
export function preSyncV02GitMirror(sourceCommit: string, options: Pick<G14ControlPlaneOptions, "dryRun" | "timeoutSeconds">): Record<string, unknown> {
|
|
const waitMs = v02GitMirrorPreSyncWaitMs(options.timeoutSeconds);
|
|
const statusStartMs = Date.now();
|
|
printProgressEvent("hwlab.v02.trigger.progress", { stage: "git-mirror-pre-sync-status", status: "started", sourceCommit });
|
|
const before = runGitMirrorStatus();
|
|
const beforeSummary = compactGitMirrorStatus(before, sourceCommit);
|
|
printProgressEvent("hwlab.v02.trigger.progress", {
|
|
stage: "git-mirror-pre-sync-status",
|
|
status: beforeSummary.ok === true ? "succeeded" : "failed",
|
|
sourceCommit,
|
|
durationMs: Date.now() - statusStartMs,
|
|
required: beforeSummary.required,
|
|
localV02: beforeSummary.localV02,
|
|
pendingFlush: beforeSummary.pendingFlush,
|
|
reason: beforeSummary.reason,
|
|
});
|
|
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 existingMarker = readV02GitMirrorPreSyncMarker(sourceCommit, Date.now(), statusStartMs);
|
|
if (existingMarker !== null) {
|
|
return {
|
|
ok: true,
|
|
mode: "reused-recent-git-mirror-presync-marker",
|
|
sourceCommit,
|
|
before: beforeSummary,
|
|
marker: existingMarker,
|
|
waitMs,
|
|
};
|
|
}
|
|
const lock = acquireV02GitMirrorPreSyncLock(sourceCommit, waitMs, statusStartMs);
|
|
if (!lock.acquired) {
|
|
const markerAfterLockTimeout = lock.marker ?? readV02GitMirrorPreSyncMarker(sourceCommit, Date.now(), statusStartMs);
|
|
if (markerAfterLockTimeout !== null) {
|
|
return {
|
|
ok: true,
|
|
mode: "waited-for-recent-git-mirror-presync-marker",
|
|
sourceCommit,
|
|
before: beforeSummary,
|
|
marker: markerAfterLockTimeout,
|
|
waitedMs: lock.waitedMs,
|
|
waitMs,
|
|
};
|
|
}
|
|
return {
|
|
ok: false,
|
|
mode: "local-git-mirror-presync-lock",
|
|
sourceCommit,
|
|
before: beforeSummary,
|
|
lockDir: lock.lockDir,
|
|
waitedMs: lock.waitedMs,
|
|
waitMs,
|
|
degradedReason: "git-mirror-pre-sync-lock-timeout",
|
|
};
|
|
}
|
|
try {
|
|
const markerAfterWait = readV02GitMirrorPreSyncMarker(sourceCommit, Date.now(), statusStartMs);
|
|
if (markerAfterWait !== null) {
|
|
return {
|
|
ok: true,
|
|
mode: "waited-for-recent-git-mirror-presync-marker",
|
|
sourceCommit,
|
|
before: beforeSummary,
|
|
marker: markerAfterWait,
|
|
waitedMs: lock.waitedMs,
|
|
waitMs,
|
|
};
|
|
}
|
|
const recheckStartMs = Date.now();
|
|
printProgressEvent("hwlab.v02.trigger.progress", {
|
|
stage: "git-mirror-pre-sync-recheck",
|
|
status: "started",
|
|
sourceCommit,
|
|
waitedMs: lock.waitedMs,
|
|
});
|
|
const recheck = runGitMirrorStatus();
|
|
const recheckSummary = compactGitMirrorStatus(recheck, sourceCommit);
|
|
printProgressEvent("hwlab.v02.trigger.progress", {
|
|
stage: "git-mirror-pre-sync-recheck",
|
|
status: recheckSummary.ok === true ? "succeeded" : "failed",
|
|
sourceCommit,
|
|
durationMs: Date.now() - recheckStartMs,
|
|
required: recheckSummary.required,
|
|
localV02: recheckSummary.localV02,
|
|
githubV02: recheckSummary.githubV02,
|
|
pendingFlush: recheckSummary.pendingFlush,
|
|
reason: recheckSummary.reason,
|
|
});
|
|
if (recheckSummary.ok === true && recheckSummary.required === false) {
|
|
const marker = writeV02GitMirrorPreSyncMarker(sourceCommit, recheckSummary);
|
|
return {
|
|
ok: true,
|
|
mode: "became-current-after-lock-wait",
|
|
sourceCommit,
|
|
before: beforeSummary,
|
|
recheck: recheckSummary,
|
|
marker,
|
|
waitedMs: lock.waitedMs,
|
|
waitMs,
|
|
};
|
|
}
|
|
printProgressEvent("hwlab.v02.trigger.progress", {
|
|
stage: "git-mirror-pre-sync-sync",
|
|
status: "started",
|
|
sourceCommit,
|
|
reason: recheckSummary.reason,
|
|
localV02: recheckSummary.localV02,
|
|
pendingFlush: recheckSummary.pendingFlush,
|
|
});
|
|
const sync = runGitMirrorSync({
|
|
action: "sync",
|
|
lane: "v02",
|
|
confirm: true,
|
|
dryRun: false,
|
|
wait: true,
|
|
timeoutSeconds: options.timeoutSeconds,
|
|
});
|
|
printProgressEvent("hwlab.v02.trigger.progress", {
|
|
stage: "git-mirror-pre-sync-sync",
|
|
status: sync.ok === true ? "succeeded" : "failed",
|
|
sourceCommit,
|
|
durationMs: sync.elapsedMs ?? null,
|
|
jobName: sync.jobName ?? null,
|
|
});
|
|
const syncStatus = record(sync.status);
|
|
const after = syncStatus.ok === true ? syncStatus : runGitMirrorStatus();
|
|
const afterSummary = compactGitMirrorStatus(after, sourceCommit);
|
|
const wait = sync.ok === true && afterSummary.required === true ? waitForV02GitMirrorPreSync(sourceCommit, waitMs) : null;
|
|
const waitFinal = wait === null ? null : record(record(wait).final);
|
|
const finalSummary = wait !== null && wait.ok === true ? waitFinal : afterSummary;
|
|
const ok = sync.ok === true && finalSummary.required === false;
|
|
const marker = ok ? writeV02GitMirrorPreSyncMarker(sourceCommit, finalSummary) : null;
|
|
return {
|
|
ok,
|
|
mode: "auto-sync-before-trigger",
|
|
sourceCommit,
|
|
before: beforeSummary,
|
|
recheck: recheckSummary,
|
|
sync: compactGitMirrorSync(sync),
|
|
after: afterSummary,
|
|
wait,
|
|
final: finalSummary,
|
|
marker,
|
|
waitedMs: lock.waitedMs,
|
|
waitMs,
|
|
degradedReason: ok ? undefined : stringOrNull(record(wait).degradedReason) ?? "git-mirror-local-v02-not-current-after-sync",
|
|
};
|
|
} finally {
|
|
releaseV02GitMirrorPreSyncLock(lock.lockDir);
|
|
}
|
|
}
|
|
|
|
export function runGitMirrorStatus(lane: HwlabRuntimeLane = "v02"): Record<string, unknown> {
|
|
const startedAtMs = Date.now();
|
|
const spec = hwlabRuntimeLaneSpec(lane);
|
|
const script = [
|
|
"set +e",
|
|
"section() {",
|
|
" name=\"$1\"",
|
|
" shift",
|
|
" printf '__UNIDESK_SECTION_BEGIN__ %s\\n' \"$name\"",
|
|
" \"$@\"",
|
|
" code=$?",
|
|
" printf '\\n__UNIDESK_SECTION_END__ %s exit=%s\\n' \"$name\" \"$code\"",
|
|
"}",
|
|
[
|
|
"section resources kubectl get deploy,svc,pvc,cm",
|
|
`-n ${shellQuote(GIT_MIRROR_NAMESPACE)}`,
|
|
"-l app.kubernetes.io/name=git-mirror",
|
|
"-o name",
|
|
].join(" "),
|
|
[
|
|
"section legacyCronJob kubectl get cronjob",
|
|
`-n ${shellQuote(GIT_MIRROR_NAMESPACE)}`,
|
|
shellQuote(GIT_MIRROR_LEGACY_CRONJOB),
|
|
"--ignore-not-found",
|
|
"-o name",
|
|
].join(" "),
|
|
[
|
|
`section jobs kubectl get job -n ${shellQuote(GIT_MIRROR_NAMESPACE)}`,
|
|
"-l app.kubernetes.io/name=git-mirror",
|
|
"--sort-by=.metadata.creationTimestamp",
|
|
"-o custom-columns=NAME:.metadata.name,SUCCEEDED:.status.succeeded,FAILED:.status.failed,START:.status.startTime,COMPLETION:.status.completionTime",
|
|
"--no-headers",
|
|
].join(" "),
|
|
[
|
|
"section cache kubectl exec",
|
|
`-n ${shellQuote(GIT_MIRROR_NAMESPACE)}`,
|
|
"deploy/git-mirror-http",
|
|
"-- sh -lc",
|
|
shellQuote(gitMirrorCacheProbeScript()),
|
|
].join(" "),
|
|
].join("\n");
|
|
const bundle = g14K3s(["sh", "--", script], 60_000);
|
|
const sections = parseShellSections(statusText(bundle));
|
|
const resources = sections.resources;
|
|
const legacyCronJob = sections.legacyCronJob;
|
|
const jobs = sections.jobs;
|
|
const cache = sections.cache;
|
|
const bundleStderr = bundle.stderr.trim().slice(0, 2000);
|
|
return {
|
|
ok: isCommandSuccess(bundle) && shellSectionOk(resources),
|
|
command: `hwlab g14 git-mirror status --lane ${lane}`,
|
|
lane,
|
|
namespace: GIT_MIRROR_NAMESPACE,
|
|
readUrl: V02_GIT_READ_URL,
|
|
writeUrl: V02_GIT_WRITE_URL,
|
|
elapsedMs: Date.now() - startedAtMs,
|
|
resources: {
|
|
ok: shellSectionOk(resources),
|
|
names: String(resources?.stdout ?? "").split(/\r?\n/u).map((line) => line.trim()).filter(Boolean),
|
|
stderr: shellSectionOk(resources) ? "" : bundleStderr,
|
|
},
|
|
legacyCronJob: {
|
|
ok: shellSectionOk(legacyCronJob),
|
|
exists: String(legacyCronJob?.stdout ?? "").trim().length > 0,
|
|
name: String(legacyCronJob?.stdout ?? "").trim() || null,
|
|
cleanupCommand: "bun scripts/cli.ts hwlab g14 git-mirror apply --confirm",
|
|
},
|
|
jobs: {
|
|
ok: shellSectionOk(jobs),
|
|
raw: String(jobs?.stdout ?? "").trim(),
|
|
stderr: shellSectionOk(jobs) ? "" : bundleStderr,
|
|
},
|
|
cache: {
|
|
ok: shellSectionOk(cache),
|
|
summary: gitMirrorStatusSummary(String(cache?.stdout ?? "").trim()),
|
|
raw: String(cache?.stdout ?? "").trim(),
|
|
stderr: shellSectionOk(cache) ? "" : bundleStderr,
|
|
},
|
|
next: {
|
|
apply: `bun scripts/cli.ts hwlab g14 git-mirror apply --lane ${spec.lane} --confirm`,
|
|
sync: `bun scripts/cli.ts hwlab g14 git-mirror sync --lane ${spec.lane} --confirm`,
|
|
flush: `bun scripts/cli.ts hwlab g14 git-mirror flush --lane ${spec.lane} --confirm`,
|
|
},
|
|
};
|
|
}
|
|
|
|
export function runGitMirrorApply(options: G14GitMirrorOptions): Record<string, unknown> {
|
|
const spec = hwlabRuntimeLaneSpec(options.lane);
|
|
const head = resolveRuntimeLaneHead(spec);
|
|
const sourceCommit = head.sourceCommit;
|
|
if (sourceCommit === null) {
|
|
return { ok: false, command: `hwlab g14 git-mirror apply --lane ${spec.lane}`, lane: spec.lane, degradedReason: `${spec.lane}-head-unresolved`, sourceRepo: spec.cicdRepo, workspace: spec.workspace, headProbe: compactCommandResult(head.result) };
|
|
}
|
|
const render = runRuntimeLaneRenderToTemp(spec, sourceCommit);
|
|
if (!isCommandSuccess(render.result)) {
|
|
return {
|
|
ok: false,
|
|
command: `hwlab g14 git-mirror apply --lane ${spec.lane}`,
|
|
lane: spec.lane,
|
|
phase: "source-render",
|
|
sourceCommit,
|
|
renderDir: render.renderDir,
|
|
render: compactCommandResult(render.result),
|
|
};
|
|
}
|
|
const apply = applyGitMirrorManifestFile(render.renderDir, options.dryRun, options.timeoutSeconds);
|
|
const cleanupLegacyCronJob = isCommandSuccess(apply)
|
|
? deleteLegacyGitMirrorCronJob(options.dryRun)
|
|
: {
|
|
ok: false,
|
|
command: [],
|
|
exitCode: null,
|
|
stdout: "",
|
|
stderr: "skipped because apply failed",
|
|
parsed: null,
|
|
};
|
|
const cleanupRenderDir = isCommandSuccess(apply) ? cleanupRuntimeLaneRenderDir(spec, render.renderDir) : null;
|
|
return {
|
|
ok: isCommandSuccess(apply) && isCommandSuccess(cleanupLegacyCronJob),
|
|
command: `hwlab g14 git-mirror apply --lane ${spec.lane}`,
|
|
lane: spec.lane,
|
|
mode: options.dryRun ? "dry-run" : "confirmed-apply",
|
|
sourceCommit,
|
|
manifest: `${render.renderDir}/devops-infra/git-mirror.yaml`,
|
|
render: compactCommandResult(render.result),
|
|
apply: compactCommandResult(apply),
|
|
cleanupLegacyCronJob: compactCommandResult(cleanupLegacyCronJob),
|
|
cleanupRenderDir: cleanupRenderDir === null ? null : compactCommandResult(cleanupRenderDir),
|
|
status: runGitMirrorStatus(spec.lane),
|
|
next: options.dryRun
|
|
? { apply: `bun scripts/cli.ts hwlab g14 git-mirror apply --lane ${spec.lane} --confirm` }
|
|
: { sync: `bun scripts/cli.ts hwlab g14 git-mirror sync --lane ${spec.lane} --confirm` },
|
|
};
|
|
}
|
|
|
|
export function runGitMirrorSync(options: G14GitMirrorOptions): Record<string, unknown> {
|
|
const startedAtMs = Date.now();
|
|
const spec = hwlabRuntimeLaneSpec(options.lane);
|
|
const jobName = gitMirrorSyncJobName();
|
|
const manifest = gitMirrorSyncJobManifest(jobName);
|
|
const manifestB64 = Buffer.from(JSON.stringify(manifest), "utf8").toString("base64");
|
|
const script = [
|
|
"set -eu",
|
|
`job=${shellQuote(jobName)}`,
|
|
`manifest_b64=${shellQuote(manifestB64)}`,
|
|
"manifest_path=\"/tmp/$job.json\"",
|
|
"printf '%s' \"$manifest_b64\" | base64 -d > \"$manifest_path\"",
|
|
options.dryRun
|
|
? "kubectl create --dry-run=server -f \"$manifest_path\" -o name"
|
|
: [
|
|
`kubectl delete job -n ${shellQuote(GIT_MIRROR_NAMESPACE)} "$job" --ignore-not-found=true >/dev/null`,
|
|
"kubectl create -f \"$manifest_path\"",
|
|
`deadline=$(( $(date +%s) + ${options.timeoutSeconds} ))`,
|
|
"while :; do",
|
|
` status=$(kubectl get job -n ${shellQuote(GIT_MIRROR_NAMESPACE)} "$job" -o jsonpath='succeeded={.status.succeeded} failed={.status.failed}' 2>/dev/null || true)`,
|
|
" succeeded=$(printf '%s\\n' \"$status\" | awk '{for (i = 1; i <= NF; i++) { split($i, a, \"=\"); if (a[1] == \"succeeded\") print a[2]; }}')",
|
|
" failed=$(printf '%s\\n' \"$status\" | awk '{for (i = 1; i <= NF; i++) { split($i, a, \"=\"); if (a[1] == \"failed\") print a[2]; }}')",
|
|
" if [ \"${succeeded:-0}\" = \"1\" ]; then break; fi",
|
|
" if [ \"${failed:-0}\" != \"\" ] && [ \"${failed:-0}\" != \"0\" ]; then",
|
|
` kubectl logs -n ${shellQuote(GIT_MIRROR_NAMESPACE)} "job/$job" --tail=200 || true`,
|
|
" exit 44",
|
|
" fi",
|
|
" if [ \"$(date +%s)\" -ge \"$deadline\" ]; then",
|
|
` kubectl get job,pod -n ${shellQuote(GIT_MIRROR_NAMESPACE)} -l job-name="$job" -o wide || true`,
|
|
" exit 45",
|
|
" fi",
|
|
" sleep 2",
|
|
"done",
|
|
`kubectl logs -n ${shellQuote(GIT_MIRROR_NAMESPACE)} "job/$job" --tail=200 || true`,
|
|
`kubectl exec -n ${shellQuote(GIT_MIRROR_NAMESPACE)} deploy/git-mirror-http -- sh -lc ${shellQuote(`cat /cache/HWLAB.last-sync.json 2>/dev/null || true; printf "\\n"; git --git-dir=/cache/pikasTech/HWLAB.git rev-parse refs/heads/${spec.sourceBranch} refs/mirror-stage/heads/${spec.sourceBranch} refs/heads/${spec.gitopsBranch} refs/mirror-stage/heads/${spec.gitopsBranch} 2>/dev/null || true`)}`,
|
|
].join("\n"),
|
|
].join("\n");
|
|
const result = g14K3s(["sh", "--", script], options.timeoutSeconds * 1000 + 30_000);
|
|
const syncCommand = spec.lane === "v02"
|
|
? "bun scripts/cli.ts hwlab g14 git-mirror sync --lane v02 --confirm"
|
|
: `bun scripts/cli.ts hwlab nodes git-mirror sync --node ${spec.nodeId} --lane ${spec.lane} --confirm`;
|
|
const triggerCommand = spec.lane === "v02"
|
|
? "bun scripts/cli.ts hwlab g14 control-plane trigger-current --lane v02 --confirm"
|
|
: `bun scripts/cli.ts hwlab nodes control-plane trigger-current --node ${spec.nodeId} --lane ${spec.lane} --confirm`;
|
|
return {
|
|
ok: isCommandSuccess(result),
|
|
command: spec.lane === "v02" ? "hwlab g14 git-mirror sync" : "hwlab nodes git-mirror sync",
|
|
mode: options.dryRun ? "dry-run" : "confirmed-sync",
|
|
namespace: GIT_MIRROR_NAMESPACE,
|
|
jobName,
|
|
elapsedMs: Date.now() - startedAtMs,
|
|
manifest: options.dryRun ? manifest : undefined,
|
|
result: compactCommandResult(result),
|
|
status: options.dryRun ? undefined : runGitMirrorStatus(spec.lane),
|
|
next: options.dryRun ? { sync: syncCommand } : { triggerCurrent: triggerCommand },
|
|
};
|
|
}
|
|
|
|
export function runGitMirrorFlush(options: G14GitMirrorOptions): Record<string, unknown> {
|
|
const jobName = gitMirrorFlushJobName();
|
|
const manifest = gitMirrorFlushJobManifest(jobName);
|
|
const manifestB64 = Buffer.from(JSON.stringify(manifest), "utf8").toString("base64");
|
|
const script = [
|
|
"set -eu",
|
|
`job=${shellQuote(jobName)}`,
|
|
`manifest_b64=${shellQuote(manifestB64)}`,
|
|
"manifest_path=\"/tmp/$job.json\"",
|
|
"printf '%s' \"$manifest_b64\" | base64 -d > \"$manifest_path\"",
|
|
options.dryRun
|
|
? "kubectl create --dry-run=server -f \"$manifest_path\" -o name"
|
|
: [
|
|
`kubectl delete job -n ${shellQuote(GIT_MIRROR_NAMESPACE)} "$job" --ignore-not-found=true >/dev/null`,
|
|
"kubectl create -f \"$manifest_path\"",
|
|
`deadline=$(( $(date +%s) + ${options.timeoutSeconds} ))`,
|
|
"while :; do",
|
|
` status=$(kubectl get job -n ${shellQuote(GIT_MIRROR_NAMESPACE)} "$job" -o jsonpath='succeeded={.status.succeeded} failed={.status.failed}' 2>/dev/null || true)`,
|
|
" succeeded=$(printf '%s\n' \"$status\" | awk '{for (i = 1; i <= NF; i++) { split($i, a, \"=\"); if (a[1] == \"succeeded\") print a[2]; }}')",
|
|
" failed=$(printf '%s\n' \"$status\" | awk '{for (i = 1; i <= NF; i++) { split($i, a, \"=\"); if (a[1] == \"failed\") print a[2]; }}')",
|
|
" if [ \"${succeeded:-0}\" = \"1\" ]; then break; fi",
|
|
" if [ \"${failed:-0}\" != \"\" ] && [ \"${failed:-0}\" != \"0\" ]; then",
|
|
` kubectl logs -n ${shellQuote(GIT_MIRROR_NAMESPACE)} "job/$job" --tail=200 || true`,
|
|
" exit 54",
|
|
" fi",
|
|
" if [ \"$(date +%s)\" -ge \"$deadline\" ]; then",
|
|
` kubectl get job,pod -n ${shellQuote(GIT_MIRROR_NAMESPACE)} -l job-name="$job" -o wide || true`,
|
|
" exit 55",
|
|
" fi",
|
|
" sleep 2",
|
|
"done",
|
|
`kubectl logs -n ${shellQuote(GIT_MIRROR_NAMESPACE)} "job/$job" --tail=200 || true`,
|
|
`kubectl exec -n ${shellQuote(GIT_MIRROR_NAMESPACE)} deploy/git-mirror-http -- sh -lc ${shellQuote(`cat /cache/HWLAB.last-flush.json 2>/dev/null || true; printf "\\n"; git --git-dir=/cache/pikasTech/HWLAB.git rev-parse refs/heads/${options.lane === "v02" ? "v0.2-gitops" : hwlabRuntimeLaneSpec(options.lane).gitopsBranch} refs/mirror-stage/heads/${options.lane === "v02" ? "v0.2-gitops" : hwlabRuntimeLaneSpec(options.lane).gitopsBranch} 2>/dev/null || true`)}`,
|
|
].join("\n"),
|
|
].join("\n");
|
|
const result = g14K3s(["sh", "--", script], options.timeoutSeconds * 1000 + 30_000);
|
|
return {
|
|
ok: isCommandSuccess(result),
|
|
command: "hwlab g14 git-mirror flush",
|
|
mode: options.dryRun ? "dry-run" : "confirmed-flush",
|
|
namespace: GIT_MIRROR_NAMESPACE,
|
|
jobName,
|
|
manifest: options.dryRun ? manifest : undefined,
|
|
result: compactCommandResult(result),
|
|
status: options.dryRun ? undefined : runGitMirrorStatus(options.lane),
|
|
next: options.dryRun
|
|
? { flush: `bun scripts/cli.ts hwlab g14 git-mirror flush --lane ${options.lane} --confirm` }
|
|
: { status: `bun scripts/cli.ts hwlab g14 git-mirror status --lane ${options.lane}` },
|
|
};
|
|
}
|
|
|
|
export function runG14GitMirror(options: G14GitMirrorOptions): Record<string, unknown> {
|
|
if (options.action === "status") return runGitMirrorStatus(options.lane);
|
|
if (options.action === "apply") return runGitMirrorApply(options);
|
|
if (options.action === "flush") return runGitMirrorFlush(options);
|
|
return runGitMirrorSync(options);
|
|
}
|