fix: optimize go env build cache path
This commit is contained in:
@@ -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
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user