feat: add manual G14 git mirror control

This commit is contained in:
Codex
2026-05-29 18:54:50 +00:00
parent 4c8926fd7f
commit 4326ffe2f5
4 changed files with 311 additions and 5 deletions
+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 tools-image status|build", description: "Start the G14 PR monitor, run bounded v0.2 Tekton/Argo control-plane, manual PipelineRun trigger, runtime migration, and CI workspace retention actions, or build/status fixed HWLAB CI tools images 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 | 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 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." },
+297 -2
View File
@@ -27,6 +27,10 @@ const V02_CATALOG_PATH = "deploy/artifact-catalog.v02.json";
const V02_RUNTIME_PATH = "deploy/gitops/g14/runtime-v02";
const V02_REGISTRY_PREFIX = "127.0.0.1:5000/hwlab";
const V02_BASE_IMAGE = "127.0.0.1:5000/hwlab/hwlab-node20-base:20-bookworm-slim";
const GIT_MIRROR_NAMESPACE = "devops-infra";
const GIT_MIRROR_MANIFEST_FIELD_MANAGER = "unidesk-hwlab-git-mirror";
const GIT_MIRROR_SYNC_JOB_PREFIX = "git-mirror-hwlab-sync-manual";
const GIT_MIRROR_LEGACY_CRONJOB = "git-mirror-hwlab-sync";
const V02_SERVICE_IDS = [
"hwlab-cloud-api",
"hwlab-cloud-web",
@@ -87,6 +91,13 @@ interface G14ToolsImageOptions {
timeoutSeconds: number;
}
interface G14GitMirrorOptions {
action: "status" | "apply" | "sync";
dryRun: boolean;
confirm: boolean;
timeoutSeconds: number;
}
interface CommandJsonResult {
ok: boolean;
command: string[];
@@ -244,6 +255,22 @@ 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]");
}
const confirm = args.includes("--confirm");
const explicitDryRun = args.includes("--dry-run");
if (confirm && explicitDryRun) throw new Error("git-mirror accepts only one of --confirm or --dry-run");
return {
action: actionRaw,
confirm,
dryRun: actionRaw === "status" ? true : explicitDryRun || !confirm,
timeoutSeconds: positiveIntegerOption(args, "--timeout-seconds", actionRaw === "sync" ? 300 : 120, 900),
};
}
function positiveIntegerOption(args: string[], name: string, defaultValue: number, maxValue: number): number {
const index = args.indexOf(name);
if (index === -1) return defaultValue;
@@ -946,6 +973,267 @@ function runV02ControlPlane(options: G14ControlPlaneOptions): Record<string, unk
};
}
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);
}
function applyGitMirrorManifestFile(sourceCommit: string, dryRun: boolean, timeoutSeconds: number): CommandJsonResult {
const renderDir = v02RenderDir(sourceCommit);
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);
}
function gitMirrorSyncJobName(): string {
return `${GIT_MIRROR_SYNC_JOB_PREFIX}-${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 },
],
}],
},
},
},
};
}
function runGitMirrorStatus(): Record<string, unknown> {
const resources = g14K3s([
"kubectl",
"get",
"deploy,svc,pvc,cm",
"-n",
GIT_MIRROR_NAMESPACE,
"-l",
"app.kubernetes.io/name=git-mirror",
"-o",
"name",
], 60_000);
const legacyCronJob = g14K3s([
"kubectl",
"get",
"cronjob",
"-n",
GIT_MIRROR_NAMESPACE,
GIT_MIRROR_LEGACY_CRONJOB,
"--ignore-not-found",
"-o",
"name",
], 60_000);
const jobs = g14K3s([
"kubectl",
"get",
"job",
"-n",
GIT_MIRROR_NAMESPACE,
"-l",
"app.kubernetes.io/name=git-mirror,app.kubernetes.io/component=sync-controller",
"--sort-by=.metadata.creationTimestamp",
"-o",
"custom-columns=NAME:.metadata.name,SUCCEEDED:.status.succeeded,FAILED:.status.failed,START:.status.startTime,COMPLETION:.status.completionTime",
"--no-headers",
], 60_000);
const cache = g14K3s([
"kubectl",
"exec",
"-n",
GIT_MIRROR_NAMESPACE,
"deploy/git-mirror-http",
"--",
"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",
], 60_000);
return {
ok: isCommandSuccess(resources),
command: "hwlab g14 git-mirror status",
namespace: GIT_MIRROR_NAMESPACE,
readUrl: V02_GIT_READ_URL,
resources: {
ok: isCommandSuccess(resources),
names: statusText(resources).split(/\r?\n/u).map((line) => line.trim()).filter(Boolean),
stderr: resources.stderr.trim().slice(0, 2000),
},
legacyCronJob: {
ok: isCommandSuccess(legacyCronJob),
exists: statusText(legacyCronJob).trim().length > 0,
name: statusText(legacyCronJob).trim() || null,
cleanupCommand: "bun scripts/cli.ts hwlab g14 git-mirror apply --confirm",
},
jobs: {
ok: isCommandSuccess(jobs),
raw: statusText(jobs),
stderr: jobs.stderr.trim().slice(0, 2000),
},
cache: {
ok: isCommandSuccess(cache),
raw: statusText(cache),
stderr: cache.stderr.trim().slice(0, 2000),
},
next: {
apply: "bun scripts/cli.ts hwlab g14 git-mirror apply --confirm",
sync: "bun scripts/cli.ts hwlab g14 git-mirror sync --confirm",
},
};
}
function runGitMirrorApply(options: G14GitMirrorOptions): Record<string, unknown> {
const sourceCommit = getV02Head();
if (sourceCommit === null) {
return { ok: false, command: "hwlab g14 git-mirror apply", degradedReason: "v02-head-unresolved", workspace: V02_WORKSPACE };
}
const renderCheck = runV02RenderCheck(sourceCommit);
if (!isCommandSuccess(renderCheck)) {
return {
ok: false,
command: "hwlab g14 git-mirror apply",
phase: "source-render-check",
sourceCommit,
renderCheck,
};
}
const apply = applyGitMirrorManifestFile(sourceCommit, options.dryRun, options.timeoutSeconds);
const cleanupLegacyCronJob = isCommandSuccess(apply)
? deleteLegacyGitMirrorCronJob(options.dryRun)
: {
ok: false,
command: [],
exitCode: null,
stdout: "",
stderr: "skipped because apply failed",
parsed: null,
};
return {
ok: isCommandSuccess(apply) && isCommandSuccess(cleanupLegacyCronJob),
command: "hwlab g14 git-mirror apply",
mode: options.dryRun ? "dry-run" : "confirmed-apply",
sourceCommit,
manifest: `${v02RenderDir(sourceCommit)}/devops-infra/git-mirror.yaml`,
renderCheck: commandData(renderCheck),
apply,
cleanupLegacyCronJob,
status: runGitMirrorStatus(),
next: options.dryRun
? { apply: "bun scripts/cli.ts hwlab g14 git-mirror apply --confirm" }
: { sync: "bun scripts/cli.ts hwlab g14 git-mirror sync --confirm" },
};
}
function runGitMirrorSync(options: G14GitMirrorOptions): Record<string, unknown> {
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='{.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 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 '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'`,
].join("\n"),
].join("\n");
const result = g14K3s(["script", "--", script], options.timeoutSeconds * 1000 + 30_000);
return {
ok: isCommandSuccess(result),
command: "hwlab g14 git-mirror sync",
mode: options.dryRun ? "dry-run" : "confirmed-sync",
namespace: GIT_MIRROR_NAMESPACE,
jobName,
manifest: options.dryRun ? manifest : undefined,
result,
status: options.dryRun ? undefined : runGitMirrorStatus(),
next: options.dryRun ? { sync: "bun scripts/cli.ts hwlab g14 git-mirror sync --confirm" } : { triggerCurrent: "bun scripts/cli.ts hwlab g14 control-plane trigger-current --lane v02 --confirm" },
};
}
function runG14GitMirror(options: G14GitMirrorOptions): Record<string, unknown> {
if (options.action === "status") return runGitMirrorStatus();
if (options.action === "apply") return runGitMirrorApply(options);
return runGitMirrorSync(options);
}
function g14HostScript(script: string, timeoutMs = 120_000): CommandJsonResult {
return cliJson(["ssh", G14_PROVIDER, "script", "--", script], timeoutMs);
}
@@ -1804,11 +2092,14 @@ export function hwlabG14Help(): Record<string, unknown> {
"bun scripts/cli.ts hwlab g14 control-plane runtime-migration --lane v02 --dry-run",
"bun scripts/cli.ts hwlab g14 control-plane runtime-migration --lane v02 --allow-live-db-read --dry-run",
"bun scripts/cli.ts hwlab g14 control-plane runtime-migration --lane v02 --confirm",
"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 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, 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.",
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.",
defaults: {
repo: HWLAB_REPO,
base: G14_SOURCE_BRANCH,
@@ -1845,8 +2136,12 @@ export async function runHwlabG14Command(_config: Config, args: string[]): Promi
const options = parseToolsImageOptions(args.slice(1));
return runG14ToolsImage(options);
}
if (action === "git-mirror") {
const options = parseGitMirrorOptions(args.slice(1));
return runG14GitMirror(options);
}
if (action !== "monitor-prs") {
return { ok: false, command: `hwlab g14 ${action ?? ""}`.trim(), degradedReason: "unsupported-command", message: "supported commands: hwlab g14 monitor-prs, hwlab g14 record-rollout, hwlab g14 control-plane, hwlab g14 tools-image" };
return { ok: false, command: `hwlab g14 ${action ?? ""}`.trim(), degradedReason: "unsupported-command", message: "supported commands: hwlab g14 monitor-prs, hwlab g14 record-rollout, hwlab g14 control-plane, hwlab g14 git-mirror, hwlab g14 tools-image" };
}
const options = parseOptions(args.slice(1));
if (options.worker) return runMonitorWorker(options);