From 56ea5199f36af759eabd420efcd18506d81cd79e Mon Sep 17 00:00:00 2001 From: UniDesk Codex Date: Tue, 7 Jul 2026 00:23:35 +0800 Subject: [PATCH] fix: remove npm dependency install from go env images --- .../launcher/hwlab-go-env-reuse-launcher.mjs | 95 +++++++++++++++++++ scripts/artifact-publish.mjs | 28 ++++-- scripts/ci-plan.test.mjs | 5 +- 3 files changed, 117 insertions(+), 11 deletions(-) create mode 100644 deploy/runtime/launcher/hwlab-go-env-reuse-launcher.mjs diff --git a/deploy/runtime/launcher/hwlab-go-env-reuse-launcher.mjs b/deploy/runtime/launcher/hwlab-go-env-reuse-launcher.mjs new file mode 100644 index 00000000..eff98d9e --- /dev/null +++ b/deploy/runtime/launcher/hwlab-go-env-reuse-launcher.mjs @@ -0,0 +1,95 @@ +import { spawn, spawnSync } from "node:child_process"; +import { existsSync, mkdirSync, rmSync } from "node:fs"; +import path from "node:path"; + +const bootRepo = requiredEnv("HWLAB_BOOT_REPO"); +const bootCommit = requiredEnv("HWLAB_BOOT_COMMIT"); +const bootSh = requiredEnv("HWLAB_BOOT_SH"); +const bootRef = requiredEnv("HWLAB_BOOT_REF"); +const checkoutDir = process.env.HWLAB_BOOT_CHECKOUT_DIR || "/workspace/hwlab-boot/repo"; + +if (!/^[a-f0-9]{40}$/u.test(bootCommit)) fail("HWLAB_BOOT_COMMIT must be a full 40-char SHA"); +if (path.isAbsolute(bootSh) || bootSh.split("/").includes("..")) fail("HWLAB_BOOT_SH must be repo-relative"); +if (!/^[A-Za-z0-9._/-]+$/u.test(bootRef) || bootRef.startsWith("-") || bootRef.includes("..")) { + fail("HWLAB_BOOT_REF must be a safe branch or ref name"); +} + +const readUrl = process.env.HWLAB_BOOT_READ_URL || mirrorReadUrl(bootRepo); +mkdirSync(path.dirname(checkoutDir), { recursive: true }); +rmSync(checkoutDir, { recursive: true, force: true }); +run("git", ["clone", "--no-checkout", "--single-branch", "--branch", bootRef, readUrl, checkoutDir], "/"); +run("git", ["checkout", "--detach", bootCommit], checkoutDir); + +const scriptPath = path.join(checkoutDir, bootSh); +if (!existsSync(scriptPath)) fail(`boot script not found: ${bootSh}`); +run("sh", [scriptPath], checkoutDir, { replace: true }); + +function requiredEnv(name) { + const value = process.env[name]; + if (!value) fail(`${name} is required`); + return value; +} + +function mirrorReadUrl(repo) { + if (/github\.com[:/]pikasTech\/HWLAB(?:\.git)?$/u.test(repo)) { + return "http://git-mirror-http.devops-infra.svc.cluster.local/pikasTech/HWLAB.git"; + } + fail("no mirror resolver for HWLAB_BOOT_REPO; refusing direct runtime GitHub fallback"); +} + +function run(command, args, cwd, options = {}) { + if (options.replace) { + runReplacingProcess(command, args, cwd); + return; + } + const result = spawnSync(command, args, { cwd, env: process.env, stdio: "inherit" }); + if (result.error) fail(`${command} failed to start: ${result.error.message}`); + if (result.signal) process.kill(process.pid, result.signal); + const code = result.status ?? 0; + if (code !== 0) process.exit(code); +} + +function runReplacingProcess(command, args, cwd) { + const child = spawn(command, args, { cwd, env: process.env, stdio: "inherit", detached: true }); + let settled = false; + const signalHandlers = []; + const cleanup = () => { + for (const { signal, handler } of signalHandlers.splice(0)) process.off(signal, handler); + }; + const forwardSignal = (signal) => { + if (!child.pid) return; + try { + process.kill(-child.pid, signal); + } catch { + try { child.kill(signal); } catch {} + } + }; + for (const signal of ["SIGINT", "SIGTERM"]) { + const handler = () => forwardSignal(signal); + process.on(signal, handler); + signalHandlers.push({ signal, handler }); + } + child.on("error", (error) => { + if (settled) return; + settled = true; + cleanup(); + fail(`${command} failed to start: ${error.message}`); + }); + child.on("exit", (code, signal) => { + if (settled) return; + settled = true; + cleanup(); + if (signal) process.exit(signalExitCode(signal)); + process.exit(code ?? 0); + }); +} + +function signalExitCode(signal) { + const signals = { SIGHUP: 1, SIGINT: 2, SIGQUIT: 3, SIGTERM: 15 }; + return 128 + (signals[signal] ?? 1); +} + +function fail(message) { + process.stderr.write(`${JSON.stringify({ status: "failed", error: "hwlab_go_env_reuse_launcher_failed", message })}\n`); + process.exit(1); +} diff --git a/scripts/artifact-publish.mjs b/scripts/artifact-publish.mjs index 99bdf45b..f84e5361 100644 --- a/scripts/artifact-publish.mjs +++ b/scripts/artifact-publish.mjs @@ -908,6 +908,7 @@ function envReuseDockerfile(baseImage, labels = [], recipe, service = null) { const npmConfigScript = npmDownloadConfigScript(normalizedRecipe.downloadStack); const goService = service?.runtimeKind === "go-service" || service?.artifactKind === "go-service"; const goCacheEnabled = Boolean(goService && normalizedRecipe.runtimeGoModCachePath && normalizedRecipe.runtimeGoBuildCachePath); + const goLauncherPath = "deploy/runtime/launcher/hwlab-go-env-reuse-launcher.mjs"; const goProxy = normalizedRecipe.downloadStack.goProxy || "https://proxy.golang.org,direct"; const goEnv = goCacheEnabled ? [ `ENV GOMODCACHE=${normalizedRecipe.runtimeGoModCachePath}`, @@ -944,24 +945,29 @@ function envReuseDockerfile(baseImage, labels = [], recipe, service = null) { const osPackageSections = [aptInstallSection("base", baseOsPackages), goPackageSection].filter(Boolean).join("; "); const osPackageScript = `set -eu; export DEBIAN_FRONTEND=noninteractive TZ=Etc/UTC; ${aptRetryHelpers}; ${osPackageSections}; rm -rf /var/lib/apt/lists/*`; const aliasScript = normalizedRecipe.hwpodAliases.map((alias) => `printf '%s\\n' '#!/usr/bin/env sh' 'exec node /workspace/hwlab-boot/repo/scripts/run-bun.mjs /workspace/hwlab-boot/repo/${alias.script} \"$@\"' > /usr/local/bin/${alias.command}; chmod 755 /usr/local/bin/${alias.command}`).join("; "); - const readinessScript = `set -eu; command -v node >/dev/null; command -v npm >/dev/null; command -v git >/dev/null; command -v sh >/dev/null; test -d ${shellQuote(normalizedRecipe.runtimeNodeModulesPath)}; test -x /usr/local/bin/bun; /usr/local/bin/bun --version >/tmp/hwlab-env-bun-version.txt${goReadiness}; ${aliasScript}`; + const readinessScript = goCacheEnabled + ? `set -eu; command -v node >/dev/null; command -v git >/dev/null; command -v sh >/dev/null${goReadiness}; test -f /usr/local/bin/hwlab-go-env-reuse-launcher.mjs` + : `set -eu; command -v node >/dev/null; command -v npm >/dev/null; command -v git >/dev/null; command -v sh >/dev/null; test -d ${shellQuote(normalizedRecipe.runtimeNodeModulesPath)}; test -x /usr/local/bin/bun; /usr/local/bin/bun --version >/tmp/hwlab-env-bun-version.txt; ${aliasScript}`; return [ `FROM ${baseImage}`, `WORKDIR ${runtimeRoot}`, ...labels.map(([name, value]) => `LABEL ${name}=${dockerfileQuote(value)}`), - "COPY package.json ./package.json", - "COPY package-lock.json* ./", + ...(goCacheEnabled ? [] : ["COPY package.json ./package.json", "COPY package-lock.json* ./"]), `RUN ${downloadEnvPrefix}${osPackageScript}`, - `RUN ${downloadEnvPrefix}${downloadDependencyScript}`, + ...(goCacheEnabled ? [] : [`RUN ${downloadEnvPrefix}${downloadDependencyScript}`]), ...(goCacheEnabled ? ["COPY go.mod ./go.mod", "COPY go.sum* ./"] : []), ...(goDownloadScript ? [goDownloadScript] : []), - `COPY ${normalizedRecipe.launcherPath} /usr/local/bin/hwlab-env-reuse-launcher.ts`, + goCacheEnabled + ? `COPY ${goLauncherPath} /usr/local/bin/hwlab-go-env-reuse-launcher.mjs` + : `COPY ${normalizedRecipe.launcherPath} /usr/local/bin/hwlab-env-reuse-launcher.ts`, `RUN ${readinessScript}`, "ENV HWLAB_RUNTIME_MODE=env-reuse-git-mirror-checkout", - `ENV HWLAB_RUNTIME_NODE_MODULES=${normalizedRecipe.runtimeNodeModulesPath}`, + ...(goCacheEnabled ? [] : [`ENV HWLAB_RUNTIME_NODE_MODULES=${normalizedRecipe.runtimeNodeModulesPath}`]), ...goEnv, "ENV PATH=/usr/local/bin:$PATH", - "CMD [\"/usr/local/bin/bun\", \"/usr/local/bin/hwlab-env-reuse-launcher.ts\"]", + goCacheEnabled + ? "CMD [\"node\", \"/usr/local/bin/hwlab-go-env-reuse-launcher.mjs\"]" + : "CMD [\"/usr/local/bin/bun\", \"/usr/local/bin/hwlab-env-reuse-launcher.ts\"]", "" ].join("\n"); } @@ -983,13 +989,15 @@ async function copyRepoFileToContext(contextDir, repoRelativePath, { required = async function prepareEnvReuseBuildContext(tmpDir, normalizedRecipe, goCacheEnabled) { const contextDir = path.join(tmpDir, "context"); await mkdir(contextDir, { recursive: true }); - await copyRepoFileToContext(contextDir, "package.json"); - await copyRepoFileToContext(contextDir, "package-lock.json", { required: false }); + if (!goCacheEnabled) { + await copyRepoFileToContext(contextDir, "package.json"); + await copyRepoFileToContext(contextDir, "package-lock.json", { required: false }); + } if (goCacheEnabled) { await copyRepoFileToContext(contextDir, "go.mod"); await copyRepoFileToContext(contextDir, "go.sum", { required: false }); } - await copyRepoFileToContext(contextDir, normalizedRecipe.launcherPath); + await copyRepoFileToContext(contextDir, goCacheEnabled ? "deploy/runtime/launcher/hwlab-go-env-reuse-launcher.mjs" : normalizedRecipe.launcherPath); return contextDir; } diff --git a/scripts/ci-plan.test.mjs b/scripts/ci-plan.test.mjs index 8957b4dc..3fa1e733 100644 --- a/scripts/ci-plan.test.mjs +++ b/scripts/ci-plan.test.mjs @@ -362,7 +362,10 @@ test("artifact reuse paths preserve catalog provenance instead of current planne const artifactPublish = await readFile("scripts/artifact-publish.mjs", "utf8"); assert.match(artifactPublish, /downloadDependencyScript/u); assert.match(artifactPublish, /RUN \$\{downloadEnvPrefix\}\$\{downloadDependencyScript\}/u); - assert.match(artifactPublish, /RUN \$\{downloadEnvPrefix\}\$\{downloadDependencyScript\}`,[\s\S]{0,120}\["COPY go\.mod \.\/go\.mod", "COPY go\.sum\* \.\/"\]/u); + assert.match(artifactPublish, /goLauncherPath = "deploy\/runtime\/launcher\/hwlab-go-env-reuse-launcher\.mjs"/u); + assert.match(artifactPublish, /goCacheEnabled \? \[\] : \["COPY package\.json \.\/package\.json", "COPY package-lock\.json\* \.\/"\]/u); + assert.match(artifactPublish, /goCacheEnabled \? \[\] : \[`RUN \$\{downloadEnvPrefix\}\$\{downloadDependencyScript\}`\]/u); + assert.match(artifactPublish, /CMD \[\\"node\\", \\"\/usr\/local\/bin\/hwlab-go-env-reuse-launcher\.mjs\\"\]/u); assert.match(artifactPublish, /ln -sf "\$\{runtimeRoot\}\/\$bun_bin" \/usr\/local\/bin\/bun/u); assert.doesNotMatch(artifactPublish, /RUN \$\{dependencyScript\}/u); assert.match(artifactPublish, /const catalogProvenance = serviceImageCatalogProvenance\(catalogService\);/u);