feat: add hwlab git mirror flush command

This commit is contained in:
Codex
2026-05-30 00:39:57 +00:00
parent e7c2d97e46
commit 5c88f1df58
4 changed files with 111 additions and 10 deletions
+11 -1
View File
@@ -1,4 +1,4 @@
import { gitMirrorSyncJobManifest, hwlabG14MonitorStateFileName, parsePipelineTaskRunMetrics, rolloutRecordBody, semanticChangelogBullets } from "./src/hwlab-g14";
import { gitMirrorFlushJobManifest, gitMirrorSyncJobManifest, hwlabG14MonitorStateFileName, parsePipelineTaskRunMetrics, rolloutRecordBody, semanticChangelogBullets } from "./src/hwlab-g14";
function assertCondition(condition: unknown, message: string, detail: unknown = {}): void {
if (!condition) throw new Error(`${message}: ${JSON.stringify(detail)}`);
@@ -31,6 +31,15 @@ assertCondition(record(gitMirrorJob.metadata).namespace === "devops-infra", "git
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 prBody = [
"## 背景",
"",
@@ -135,6 +144,7 @@ console.log(JSON.stringify({
"dry-run jobs do not overwrite live monitor state",
"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",
"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",
+1 -1
View File
@@ -57,7 +57,7 @@ export function rootHelp(): unknown {
{ command: "auth-broker contract|health --dry-run|credential-request --dry-run|pr-preflight --dry-run", description: "Inspect the P0 Rust auth broker and CLI adapter contract without reading token values, writing GitHub, or starting services." },
{ command: "gh preflight|auth|issue|pr", description: "Run safe GitHub issue and PR CRUD/lifecycle operations through REST with body-file update replace/append, comment delete, token diagnostics, PR closeout preflight, hard delete unsupported, and guarded PR merge." },
{ command: "commander contract|plan --dry-run|smoke --dry-run|approval request --dry-run|prompt-lint --kind gpt55-pr", description: "Host Codex commander skeleton contract, no-daemon smoke plan, dry-run approval preview, and advisory GPT-5.5 PR prompt boundary lint without live bridges, message sends, or submit gating." },
{ command: "hwlab g14 monitor-prs | hwlab g14 control-plane status|apply|trigger-current|runtime-migration|cleanup-runs|cleanup-released-pvs | hwlab g14 git-mirror status|apply|sync | hwlab g14 tools-image status|build", description: "Start the G14 PR monitor, run bounded v0.2 Tekton/Argo control-plane, manual PipelineRun trigger, runtime migration, CI workspace retention, manual devops-infra git mirror maintenance, or fixed HWLAB CI tools image actions through UniDesk G14 routes." },
{ command: "hwlab g14 monitor-prs | hwlab g14 control-plane status|apply|trigger-current|runtime-migration|cleanup-runs|cleanup-released-pvs | hwlab g14 git-mirror status|apply|sync|flush | hwlab g14 tools-image status|build", description: "Start the G14 PR monitor, run bounded v0.2 Tekton/Argo control-plane, manual PipelineRun trigger, runtime migration, CI workspace retention, manual devops-infra git mirror/relay maintenance, or fixed HWLAB CI tools image actions through UniDesk G14 routes." },
{ command: "hwlab cd audit --env dev | hwlab cd status --env dev | hwlab cd apply --env dev --dry-run", description: "Legacy D601 HWLAB DEV CD wrapper kept for explicit old-path diagnostics; current HWLAB rollout uses G14 GitOps." },
{ command: "code-agent-sandbox", description: "Independent Code Agent Sandbox service skeleton for adapter, mode, and credential-boundary diagnostics." },
{ command: "schedule list|get|runs|run|retry-run|delete", description: "Manage backend-core scheduled tasks and run history; schedule run <id> supports --wait-ms N and retry-run reuses the failed run's schedule." },
+92 -7
View File
@@ -22,6 +22,7 @@ const V02_PIPELINERUN_PREFIX = "hwlab-v02-ci-poll";
const V02_CONTROL_PLANE_FIELD_MANAGER = "unidesk-hwlab-v02-control-plane";
const V02_GIT_URL = "git@github.com:pikasTech/HWLAB.git";
const V02_GIT_READ_URL = "http://git-mirror-http.devops-infra.svc.cluster.local/pikasTech/HWLAB.git";
const V02_GIT_WRITE_URL = "http://git-mirror-write.devops-infra.svc.cluster.local/pikasTech/HWLAB.git";
const V02_GITOPS_BRANCH = "v0.2-gitops";
const V02_CATALOG_PATH = "deploy/artifact-catalog.v02.json";
const V02_RUNTIME_PATH = "deploy/gitops/g14/runtime-v02";
@@ -92,7 +93,7 @@ interface G14ToolsImageOptions {
}
interface G14GitMirrorOptions {
action: "status" | "apply" | "sync";
action: "status" | "apply" | "sync" | "flush";
dryRun: boolean;
confirm: boolean;
timeoutSeconds: number;
@@ -257,8 +258,8 @@ function parseToolsImageOptions(args: string[]): G14ToolsImageOptions {
function parseGitMirrorOptions(args: string[]): G14GitMirrorOptions {
const [actionRaw] = args;
if (actionRaw !== "status" && actionRaw !== "apply" && actionRaw !== "sync") {
throw new Error("git-mirror usage: status|apply|sync [--dry-run|--confirm]");
if (actionRaw !== "status" && actionRaw !== "apply" && actionRaw !== "sync" && actionRaw !== "flush") {
throw new Error("git-mirror usage: status|apply|sync|flush [--dry-run|--confirm]");
}
const confirm = args.includes("--confirm");
const explicitDryRun = args.includes("--dry-run");
@@ -267,7 +268,7 @@ function parseGitMirrorOptions(args: string[]): G14GitMirrorOptions {
action: actionRaw,
confirm,
dryRun: actionRaw === "status" ? true : explicitDryRun || !confirm,
timeoutSeconds: positiveIntegerOption(args, "--timeout-seconds", actionRaw === "sync" ? 300 : 120, 900),
timeoutSeconds: positiveIntegerOption(args, "--timeout-seconds", actionRaw === "sync" || actionRaw === "flush" ? 300 : 120, 900),
};
}
@@ -1004,6 +1005,10 @@ function gitMirrorSyncJobName(): string {
return `${GIT_MIRROR_SYNC_JOB_PREFIX}-${Date.now().toString(36)}`.slice(0, 63);
}
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",
@@ -1057,6 +1062,20 @@ export function gitMirrorSyncJobManifest(name: string): Record<string, unknown>
};
}
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;
}
function runGitMirrorStatus(): Record<string, unknown> {
const resources = g14K3s([
"kubectl",
@@ -1087,7 +1106,7 @@ function runGitMirrorStatus(): Record<string, unknown> {
"-n",
GIT_MIRROR_NAMESPACE,
"-l",
"app.kubernetes.io/name=git-mirror,app.kubernetes.io/component=sync-controller",
"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",
@@ -1102,13 +1121,27 @@ function runGitMirrorStatus(): Record<string, unknown> {
"--",
"sh",
"-lc",
"cat /cache/HWLAB.last-sync.json 2>/dev/null || true; printf '\\n'; git --git-dir=/cache/pikasTech/HWLAB.git rev-parse refs/heads/v0.2 refs/heads/v0.2-gitops refs/heads/G14 2>/dev/null || true",
[
"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 localGitops = rev('refs/heads/v0.2-gitops');",
"const githubGitops = rev('refs/mirror-stage/heads/v0.2-gitops');",
"console.log(JSON.stringify({ refs: { localV02: rev('refs/heads/v0.2'), localG14: rev('refs/heads/G14'), localGitops, githubGitops }, pendingFlush: Boolean(localGitops && githubGitops && localGitops !== githubGitops) }));",
"NODE",
].join("\n"),
], 60_000);
return {
ok: isCommandSuccess(resources),
command: "hwlab g14 git-mirror status",
namespace: GIT_MIRROR_NAMESPACE,
readUrl: V02_GIT_READ_URL,
writeUrl: V02_GIT_WRITE_URL,
resources: {
ok: isCommandSuccess(resources),
names: statusText(resources).split(/\r?\n/u).map((line) => line.trim()).filter(Boolean),
@@ -1133,6 +1166,7 @@ function runGitMirrorStatus(): Record<string, unknown> {
next: {
apply: "bun scripts/cli.ts hwlab g14 git-mirror apply --confirm",
sync: "bun scripts/cli.ts hwlab g14 git-mirror sync --confirm",
flush: "bun scripts/cli.ts hwlab g14 git-mirror flush --confirm",
},
};
}
@@ -1228,9 +1262,59 @@ function runGitMirrorSync(options: G14GitMirrorOptions): Record<string, unknown>
};
}
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='{.status.succeeded} {.status.failed}' 2>/dev/null || true)`,
" succeeded=$(printf '%s\n' \"$status\" | awk '{print $1}')",
" failed=$(printf '%s\n' \"$status\" | awk '{print $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 'cat /cache/HWLAB.last-flush.json 2>/dev/null || true; printf "\\n"; git --git-dir=/cache/pikasTech/HWLAB.git rev-parse refs/heads/v0.2-gitops refs/mirror-stage/heads/v0.2-gitops 2>/dev/null || true'`,
].join("\n"),
].join("\n");
const result = g14K3s(["script", "--", 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,
status: options.dryRun ? undefined : runGitMirrorStatus(),
next: options.dryRun ? { flush: "bun scripts/cli.ts hwlab g14 git-mirror flush --confirm" } : { status: "bun scripts/cli.ts hwlab g14 git-mirror status" },
};
}
function runG14GitMirror(options: G14GitMirrorOptions): Record<string, unknown> {
if (options.action === "status") return runGitMirrorStatus();
if (options.action === "apply") return runGitMirrorApply(options);
if (options.action === "flush") return runGitMirrorFlush(options);
return runGitMirrorSync(options);
}
@@ -2095,11 +2179,12 @@ export function hwlabG14Help(): Record<string, unknown> {
"bun scripts/cli.ts hwlab g14 git-mirror status",
"bun scripts/cli.ts hwlab g14 git-mirror apply --confirm",
"bun scripts/cli.ts hwlab g14 git-mirror sync --confirm",
"bun scripts/cli.ts hwlab g14 git-mirror flush --confirm",
"bun scripts/cli.ts hwlab g14 tools-image status --name ci-node-tools --tag node22-alpine-bun-v1",
"bun scripts/cli.ts hwlab g14 tools-image build --name ci-node-tools --tag node22-alpine-bun-v1 --confirm",
"bun scripts/cli.ts job status <jobId> --tail-bytes 30000",
],
description: "G14 HWLAB PR monitor, DEV rollout command, bounded v0.2 control-plane bootstrap/cleanup/runtime-migration helper, devops-infra git mirror maintenance, and controlled CI tools image build/status entry. The public monitor starts a fire-and-forget job; control-plane status/apply/trigger-current/cleanup-runs/cleanup-released-pvs/runtime-migration uses UniDesk G14:k3s routes for v0.2 Tekton/Argo control resources, manual PipelineRun trigger, runtime migration, and completed CI workspace retention only. git-mirror status/apply/sync is the manual devops-infra mirror control path and does not install a CronJob.",
description: "G14 HWLAB PR monitor, DEV rollout command, bounded v0.2 control-plane bootstrap/cleanup/runtime-migration helper, devops-infra git mirror maintenance, and controlled CI tools image build/status entry. The public monitor starts a fire-and-forget job; control-plane status/apply/trigger-current/cleanup-runs/cleanup-released-pvs/runtime-migration uses UniDesk G14:k3s routes for v0.2 Tekton/Argo control resources, manual PipelineRun trigger, runtime migration, and completed CI workspace retention only. git-mirror status/apply/sync/flush is the manual devops-infra mirror/relay control path and does not install a CronJob.",
defaults: {
repo: HWLAB_REPO,
base: G14_SOURCE_BRANCH,