fix: optimize go env build cache path

This commit is contained in:
UniDesk Codex
2026-07-07 07:59:24 +08:00
parent f29155bdbf
commit 765e890027
3 changed files with 198 additions and 16 deletions
+1
View File
@@ -665,6 +665,7 @@ lanes:
- tzdata
goOsPackages:
- golang-go
goBaseImage: 127.0.0.1:5000/hwlab/hwlab-go-env-base:go1-bookworm-slim
bunVersion: 1.3.13
downloadStack:
httpProxy: http://127.0.0.1:10808
+175 -16
View File
@@ -495,6 +495,21 @@ function buildkitCacheEvidence(args, cacheRef) {
: { buildkitCacheMode: "registry", buildkitCacheRef: cacheRef };
}
function goBaseCacheRef(registryPrefix) {
return `${registryPrefix}/cache/hwlab-go-env-base`;
}
function goBaseEvidenceFields(evidence) {
if (!evidence) return {};
return {
goBaseImage: evidence.image,
goBaseImageStatus: evidence.status,
goBaseDigest: evidence.digest ?? null,
goBaseBuildDurationMs: evidence.durationMs ?? null,
goBaseBuildkitCacheRef: evidence.cacheRef ?? null
};
}
function buildkitDigestFromMetadata(value) {
const keys = [
"containerimage.digest",
@@ -550,6 +565,53 @@ async function run(command, args, options = {}) {
};
}
async function prepareBuildkitEnv(args) {
const buildEnv = { ...process.env };
if (!args.buildkitAddr) {
buildEnv.BUILDKITD_FLAGS = process.env.BUILDKITD_FLAGS || "--oci-worker-no-process-sandbox";
buildEnv.XDG_RUNTIME_DIR = process.env.XDG_RUNTIME_DIR || path.join(os.tmpdir(), "hwlab-buildkit-runtime");
await mkdir(buildEnv.XDG_RUNTIME_DIR, { recursive: true });
}
return buildEnv;
}
function parseRegistryImageRef(image) {
const value = String(image ?? "").trim();
const slashIndex = value.indexOf("/");
assert.ok(slashIndex > 0, `image reference ${value} must include a registry host`);
const registry = value.slice(0, slashIndex);
const remainder = value.slice(slashIndex + 1);
const digestIndex = remainder.indexOf("@");
if (digestIndex >= 0) {
return { registry, repository: remainder.slice(0, digestIndex), reference: remainder.slice(digestIndex + 1) };
}
const colonIndex = remainder.lastIndexOf(":");
assert.ok(colonIndex > 0, `image reference ${value} must include an immutable tag`);
return { registry, repository: remainder.slice(0, colonIndex), reference: remainder.slice(colonIndex + 1) };
}
async function registryManifestExists(image) {
const { registry, repository, reference } = parseRegistryImageRef(image);
const protocol = registry.startsWith("127.0.0.1") || registry.startsWith("localhost") ? "http" : "https";
const url = `${protocol}://${registry}/v2/${repository}/manifests/${reference}`;
try {
const response = await fetch(url, {
method: "GET",
headers: {
Accept: [
"application/vnd.oci.image.index.v1+json",
"application/vnd.oci.image.manifest.v1+json",
"application/vnd.docker.distribution.manifest.v2+json"
].join(", ")
}
});
await response.arrayBuffer().catch(() => null);
return response.status === 200;
} catch {
return false;
}
}
function bunExecutable() {
return process.env.HWLAB_BUN_BINARY || process.env.BUN_BINARY || "bun";
}
@@ -908,6 +970,8 @@ 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 goBaseImageEnabled = Boolean(goCacheEnabled && normalizedRecipe.goBaseImage);
const effectiveBaseImage = goBaseImageEnabled ? normalizedRecipe.goBaseImage : baseImage;
const goLauncherPath = "deploy/runtime/launcher/hwlab-go-env-reuse-launcher.mjs";
const goProxy = normalizedRecipe.downloadStack.goProxy || "https://proxy.golang.org,direct";
const goEnv = goCacheEnabled ? [
@@ -923,8 +987,9 @@ function envReuseDockerfile(baseImage, labels = [], recipe, service = null) {
["GOTOOLCHAIN", normalizedRecipe.goToolchain || "local"]
]) : "";
const goDownloadRetryScript = `retry_go_mod_download() { attempts=6; n=1; while ! go mod download; do if [ "$n" -ge "$attempts" ]; then return 1; fi; echo "go mod download attempt $n/$attempts failed; retrying" >&2; n=$((n + 1)); sleep 5; done; }`;
const stepTimingHelpers = `emit_hwlab_timing() { stage="$1"; status="$2"; duration_ms="$3"; node -e "console.error(JSON.stringify({event:'node-cicd-build-step-timing',stage:process.argv[1],status:process.argv[2],durationMs:Number(process.argv[3]),source:'dockerfile-run'}))" "$stage" "$status" "$duration_ms"; }; timed_stage() { label="$1"; shift; start="$(node -e "console.log(Date.now())")"; status=succeeded; "$@" || status=failed; end="$(node -e "console.log(Date.now())")"; elapsed=$((end - start)); emit_hwlab_timing "$label" "$status" "$elapsed"; if [ "$elapsed" -ge 120000 ]; then echo "{\"event\":\"env-reuse-warning\",\"warning\":\"stage-over-120s\",\"stage\":\"$label\",\"elapsedMs\":$elapsed}" >&2; fi; test "$status" = succeeded; }; timed_skip() { emit_hwlab_timing "$1" skipped 0; }`;
const goDownloadScript = goCacheEnabled
? `RUN ${downloadEnvPrefix}${goBuildEnvPrefix}set -eu; mkdir -p ${shellQuote(normalizedRecipe.runtimeGoModCachePath)} ${shellQuote(normalizedRecipe.runtimeGoBuildCachePath)}; ${goDownloadRetryScript}; retry_go_mod_download`
? `RUN ${downloadEnvPrefix}${goBuildEnvPrefix}set -eu; ${stepTimingHelpers}; mkdir -p ${shellQuote(normalizedRecipe.runtimeGoModCachePath)} ${shellQuote(normalizedRecipe.runtimeGoBuildCachePath)}; ${goDownloadRetryScript}; timed_stage "go-mod-download" retry_go_mod_download`
: null;
const goReadiness = goCacheEnabled
? `; mkdir -p ${shellQuote(normalizedRecipe.runtimeGoModCachePath)} ${shellQuote(normalizedRecipe.runtimeGoBuildCachePath)}; command -v go >/dev/null; go version >/tmp/hwlab-env-go-version.txt`
@@ -932,26 +997,27 @@ function envReuseDockerfile(baseImage, labels = [], recipe, service = null) {
const baseOsPackages = uniquePreserveOrder(normalizedRecipe.osPackages);
const goOsPackages = goCacheEnabled ? uniquePreserveOrder(normalizedRecipe.goOsPackages) : [];
const downloadDependencyScript = `set -eu; ${npmConfigScript}; if [ -f package-lock.json ]; then npm ci --omit=dev --include=optional --ignore-scripts; else npm install --omit=dev --include=optional --ignore-scripts; fi; if command -v bun >/dev/null 2>&1; then bun_path="$(command -v bun)"; if [ "$bun_path" != "/usr/local/bin/bun" ]; then ln -sf "$bun_path" /usr/local/bin/bun; fi; fi; if [ ! -x /usr/local/bin/bun ]; then bun_pkg=""; case "$(uname -m)" in x86_64|amd64) bun_pkg="${normalizedRecipe.downloadStack.bunPackages.x64}@${normalizedRecipe.bunVersion}" ;; aarch64|arm64) bun_pkg="${normalizedRecipe.downloadStack.bunPackages.arm64}@${normalizedRecipe.bunVersion}" ;; *) echo "unsupported bun architecture: $(uname -m)" >&2; false ;; esac; npm install --no-save --omit=dev --include=optional --ignore-scripts "$bun_pkg"; bun_bin="node_modules/\${bun_pkg%@*}/bin/bun"; test -x "$bun_bin"; ln -sf "${runtimeRoot}/$bun_bin" /usr/local/bin/bun; fi`;
const aptRetryHelpers = `timed_stage() { label="$1"; shift; start="$(date +%s)"; "$@"; status="$?"; end="$(date +%s)"; elapsed=$((end - start)); if [ "$elapsed" -ge 120 ]; then echo "{\"event\":\"env-reuse-warning\",\"warning\":\"stage-over-120s\",\"stage\":\"$label\",\"elapsedSeconds\":$elapsed}" >&2; fi; return "$status"; }; retry_apt_update() { attempts=6; n=1; while ! apt-get update -o Acquire::Retries=3; do if [ "$n" -ge "$attempts" ]; then return 1; fi; echo "apt-get update attempt $n/$attempts failed; retrying" >&2; n=$((n + 1)); rm -rf /var/lib/apt/lists/*; sleep 5; done; }; apt_packages_ready() { for pkg in "$@"; do dpkg-query -W -f='\${Status}' "$pkg" 2>/dev/null | grep -q 'install ok installed' || return 1; done; }; retry_apt_install() { attempts=8; n=1; while true; do apt-get install -o Acquire::Retries=3 -y --no-install-recommends --fix-missing "$@" || true; if apt_packages_ready "$@"; then return 0; fi; if [ "$n" -ge "$attempts" ]; then return 1; fi; echo "apt-get install attempt $n/$attempts incomplete; retrying" >&2; n=$((n + 1)); sleep 5; timed_stage "apt-update-retry" apt-get update -o Acquire::Retries=3 || true; done; }`;
const aptRetryHelpers = `retry_apt_update() { attempts=6; n=1; while ! apt-get update -o Acquire::Retries=3; do if [ "$n" -ge "$attempts" ]; then return 1; fi; echo "apt-get update attempt $n/$attempts failed; retrying" >&2; n=$((n + 1)); rm -rf /var/lib/apt/lists/*; sleep 5; done; }; apt_packages_ready() { for pkg in "$@"; do dpkg-query -W -f='\${Status}' "$pkg" 2>/dev/null | grep -q 'install ok installed' || return 1; done; }; retry_apt_install() { attempts=8; n=1; while true; do apt-get install -o Acquire::Retries=3 -y --no-install-recommends --fix-missing "$@" || true; if apt_packages_ready "$@"; then return 0; fi; if [ "$n" -ge "$attempts" ]; then return 1; fi; echo "apt-get install attempt $n/$attempts incomplete; retrying" >&2; n=$((n + 1)); sleep 5; timed_stage "apt-update-retry" apt-get update -o Acquire::Retries=3 || true; done; }`;
const aptUpdateScript = `timed_stage "apt-update" retry_apt_update || (for f in /etc/apt/sources.list /etc/apt/sources.list.d/*.sources; do [ -f "$f" ] || continue; sed -i -e '/^[[:space:]]*deb .* bookworm-updates /d' -e 's/[[:space:]]bookworm-updates//g' -e 's/bookworm-updates[[:space:]]//g' "$f"; done; timed_stage "apt-update-fallback" retry_apt_update)`;
const aptInstallSection = (label, packages) => {
const packageArgs = packages.map(shellQuote).join(" ");
if (!packageArgs) return "";
return `if apt_packages_ready ${packageArgs}; then echo "{\"event\":\"env-reuse-apt-skip\",\"stage\":\"${label}\",\"reason\":\"packages-ready\"}" >&2; else ${aptUpdateScript}; timed_stage "apt-install-${label}" retry_apt_install ${packageArgs}; fi`;
if (!packageArgs) return `timed_skip "apt-install-${label}"`;
return `if apt_packages_ready ${packageArgs}; then timed_skip "apt-install-${label}"; else ${aptUpdateScript}; timed_stage "apt-install-${label}" retry_apt_install ${packageArgs}; fi`;
};
const goPackageSection = goOsPackages.length > 0
? `if command -v go >/dev/null 2>&1; then echo "{\"event\":\"env-reuse-go-toolchain-skip\",\"reason\":\"go-preinstalled\"}" >&2; else ${aptInstallSection("go", goOsPackages)}; fi`
: "";
? `if command -v go >/dev/null 2>&1; then timed_skip "apt-install-go"; else ${aptInstallSection("go", goOsPackages)}; fi`
: `timed_skip "apt-install-go"`;
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 osPackageScript = goBaseImageEnabled
? `set -eu; ${stepTimingHelpers}; timed_skip "apt-update"; timed_skip "apt-install-base"; timed_skip "apt-install-go"`
: `set -eu; export DEBIAN_FRONTEND=noninteractive TZ=Etc/UTC; ${stepTimingHelpers}; ${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 = 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}`,
`FROM ${effectiveBaseImage}`,
`WORKDIR ${runtimeRoot}`,
...labels.map(([name, value]) => `LABEL ${name}=${dockerfileQuote(value)}`),
...(goCacheEnabled ? [] : ["COPY package.json ./package.json", "COPY package-lock.json* ./"]),
`RUN ${downloadEnvPrefix}${osPackageScript}`,
...(goCacheEnabled ? [] : [`RUN ${downloadEnvPrefix}${downloadDependencyScript}`]),
@@ -960,7 +1026,8 @@ function envReuseDockerfile(baseImage, labels = [], recipe, service = null) {
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}`,
`RUN ${stepTimingHelpers}; timed_stage "env-readiness" sh -c ${shellQuote(readinessScript)}`,
...labels.map(([name, value]) => `LABEL ${name}=${dockerfileQuote(value)}`),
"ENV HWLAB_RUNTIME_MODE=env-reuse-git-mirror-checkout",
...(goCacheEnabled ? [] : [`ENV HWLAB_RUNTIME_NODE_MODULES=${normalizedRecipe.runtimeNodeModulesPath}`]),
...goEnv,
@@ -1006,6 +1073,7 @@ function normalizeEnvReuseRecipe(recipe) {
const normalized = {
osPackages: normalizeStringList(recipe.osPackages),
goOsPackages: normalizeStringList(recipe.goOsPackages),
goBaseImage: String(recipe.goBaseImage ?? "").trim(),
bunVersion: String(recipe.bunVersion ?? "").trim(),
launcherPath: normalizeRepoPath(recipe.launcherPath),
runtimeNodeModulesPath: normalizeAbsolutePath(recipe.runtimeNodeModulesPath),
@@ -1019,6 +1087,9 @@ function normalizeEnvReuseRecipe(recipe) {
if (normalized.runtimeGoModCachePath || normalized.runtimeGoBuildCachePath) {
assert.ok(normalized.goOsPackages.length > 0, "envRecipe.goOsPackages must not be empty when Go cache paths are configured");
}
if (normalized.goBaseImage) {
assert.match(normalized.goBaseImage, /^[^/]+\/.+:[^:@]+$/u, "envRecipe.goBaseImage must include registry/repository:tag");
}
assert.match(normalized.bunVersion, /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/u, "envRecipe.bunVersion must be a semver-like version");
assert.ok(normalized.launcherPath && !normalized.launcherPath.split("/").includes(".."), "envRecipe.launcherPath must be repo-relative");
assert.ok(normalized.runtimeNodeModulesPath, "envRecipe.runtimeNodeModulesPath must be absolute");
@@ -1325,6 +1396,70 @@ async function buildEnvReuseService({ args, repo, commitId, service, buildCreate
return withEnvReuseArtifactFields({ artifact, service, commitId, buildCreatedAt, buildSource });
}
function goEnvBaseDockerfile(baseImage, recipe) {
const packages = uniquePreserveOrder([...recipe.osPackages, ...recipe.goOsPackages]).map(shellQuote).join(" ");
const stepTimingHelpers = `emit_hwlab_timing() { stage="$1"; status="$2"; duration_ms="$3"; node -e "console.error(JSON.stringify({event:'node-cicd-build-step-timing',stage:process.argv[1],status:process.argv[2],durationMs:Number(process.argv[3]),source:'dockerfile-run'}))" "$stage" "$status" "$duration_ms"; }; timed_stage() { label="$1"; shift; start="$(node -e "console.log(Date.now())")"; status=succeeded; "$@" || status=failed; end="$(node -e "console.log(Date.now())")"; elapsed=$((end - start)); emit_hwlab_timing "$label" "$status" "$elapsed"; test "$status" = succeeded; }; timed_skip() { emit_hwlab_timing "$1" skipped 0; }`;
const aptHelpers = `retry_apt_update() { attempts=6; n=1; while ! apt-get update -o Acquire::Retries=3; do if [ "$n" -ge "$attempts" ]; then return 1; fi; echo "apt-get update attempt $n/$attempts failed; retrying" >&2; n=$((n + 1)); rm -rf /var/lib/apt/lists/*; sleep 5; done; }; apt_packages_ready() { for pkg in "$@"; do dpkg-query -W -f='\${Status}' "$pkg" 2>/dev/null | grep -q 'install ok installed' || return 1; done; }; retry_apt_install() { attempts=8; n=1; while true; do apt-get install -o Acquire::Retries=3 -y --no-install-recommends --fix-missing "$@" || true; if apt_packages_ready "$@"; then return 0; fi; if [ "$n" -ge "$attempts" ]; then return 1; fi; echo "apt-get install attempt $n/$attempts incomplete; retrying" >&2; n=$((n + 1)); sleep 5; timed_stage "go-base-apt-update-retry" apt-get update -o Acquire::Retries=3 || true; done; }`;
const aptUpdateScript = `timed_stage "go-base-apt-update" retry_apt_update || (for f in /etc/apt/sources.list /etc/apt/sources.list.d/*.sources; do [ -f "$f" ] || continue; sed -i -e '/^[[:space:]]*deb .* bookworm-updates /d' -e 's/[[:space:]]bookworm-updates//g' -e 's/bookworm-updates[[:space:]]//g' "$f"; done; timed_stage "go-base-apt-update-fallback" retry_apt_update)`;
const installScript = packages
? `if apt_packages_ready ${packages}; then timed_skip "go-base-apt-install"; else ${aptUpdateScript}; timed_stage "go-base-apt-install" retry_apt_install ${packages}; fi`
: `timed_skip "go-base-apt-install"`;
return [
`FROM ${baseImage}`,
"WORKDIR /opt/hwlab-env",
`RUN set -eu; export DEBIAN_FRONTEND=noninteractive TZ=Etc/UTC; ${stepTimingHelpers}; ${aptHelpers}; ${installScript}; command -v go >/dev/null; go version >/tmp/hwlab-go-base-version.txt; rm -rf /var/lib/apt/lists/*`,
"ENV PATH=/usr/local/bin:$PATH",
""
].join("\n");
}
async function ensureGoEnvBaseImage({ args, normalizedRecipe, tmpDir, registryOption, buildkitAddrArgs, buildEnv }) {
if (!normalizedRecipe.goBaseImage) return null;
const image = normalizedRecipe.goBaseImage;
const cacheRef = goBaseCacheRef(args.registryPrefix);
if (await registryManifestExists(image)) {
return { image, status: "present", cacheRef, durationMs: 0 };
}
const dockerfilePath = path.join(tmpDir, "Dockerfile.go-base");
const metadataPath = path.join(tmpDir, "metadata.go-base.json");
await writeFile(dockerfilePath, goEnvBaseDockerfile(args.baseImage, normalizedRecipe));
const startedAt = Date.now();
const argsList = [
...buildkitAddrArgs,
"build",
"--frontend", "dockerfile.v0",
"--local", `context=${tmpDir}`,
"--local", `dockerfile=${tmpDir}`,
"--opt", "filename=Dockerfile.go-base",
"--metadata-file", metadataPath,
...buildkitCacheArgs(args, cacheRef, registryOption),
"--output", `type=image,name=${image},push=true${registryOption}`
];
if (!args.quietBuild || ciTimingEnabled()) argsList.push("--progress", "plain");
const result = await run(args.buildkitCommand, argsList, { env: buildEnv });
emitBuildkitTimingEvents({ service: { serviceId: "hwlab-go-env-base" }, result, timingEnabled: ciTimingEnabled(buildEnv) });
if (result.code !== 0) {
return {
image,
status: "build_failed",
cacheRef,
durationMs: Date.now() - startedAt,
logTail: tailText(`${result.stdout}\n${result.stderr}`, 12000)
};
}
let metadata = {};
try {
metadata = JSON.parse(await readFile(metadataPath, "utf8"));
} catch {}
return {
image,
status: "built",
digest: buildkitDigestFromMetadata(metadata),
cacheRef,
durationMs: Date.now() - startedAt
};
}
async function buildEnvReuseServiceWithBuildkit({ args, service, ref, buildCreatedAt, buildSource, labels }) {
const tmpDir = await mkdtemp(path.join(os.tmpdir(), `hwlab-env-reuse-${service.serviceId}-`));
const normalizedRecipe = normalizeEnvReuseRecipe(service.envReuseRecipe);
@@ -1337,7 +1472,34 @@ async function buildEnvReuseServiceWithBuildkit({ args, service, ref, buildCreat
const registryOption = buildkitRegistryOption(args.registryPrefix);
const buildkitAddrArgs = args.buildkitAddr ? ["--addr", args.buildkitAddr] : [];
const startedAt = Date.now();
const buildEnv = await prepareBuildkitEnv(args);
let goBaseEvidence = null;
try {
goBaseEvidence = goCacheEnabled
? await ensureGoEnvBaseImage({ args, normalizedRecipe, tmpDir, registryOption, buildkitAddrArgs, buildEnv })
: null;
if (goBaseEvidence?.status === "build_failed") {
return {
...service,
image: ref,
imageTag: imageTagFromReference(ref),
buildCreatedAt,
buildSource,
status: "build_failed",
digest: "not_published",
blocker: blocker({
type: "environment_blocker",
scope: service.serviceId,
summary: `BuildKit Go env base image build failed for ${service.serviceId}`,
next: "Inspect the Go env base BuildKit output and registry/cache availability."
}),
logTail: goBaseEvidence.logTail,
buildBackend: "buildkit-env-reuse",
...buildkitCacheEvidence(args, cacheRef),
...goBaseEvidenceFields(goBaseEvidence),
buildkitBuildDurationMs: Date.now() - startedAt
};
}
await writeFile(dockerfilePath, envReuseDockerfile(args.baseImage, labels, normalizedRecipe, service));
const argsList = [
...buildkitAddrArgs,
@@ -1351,12 +1513,6 @@ async function buildEnvReuseServiceWithBuildkit({ args, service, ref, buildCreat
"--output", `type=image,name=${ref},push=true${registryOption}`
];
if (!args.quietBuild || ciTimingEnabled()) argsList.push("--progress", "plain");
const buildEnv = { ...process.env };
if (!args.buildkitAddr) {
buildEnv.BUILDKITD_FLAGS = process.env.BUILDKITD_FLAGS || "--oci-worker-no-process-sandbox";
buildEnv.XDG_RUNTIME_DIR = process.env.XDG_RUNTIME_DIR || path.join(os.tmpdir(), "hwlab-buildkit-runtime");
await mkdir(buildEnv.XDG_RUNTIME_DIR, { recursive: true });
}
const result = await run(args.buildkitCommand, argsList, { env: buildEnv });
emitBuildkitTimingEvents({ service, result, timingEnabled: ciTimingEnabled(buildEnv) });
if (result.code !== 0) {
@@ -1377,6 +1533,7 @@ async function buildEnvReuseServiceWithBuildkit({ args, service, ref, buildCreat
logTail: tailText(`${result.stdout}\n${result.stderr}`, 12000),
buildBackend: "buildkit-env-reuse",
...buildkitCacheEvidence(args, cacheRef),
...goBaseEvidenceFields(goBaseEvidence),
buildkitBuildDurationMs: result.durationMs
};
}
@@ -1403,6 +1560,7 @@ async function buildEnvReuseServiceWithBuildkit({ args, service, ref, buildCreat
pushLogTail: tailText(`${result.stdout}\n${result.stderr}`, 1200),
buildBackend: "buildkit-env-reuse",
...buildkitCacheEvidence(args, cacheRef),
...goBaseEvidenceFields(goBaseEvidence),
buildkitBuildDurationMs: Date.now() - startedAt
};
}
@@ -1417,6 +1575,7 @@ async function buildEnvReuseServiceWithBuildkit({ args, service, ref, buildCreat
repositoryDigest: `${repositoryFromImageRef(ref)}@${digest}`,
buildBackend: "buildkit-env-reuse",
...buildkitCacheEvidence(args, cacheRef),
...goBaseEvidenceFields(goBaseEvidence),
buildkitBuildDurationMs: Date.now() - startedAt,
publishDurationMs: args.mode === "publish" ? Date.now() - startedAt : null,
pushLogTail: tailText(`${result.stdout}\n${result.stderr}`, 1200)
+22
View File
@@ -1218,6 +1218,28 @@ test("v03 planner ignores Go launcher comment-only changes for env identity", as
}
});
test("Go env build path declares base image, stable timing, and late volatile labels", async () => {
const deploy = await readStructuredFile(process.cwd(), path.join(process.cwd(), "deploy/deploy.yaml"));
assert.equal(
deploy.lanes?.v03?.envRecipe?.goBaseImage,
"127.0.0.1:5000/hwlab/hwlab-go-env-base:go1-bookworm-slim"
);
const source = await readFile(path.join(process.cwd(), "scripts/artifact-publish.mjs"), "utf8");
const envDockerfileSource = source.slice(
source.indexOf("function envReuseDockerfile"),
source.indexOf("async function copyRepoFileToContext")
);
assert.match(source, /const effectiveBaseImage = goBaseImageEnabled \? normalizedRecipe\.goBaseImage : baseImage/u);
assert.match(envDockerfileSource, /timed_stage "go-mod-download"/u);
assert.match(envDockerfileSource, /timed_stage "env-readiness"/u);
const labelIndex = envDockerfileSource.indexOf("...labels.map(([name, value]) => `LABEL");
const readinessIndex = envDockerfileSource.indexOf('timed_stage "env-readiness"');
assert.ok(readinessIndex >= 0, "env readiness timing should be emitted from the Dockerfile");
assert.ok(labelIndex > readinessIndex, "volatile labels should not invalidate expensive env build layers");
});
async function createFixtureRepo(options = {}) {
const serviceIds = options.serviceIds ?? ["hwlab-cloud-api", "hwlab-cloud-web"];
const laneServiceIds = options.laneServiceIds ?? V02_RUNTIME_SERVICE_IDS;