UNPKG

trigger.dev

Version:

A Command-Line Interface for Trigger.dev projects

875 lines (827 loc) • 29.6 kB
import { logger } from "../utilities/logger.js"; import { depot } from "@depot/cli"; import { x } from "tinyexec"; import { networkInterfaces } from "os"; import { join } from "path"; import { safeReadJSONFile } from "../utilities/fileSystem.js"; import { readFileSync } from "fs"; import { isLinux } from "std-env"; import { z } from "zod"; import { assertExhaustive } from "../utilities/assertExhaustive.js"; import { tryCatch } from "@trigger.dev/core"; export async function buildImage(options) { const { isLocalBuild, useRegistryCache, imagePlatform, noCache, push, authenticateToRegistry, load, authAccessToken, imageTag, deploymentId, deploymentVersion, contentHash, externalBuildId, externalBuildToken, externalBuildProjectId, compilationPath, projectId, projectRef, extraCACerts, apiUrl, apiKey, apiClient, branchName, buildEnvVars, network, builder, compression, cacheCompression, compressionLevel, forceCompression, onLog, } = options; if (isLocalBuild) { return localBuildImage({ imageTag, imagePlatform, cwd: compilationPath, projectId, deploymentId, deploymentVersion, contentHash, projectRef, push, authenticateToRegistry, load, noCache, useRegistryCache, extraCACerts, apiUrl, apiKey, apiClient, branchName, buildEnvVars, network, builder, compression, cacheCompression, compressionLevel, forceCompression, onLog, }); } if (!externalBuildId || !externalBuildToken || !externalBuildProjectId) { throw new Error("Failed to initialize deployment. The deployment does not have any external build data. To deploy this project, you must use the --self-hosted flag to build and push the image yourself."); } return remoteBuildImage({ auth: authAccessToken, buildId: externalBuildId, buildToken: externalBuildToken, buildProjectId: externalBuildProjectId, cwd: compilationPath, projectId, deploymentId, deploymentVersion, contentHash, projectRef, load, imagePlatform, noCache, extraCACerts, apiUrl, apiKey, branchName, buildEnvVars, compression, compressionLevel, forceCompression, onLog, }); } async function remoteBuildImage(options) { const buildArgs = Object.entries(options.buildEnvVars || {}) .filter(([key, value]) => value) .flatMap(([key, value]) => ["--build-arg", `${key}=${value}`]); const outputOptions = getOutputOptions({ imageTag: undefined, // This is already handled via the --save flag push: true, // We always push the image to the registry load: options.load, compression: options.compression, compressionLevel: options.compressionLevel, forceCompression: options.forceCompression, isLocalBuild: false, }); const args = [ "build", "-f", "Containerfile", options.noCache ? "--no-cache" : undefined, "--platform", options.imagePlatform, "--provenance", "false", "--metadata-file", "metadata.json", "--build-arg", `SOURCE_DATE_EPOCH=0`, "--build-arg", `TRIGGER_PROJECT_ID=${options.projectId}`, "--build-arg", `TRIGGER_DEPLOYMENT_ID=${options.deploymentId}`, "--build-arg", `TRIGGER_CONTENT_HASH=${options.contentHash}`, "--build-arg", `TRIGGER_PROJECT_REF=${options.projectRef}`, "--build-arg", `TRIGGER_API_URL=${options.apiUrl}`, "--build-arg", `TRIGGER_PREVIEW_BRANCH=${options.branchName ?? ""}`, "--build-arg", `TRIGGER_SECRET_KEY=${options.apiKey}`, ...(buildArgs || []), ...(options.extraCACerts ? ["--build-arg", `NODE_EXTRA_CA_CERTS=${options.extraCACerts}`] : []), "--progress", "plain", ".", "--save", "--output", outputOptions.join(","), ].filter(Boolean); logger.debug(`depot ${args.join(" ")}`, { cwd: options.cwd }); // Step 4: Build and push the image const childProcess = depot(args, { cwd: options.cwd, env: { DEPOT_BUILD_ID: options.buildId, DEPOT_TOKEN: options.buildToken, DEPOT_PROJECT_ID: options.buildProjectId, DEPOT_NO_SUMMARY_LINK: "1", DEPOT_NO_UPDATE_NOTIFIER: "1", }, }); const errors = []; try { const processCode = await new Promise((res, rej) => { // For some reason everything is output on stderr, not stdout childProcess.stderr?.on("data", (data) => { const text = data.toString(); // Emitted data chunks can contain multiple lines. Remove empty lines. const lines = text.split("\n").filter(Boolean); for (const line of lines) { options.onLog?.(line); } errors.push(...lines); logger.debug(text); }); childProcess.on("error", (e) => rej(e)); childProcess.on("close", (code) => res(code)); }); const logs = extractLogs(errors); if (processCode !== 0) { return { ok: false, error: `Error building image`, logs, }; } const metadataPath = join(options.cwd, "metadata.json"); const rawMetadata = await safeReadJSONFile(metadataPath); const meta = BuildKitMetadata.safeParse(rawMetadata); let digest; if (!meta.success) { logger.error("Failed to parse metadata.json", { errors: meta.error.message, path: metadataPath, }); } else { logger.debug("Parsed metadata.json", { metadata: meta.data, path: metadataPath }); digest = meta.data["containerimage.digest"]; } return { ok: true, imageSizeBytes: 0, logs, digest, }; } catch (e) { return { ok: false, error: e instanceof Error ? e.message : JSON.stringify(e), logs: extractLogs(errors), }; } } async function localBuildImage(options) { const { builder, imageTag, deploymentId, apiClient, useRegistryCache, compression, cacheCompression, compressionLevel, forceCompression, } = options; // Ensure multi-platform build is supported on the local machine let builderExists = false; const lsLogs = []; // List existing builders const lsProcess = x("docker", ["buildx", "ls", "--format", "{{.Name}}"]); for await (const line of lsProcess) { lsLogs.push(line); logger.debug(line); if (line === builder) { builderExists = true; } } if (lsProcess.exitCode !== 0) { return { ok: false, error: `Failed to list buildx builders`, logs: lsLogs.join("\n"), }; } if (builderExists && options.network) { // We need to ensure the current builder network matches const inspectProcess = x("docker", ["buildx", "inspect", builder]); const inspectLogs = []; let hasCorrectNetwork = false; for await (const line of inspectProcess) { inspectLogs.push(line); if (line.match(/Driver Options:\s+network="([^"]+)"/)?.at(1) === options.network) { hasCorrectNetwork = true; } } if (inspectProcess.exitCode !== 0) { return { ok: false, error: `Failed to inspect buildx builder '${builder}'`, logs: inspectLogs.join("\n"), }; } if (!hasCorrectNetwork) { // Delete the existing builder and signal to create a new one const deleteProcess = x("docker", ["buildx", "rm", builder]); const deleteLogs = []; for await (const line of deleteProcess) { deleteLogs.push(line); } if (deleteProcess.exitCode !== 0) { return { ok: false, error: `Failed to delete buildx builder '${builder}'`, logs: deleteLogs.join("\n"), }; } builderExists = false; } } // If the builder does not exist, create it and is compatible with multi-platform builds if (!builderExists) { const createLogs = []; const args = [ "buildx", "create", "--name", builder, "--driver", "docker-container", options.network ? `--driver-opt=network=${options.network}` : undefined, ].filter(Boolean); const createProcess = x("docker", args); for await (const line of createProcess) { createLogs.push(line); logger.debug(line); options.onLog?.(line); } if (createProcess.exitCode !== 0) { return { ok: false, error: `Failed to create buildx builder '${builder}'`, logs: [...lsLogs, ...createLogs].join("\n"), }; } } const buildArgs = Object.entries(options.buildEnvVars || {}) .filter(([key, value]) => value) .flatMap(([key, value]) => ["--build-arg", `${key}=${value}`]); const apiUrl = normalizeApiUrlForBuild(options.apiUrl); const addHost = getAddHost(apiUrl); const push = shouldPush(options.imageTag, options.push); const load = shouldLoad(options.load, push); await ensureQemuRegistered(options.imagePlatform); const errors = []; let cloudRegistryHost; if (push && options.authenticateToRegistry) { cloudRegistryHost = process.env.TRIGGER_DOCKER_REGISTRY ?? extractRegistryHostFromImageTag(imageTag); if (!cloudRegistryHost) { return { ok: false, error: "Failed to extract registry host from image tag", logs: "", }; } const [credentialsError, credentials] = await tryCatch(getDockerUsernameAndPassword(apiClient, deploymentId)); if (credentialsError) { return { ok: false, error: `Failed to get docker credentials: ${credentialsError.message}`, logs: "", }; } logger.debug(`Logging in to docker registry: ${cloudRegistryHost}`); const loginProcess = x("docker", ["login", "--username", credentials.username, "--password-stdin", cloudRegistryHost], { nodeOptions: { cwd: options.cwd, }, }); loginProcess.process?.stdin?.write(credentials.password); loginProcess.process?.stdin?.end(); for await (const line of loginProcess) { errors.push(line); logger.debug(line); } if (loginProcess.exitCode !== 0) { return { ok: false, error: `Failed to login to registry: ${cloudRegistryHost}`, logs: extractLogs(errors), }; } options.onLog?.(`Successfully logged in to the remote registry`); } const projectCacheRef = getProjectCacheRefFromImageTag(imageTag); const outputOptions = getOutputOptions({ imageTag, push, load, compression, compressionLevel, forceCompression, isLocalBuild: true, }); const args = [ "buildx", "build", "--builder", builder, "-f", "Containerfile", options.noCache ? "--no-cache" : undefined, ...(useRegistryCache ? [ "--cache-to", `type=registry,mode=max,image-manifest=true,oci-mediatypes=true,ref=${projectCacheRef}${cacheCompression === "zstd" ? ",compression=zstd" : ""}`, "--cache-from", `type=registry,ref=${projectCacheRef}`, ] : []), "--output", outputOptions.join(","), "--platform", options.imagePlatform, options.network ? `--network=${options.network}` : undefined, addHost ? `--add-host=${addHost}` : undefined, "--provenance", "false", "--metadata-file", "metadata.json", "--build-arg", `SOURCE_DATE_EPOCH=0`, "--build-arg", `TRIGGER_PROJECT_ID=${options.projectId}`, "--build-arg", `TRIGGER_DEPLOYMENT_ID=${options.deploymentId}`, "--build-arg", `TRIGGER_CONTENT_HASH=${options.contentHash}`, "--build-arg", `TRIGGER_PROJECT_REF=${options.projectRef}`, "--build-arg", `TRIGGER_API_URL=${apiUrl}`, "--build-arg", `TRIGGER_PREVIEW_BRANCH=${options.branchName ?? ""}`, "--build-arg", `TRIGGER_SECRET_KEY=${options.apiKey}`, ...(buildArgs || []), ...(options.extraCACerts ? ["--build-arg", `NODE_EXTRA_CA_CERTS=${options.extraCACerts}`] : []), "--progress", "plain", ".", // The build context ].filter(Boolean); logger.debug(`docker ${args.join(" ")}`, { cwd: options.cwd }); const buildProcess = x("docker", args, { nodeOptions: { cwd: options.cwd, }, }); for await (const line of buildProcess) { // line will be from stderr/stdout in the order you'd see it in a term errors.push(line); logger.debug(line); options.onLog?.(line); } if (buildProcess.exitCode !== 0) { if (cloudRegistryHost) { logger.debug(`Logging out from docker registry: ${cloudRegistryHost}`); await x("docker", ["logout", cloudRegistryHost]); } return { ok: false, error: "Error building image", logs: extractLogs(errors), }; } const metadataPath = join(options.cwd, "metadata.json"); const rawMetadata = await safeReadJSONFile(metadataPath); const meta = BuildKitMetadata.safeParse(rawMetadata); let digest; if (!meta.success) { logger.error("Failed to parse metadata.json", { errors: meta.error.message, path: metadataPath, }); } else { logger.debug("Parsed metadata.json", { metadata: meta.data, path: metadataPath }); // Always use the manifest (list) digest digest = meta.data["containerimage.digest"]; } // Get the image size const sizeProcess = x("docker", ["image", "inspect", options.imageTag, "--format={{.Size}}"], { nodeOptions: { cwd: options.cwd }, }); let imageSizeBytes = 0; for await (const line of sizeProcess) { if (line.trim() === "") { continue; } imageSizeBytes = parseInt(line, 10); break; } if (imageSizeBytes) { // Convert to MB and log options.onLog?.(`Image size: ${(imageSizeBytes / (1024 * 1024)).toFixed(2)} MB`); } if (cloudRegistryHost) { logger.debug(`Logging out from docker registry: ${cloudRegistryHost}`); await x("docker", ["logout", cloudRegistryHost]); } return { ok: true, imageSizeBytes, digest, logs: extractLogs(errors), }; } function extractLogs(outputs) { // Remove empty lines const cleanedOutputs = outputs.map((line) => line.trim()).filter((line) => line !== ""); return cleanedOutputs.map((line) => line.trim()).join("\n"); } const BASE_IMAGE = { bun: "imbios/bun-node:1.3.3-20-slim@sha256:59d84856a7e31eec83afedadb542f7306f672343b8b265c70d733404a6e8834b", node: "node:21.7.3-bookworm-slim@sha256:dfc05dee209a1d7adf2ef189bd97396daad4e97c6eaa85778d6f75205ba1b0fb", "node-22": "node:22.16.0-bookworm-slim@sha256:048ed02c5fd52e86fda6fbd2f6a76cf0d4492fd6c6fee9e2c463ed5108da0e34", }; const DEFAULT_PACKAGES = ["busybox", "ca-certificates", "dumb-init", "git", "openssl"]; export async function generateContainerfile(options) { switch (options.runtime) { case "node": case "node-22": { return await generateNodeContainerfile(options); } case "bun": { return await generateBunContainerfile(options); } } } const parseGenerateOptions = (options) => { const buildArgs = Object.entries(options.build.env || {}) .flatMap(([key]) => `ARG ${key}`) .join("\n"); const buildEnvVars = Object.entries(options.build.env || {}) .flatMap(([key]) => `ENV ${key}=$${key}`) .join("\n"); const postInstallCommands = (options.build.commands || []).map((cmd) => `RUN ${cmd}`).join("\n"); const baseInstructions = (options.image?.instructions || []).join("\n"); const packages = Array.from(new Set(DEFAULT_PACKAGES.concat(options.image?.pkgs || []))).join(" "); return { baseImage: BASE_IMAGE[options.runtime], baseInstructions, buildArgs, buildEnvVars, packages, postInstallCommands, }; }; async function generateBunContainerfile(options) { const { baseImage, buildArgs, buildEnvVars, postInstallCommands, baseInstructions, packages } = parseGenerateOptions(options); return `# syntax=docker/dockerfile:1 # check=skip=SecretsUsedInArgOrEnv FROM ${baseImage} AS base ${baseInstructions} ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update && \ apt-get --fix-broken install -y && \ apt-get install -y --no-install-recommends ${packages} && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* FROM base AS build RUN apt-get update && \ apt-get install -y --no-install-recommends python3 make g++ && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* USER bun WORKDIR /app ${buildArgs} ${buildEnvVars} COPY --chown=bun:bun package.json ./ RUN bun install --production --no-save # Now copy all the files # IMPORTANT: Do this after running npm install because npm i will wipe out the node_modules directory COPY --chown=bun:bun . . ${postInstallCommands} FROM build AS indexer USER bun WORKDIR /app ARG TRIGGER_PROJECT_ID ARG TRIGGER_DEPLOYMENT_ID ARG TRIGGER_DEPLOYMENT_VERSION ARG TRIGGER_CONTENT_HASH ARG TRIGGER_PROJECT_REF ARG NODE_EXTRA_CA_CERTS ARG TRIGGER_SECRET_KEY ARG TRIGGER_API_URL ARG TRIGGER_PREVIEW_BRANCH ENV TRIGGER_PROJECT_ID=\${TRIGGER_PROJECT_ID} \ TRIGGER_DEPLOYMENT_ID=\${TRIGGER_DEPLOYMENT_ID} \ TRIGGER_DEPLOYMENT_VERSION=\${TRIGGER_DEPLOYMENT_VERSION} \ TRIGGER_PROJECT_REF=\${TRIGGER_PROJECT_REF} \ TRIGGER_CONTENT_HASH=\${TRIGGER_CONTENT_HASH} \ TRIGGER_SECRET_KEY=\${TRIGGER_SECRET_KEY} \ TRIGGER_API_URL=\${TRIGGER_API_URL} \ TRIGGER_PREVIEW_BRANCH=\${TRIGGER_PREVIEW_BRANCH} \ NODE_EXTRA_CA_CERTS=\${NODE_EXTRA_CA_CERTS} \ NODE_ENV=production ARG TARGETPLATFORM ARG BUILDPLATFORM ENV BUILDPLATFORM=$BUILDPLATFORM TARGETPLATFORM=$TARGETPLATFORM # Run the indexer RUN bun run ${options.indexScript} # Development or production stage builds upon the base stage FROM base AS final USER bun WORKDIR /app ARG TRIGGER_PROJECT_ID ARG TRIGGER_CONTENT_HASH ARG TRIGGER_PROJECT_REF ARG NODE_EXTRA_CA_CERTS ENV TRIGGER_PROJECT_ID=\${TRIGGER_PROJECT_ID} \ TRIGGER_CONTENT_HASH=\${TRIGGER_CONTENT_HASH} \ TRIGGER_PROJECT_REF=\${TRIGGER_PROJECT_REF} \ UV_USE_IO_URING=0 \ NODE_EXTRA_CA_CERTS=\${NODE_EXTRA_CA_CERTS} \ NODE_ENV=production # Copy the files from the build stage COPY --from=build --chown=bun:bun /app ./ # Copy the index.json file from the indexer stage COPY --from=indexer --chown=bun:bun /app/index.json ./ ENTRYPOINT [ "dumb-init", "node", "${options.entrypoint}" ] CMD [] `; } async function generateNodeContainerfile(options) { const { baseImage, buildArgs, buildEnvVars, postInstallCommands, baseInstructions, packages } = parseGenerateOptions(options); return `# syntax=docker/dockerfile:1 # check=skip=SecretsUsedInArgOrEnv FROM ${baseImage} AS base ${baseInstructions} ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update && \ apt-get --fix-broken install -y && \ apt-get install -y --no-install-recommends ${packages} && \ apt-get clean && rm -rf /var/lib/apt/lists/* FROM base AS build # Install build dependencies RUN apt-get update && \ apt-get install -y --no-install-recommends python3 make g++ && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* USER node WORKDIR /app ${buildArgs} ${buildEnvVars} ENV NODE_ENV=production ENV NPM_CONFIG_UPDATE_NOTIFIER=false COPY --chown=node:node package.json ./ RUN npm i --no-audit --no-fund --no-save --no-package-lock # Now copy all the files # IMPORTANT: Do this after running npm install because npm i will wipe out the node_modules directory COPY --chown=node:node . . ${postInstallCommands} # IMPORTANT: Doing this again to fix an issue with prisma generate removing the files in node_modules/trigger.dev for some reason... COPY --chown=node:node . . FROM build AS indexer USER node WORKDIR /app ARG TRIGGER_PROJECT_ID ARG TRIGGER_DEPLOYMENT_ID ARG TRIGGER_DEPLOYMENT_VERSION ARG TRIGGER_CONTENT_HASH ARG TRIGGER_PROJECT_REF ARG NODE_EXTRA_CA_CERTS ARG TRIGGER_SECRET_KEY ARG TRIGGER_API_URL ARG TRIGGER_PREVIEW_BRANCH ENV TRIGGER_PROJECT_ID=\${TRIGGER_PROJECT_ID} \ TRIGGER_DEPLOYMENT_ID=\${TRIGGER_DEPLOYMENT_ID} \ TRIGGER_DEPLOYMENT_VERSION=\${TRIGGER_DEPLOYMENT_VERSION} \ TRIGGER_PROJECT_REF=\${TRIGGER_PROJECT_REF} \ TRIGGER_CONTENT_HASH=\${TRIGGER_CONTENT_HASH} \ TRIGGER_SECRET_KEY=\${TRIGGER_SECRET_KEY} \ TRIGGER_API_URL=\${TRIGGER_API_URL} \ TRIGGER_PREVIEW_BRANCH=\${TRIGGER_PREVIEW_BRANCH} \ TRIGGER_LOG_LEVEL=debug \ NODE_EXTRA_CA_CERTS=\${NODE_EXTRA_CA_CERTS} \ NODE_ENV=production \ NODE_OPTIONS="--max_old_space_size=8192" ARG TARGETPLATFORM ARG BUILDPLATFORM ENV BUILDPLATFORM=$BUILDPLATFORM TARGETPLATFORM=$TARGETPLATFORM # Run the indexer RUN node ${options.indexScript} # Development or production stage builds upon the base stage FROM base AS final USER node WORKDIR /app ARG TRIGGER_PROJECT_ID ARG TRIGGER_CONTENT_HASH ARG TRIGGER_PROJECT_REF ARG NODE_EXTRA_CA_CERTS ENV TRIGGER_PROJECT_ID=\${TRIGGER_PROJECT_ID} \ TRIGGER_CONTENT_HASH=\${TRIGGER_CONTENT_HASH} \ TRIGGER_PROJECT_REF=\${TRIGGER_PROJECT_REF} \ UV_USE_IO_URING=0 \ NODE_EXTRA_CA_CERTS=\${NODE_EXTRA_CA_CERTS} \ NODE_ENV=production # Copy the files from the install stage COPY --from=build --chown=node:node /app ./ # Copy the index.json file from the indexer stage COPY --from=indexer --chown=node:node /app/index.json ./ ENTRYPOINT [ "dumb-init", "node", "${options.entrypoint}" ] CMD [] `; } // If apiUrl is something like http://localhost:3030, we need to convert it to http://host.docker.internal:3030 // this way the indexing will work because the docker image can reach the local server function normalizeApiUrlForBuild(apiUrl) { return apiUrl.replace("localhost", "host.docker.internal"); } function getHostIP() { const interfaces = networkInterfaces(); for (const [name, iface] of Object.entries(interfaces)) { if (!iface) { continue; } for (const net of iface) { // Skip internal/loopback and non-IPv4 addresses if (!net.internal && net.family === "IPv4") { return net.address; } } } return "127.0.0.1"; } function getAddHost(apiUrl) { if (apiUrl.includes("host.docker.internal")) { return `host.docker.internal:${getHostIP()}`; } return; } function extractRegistryHostFromImageTag(imageTag) { const host = imageTag.split("/")[0]; if (!host || !host.includes(".")) { return undefined; } return host; } function getProjectCacheRefFromImageTag(imageTag) { const lastColonIndex = imageTag.lastIndexOf(":"); return `${imageTag.substring(0, lastColonIndex)}:cache`; } async function getDockerUsernameAndPassword(apiClient, deploymentId) { if (process.env.TRIGGER_DOCKER_USERNAME && process.env.TRIGGER_DOCKER_PASSWORD) { return { username: process.env.TRIGGER_DOCKER_USERNAME, password: process.env.TRIGGER_DOCKER_PASSWORD, }; } const result = await apiClient.generateRegistryCredentials(deploymentId); if (!result.success) { logger.debug("Failed to generate registry credentials", { error: result.error, deploymentId, }); throw new Error("Failed to generate registry credentials"); } return { username: result.data.username, password: result.data.password, }; } function isQemuRegistered() { try { // Check a single QEMU handler const binfmt = readFileSync("/proc/sys/fs/binfmt_misc/qemu-aarch64", "utf8"); return binfmt.includes("enabled"); } catch (e) { return false; } } function isMultiPlatform(imagePlatform) { return imagePlatform.split(",").length > 1; } async function ensureQemuRegistered(imagePlatform) { if (isLinux && isMultiPlatform(imagePlatform) && !isQemuRegistered()) { logger.debug("Registering QEMU for multi-platform build..."); const ensureQemuProcess = x("docker", [ "run", "--rm", "--privileged", "multiarch/qemu-user-static", "--reset", "-p", "yes", ]); const logs = []; for await (const line of ensureQemuProcess) { logger.debug(line); logs.push(line); } if (ensureQemuProcess.exitCode !== 0) { logger.error("Failed to register QEMU for multi-platform build", { exitCode: ensureQemuProcess.exitCode, logs: logs.join("\n"), }); } } } const BuildKitMetadata = z.object({ "buildx.build.ref": z.string().optional(), "containerimage.descriptor": z .object({ mediaType: z.string(), digest: z.string(), size: z.number(), }) .optional(), "containerimage.digest": z.string().optional(), "containerimage.config.digest": z.string().optional(), "image.name": z.string().optional(), }); // Don't push if the image tag is a local address, unless the user explicitly wants to push function shouldPush(imageTag, push) { switch (push) { case true: { return true; } case false: { return false; } case undefined: { return imageTag.startsWith("localhost") || imageTag.startsWith("127.0.0.1") || imageTag.startsWith("0.0.0.0") ? false : true; } default: { assertExhaustive(push); } } } // Don't load if we're pushing, unless the user explicitly wants to load function shouldLoad(load, push) { switch (load) { case true: { return true; } case false: { return false; } case undefined: { return push ? false : true; } default: { assertExhaustive(load); } } } function getOutputOptions({ imageTag, push, load, compression, compressionLevel, forceCompression, isLocalBuild, }) { // type=docker exports directly into the local Docker daemon (what --load does). // type=image builds a registry-format image that can be pushed. // They're mutually exclusive: type=docker doesn't support push/compression, // and type=image silently ignores "load=true". // Only use type=docker for local builds to avoid affecting remote builds. // Note: type=docker doesn't support compression options or rewrite-timestamp, // so it will use the actual build time and default compression. if (isLocalBuild && load && !push) { const outputOptions = ["type=docker"]; if (imageTag) { outputOptions.push(`name=${imageTag}`); } return outputOptions; } // `rewrite-timestamp` is incompatible with the buildx docker driver's // implicit `unpack=true` on push (used by e.g. orbstack's default builder). // Provide an env-var escape hatch so local-dev deploys can opt out. const skipRewriteTimestamp = process.env.TRIGGER_BUILD_SKIP_REWRITE_TIMESTAMP === "1"; const outputOptions = ["type=image", "oci-mediatypes=true"]; if (!skipRewriteTimestamp) { outputOptions.push("rewrite-timestamp=true"); } if (imageTag) { outputOptions.push(`name=${imageTag}`); } if (push) { outputOptions.push("push=true"); } // Only add compression args when using zstd (gzip is the default, no args needed) if (compression === "zstd") { outputOptions.push("compression=zstd"); if (compressionLevel !== undefined) { outputOptions.push(`compression-level=${compressionLevel}`); } } if (forceCompression) { outputOptions.push("force-compression=true"); } return outputOptions; } //# sourceMappingURL=buildImage.js.map