// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. images module for scripts/src/hwlab-g14.ts. // Moved mechanically from scripts/src/hwlab-g14.ts:8075-8386 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, G14ToolsImageOptions, G14UpstreamImageOptions } from "./types"; import { statusText } from "./pr-monitor"; import { cliJson, isCommandSuccess, record, shellQuote } from "./remote"; import { G14_CI_TOOLS_IMAGE_REPO, G14_PROVIDER, GIT_MIRROR_NAMESPACE, V02_REGISTRY_PREFIX, V02_WORKSPACE } from "./types"; export function startAsyncHwlabG14Job(name: string, command: string[], note: string): Record { const job = startJob(name, command, note); const statusCommand = `bun scripts/cli.ts job status ${job.id} --tail-bytes 12000`; return { ok: true, mode: "async-job", job, statusCommand, tailCommand: `tail -f ${job.stdoutFile}`, next: { status: statusCommand, tail: `tail -f ${job.stdoutFile}`, }, }; } export function startControlPlaneTriggerJob(options: G14ControlPlaneOptions): Record { const command = [ "bun", "scripts/cli.ts", "hwlab", "g14", "control-plane", "trigger-current", "--lane", options.lane, "--confirm", "--timeout-seconds", String(options.timeoutSeconds), "--wait", ]; if (options.rerun) command.splice(command.length - 1, 0, "--rerun"); return { command: `hwlab g14 control-plane trigger-current --lane ${options.lane}`, lane: options.lane, reason: "confirmed trigger can spend tens of seconds syncing git mirror and creating PipelineRun; default is fire-and-forget to avoid silent blocking", waitCommand: command.join(" "), ...startAsyncHwlabG14Job( `hwlab_g14_${options.lane}_trigger_current`, command, `Trigger HWLAB ${options.lane} current commit PipelineRun with git mirror pre-sync through G14 control-plane`, ), }; } export function startGitMirrorJob(options: G14GitMirrorOptions): Record { const command = [ "bun", "scripts/cli.ts", "hwlab", "g14", "git-mirror", options.action, "--lane", options.lane, "--confirm", "--timeout-seconds", String(options.timeoutSeconds), "--wait", ]; return { command: `hwlab g14 git-mirror ${options.action} --lane ${options.lane}`, lane: options.lane, namespace: GIT_MIRROR_NAMESPACE, reason: "manual git mirror sync/flush waits for a Kubernetes Job; default is fire-and-forget to keep CLI output immediately visible", waitCommand: command.join(" "), ...startAsyncHwlabG14Job( `hwlab_g14_git_mirror_${options.action}`, command, `Run HWLAB devops-infra git mirror ${options.action} through a bounded manual Kubernetes Job`, ), }; } export function g14HostScript(script: string, timeoutMs = 120_000): CommandJsonResult { return cliJson(["ssh", G14_PROVIDER, "sh", "--", script], timeoutMs); } export function g14CiToolsImage(tag: string): string { return `${G14_CI_TOOLS_IMAGE_REPO}:${tag}`; } export function runG14ToolsImageStatus(options: G14ToolsImageOptions): Record { const image = g14CiToolsImage(options.tag); const script = [ "set -u", `image=${shellQuote(image)}`, "exists=false", "image_id=", "created=", "size=", "node_version=", "npm_version=", "bun_version=", "git_version=", "docker_version=", "python_version=", "registry_tags=", "if docker image inspect \"$image\" >/tmp/hwlab-tools-image-inspect.json 2>/tmp/hwlab-tools-image-inspect.err; then", " exists=true", " image_id=$(docker image inspect \"$image\" --format '{{.Id}}' 2>/dev/null || true)", " created=$(docker image inspect \"$image\" --format '{{.Created}}' 2>/dev/null || true)", " size=$(docker image inspect \"$image\" --format '{{.Size}}' 2>/dev/null || true)", " node_version=$(docker run --rm \"$image\" node --version 2>/dev/null || true)", " npm_version=$(docker run --rm \"$image\" npm --version 2>/dev/null || true)", " bun_version=$(docker run --rm \"$image\" bun --version 2>/dev/null || true)", " git_version=$(docker run --rm \"$image\" git --version 2>/dev/null || true)", " docker_version=$(docker run --rm \"$image\" docker --version 2>/dev/null || true)", " python_version=$(docker run --rm \"$image\" python3 --version 2>/dev/null || true)", "fi", `registry_tags=$(curl -fsS --max-time 10 http://127.0.0.1:5000/v2/hwlab/hwlab-ci-node-tools/tags/list 2>/dev/null || true)`, "export image exists image_id created size node_version npm_version bun_version git_version docker_version python_version registry_tags", "node - <<'NODE'", "const fs = require('node:fs');", "const env = process.env;", "const payload = {", " image: env.image,", " exists: env.exists === 'true',", " imageId: env.image_id || null,", " created: env.created || null,", " sizeBytes: env.size ? Number(env.size) : null,", " tools: {", " node: env.node_version || null,", " npm: env.npm_version || null,", " bun: env.bun_version || null,", " git: env.git_version || null,", " docker: env.docker_version || null,", " python: env.python_version || null", " },", " registryTagsRaw: env.registry_tags || null", "};", "console.log(JSON.stringify(payload, null, 2));", "NODE", ].join("\n"); const result = g14HostScript(script, 180_000); let parsedStatus: unknown = null; const text = statusText(result); if (text.length > 0) { try { parsedStatus = JSON.parse(text) as unknown; } catch { parsedStatus = null; } } return { ok: isCommandSuccess(result) && record(parsedStatus).exists === true, command: "hwlab g14 tools-image status --name ci-node-tools", name: options.name, image, status: parsedStatus ?? text, result, }; } export function runG14ToolsImageBuild(options: G14ToolsImageOptions): Record { const image = g14CiToolsImage(options.tag); const script = [ "set -eu", `cd ${shellQuote(V02_WORKSPACE)}`, "git fetch origin v0.2 --prune", "git checkout v0.2 >/dev/null 2>&1 || true", "git merge --ff-only origin/v0.2", `test -f ${shellQuote(options.dockerfile)}`, `image=${shellQuote(image)}`, `dockerfile=${shellQuote(options.dockerfile)}`, "echo \"{\\\"phase\\\":\\\"preflight\\\",\\\"image\\\":\\\"$image\\\",\\\"dockerfile\\\":\\\"$dockerfile\\\",\\\"commit\\\":\\\"$(git rev-parse HEAD)\\\"}\"", "export HTTP_PROXY=http://127.0.0.1:10808 HTTPS_PROXY=http://127.0.0.1:10808 http_proxy=http://127.0.0.1:10808 https_proxy=http://127.0.0.1:10808", "export NO_PROXY=localhost,127.0.0.1,::1,host.docker.internal,74.48.78.17,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,10.42.0.0/16,10.43.0.0/16,.svc,.svc.cluster.local,.cluster.local,kubernetes,kubernetes.default,kubernetes.default.svc,127.0.0.1:5000,localhost:5000", "export no_proxy=$NO_PROXY", "docker build --pull --build-arg HTTP_PROXY --build-arg HTTPS_PROXY --build-arg http_proxy --build-arg https_proxy --build-arg NO_PROXY --build-arg no_proxy -f \"$dockerfile\" -t \"$image\" .", "docker run --rm \"$image\" sh -lc 'node --version && npm --version && bun --version && git --version && python3 --version && docker --version && ssh -V'", "docker push \"$image\"", "digest=$(docker image inspect \"$image\" --format '{{index .RepoDigests 0}}' 2>/dev/null || true)", "echo \"{\\\"phase\\\":\\\"published\\\",\\\"image\\\":\\\"$image\\\",\\\"digest\\\":\\\"$digest\\\"}\"", ].join("\n"); if (options.dryRun) { return { ok: true, command: "hwlab g14 tools-image build --name ci-node-tools", mode: "dry-run", image, workspace: V02_WORKSPACE, dockerfile: options.dockerfile, buildScriptPreview: script.split(/\n/u), next: { build: `bun scripts/cli.ts hwlab g14 tools-image build --name ci-node-tools --tag ${options.tag} --confirm` }, }; } const result = g14HostScript(script, options.timeoutSeconds * 1000); return { ok: isCommandSuccess(result), command: "hwlab g14 tools-image build --name ci-node-tools", mode: "confirmed-build", image, workspace: V02_WORKSPACE, dockerfile: options.dockerfile, result, status: runG14ToolsImageStatus({ ...options, action: "status", dryRun: true, confirm: false }), }; } export function runG14ToolsImage(options: G14ToolsImageOptions): Record { if (options.action === "status") return runG14ToolsImageStatus(options); return runG14ToolsImageBuild(options); } export function g14UpstreamImageTarget(options: G14UpstreamImageOptions): string { return `${V02_REGISTRY_PREFIX}/${options.name}:${options.tag}`; } export function g14UpstreamImageSource(options: G14UpstreamImageOptions): string { if (options.name === "openfga") return `docker.io/openfga/openfga:${options.tag}`; return `${options.name}:${options.tag}`; } export function runG14UpstreamImageStatus(options: G14UpstreamImageOptions): Record { const sourceImage = g14UpstreamImageSource(options); const targetImage = g14UpstreamImageTarget(options); const script = [ "set +e", `source_image=${shellQuote(sourceImage)}`, `target_image=${shellQuote(targetImage)}`, `repo_path=${shellQuote(`hwlab/${options.name}`)}`, `tag=${shellQuote(options.tag)}`, "local_id=$(docker image inspect \"$target_image\" --format '{{.Id}}' 2>/dev/null || true)", "local_created=$(docker image inspect \"$target_image\" --format '{{.Created}}' 2>/dev/null || true)", "registry_tags=$(curl -fsS --max-time 10 \"http://127.0.0.1:5000/v2/$repo_path/tags/list\" 2>/dev/null || true)", "registry_has_tag=false", "if printf '%s' \"$registry_tags\" | grep -F '\"'$tag'\"' >/dev/null 2>&1; then registry_has_tag=true; fi", "export source_image target_image local_id local_created registry_tags registry_has_tag", "node <<'NODE'", "const env = process.env;", "console.log(JSON.stringify({", " sourceImage: env.source_image,", " targetImage: env.target_image,", " localExists: Boolean(env.local_id),", " localImageId: env.local_id || null,", " localCreated: env.local_created || null,", " registryHasTag: env.registry_has_tag === 'true',", " registryTagsRaw: env.registry_tags || null", "}, null, 2));", "NODE", ].join("\n"); const result = g14HostScript(script, 120_000); let parsedStatus: unknown = null; const text = statusText(result); if (text.length > 0) { try { parsedStatus = JSON.parse(text) as unknown; } catch { parsedStatus = null; } } return { ok: isCommandSuccess(result) && record(parsedStatus).registryHasTag === true, command: "hwlab g14 upstream-image status --name openfga", name: options.name, tag: options.tag, sourceImage, targetImage, status: parsedStatus ?? text, result, }; } export function runG14UpstreamImageEnsure(options: G14UpstreamImageOptions): Record { const sourceImage = g14UpstreamImageSource(options); const targetImage = g14UpstreamImageTarget(options); if (options.dryRun) { return { ok: true, command: "hwlab g14 upstream-image ensure --name openfga", mode: "dry-run", name: options.name, tag: options.tag, sourceImage, targetImage, mutation: false, next: { confirm: `bun scripts/cli.ts hwlab g14 upstream-image ensure --name ${options.name} --tag ${options.tag} --confirm` }, }; } const script = [ "set -eu", `source_image=${shellQuote(sourceImage)}`, `target_image=${shellQuote(targetImage)}`, "export HTTP_PROXY=http://127.0.0.1:10808 HTTPS_PROXY=http://127.0.0.1:10808 http_proxy=http://127.0.0.1:10808 https_proxy=http://127.0.0.1:10808", "export NO_PROXY=localhost,127.0.0.1,::1,host.docker.internal,74.48.78.17,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,10.42.0.0/16,10.43.0.0/16,.svc,.svc.cluster.local,.cluster.local,kubernetes,kubernetes.default,kubernetes.default.svc,127.0.0.1:5000,localhost:5000", "export no_proxy=$NO_PROXY", "echo \"{\\\"phase\\\":\\\"pull\\\",\\\"sourceImage\\\":\\\"$source_image\\\"}\"", "docker pull \"$source_image\"", "docker tag \"$source_image\" \"$target_image\"", "echo \"{\\\"phase\\\":\\\"push\\\",\\\"targetImage\\\":\\\"$target_image\\\"}\"", "docker push \"$target_image\"", "digest=$(docker image inspect \"$target_image\" --format '{{index .RepoDigests 0}}' 2>/dev/null || true)", "echo \"{\\\"phase\\\":\\\"published\\\",\\\"targetImage\\\":\\\"$target_image\\\",\\\"digest\\\":\\\"$digest\\\"}\"", ].join("\n"); const result = g14HostScript(script, options.timeoutSeconds * 1000); const status = runG14UpstreamImageStatus(options); return { ok: isCommandSuccess(result) && status.ok === true, command: "hwlab g14 upstream-image ensure --name openfga", mode: "confirmed-ensure", name: options.name, tag: options.tag, sourceImage, targetImage, mutation: true, result, status, }; } export function runG14UpstreamImage(options: G14UpstreamImageOptions): Record { if (options.action === "status") return runG14UpstreamImageStatus(options); return runG14UpstreamImageEnsure(options); }