From 3f6c446b2698f36e82ddf27c4917a4c82c33cf88 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Thu, 29 May 2025 12:04:00 +0100 Subject: [PATCH 01/28] remove registry proxy --- .env.example | 5 - apps/webapp/app/entry.server.tsx | 1 - apps/webapp/app/env.server.ts | 4 - apps/webapp/app/v3/registryProxy.server.ts | 469 ------------------ .../v3/services/finalizeDeployment.server.ts | 7 +- apps/webapp/package.json | 1 - apps/webapp/server.ts | 16 - pnpm-lock.yaml | 175 +++++-- 8 files changed, 126 insertions(+), 552 deletions(-) delete mode 100644 apps/webapp/app/v3/registryProxy.server.ts diff --git a/.env.example b/.env.example index 06cefc0aec..4da4d83266 100644 --- a/.env.example +++ b/.env.example @@ -80,14 +80,9 @@ CLOUD_SLACK_CLIENT_SECRET= PROVIDER_SECRET=provider-secret # generate the actual secret with `openssl rand -hex 32` COORDINATOR_SECRET=coordinator-secret # generate the actual secret with `openssl rand -hex 32` -# Uncomment the following line to enable the registry proxy -# ENABLE_REGISTRY_PROXY=true # DEPOT_TOKEN= # DEPOT_PROJECT_ID= # DEPLOY_REGISTRY_HOST=${APP_ORIGIN} # This is the host that the deploy CLI will use to push images to the registry -# CONTAINER_REGISTRY_ORIGIN= -# CONTAINER_REGISTRY_USERNAME= -# CONTAINER_REGISTRY_PASSWORD= # DEV_OTEL_EXPORTER_OTLP_ENDPOINT="http://0.0.0.0:4318" # These are needed for the object store (for handling large payloads/outputs) # OBJECT_STORE_BASE_URL="https://{bucket}.{accountId}.r2.cloudflarestorage.com" diff --git a/apps/webapp/app/entry.server.tsx b/apps/webapp/app/entry.server.tsx index 2463f7fb1d..85264294d4 100644 --- a/apps/webapp/app/entry.server.tsx +++ b/apps/webapp/app/entry.server.tsx @@ -215,7 +215,6 @@ export { apiRateLimiter } from "./services/apiRateLimit.server"; export { engineRateLimiter } from "./services/engineRateLimit.server"; export { socketIo } from "./v3/handleSocketIo.server"; export { wss } from "./v3/handleWebsockets.server"; -export { registryProxy } from "./v3/registryProxy.server"; export { runWithHttpContext } from "./services/httpAsyncStorage.server"; import { eventLoopMonitor } from "./eventLoopMonitor.server"; import { env } from "./env.server"; diff --git a/apps/webapp/app/env.server.ts b/apps/webapp/app/env.server.ts index 38360c93c3..d90b0d8c38 100644 --- a/apps/webapp/app/env.server.ts +++ b/apps/webapp/app/env.server.ts @@ -267,10 +267,6 @@ const EnvironmentSchema = z.object({ DEPOT_PROJECT_ID: z.string().optional(), DEPOT_ORG_ID: z.string().optional(), DEPOT_REGION: z.string().default("us-east-1"), - CONTAINER_REGISTRY_ORIGIN: z.string().optional(), - CONTAINER_REGISTRY_USERNAME: z.string().optional(), - CONTAINER_REGISTRY_PASSWORD: z.string().optional(), - ENABLE_REGISTRY_PROXY: z.string().optional(), DEPLOY_REGISTRY_HOST: z.string().optional(), DEPLOY_REGISTRY_USERNAME: z.string().optional(), DEPLOY_REGISTRY_PASSWORD: z.string().optional(), diff --git a/apps/webapp/app/v3/registryProxy.server.ts b/apps/webapp/app/v3/registryProxy.server.ts deleted file mode 100644 index f253c89195..0000000000 --- a/apps/webapp/app/v3/registryProxy.server.ts +++ /dev/null @@ -1,469 +0,0 @@ -import { IncomingMessage, ServerResponse } from "http"; -import Redis, { RedisOptions } from "ioredis"; -import { RequestOptions, request as httpRequest } from "node:https"; -import { z } from "zod"; -import { env } from "~/env.server"; -import { logger } from "~/services/logger.server"; -import { authenticatePersonalAccessToken } from "~/services/personalAccessToken.server"; -import { singleton } from "~/utils/singleton"; -import { jwtDecode } from "jwt-decode"; -import { createHash } from "node:crypto"; -import { tmpdir } from "node:os"; -import { mkdtemp } from "fs/promises"; -import { createReadStream, createWriteStream } from "node:fs"; -import { pipeline } from "node:stream/promises"; -import { unlinkSync } from "fs"; -import { parseDockerImageReference, rebuildDockerImageReference } from "@trigger.dev/core/v3"; - -const TokenResponseBody = z.object({ - token: z.string(), -}); - -const CACHED_BEARER_TOKEN_BUFFER_IN_SECONDS = 10; - -type RegistryProxyOptions = { - origin: string; - auth: { username: string; password: string }; - redis?: RedisOptions; -}; - -export class RegistryProxy { - private redis?: Redis; - - constructor(private readonly options: RegistryProxyOptions) { - if (options.redis) { - this.redis = new Redis(options.redis); - } - } - - get origin() { - return this.options.origin; - } - - get host() { - return new URL(this.options.origin).host; - } - - // If the imageReference includes a hostname, rewrite it to point to the proxy - // e.g. eric-webapp.trigger.dev/trigger/yubjwjsfkxnylobaqvqz:20240306.41.prod@sha256:8b48dd2866bc8878644d2880bbe35a27e66cf6ff78aa1e489d7fdde5e228faf1 - // should be rewritten to ${this.host}/trigger/yubjwjsfkxnylobaqvqz:20240306.41.prod@sha256:8b48dd2866bc8878644d2880bbe35a27e66cf6ff78aa1e489d7fdde5e228faf1 - // This will work with image references that don't include the @sha256:... part - public rewriteImageReference(imageReference: string) { - const parts = parseDockerImageReference(imageReference); - - logger.debug("Rewriting image reference parts", { parts }); - - if (parts.registry) { - return rebuildDockerImageReference({ - ...parts, - registry: this.host, - }); - } - - return imageReference; - } - - public async call(request: IncomingMessage, response: ServerResponse) { - await this.#proxyRequest(request, response); - } - - // Proxies the request to the registry - async #proxyRequest(request: IncomingMessage, response: ServerResponse) { - const credentials = this.#getBasicAuthCredentials(request); - - if (!credentials) { - logger.debug("Returning 401 because credentials are missing"); - - response.writeHead(401, { - "WWW-Authenticate": 'Basic realm="Access to the registry"', - }); - - response.end("Unauthorized"); - - return; - } - - // Authenticate the request - const authentication = await authenticatePersonalAccessToken(credentials.password); - - if (!authentication) { - logger.debug("Returning 401 because authentication failed"); - - response.writeHead(401, { - "WWW-Authenticate": 'Basic realm="Access to the registry"', - }); - - response.end("Unauthorized"); - - return; - } - - // construct a new url based on the url passed in and the registry url - const url = new URL(this.options.origin); - const options = { - hostname: url.hostname, - path: request.url, - method: request.method, - headers: { ...request.headers }, - }; - - delete options.headers["host"]; - // delete options.headers["connection"]; - // delete options.headers["accept-encoding"]; - delete options.headers["authorization"]; - // delete options.headers["content-length"]; - delete options.headers["cf-ray"]; - delete options.headers["cf-visitor"]; - delete options.headers["cf-ipcountry"]; - delete options.headers["cf-connecting-ip"]; - delete options.headers["cf-warp-tag-id"]; - - // Add a custom Authorization header for the proxied request - options.headers["authorization"] = `Basic ${Buffer.from( - `${this.options.auth.username}:${this.options.auth.password}` - ).toString("base64")}`; - - let tempFilePath: string | undefined; - let cleanupTempFile: () => void = () => {}; - - if ( - options.method === "POST" || - (options.method === "PUT" && request.headers["content-length"]) - ) { - tempFilePath = await streamRequestBodyToTempFile(request); - - cleanupTempFile = () => { - if (tempFilePath) { - logger.debug("Cleaning up temp file", { tempFilePath }); - unlinkSync(tempFilePath); - } - }; - - logger.debug("Streamed request body to temp file", { tempFilePath }); - } - - const makeProxiedRequest = (tokenOptions: RequestOptions, attempts: number = 1) => { - logger.debug("Proxying request", { - request: tokenOptions, - attempts, - originalHeaders: request.headers, - }); - - if (attempts > 10) { - logger.error("Too many attempts to proxy request", { - attempts, - }); - - response.writeHead(500, { "Content-Type": "text/plain" }); - response.end("Internal Server Error: Too many attempts to proxy request"); - - return cleanupTempFile(); - } - - const proxyReq = httpRequest(tokenOptions, async (proxyRes) => { - // If challenged for bearer token auth, handle it here - if (proxyRes.statusCode === 401 && proxyRes.headers["www-authenticate"]) { - logger.debug("Received 401 with WWW-Authenticate, attempting to fetch bearer token", { - authenticate: proxyRes.headers["www-authenticate"], - }); - - const bearerToken = await this.#getBearerToken(proxyRes.headers["www-authenticate"]); - - if (bearerToken && tokenOptions.headers) { - tokenOptions.headers["authorization"] = `Bearer ${bearerToken}`; - makeProxiedRequest(tokenOptions, attempts + 1); // Retry request with bearer token - return; - } else { - // Handle failed token fetch or lack of WWW-Authenticate handling - response.writeHead(401, { "Content-Type": "text/plain" }); - response.end("Failed to authenticate with the registry using bearer token"); - return cleanupTempFile(); - } - } - - if (proxyRes.statusCode === 401) { - logger.debug("Received 401, but there is no www-authenticate value", { - headers: proxyRes.headers, - }); - - response.writeHead(401, { "Content-Type": "text/plain" }); - response.end("Unauthorized"); - return cleanupTempFile(); - } - - if (proxyRes.statusCode === 301) { - logger.debug("Received 301, attempting to follow redirect", { - location: proxyRes.headers["location"], - }); - - const redirectOptions = { - ...tokenOptions, - path: proxyRes.headers["location"], - }; - - makeProxiedRequest(redirectOptions, attempts + 1); - return; - } - - if (!proxyRes.statusCode) { - logger.error("No status code in the response", { - headers: proxyRes.headers, - statusMessage: proxyRes.statusMessage, - }); - - response.writeHead(500, { "Content-Type": "text/plain" }); - response.end("Internal Server Error: No status code in the response"); - - return cleanupTempFile(); - } - - const headers = { ...proxyRes.headers }; - - // Rewrite location headers to point to the proxy - if (headers["location"]) { - const proxiedLocation = new URL(headers.location); - - // Only rewrite the location header if the host is the same as the registry - if (proxiedLocation.host === this.host) { - if (!request.headers.host) { - // Return a 500 if the host header is missing - logger.error("Host header is missing in the request", { - headers: request.headers, - }); - - response.writeHead(500, { "Content-Type": "text/plain" }); - response.end("Internal Server Error: Host header is missing in the request"); - return cleanupTempFile(); - } - - proxiedLocation.host = request.headers.host; - - headers["location"] = proxiedLocation.href; - - logger.debug("Rewriting location response header", { - originalLocation: proxyRes.headers["location"], - proxiedLocation: headers["location"], - proxiedLocationUrl: proxiedLocation.href, - }); - } - } - - logger.debug("Proxying successful response", { - method: tokenOptions.method, - path: tokenOptions.path, - statusCode: proxyRes.statusCode, - responseHeaders: headers, - }); - - // Proceed as normal if not a 401 or after getting a bearer token - response.writeHead(proxyRes.statusCode, headers); - proxyRes.pipe(response, { end: true }); - }); - - request.on("close", () => { - logger.debug("Client closed the connection"); - proxyReq.destroy(); - cleanupTempFile(); - }); - - request.on("abort", () => { - logger.debug("Client aborted the connection"); - proxyReq.destroy(); // Abort the proxied request - cleanupTempFile(); // Clean up the temporary file if necessary - }); - - if (tempFilePath) { - const readStream = createReadStream(tempFilePath); - - readStream.pipe(proxyReq, { end: true }); - } else { - proxyReq.end(); - } - - proxyReq.on("error", (error) => { - logger.error("Error proxying request", { error: error.message }); - - if (response.headersSent) { - return; - } - - response.writeHead(500, { "Content-Type": "text/plain" }); - response.end(`Internal Server Error: ${error.message}`); - }); - }; - - makeProxiedRequest(options); - } - - #getBasicAuthCredentials(request: IncomingMessage) { - const headers = request.headers; - - logger.debug("Getting basic auth credentials with headers", { - headers, - }); - - const authHeader = headers["authorization"]; - - if (!authHeader) { - return; - } - - const [type, credentials] = authHeader.split(" "); - - if (type.toLowerCase() !== "basic") { - return; - } - - const decoded = Buffer.from(credentials, "base64").toString("utf-8"); - const [username, password] = decoded.split(":"); - - return { username, password }; - } - - async #getBearerToken(authenticateHeader: string): Promise { - try { - // Create a md5 hash of the authenticate header to use as a cache key - const cacheKey = `token:${createHash("md5").update(authenticateHeader).digest("hex")}`; - - const cachedToken = await this.#getCachedToken(cacheKey); - - if (cachedToken) { - return cachedToken; - } - - // Parse the WWW-Authenticate header to extract realm and service - const realmMatch = authenticateHeader.match(/realm="([^"]+)"/); - const serviceMatch = authenticateHeader.match(/service="([^"]+)"/); - // Optionally, we could also extract and use the scope parameter if required - const scopeMatch = authenticateHeader.match(/scope="([^"]+)"/); - - if (!realmMatch || !serviceMatch) { - logger.error("Failed to parse WWW-Authenticate header", { authenticateHeader }); - return; - } - - const realm = realmMatch[1]; - const service = serviceMatch[1]; - // Construct the URL for fetching the token - let authUrl = `${realm}?service=${encodeURIComponent(service)}`; - // Include scope in the request if needed - if (scopeMatch) { - const scope = scopeMatch[1]; - authUrl += `&scope=${encodeURIComponent(scope)}`; - } - - authUrl += `&account=${encodeURIComponent(this.options.auth.username)}`; - - logger.debug("Fetching bearer token", { authUrl }); - - // Make the request to the authentication service - const response = await fetch(authUrl, { - headers: { - authorization: - "Basic " + - Buffer.from(`${this.options.auth.username}:${this.options.auth.password}`).toString( - "base64" - ), - }, - }); - - if (!response.ok) { - logger.debug("Failed to fetch bearer token", { - status: response.status, - statusText: response.statusText, - }); - return; - } - - const rawBody = await response.json(); - const body = TokenResponseBody.safeParse(rawBody); - - if (!body.success) { - logger.error("Failed to parse token response", { body: rawBody }); - return; - } - - logger.debug("Fetched bearer token", { token: body.data.token }); - - await this.#setCachedToken(body.data.token, cacheKey); - - return body.data.token; - } catch (error) { - logger.error("Failed to fetch bearer token", { - error: error instanceof Error ? error.message : error, - }); - } - } - - async #getCachedToken(key: string) { - if (!this.redis) { - return; - } - - const cachedToken = await this.redis.get(key); - - if (cachedToken) { - const decoded = jwtDecode(cachedToken); - const expiry = decoded.exp; - - if (expiry && expiry > Date.now() / 1000 + CACHED_BEARER_TOKEN_BUFFER_IN_SECONDS) { - return cachedToken; - } - } - } - - async #setCachedToken(token: string, key: string) { - if (!this.redis) { - return; - } - - const decoded = jwtDecode(token); - - if (decoded.exp) { - await this.redis.set(key, token, "EXAT", decoded.exp); - } - } -} - -export const registryProxy = singleton("registryProxy", initializeProxy); - -function initializeProxy() { - if ( - !env.CONTAINER_REGISTRY_ORIGIN || - !env.CONTAINER_REGISTRY_USERNAME || - !env.CONTAINER_REGISTRY_PASSWORD - ) { - return; - } - - if (!env.ENABLE_REGISTRY_PROXY || env.ENABLE_REGISTRY_PROXY === "false") { - logger.info("Registry proxy is disabled"); - return; - } - - return new RegistryProxy({ - origin: env.CONTAINER_REGISTRY_ORIGIN, - auth: { - username: env.CONTAINER_REGISTRY_USERNAME, - password: env.CONTAINER_REGISTRY_PASSWORD, - }, - }); -} - -async function streamRequestBodyToTempFile(request: IncomingMessage): Promise { - try { - const tempDir = await mkdtemp(`${tmpdir()}/`); - const tempFilePath = `${tempDir}/requestBody.tmp`; - const writeStream = createWriteStream(tempFilePath); - - await pipeline(request, writeStream); - - return tempFilePath; - } catch (error) { - logger.error("Failed to stream request body to temp file", { - error: error instanceof Error ? error.message : error, - }); - - return; - } -} diff --git a/apps/webapp/app/v3/services/finalizeDeployment.server.ts b/apps/webapp/app/v3/services/finalizeDeployment.server.ts index 3389c21137..9cbc8f0f31 100644 --- a/apps/webapp/app/v3/services/finalizeDeployment.server.ts +++ b/apps/webapp/app/v3/services/finalizeDeployment.server.ts @@ -2,7 +2,6 @@ import { FinalizeDeploymentRequestBody } from "@trigger.dev/core/v3/schemas"; import { AuthenticatedEnvironment } from "~/services/apiAuth.server"; import { logger } from "~/services/logger.server"; import { socketIo } from "../handleSocketIo.server"; -import { registryProxy } from "../registryProxy.server"; import { updateEnvConcurrencyLimits } from "../runQueue.server"; import { PerformDeploymentAlertsService } from "./alerts/performDeploymentAlerts.server"; import { BaseService, ServiceValidationError } from "./baseService.server"; @@ -53,11 +52,7 @@ export class FinalizeDeploymentService extends BaseService { throw new ServiceValidationError("Worker deployment is not in DEPLOYING status"); } - let imageReference = body.imageReference; - - if (registryProxy && body.selfHosted !== true && body.skipRegistryProxy !== true) { - imageReference = registryProxy.rewriteImageReference(body.imageReference); - } + const imageReference = body.imageReference; // Link the deployment with the background worker const finalizedDeployment = await this._prisma.workerDeployment.update({ diff --git a/apps/webapp/package.json b/apps/webapp/package.json index 88435c4127..126590f175 100644 --- a/apps/webapp/package.json +++ b/apps/webapp/package.json @@ -137,7 +137,6 @@ "isbot": "^3.6.5", "jose": "^5.4.0", "jsonpointer": "^5.0.1", - "jwt-decode": "^4.0.0", "lodash.omit": "^4.5.0", "lucide-react": "^0.229.0", "marked": "^4.0.18", diff --git a/apps/webapp/server.ts b/apps/webapp/server.ts index 46a58ec4a7..1ad45fb538 100644 --- a/apps/webapp/server.ts +++ b/apps/webapp/server.ts @@ -10,7 +10,6 @@ import type { Server as IoServer } from "socket.io"; import { WebSocketServer } from "ws"; import { RateLimitMiddleware } from "~/services/apiRateLimit.server"; import { type RunWithHttpContextFunction } from "~/services/httpAsyncStorage.server"; -import { RegistryProxy } from "~/v3/registryProxy.server"; const app = express(); @@ -41,25 +40,10 @@ const port = process.env.REMIX_APP_PORT || process.env.PORT || 3000; if (process.env.HTTP_SERVER_DISABLED !== "true") { const socketIo: { io: IoServer } | undefined = build.entry.module.socketIo; const wss: WebSocketServer | undefined = build.entry.module.wss; - const registryProxy: RegistryProxy | undefined = build.entry.module.registryProxy; const apiRateLimiter: RateLimitMiddleware = build.entry.module.apiRateLimiter; const engineRateLimiter: RateLimitMiddleware = build.entry.module.engineRateLimiter; const runWithHttpContext: RunWithHttpContextFunction = build.entry.module.runWithHttpContext; - if (registryProxy && process.env.ENABLE_REGISTRY_PROXY === "true") { - console.log(`🐳 Enabling container registry proxy to ${registryProxy.origin}`); - - // Adjusted to match /v2 and any subpath under /v2 - app.all("/v2/*", async (req, res) => { - await registryProxy.call(req, res); - }); - - // This might also be necessary if you need to explicitly match /v2 as well - app.all("/v2", async (req, res) => { - await registryProxy.call(req, res); - }); - } - app.use((req, res, next) => { // helpful headers: res.set("Strict-Transport-Security", `max-age=${60 * 60 * 24 * 365 * 100}`); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6e55c2a198..fb30902d1f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -278,7 +278,7 @@ importers: version: 0.52.1(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': specifier: 0.52.1 - version: 0.52.1(@opentelemetry/api@1.9.0)(supports-color@10.0.0) + version: 0.52.1(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation-express': specifier: ^0.36.1 version: 0.36.1(@opentelemetry/api@1.9.0) @@ -293,7 +293,7 @@ importers: version: 0.52.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-node': specifier: 0.52.1 - version: 0.52.1(@opentelemetry/api@1.9.0)(supports-color@10.0.0) + version: 0.52.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': specifier: 1.25.1 version: 1.25.1(@opentelemetry/api@1.9.0) @@ -504,9 +504,6 @@ importers: jsonpointer: specifier: ^5.0.1 version: 5.0.1 - jwt-decode: - specifier: ^4.0.0 - version: 4.0.0 lodash.omit: specifier: ^4.5.0 version: 4.5.0 @@ -684,7 +681,7 @@ importers: version: link:../../internal-packages/testcontainers '@remix-run/dev': specifier: 2.1.0 - version: 2.1.0(@remix-run/serve@2.1.0)(@types/node@20.14.14)(ts-node@10.9.1)(typescript@5.5.4) + version: 2.1.0(@remix-run/serve@2.1.0)(@types/node@22.13.9)(ts-node@10.9.1)(typescript@5.5.4) '@remix-run/eslint-config': specifier: 2.1.0 version: 2.1.0(eslint@8.31.0)(react@18.2.0)(typescript@5.5.4) @@ -849,7 +846,7 @@ importers: version: 3.4.1(ts-node@10.9.1) ts-node: specifier: ^10.7.0 - version: 10.9.1(@swc/core@1.3.26)(@types/node@20.14.14)(typescript@5.5.4) + version: 10.9.1(@swc/core@1.3.26)(@types/node@22.13.9)(typescript@5.5.4) tsconfig-paths: specifier: ^3.14.1 version: 3.14.1 @@ -3839,7 +3836,7 @@ packages: '@babel/traverse': 7.24.7 '@babel/types': 7.24.0 convert-source-map: 1.9.0 - debug: 4.4.0(supports-color@10.0.0) + debug: 4.4.0 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -4419,7 +4416,7 @@ packages: '@babel/helper-split-export-declaration': 7.22.6 '@babel/parser': 7.24.7 '@babel/types': 7.24.0 - debug: 4.4.0(supports-color@10.0.0) + debug: 4.4.0 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -4437,7 +4434,7 @@ packages: '@babel/helper-split-export-declaration': 7.24.7 '@babel/parser': 7.27.0 '@babel/types': 7.27.0 - debug: 4.4.0(supports-color@10.0.0) + debug: 4.4.0 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -6944,7 +6941,7 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: ajv: 6.12.6 - debug: 4.4.0(supports-color@10.0.0) + debug: 4.4.0 espree: 9.6.0 globals: 13.19.0 ignore: 5.2.4 @@ -7213,7 +7210,7 @@ packages: engines: {node: '>=10.10.0'} dependencies: '@humanwhocodes/object-schema': 1.2.1 - debug: 4.4.0(supports-color@10.0.0) + debug: 4.4.0 minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -8970,7 +8967,7 @@ packages: dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0)(supports-color@10.0.0) + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.25.1 semver: 7.6.3 transitivePeerDependencies: @@ -9017,7 +9014,7 @@ packages: '@opentelemetry/api-logs': 0.49.1 '@types/shimmer': 1.0.2 import-in-the-middle: 1.7.1 - require-in-the-middle: 7.1.1(supports-color@10.0.0) + require-in-the-middle: 7.1.1 semver: 7.6.3 shimmer: 1.2.1 transitivePeerDependencies: @@ -9034,7 +9031,7 @@ packages: '@opentelemetry/api-logs': 0.49.1 '@types/shimmer': 1.0.2 import-in-the-middle: 1.7.1 - require-in-the-middle: 7.1.1(supports-color@10.0.0) + require-in-the-middle: 7.1.1 semver: 7.6.3 shimmer: 1.2.1 transitivePeerDependencies: @@ -9075,6 +9072,23 @@ packages: - supports-color dev: false + /@opentelemetry/instrumentation@0.52.1(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-uXJbYU/5/MBHjMp1FqrILLRuiJCs3Ofk0MeRDk8g1S1gD47U8X3JnSwcMO1rtRo1x1a7zKaQHaoYu49p/4eSKw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.52.1 + '@types/shimmer': 1.0.2 + import-in-the-middle: 1.11.0 + require-in-the-middle: 7.1.1 + semver: 7.6.3 + shimmer: 1.2.1 + transitivePeerDependencies: + - supports-color + dev: false + /@opentelemetry/instrumentation@0.52.1(@opentelemetry/api@1.9.0)(supports-color@10.0.0): resolution: {integrity: sha512-uXJbYU/5/MBHjMp1FqrILLRuiJCs3Ofk0MeRDk8g1S1gD47U8X3JnSwcMO1rtRo1x1a7zKaQHaoYu49p/4eSKw==} engines: {node: '>=14'} @@ -9433,6 +9447,30 @@ packages: - supports-color dev: true + /@opentelemetry/sdk-node@0.52.1(@opentelemetry/api@1.9.0): + resolution: {integrity: sha512-uEG+gtEr6eKd8CVWeKMhH2olcCHM9dEK68pe0qE0be32BcCRsvYURhHaD1Srngh1SQcnQzZ4TP324euxqtBOJA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.52.1 + '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-grpc': 0.52.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-http': 0.52.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-proto': 0.52.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-zipkin': 1.25.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.25.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.52.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 1.25.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.25.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-node': 1.25.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.25.1 + transitivePeerDependencies: + - supports-color + dev: false + /@opentelemetry/sdk-node@0.52.1(@opentelemetry/api@1.9.0)(supports-color@10.0.0): resolution: {integrity: sha512-uEG+gtEr6eKd8CVWeKMhH2olcCHM9dEK68pe0qE0be32BcCRsvYURhHaD1Srngh1SQcnQzZ4TP324euxqtBOJA==} engines: {node: '>=14'} @@ -15363,7 +15401,7 @@ packages: - encoding dev: false - /@remix-run/dev@2.1.0(@remix-run/serve@2.1.0)(@types/node@20.14.14)(ts-node@10.9.1)(typescript@5.5.4): + /@remix-run/dev@2.1.0(@remix-run/serve@2.1.0)(@types/node@22.13.9)(ts-node@10.9.1)(typescript@5.5.4): resolution: {integrity: sha512-Hn5lw46F+a48dp5uHKe68ckaHgdStW4+PmLod+LMFEqrMbkF0j4XD1ousebxlv989o0Uy/OLgfRMgMy4cBOvHg==} engines: {node: '>=18.0.0'} hasBin: true @@ -15388,7 +15426,7 @@ packages: '@remix-run/serve': 2.1.0(typescript@5.5.4) '@remix-run/server-runtime': 2.1.0(typescript@5.5.4) '@types/mdx': 2.0.5 - '@vanilla-extract/integration': 6.2.1(@types/node@20.14.14) + '@vanilla-extract/integration': 6.2.1(@types/node@22.13.9) arg: 5.0.2 cacache: 17.1.4 chalk: 4.1.2 @@ -17817,7 +17855,6 @@ packages: resolution: {integrity: sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==} dependencies: undici-types: 6.20.0 - dev: false /@types/nodemailer@6.4.17: resolution: {integrity: sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww==} @@ -18185,7 +18222,7 @@ packages: '@typescript-eslint/scope-manager': 5.59.6 '@typescript-eslint/types': 5.59.6 '@typescript-eslint/typescript-estree': 5.59.6(typescript@5.5.4) - debug: 4.4.0(supports-color@10.0.0) + debug: 4.4.0 eslint: 8.31.0 typescript: 5.5.4 transitivePeerDependencies: @@ -18212,7 +18249,7 @@ packages: dependencies: '@typescript-eslint/typescript-estree': 5.59.6(typescript@5.5.4) '@typescript-eslint/utils': 5.59.6(eslint@8.31.0)(typescript@5.5.4) - debug: 4.4.0(supports-color@10.0.0) + debug: 4.4.0 eslint: 8.31.0 tsutils: 3.21.0(typescript@5.5.4) typescript: 5.5.4 @@ -18236,7 +18273,7 @@ packages: dependencies: '@typescript-eslint/types': 5.59.6 '@typescript-eslint/visitor-keys': 5.59.6 - debug: 4.4.0(supports-color@10.0.0) + debug: 4.4.0 globby: 11.1.0 is-glob: 4.0.3 semver: 7.6.3 @@ -18411,7 +18448,7 @@ packages: outdent: 0.8.0 dev: true - /@vanilla-extract/integration@6.2.1(@types/node@20.14.14): + /@vanilla-extract/integration@6.2.1(@types/node@22.13.9): resolution: {integrity: sha512-+xYJz07G7TFAMZGrOqArOsURG+xcYvqctujEkANjw2McCBvGEK505RxQqOuNiA9Mi9hgGdNp2JedSa94f3eoLg==} dependencies: '@babel/core': 7.22.17 @@ -18425,8 +18462,8 @@ packages: lodash: 4.17.21 mlly: 1.7.1 outdent: 0.8.0 - vite: 4.4.9(@types/node@20.14.14) - vite-node: 0.28.5(@types/node@20.14.14) + vite: 4.4.9(@types/node@22.13.9) + vite-node: 0.28.5(@types/node@22.13.9) transitivePeerDependencies: - '@types/node' - less @@ -21311,6 +21348,17 @@ packages: supports-color: 10.0.0 dev: false + /debug@4.3.7: + resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.3 + /debug@4.3.7(supports-color@10.0.0): resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} engines: {node: '>=6.0'} @@ -21322,6 +21370,18 @@ packages: dependencies: ms: 2.1.3 supports-color: 10.0.0 + dev: false + + /debug@4.4.0: + resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.3 /debug@4.4.0(supports-color@10.0.0): resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} @@ -22636,7 +22696,7 @@ packages: eslint: '*' eslint-plugin-import: '*' dependencies: - debug: 4.4.0(supports-color@10.0.0) + debug: 4.4.0 enhanced-resolve: 5.15.0 eslint: 8.31.0 eslint-module-utils: 2.7.4(@typescript-eslint/parser@5.59.6)(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.5)(eslint@8.31.0) @@ -24223,7 +24283,7 @@ packages: '@types/node': 20.14.14 '@types/semver': 7.5.1 chalk: 4.1.2 - debug: 4.4.0(supports-color@10.0.0) + debug: 4.4.0 interpret: 3.1.1 semver: 7.6.3 tslib: 2.6.2 @@ -24808,7 +24868,7 @@ packages: dependencies: '@ioredis/commands': 1.2.0 cluster-key-slot: 1.1.2 - debug: 4.3.7(supports-color@10.0.0) + debug: 4.3.7 denque: 2.1.0 lodash.defaults: 4.2.0 lodash.isarguments: 3.1.0 @@ -25277,7 +25337,7 @@ packages: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} dependencies: - '@types/node': 20.14.14 + '@types/node': 22.13.9 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -25508,11 +25568,6 @@ packages: engines: {node: '>=12.20'} dev: true - /jwt-decode@4.0.0: - resolution: {integrity: sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==} - engines: {node: '>=18'} - dev: false - /keyv@3.1.0: resolution: {integrity: sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==} dependencies: @@ -26928,7 +26983,7 @@ packages: resolution: {integrity: sha512-6Mj0yHLdUZjHnOPgr5xfWIMqMWS12zDN6iws9SLuSz76W8jTtAv24MN4/CL7gJrl5vtxGInkkqDv/JIoRsQOvA==} dependencies: '@types/debug': 4.1.12 - debug: 4.4.0(supports-color@10.0.0) + debug: 4.4.0 decode-named-character-reference: 1.0.2 micromark-core-commonmark: 1.0.6 micromark-factory-space: 1.0.0 @@ -29039,7 +29094,7 @@ packages: dependencies: lilconfig: 2.1.0 postcss: 8.4.29 - ts-node: 10.9.1(@swc/core@1.3.26)(@types/node@20.14.14)(typescript@5.5.4) + ts-node: 10.9.1(@swc/core@1.3.26)(@types/node@22.13.9)(typescript@5.5.4) yaml: 2.3.1 dev: true @@ -29057,7 +29112,7 @@ packages: dependencies: lilconfig: 3.1.3 postcss: 8.5.3 - ts-node: 10.9.1(@swc/core@1.3.26)(@types/node@20.14.14)(typescript@5.5.4) + ts-node: 10.9.1(@swc/core@1.3.26)(@types/node@22.13.9)(typescript@5.5.4) yaml: 2.7.1 /postcss-load-config@6.0.1(postcss@8.5.3)(tsx@4.17.0): @@ -30791,7 +30846,7 @@ packages: remix-auth: ^3.6.0 dependencies: '@remix-run/server-runtime': 2.1.0(typescript@5.5.4) - debug: 4.4.0(supports-color@10.0.0) + debug: 4.4.0 remix-auth: 3.6.0(@remix-run/react@2.1.0)(@remix-run/server-runtime@2.1.0) transitivePeerDependencies: - supports-color @@ -30914,6 +30969,17 @@ packages: engines: {node: '>=0.10.0'} dev: true + /require-in-the-middle@7.1.1: + resolution: {integrity: sha512-OScOjQjrrjhAdFpQmnkE/qbIBGCRFhQB/YaJhcC3CPOlmhe7llnW46Ac1J5+EjcNXOTnDdpF96Erw/yedsGksQ==} + engines: {node: '>=8.6.0'} + dependencies: + debug: 4.4.0 + module-details-from-path: 1.0.3 + resolve: 1.22.8 + transitivePeerDependencies: + - supports-color + dev: false + /require-in-the-middle@7.1.1(supports-color@10.0.0): resolution: {integrity: sha512-OScOjQjrrjhAdFpQmnkE/qbIBGCRFhQB/YaJhcC3CPOlmhe7llnW46Ac1J5+EjcNXOTnDdpF96Erw/yedsGksQ==} engines: {node: '>=8.6.0'} @@ -31695,7 +31761,7 @@ packages: /socket.io-adapter@2.5.4: resolution: {integrity: sha512-wDNHGXGewWAjQPt3pyeYBtpWSq9cLE5UW1ZUPL/2eGK9jtse/FpXib7epSTsz0Q0m+6sg6Y4KtcFTlah1bdOVg==} dependencies: - debug: 4.3.7(supports-color@10.0.0) + debug: 4.3.7 ws: 8.11.0 transitivePeerDependencies: - bufferutil @@ -31731,6 +31797,16 @@ packages: - utf-8-validate dev: false + /socket.io-parser@4.2.4: + resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==} + engines: {node: '>=10.0.0'} + dependencies: + '@socket.io/component-emitter': 3.1.0 + debug: 4.3.7 + transitivePeerDependencies: + - supports-color + dev: false + /socket.io-parser@4.2.4(supports-color@10.0.0): resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==} engines: {node: '>=10.0.0'} @@ -31765,10 +31841,10 @@ packages: accepts: 1.3.8 base64id: 2.0.0 cors: 2.8.5 - debug: 4.3.7(supports-color@10.0.0) + debug: 4.3.7 engine.io: 6.5.4 socket.io-adapter: 2.5.4 - socket.io-parser: 4.2.4(supports-color@10.0.0) + socket.io-parser: 4.2.4 transitivePeerDependencies: - bufferutil - supports-color @@ -32394,7 +32470,7 @@ packages: dependencies: component-emitter: 1.3.1 cookiejar: 2.1.4 - debug: 4.4.0(supports-color@10.0.0) + debug: 4.4.0 fast-safe-stringify: 2.1.1 form-data: 4.0.0 formidable: 3.5.1 @@ -33208,7 +33284,7 @@ packages: /ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} - /ts-node@10.9.1(@swc/core@1.3.26)(@types/node@20.14.14)(typescript@5.5.4): + /ts-node@10.9.1(@swc/core@1.3.26)(@types/node@22.13.9)(typescript@5.5.4): resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} hasBin: true peerDependencies: @@ -33228,7 +33304,7 @@ packages: '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.3 - '@types/node': 20.14.14 + '@types/node': 22.13.9 acorn: 8.10.0 acorn-walk: 8.2.0 arg: 4.1.3 @@ -33847,7 +33923,6 @@ packages: /undici-types@6.20.0: resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} - dev: false /undici@5.28.4: resolution: {integrity: sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==} @@ -34411,19 +34486,19 @@ packages: d3-timer: 3.0.1 dev: false - /vite-node@0.28.5(@types/node@20.14.14): + /vite-node@0.28.5(@types/node@22.13.9): resolution: {integrity: sha512-LmXb9saMGlrMZbXTvOveJKwMTBTNUH66c8rJnQ0ZPNX+myPEol64+szRzXtV5ORb0Hb/91yq+/D3oERoyAt6LA==} engines: {node: '>=v14.16.0'} hasBin: true dependencies: cac: 6.7.14 - debug: 4.4.0(supports-color@10.0.0) + debug: 4.4.0 mlly: 1.7.1 pathe: 1.1.2 picocolors: 1.1.1 source-map: 0.6.1 source-map-support: 0.5.21 - vite: 4.4.9(@types/node@20.14.14) + vite: 4.4.9(@types/node@22.13.9) transitivePeerDependencies: - '@types/node' - less @@ -34459,7 +34534,7 @@ packages: /vite-tsconfig-paths@4.0.5(typescript@5.5.4): resolution: {integrity: sha512-/L/eHwySFYjwxoYt1WRJniuK/jPv+WGwgRGBYx3leciR5wBeqntQpUE6Js6+TJemChc+ter7fDBKieyEWDx4yQ==} dependencies: - debug: 4.3.7(supports-color@10.0.0) + debug: 4.3.7 globrex: 0.1.2 tsconfck: 2.1.2(typescript@5.5.4) transitivePeerDependencies: @@ -34501,7 +34576,7 @@ packages: fsevents: 2.3.3 dev: true - /vite@4.4.9(@types/node@20.14.14): + /vite@4.4.9(@types/node@22.13.9): resolution: {integrity: sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==} engines: {node: ^14.18.0 || >=16.0.0} hasBin: true @@ -34529,7 +34604,7 @@ packages: terser: optional: true dependencies: - '@types/node': 20.14.14 + '@types/node': 22.13.9 esbuild: 0.18.11 postcss: 8.5.3 rollup: 3.29.1 From 0e6bd602ba3ff961cf55420b2e8e22c6e4c8c3ea Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Thu, 29 May 2025 12:18:16 +0100 Subject: [PATCH 02/28] remove --self-hosted flag --- .../app/v3/remoteImageBuilder.server.ts | 6 ++++- .../services/finalizeDeploymentV2.server.ts | 6 ++--- .../services/initializeDeployment.server.ts | 14 +++++++---- packages/cli-v3/src/commands/deploy.ts | 25 ++++--------------- packages/core/src/v3/schemas/api.ts | 2 ++ 5 files changed, 24 insertions(+), 29 deletions(-) diff --git a/apps/webapp/app/v3/remoteImageBuilder.server.ts b/apps/webapp/app/v3/remoteImageBuilder.server.ts index fb9402ddae..e2308d8e1f 100644 --- a/apps/webapp/app/v3/remoteImageBuilder.server.ts +++ b/apps/webapp/app/v3/remoteImageBuilder.server.ts @@ -4,7 +4,7 @@ import { prisma } from "~/db.server"; import { env } from "~/env.server"; export async function createRemoteImageBuild(project: Project) { - if (!env.DEPOT_TOKEN || !env.DEPOT_PROJECT_ID) { + if (!remoteBuildsEnabled()) { return; } @@ -57,3 +57,7 @@ async function createBuilderProjectIfNotExists(project: Project) { return result.project.projectId; } + +export function remoteBuildsEnabled() { + return env.DEPOT_TOKEN && env.DEPOT_PROJECT_ID && env.DEPOT_ORG_ID && env.DEPOT_REGION; +} diff --git a/apps/webapp/app/v3/services/finalizeDeploymentV2.server.ts b/apps/webapp/app/v3/services/finalizeDeploymentV2.server.ts index 2028ba423a..9b511d06af 100644 --- a/apps/webapp/app/v3/services/finalizeDeploymentV2.server.ts +++ b/apps/webapp/app/v3/services/finalizeDeploymentV2.server.ts @@ -8,6 +8,7 @@ import { mkdtemp, writeFile } from "node:fs/promises"; import { env } from "~/env.server"; import { depot as execDepot } from "@depot/cli"; import { FinalizeDeploymentService } from "./finalizeDeployment.server"; +import { remoteBuildsEnabled } from "../remoteImageBuilder.server"; export class FinalizeDeploymentV2Service extends BaseService { public async call( @@ -16,10 +17,9 @@ export class FinalizeDeploymentV2Service extends BaseService { body: FinalizeDeploymentRequestBody, writer?: WritableStreamDefaultWriter ) { - // if it's self hosted, lets just use the v1 finalize deployment service - if (body.selfHosted) { + // If remote builds are not enabled, lets just use the v1 finalize deployment service + if (!remoteBuildsEnabled()) { const finalizeService = new FinalizeDeploymentService(); - return finalizeService.call(authenticatedEnv, id, body); } diff --git a/apps/webapp/app/v3/services/initializeDeployment.server.ts b/apps/webapp/app/v3/services/initializeDeployment.server.ts index dfcfb3c4f8..9d5082441a 100644 --- a/apps/webapp/app/v3/services/initializeDeployment.server.ts +++ b/apps/webapp/app/v3/services/initializeDeployment.server.ts @@ -5,7 +5,7 @@ import { env } from "~/env.server"; import { type AuthenticatedEnvironment } from "~/services/apiAuth.server"; import { logger } from "~/services/logger.server"; import { generateFriendlyId } from "../friendlyIdentifiers"; -import { createRemoteImageBuild } from "../remoteImageBuilder.server"; +import { createRemoteImageBuild, remoteBuildsEnabled } from "../remoteImageBuilder.server"; import { calculateNextBuildVersion } from "../utils/calculateNextBuildVersion"; import { BaseService, ServiceValidationError } from "./baseService.server"; import { TimeoutDeploymentService } from "./timeoutDeployment.server"; @@ -46,13 +46,17 @@ export class InitializeDeploymentService extends BaseService { const nextVersion = calculateNextBuildVersion(latestDeployment?.version); + if (payload.selfHosted && remoteBuildsEnabled()) { + throw new ServiceValidationError( + "Self-hosted deployments are not supported on this instance" + ); + } + // Try and create a depot build and get back the external build data - const externalBuildData = payload.selfHosted - ? undefined - : await createRemoteImageBuild(environment.project); + const externalBuildData = await createRemoteImageBuild(environment.project); const triggeredBy = payload.userId - ? await this._prisma.user.findUnique({ + ? await this._prisma.user.findFirst({ where: { id: payload.userId, orgMemberships: { diff --git a/packages/cli-v3/src/commands/deploy.ts b/packages/cli-v3/src/commands/deploy.ts index 63f9d87a49..a858800e74 100644 --- a/packages/cli-v3/src/commands/deploy.ts +++ b/packages/cli-v3/src/commands/deploy.ts @@ -48,7 +48,6 @@ const DeployCommandOptions = CommonCommandOptions.extend({ loadImage: z.boolean().default(false), buildPlatform: z.enum(["linux/amd64", "linux/arm64"]).default("linux/amd64"), namespace: z.string().optional(), - selfHosted: z.boolean().default(false), registry: z.string().optional(), push: z.boolean().default(false), config: z.string().optional(), @@ -103,12 +102,6 @@ export function configureDeployCommand(program: Command) { "Skip promoting the deployment to the current deployment for the environment." ) ) - .addOption( - new CommandOption( - "--self-hosted", - "Build and load the image using your local Docker. Use the --registry option to specify the registry to push the image to when using --self-hosted, or just use --push to push to the default registry." - ).hideHelp() - ) .addOption( new CommandOption( "--no-cache", @@ -319,7 +312,6 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) { const deploymentResponse = await projectClient.client.initializeDeployment({ contentHash: buildManifest.contentHash, userId: authorization.userId, - selfHosted: options.selfHosted, registryHost: options.registry, namespace: options.namespace, gitMeta, @@ -331,16 +323,10 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) { } const deployment = deploymentResponse.data; + const isLocalBuild = !deployment.externalBuildData; - // If the deployment doesn't have any externalBuildData, then we can't use the remote image builder - // TODO: handle this and allow the user to the build and push the image themselves - if (!deployment.externalBuildData && !options.selfHosted) { - throw new Error( - `Failed to start deployment, as your instance of trigger.dev does not support hosting. To deploy this project, you must use the --self-hosted flag to build and push the image yourself.` - ); - } - - if (options.selfHosted) { + // Fail fast if we know local builds will fail + if (isLocalBuild) { const result = await x("docker", ["buildx", "version"]); if (result.exitCode !== 0) { @@ -416,7 +402,7 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) { const registryHost = selfHostedRegistryHost ?? "registry.trigger.dev"; const buildResult = await buildImage({ - selfHosted: options.selfHosted, + selfHosted: isLocalBuild, buildPlatform: options.buildPlatform, noCache: options.noCache, push: options.push, @@ -512,7 +498,7 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) { throw new SkipLoggingError("Failed to get deployment with worker"); } - const imageReference = options.selfHosted + const imageReference = isLocalBuild ? `${selfHostedRegistryHost ? `${selfHostedRegistryHost}/` : ""}${buildResult.image}${ buildResult.digest ? `@${buildResult.digest}` : "" }` @@ -532,7 +518,6 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) { deployment.id, { imageReference, - selfHosted: options.selfHosted, skipPromotion: options.skipPromotion, }, (logMessage) => { diff --git a/packages/core/src/v3/schemas/api.ts b/packages/core/src/v3/schemas/api.ts index 2fea3f9b08..4e8218ba83 100644 --- a/packages/core/src/v3/schemas/api.ts +++ b/packages/core/src/v3/schemas/api.ts @@ -293,6 +293,7 @@ export type StartDeploymentIndexingResponseBody = z.infer< export const FinalizeDeploymentRequestBody = z.object({ imageReference: z.string(), + /** @deprecated This is now determined by the webapp */ selfHosted: z.boolean().optional(), skipRegistryProxy: z.boolean().optional(), skipPromotion: z.boolean().optional(), @@ -338,6 +339,7 @@ export const InitializeDeploymentRequestBody = z.object({ contentHash: z.string(), userId: z.string().optional(), registryHost: z.string().optional(), + /** @deprecated This is now determined by the webapp */ selfHosted: z.boolean().optional(), namespace: z.string().optional(), gitMeta: GitMeta.optional(), From 9090350ad94e388c89b0365f80ff1ba296c7d0a8 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Thu, 29 May 2025 12:56:28 +0100 Subject: [PATCH 03/28] automatically set network build flag --- packages/cli-v3/src/deploy/buildImage.ts | 25 +++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/packages/cli-v3/src/deploy/buildImage.ts b/packages/cli-v3/src/deploy/buildImage.ts index 1d4da183fe..879b66658c 100644 --- a/packages/cli-v3/src/deploy/buildImage.ts +++ b/packages/cli-v3/src/deploy/buildImage.ts @@ -316,6 +316,9 @@ async function selfHostedBuildImage( .filter(([key, value]) => value) .flatMap(([key, value]) => ["--build-arg", `${key}=${value}`]); + const apiUrl = normalizeApiUrlForBuild(options.apiUrl); + const network = getNetworkArgs(apiUrl, options.network); + const args = [ "build", "-f", @@ -323,7 +326,7 @@ async function selfHostedBuildImage( options.noCache ? "--no-cache" : undefined, "--platform", options.buildPlatform, - ...(options.network ? ["--network", options.network] : []), + ...network, "--build-arg", `TRIGGER_PROJECT_ID=${options.projectId}`, "--build-arg", @@ -335,7 +338,7 @@ async function selfHostedBuildImage( "--build-arg", `TRIGGER_PROJECT_REF=${options.projectRef}`, "--build-arg", - `TRIGGER_API_URL=${normalizeApiUrlForBuild(options.apiUrl)}`, + `TRIGGER_API_URL=${apiUrl}`, "--build-arg", `TRIGGER_PREVIEW_BRANCH=${options.branchName ?? ""}`, "--build-arg", @@ -740,10 +743,26 @@ CMD [] // If apiUrl is something like http://localhost:3030, AND we are on macOS, 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: string) { +function normalizeApiUrlForBuild(apiUrl: string): string { if (process.platform === "darwin") { return apiUrl.replace("localhost", "host.docker.internal"); } return apiUrl; } + +function getNetworkArgs(apiUrl: string, network?: string): string[] { + if (network) { + return [`--network`, network]; + } + + if (process.platform === "darwin") { + return []; + } + + if (apiUrl.includes("localhost") || apiUrl.includes("127.0.0.1")) { + return ["--network", "host"]; + } + + return []; +} From 9dda081b2d5bdea56367142d85efc10781773241 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Thu, 29 May 2025 12:57:15 +0100 Subject: [PATCH 04/28] update syncEnvVars debug log --- references/hello-world/trigger.config.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/references/hello-world/trigger.config.ts b/references/hello-world/trigger.config.ts index fa6cfa566b..216616ebd6 100644 --- a/references/hello-world/trigger.config.ts +++ b/references/hello-world/trigger.config.ts @@ -20,8 +20,7 @@ export default defineConfig({ build: { extensions: [ syncEnvVars(async (ctx) => { - console.log(ctx.environment); - console.log(ctx.branch); + console.log("syncEnvVars", { environment: ctx.environment, branch: ctx.branch }); return [ { name: "SYNC_ENV", value: ctx.environment }, { name: "BRANCH", value: ctx.branch ?? "–" }, From 3901a0474b13b1f79d8009a87899d7f9ee597460 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Thu, 29 May 2025 15:45:38 +0100 Subject: [PATCH 05/28] improve switch command --- packages/cli-v3/src/commands/switch.ts | 44 ++++++++++++++++++++------ 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/packages/cli-v3/src/commands/switch.ts b/packages/cli-v3/src/commands/switch.ts index f62d94099c..dc134a3656 100644 --- a/packages/cli-v3/src/commands/switch.ts +++ b/packages/cli-v3/src/commands/switch.ts @@ -1,4 +1,4 @@ -import { intro, isCancel, outro, select } from "@clack/prompts"; +import { intro, isCancel, log, outro, select } from "@clack/prompts"; import { Command } from "commander"; import { z } from "zod"; import { @@ -10,8 +10,8 @@ import { import { chalkGrey } from "../utilities/cliOutput.js"; import { readAuthConfigFile, writeAuthConfigCurrentProfileName } from "../utilities/configFiles.js"; import { printInitialBanner } from "../utilities/initialBanner.js"; -import { logger } from "../utilities/logger.js"; import { CLOUD_API_URL } from "../consts.js"; +import { hasTTY, isCI } from "std-env"; const SwitchProfilesOptions = CommonCommandOptions.pick({ logLevel: true, @@ -24,34 +24,35 @@ export function configureSwitchProfilesCommand(program: Command) { return program .command("switch") .description("Set your default CLI profile") + .argument("[profile]", "The profile to switch to. Use interactive mode if not provided.") .option( "-l, --log-level ", "The CLI log level to use (debug, info, log, warn, error, none). This does not effect the log level of your trigger.dev tasks.", "log" ) .option("--skip-telemetry", "Opt-out of sending telemetry") - .action(async (options) => { + .action(async (profile, options) => { await handleTelemetry(async () => { - await switchProfilesCommand(options); + await switchProfilesCommand(profile, options); }); }); } -export async function switchProfilesCommand(options: unknown) { +export async function switchProfilesCommand(profile: string | undefined, options: unknown) { return await wrapCommandAction("switch", SwitchProfilesOptions, options, async (opts) => { await printInitialBanner(false); - return await switchProfiles(opts); + return await switchProfiles(profile, opts); }); } -export async function switchProfiles(options: SwitchProfilesOptions) { +export async function switchProfiles(profile: string | undefined, options: SwitchProfilesOptions) { intro("Switch profiles"); const authConfig = readAuthConfigFile(); if (!authConfig) { - logger.info("No profiles found"); - return; + outro("Failed to switch profiles"); + throw new Error("No profiles found. Run `login` to create a profile."); } const profileNames = Object.keys(authConfig.profiles).sort((a, b) => { @@ -62,6 +63,26 @@ export async function switchProfiles(options: SwitchProfilesOptions) { return a.localeCompare(b); }); + if (profile) { + if (!authConfig.profiles[profile]) { + if (isCI || !hasTTY) { + outro("Failed to switch profiles"); + throw new Error(`Profile "${profile}" not found. Run \`login\` to create a profile.`); + } else { + log.message(`Profile "${profile}" not found, showing available profiles`); + } + } else { + if (authConfig.currentProfile === profile) { + outro(`Already using ${profile}`); + return; + } + + writeAuthConfigCurrentProfileName(profile); + outro(`Switched to ${profile}`); + return; + } + } + const profileSelection = await select({ message: "Please select a new profile", initialValue: authConfig.currentProfile, @@ -79,6 +100,11 @@ export async function switchProfiles(options: SwitchProfilesOptions) { throw new OutroCommandError(); } + if (authConfig.currentProfile === profileSelection) { + outro(`Already using ${profileSelection}`); + return; + } + writeAuthConfigCurrentProfileName(profileSelection); if (profileSelection === authConfig.currentProfile) { From f4261a254c660b53bc47710833a0b1c9e84f1bc2 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Fri, 30 May 2025 10:24:12 +0100 Subject: [PATCH 06/28] always display deploy errors if they exist --- .../route.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam/route.tsx index a245e8d9d8..ee6c3027a5 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam/route.tsx @@ -230,7 +230,9 @@ export default function Page() { - {deployment.tasks ? ( + {deployment.errorData && } + + {deployment.tasks && (
@@ -260,9 +262,7 @@ export default function Page() {
- ) : deployment.errorData ? ( - - ) : null} + )} From 6712571115d54ee215d768be33868752488181d9 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Fri, 30 May 2025 11:13:07 +0100 Subject: [PATCH 07/28] fix stuck deploy command after finalize error --- packages/cli-v3/src/apiClient.ts | 16 ++++++++++------ packages/core/src/v3/apiClient/core.ts | 4 ++-- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/cli-v3/src/apiClient.ts b/packages/cli-v3/src/apiClient.ts index 1e4398b539..9470e56beb 100644 --- a/packages/cli-v3/src/apiClient.ts +++ b/packages/cli-v3/src/apiClient.ts @@ -289,11 +289,9 @@ export class CliApiClient { } let resolvePromise: (value: ApiResult) => void; - let rejectPromise: (reason: any) => void; - const promise = new Promise>((resolve, reject) => { + const promise = new Promise>((resolve) => { resolvePromise = resolve; - rejectPromise = reject; }); const source = zodfetchSSE({ @@ -311,9 +309,15 @@ export class CliApiClient { }); source.onConnectionError((error) => { - rejectPromise({ + let message = error.message ?? "Unknown error"; + + if (error.status !== undefined) { + message = `HTTP ${error.status} ${message}`; + } + + resolvePromise({ success: false, - error, + error: message, }); }); @@ -325,7 +329,7 @@ export class CliApiClient { }); source.onMessage("error", ({ error }) => { - rejectPromise({ + resolvePromise({ success: false, error, }); diff --git a/packages/core/src/v3/apiClient/core.ts b/packages/core/src/v3/apiClient/core.ts index 60979ba498..5f1016cb6a 100644 --- a/packages/core/src/v3/apiClient/core.ts +++ b/packages/core/src/v3/apiClient/core.ts @@ -17,7 +17,7 @@ import { OffsetLimitPageParams, OffsetLimitPageResponse, } from "./pagination.js"; -import { EventSource } from "eventsource"; +import { EventSource, type ErrorEvent } from "eventsource"; export const defaultRetryOptions = { maxAttempts: 3, @@ -665,7 +665,7 @@ export class ZodFetchSSEResult void) { + public onConnectionError(handler: (error: ErrorEvent) => void) { this._eventSource.onerror = handler; } From f18eacba3240453d177084bc256a22d314d751da Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Fri, 30 May 2025 13:59:26 +0100 Subject: [PATCH 08/28] webapp-driven deploys, multi-platform support, lots of fixes --- .env.example | 2 +- apps/webapp/app/entry.server.tsx | 7 + apps/webapp/app/env.server.ts | 4 +- .../v3/DeploymentPresenter.server.ts | 2 + .../route.tsx | 4 + ...yments.$deploymentId.background-workers.ts | 6 +- apps/webapp/app/routes/api.v1.deployments.ts | 7 +- .../runsReplicationInstance.server.ts | 4 +- apps/webapp/app/v3/handleSocketIo.server.ts | 4 +- .../app/v3/remoteImageBuilder.server.ts | 2 +- ...ateDeploymentBackgroundWorkerV3.server.ts} | 8 +- ...ateDeploymentBackgroundWorkerV4.server.ts} | 36 ++- .../app/v3/services/failDeployment.server.ts | 2 +- .../v3/services/finalizeDeployment.server.ts | 19 +- .../services/finalizeDeploymentV2.server.ts | 22 +- .../services/initializeDeployment.server.ts | 32 +- .../v3/services/timeoutDeployment.server.ts | 6 +- .../database/prisma/schema.prisma | 1 + packages/cli-v3/src/commands/deploy.ts | 205 ++++++------ packages/cli-v3/src/commands/workers/build.ts | 10 +- packages/cli-v3/src/deploy/buildImage.ts | 299 +++++++++++------- .../entryPoints/managed-index-controller.ts | 32 +- packages/core/src/v3/schemas/api.ts | 11 +- 23 files changed, 419 insertions(+), 306 deletions(-) rename apps/webapp/app/v3/services/{createDeployedBackgroundWorker.server.ts => createDeploymentBackgroundWorkerV3.server.ts} (95%) rename apps/webapp/app/v3/services/{createDeploymentBackgroundWorker.server.ts => createDeploymentBackgroundWorkerV4.server.ts} (79%) diff --git a/.env.example b/.env.example index 4da4d83266..626b9389dd 100644 --- a/.env.example +++ b/.env.example @@ -80,8 +80,8 @@ CLOUD_SLACK_CLIENT_SECRET= PROVIDER_SECRET=provider-secret # generate the actual secret with `openssl rand -hex 32` COORDINATOR_SECRET=coordinator-secret # generate the actual secret with `openssl rand -hex 32` +# DEPOT_ORG_ID= # DEPOT_TOKEN= -# DEPOT_PROJECT_ID= # DEPLOY_REGISTRY_HOST=${APP_ORIGIN} # This is the host that the deploy CLI will use to push images to the registry # DEV_OTEL_EXPORTER_OTLP_ENDPOINT="http://0.0.0.0:4318" # These are needed for the object store (for handling large payloads/outputs) diff --git a/apps/webapp/app/entry.server.tsx b/apps/webapp/app/entry.server.tsx index 85264294d4..407d09c15c 100644 --- a/apps/webapp/app/entry.server.tsx +++ b/apps/webapp/app/entry.server.tsx @@ -221,7 +221,14 @@ import { env } from "./env.server"; import { logger } from "./services/logger.server"; import { Prisma } from "./db.server"; import { registerRunEngineEventBusHandlers } from "./v3/runEngineHandlers.server"; +import { remoteBuildsEnabled } from "./v3/remoteImageBuilder.server"; if (env.EVENT_LOOP_MONITOR_ENABLED === "1") { eventLoopMonitor.enable(); } + +if (remoteBuildsEnabled()) { + console.log("🏗️ Remote builds enabled"); +} else { + console.log("🏗️ Local builds enabled"); +} diff --git a/apps/webapp/app/env.server.ts b/apps/webapp/app/env.server.ts index d90b0d8c38..d0ebf16b82 100644 --- a/apps/webapp/app/env.server.ts +++ b/apps/webapp/app/env.server.ts @@ -264,13 +264,13 @@ const EnvironmentSchema = z.object({ PROVIDER_SECRET: z.string().default("provider-secret"), COORDINATOR_SECRET: z.string().default("coordinator-secret"), DEPOT_TOKEN: z.string().optional(), - DEPOT_PROJECT_ID: z.string().optional(), DEPOT_ORG_ID: z.string().optional(), DEPOT_REGION: z.string().default("us-east-1"), - DEPLOY_REGISTRY_HOST: z.string().optional(), + DEPLOY_REGISTRY_HOST: z.string(), DEPLOY_REGISTRY_USERNAME: z.string().optional(), DEPLOY_REGISTRY_PASSWORD: z.string().optional(), DEPLOY_REGISTRY_NAMESPACE: z.string().default("trigger"), + DEPLOY_BUILD_PLATFORM: z.string().default("linux/amd64"), DEPLOY_TIMEOUT_MS: z.coerce .number() .int() diff --git a/apps/webapp/app/presenters/v3/DeploymentPresenter.server.ts b/apps/webapp/app/presenters/v3/DeploymentPresenter.server.ts index b8e2c00a68..e62a68f844 100644 --- a/apps/webapp/app/presenters/v3/DeploymentPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/DeploymentPresenter.server.ts @@ -74,6 +74,7 @@ export class DeploymentPresenter { version: true, errorData: true, imageReference: true, + imagePlatform: true, externalBuildData: true, projectId: true, type: true, @@ -157,6 +158,7 @@ export class DeploymentPresenter { sdkVersion: deployment.worker?.sdkVersion, cliVersion: deployment.worker?.cliVersion, imageReference: deployment.imageReference, + imagePlatform: deployment.imagePlatform, externalBuildData: externalBuildData && externalBuildData.success ? externalBuildData.data : undefined, projectId: deployment.projectId, diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam/route.tsx index ee6c3027a5..24e651cf73 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam/route.tsx @@ -91,6 +91,10 @@ export default function Page() { {deployment.imageReference} )} + + Platform + {deployment.imagePlatform} + {deployment.externalBuildData && ( Build Server diff --git a/apps/webapp/app/routes/api.v1.deployments.$deploymentId.background-workers.ts b/apps/webapp/app/routes/api.v1.deployments.$deploymentId.background-workers.ts index 1d8f297582..edaa1b257e 100644 --- a/apps/webapp/app/routes/api.v1.deployments.$deploymentId.background-workers.ts +++ b/apps/webapp/app/routes/api.v1.deployments.$deploymentId.background-workers.ts @@ -5,7 +5,7 @@ import { authenticateApiRequest } from "~/services/apiAuth.server"; import { logger } from "~/services/logger.server"; import { ServiceValidationError } from "~/v3/services/baseService.server"; import { CreateDeclarativeScheduleError } from "~/v3/services/createBackgroundWorker.server"; -import { CreateDeploymentBackgroundWorkerService } from "~/v3/services/createDeploymentBackgroundWorker.server"; +import { CreateDeploymentBackgroundWorkerServiceV4 } from "~/v3/services/createDeploymentBackgroundWorkerV4.server"; const ParamsSchema = z.object({ deploymentId: z.string(), @@ -42,7 +42,7 @@ export async function action({ request, params }: ActionFunctionArgs) { return json({ error: "Invalid body", issues: body.error.issues }, { status: 400 }); } - const service = new CreateDeploymentBackgroundWorkerService(); + const service = new CreateDeploymentBackgroundWorkerServiceV4(); try { const backgroundWorker = await service.call(authenticatedEnv, deploymentId, body.data); @@ -63,7 +63,7 @@ export async function action({ request, params }: ActionFunctionArgs) { logger.error("Failed to create background worker", { error: e }); if (e instanceof ServiceValidationError) { - return json({ error: e.message }, { status: 400 }); + return json({ error: e.message }, { status: e.status ?? 400 }); } else if (e instanceof CreateDeclarativeScheduleError) { return json({ error: e.message }, { status: 400 }); } diff --git a/apps/webapp/app/routes/api.v1.deployments.ts b/apps/webapp/app/routes/api.v1.deployments.ts index c3dcfb13d0..65410761b9 100644 --- a/apps/webapp/app/routes/api.v1.deployments.ts +++ b/apps/webapp/app/routes/api.v1.deployments.ts @@ -3,7 +3,6 @@ import { InitializeDeploymentRequestBody, InitializeDeploymentResponseBody, } from "@trigger.dev/core/v3"; -import { env } from "~/env.server"; import { authenticateApiRequest } from "~/services/apiAuth.server"; import { logger } from "~/services/logger.server"; import { ServiceValidationError } from "~/v3/services/baseService.server"; @@ -35,7 +34,7 @@ export async function action({ request, params }: ActionFunctionArgs) { const service = new InitializeDeploymentService(); try { - const { deployment, imageTag } = await service.call(authenticatedEnv, body.data); + const { deployment, imageRef } = await service.call(authenticatedEnv, body.data); const responseBody: InitializeDeploymentResponseBody = { id: deployment.friendlyId, @@ -44,8 +43,8 @@ export async function action({ request, params }: ActionFunctionArgs) { version: deployment.version, externalBuildData: deployment.externalBuildData as InitializeDeploymentResponseBody["externalBuildData"], - imageTag, - registryHost: body.data.registryHost ?? env.DEPLOY_REGISTRY_HOST, + imageTag: imageRef, + imagePlatform: deployment.imagePlatform, }; return json(responseBody, { status: 200 }); diff --git a/apps/webapp/app/services/runsReplicationInstance.server.ts b/apps/webapp/app/services/runsReplicationInstance.server.ts index 57c5754e2a..c332afa6d8 100644 --- a/apps/webapp/app/services/runsReplicationInstance.server.ts +++ b/apps/webapp/app/services/runsReplicationInstance.server.ts @@ -16,11 +16,11 @@ function initializeRunsReplicationInstance() { invariant(typeof DATABASE_URL === "string", "DATABASE_URL env var not set"); if (!env.RUN_REPLICATION_CLICKHOUSE_URL) { - console.log("🗃️ Runs replication service not enabled"); + console.log("🗃️ Runs replication service not enabled"); return; } - console.log("🗃️ Runs replication service enabled"); + console.log("🗃️ Runs replication service enabled"); const clickhouse = new ClickHouse({ url: env.RUN_REPLICATION_CLICKHOUSE_URL, diff --git a/apps/webapp/app/v3/handleSocketIo.server.ts b/apps/webapp/app/v3/handleSocketIo.server.ts index 5ef5a47ce9..3ed00f3e07 100644 --- a/apps/webapp/app/v3/handleSocketIo.server.ts +++ b/apps/webapp/app/v3/handleSocketIo.server.ts @@ -28,7 +28,7 @@ import { engine } from "./runEngine.server"; import { CompleteAttemptService } from "./services/completeAttempt.server"; import { CrashTaskRunService } from "./services/crashTaskRun.server"; import { CreateCheckpointService } from "./services/createCheckpoint.server"; -import { CreateDeployedBackgroundWorkerService } from "./services/createDeployedBackgroundWorker.server"; +import { CreateDeploymentBackgroundWorkerServiceV3 } from "./services/createDeploymentBackgroundWorkerV3.server"; import { CreateTaskRunAttemptService } from "./services/createTaskRunAttempt.server"; import { DeploymentIndexFailed } from "./services/deploymentIndexFailed.server"; import { ResumeAttemptService } from "./services/resumeAttempt.server"; @@ -245,7 +245,7 @@ function createCoordinatorNamespace(io: Server) { return { success: false }; } - const service = new CreateDeployedBackgroundWorkerService(); + const service = new CreateDeploymentBackgroundWorkerServiceV3(); const worker = await service.call(message.projectRef, environment, message.deploymentId, { localOnly: false, metadata: message.metadata, diff --git a/apps/webapp/app/v3/remoteImageBuilder.server.ts b/apps/webapp/app/v3/remoteImageBuilder.server.ts index e2308d8e1f..e1e6916c41 100644 --- a/apps/webapp/app/v3/remoteImageBuilder.server.ts +++ b/apps/webapp/app/v3/remoteImageBuilder.server.ts @@ -59,5 +59,5 @@ async function createBuilderProjectIfNotExists(project: Project) { } export function remoteBuildsEnabled() { - return env.DEPOT_TOKEN && env.DEPOT_PROJECT_ID && env.DEPOT_ORG_ID && env.DEPOT_REGION; + return env.DEPOT_TOKEN && env.DEPOT_ORG_ID && env.DEPOT_REGION; } diff --git a/apps/webapp/app/v3/services/createDeployedBackgroundWorker.server.ts b/apps/webapp/app/v3/services/createDeploymentBackgroundWorkerV3.server.ts similarity index 95% rename from apps/webapp/app/v3/services/createDeployedBackgroundWorker.server.ts rename to apps/webapp/app/v3/services/createDeploymentBackgroundWorkerV3.server.ts index dec39c45bb..1856bf3ef5 100644 --- a/apps/webapp/app/v3/services/createDeployedBackgroundWorker.server.ts +++ b/apps/webapp/app/v3/services/createDeploymentBackgroundWorkerV3.server.ts @@ -12,7 +12,13 @@ import { projectPubSub } from "./projectPubSub.server"; import { TimeoutDeploymentService } from "./timeoutDeployment.server"; import { CURRENT_DEPLOYMENT_LABEL, BackgroundWorkerId } from "@trigger.dev/core/v3/isomorphic"; -export class CreateDeployedBackgroundWorkerService extends BaseService { +/** + * This service was only used before the new build system was introduced in v3. + * It's now replaced by the CreateDeploymentBackgroundWorkerServiceV4. + * + * @deprecated + */ +export class CreateDeploymentBackgroundWorkerServiceV3 extends BaseService { public async call( projectRef: string, environment: AuthenticatedEnvironment, diff --git a/apps/webapp/app/v3/services/createDeploymentBackgroundWorker.server.ts b/apps/webapp/app/v3/services/createDeploymentBackgroundWorkerV4.server.ts similarity index 79% rename from apps/webapp/app/v3/services/createDeploymentBackgroundWorker.server.ts rename to apps/webapp/app/v3/services/createDeploymentBackgroundWorkerV4.server.ts index e10d448a9b..818b7e7c59 100644 --- a/apps/webapp/app/v3/services/createDeploymentBackgroundWorker.server.ts +++ b/apps/webapp/app/v3/services/createDeploymentBackgroundWorkerV4.server.ts @@ -9,8 +9,9 @@ import { syncDeclarativeSchedules, } from "./createBackgroundWorker.server"; import { TimeoutDeploymentService } from "./timeoutDeployment.server"; +import { env } from "~/env.server"; -export class CreateDeploymentBackgroundWorkerService extends BaseService { +export class CreateDeploymentBackgroundWorkerServiceV4 extends BaseService { public async call( environment: AuthenticatedEnvironment, deploymentId: string, @@ -19,6 +20,15 @@ export class CreateDeploymentBackgroundWorkerService extends BaseService { return this.traceWithEnv("call", environment, async (span) => { span.setAttribute("deploymentId", deploymentId); + const { buildPlatform, targetPlatform } = body; + + if (buildPlatform) { + span.setAttribute("buildPlatform", buildPlatform); + } + if (targetPlatform) { + span.setAttribute("targetPlatform", targetPlatform); + } + const deployment = await this._prisma.workerDeployment.findFirst({ where: { friendlyId: deploymentId, @@ -29,6 +39,22 @@ export class CreateDeploymentBackgroundWorkerService extends BaseService { return; } + // Handle multi-platform builds + const deploymentPlatforms = deployment.imagePlatform?.split(",") ?? []; + if (deploymentPlatforms.length > 1) { + span.setAttribute("deploymentPlatforms", deploymentPlatforms.join(",")); + + // We will only create a background worker for the first platform + const firstPlatform = deploymentPlatforms[0]; + + if (targetPlatform && firstPlatform !== targetPlatform) { + throw new ServiceValidationError( + `Ignoring target platform ${targetPlatform} for multi-platform deployment ${deployment.imagePlatform}`, + 400 + ); + } + } + if (deployment.status !== "BUILDING") { return; } @@ -134,7 +160,13 @@ export class CreateDeploymentBackgroundWorkerService extends BaseService { }, }); - await TimeoutDeploymentService.dequeue(deployment.id, this._prisma); + await TimeoutDeploymentService.enqueue( + deployment.id, + "DEPLOYING", + "Indexing timed out", + new Date(Date.now() + env.DEPLOY_TIMEOUT_MS), + this._prisma + ); return backgroundWorker; }); diff --git a/apps/webapp/app/v3/services/failDeployment.server.ts b/apps/webapp/app/v3/services/failDeployment.server.ts index dbb9194050..0f4fda1cbb 100644 --- a/apps/webapp/app/v3/services/failDeployment.server.ts +++ b/apps/webapp/app/v3/services/failDeployment.server.ts @@ -18,7 +18,7 @@ export class FailDeploymentService extends BaseService { id: string, params: FailDeploymentRequestBody ) { - const deployment = await this._prisma.workerDeployment.findUnique({ + const deployment = await this._prisma.workerDeployment.findFirst({ where: { friendlyId: id, environmentId: authenticatedEnv.id, diff --git a/apps/webapp/app/v3/services/finalizeDeployment.server.ts b/apps/webapp/app/v3/services/finalizeDeployment.server.ts index 9cbc8f0f31..8502929820 100644 --- a/apps/webapp/app/v3/services/finalizeDeployment.server.ts +++ b/apps/webapp/app/v3/services/finalizeDeployment.server.ts @@ -7,6 +7,8 @@ import { PerformDeploymentAlertsService } from "./alerts/performDeploymentAlerts import { BaseService, ServiceValidationError } from "./baseService.server"; import { ChangeCurrentDeploymentService } from "./changeCurrentDeployment.server"; import { projectPubSub } from "./projectPubSub.server"; +import { FailDeploymentService } from "./failDeployment.server"; +import { TimeoutDeploymentService } from "./timeoutDeployment.server"; export class FinalizeDeploymentService extends BaseService { public async call( @@ -36,7 +38,13 @@ export class FinalizeDeploymentService extends BaseService { if (!deployment.worker) { logger.error("Worker deployment does not have a worker", { id }); - // TODO: We need to fail the deployment here because it's not possible to deploy a worker without a worker + const failService = new FailDeploymentService(); + await failService.call(authenticatedEnv, deployment.id, { + error: { + name: "MissingWorker", + message: "Deployment does not have a worker", + }, + }); throw new ServiceValidationError("Worker deployment does not have a worker"); } @@ -52,8 +60,6 @@ export class FinalizeDeploymentService extends BaseService { throw new ServiceValidationError("Worker deployment is not in DEPLOYING status"); } - const imageReference = body.imageReference; - // Link the deployment with the background worker const finalizedDeployment = await this._prisma.workerDeployment.update({ where: { @@ -62,10 +68,15 @@ export class FinalizeDeploymentService extends BaseService { data: { status: "DEPLOYED", deployedAt: new Date(), - imageReference, + // Only add the digest, if any + imageReference: body.imageDigest + ? `${deployment.imageReference}@${body.imageDigest}` + : undefined, }, }); + await TimeoutDeploymentService.dequeue(deployment.id, this._prisma); + if (typeof body.skipPromotion === "undefined" || !body.skipPromotion) { const promotionService = new ChangeCurrentDeploymentService(); diff --git a/apps/webapp/app/v3/services/finalizeDeploymentV2.server.ts b/apps/webapp/app/v3/services/finalizeDeploymentV2.server.ts index 9b511d06af..5c9f969265 100644 --- a/apps/webapp/app/v3/services/finalizeDeploymentV2.server.ts +++ b/apps/webapp/app/v3/services/finalizeDeploymentV2.server.ts @@ -87,7 +87,7 @@ export class FinalizeDeploymentV2Service extends BaseService { throw new ServiceValidationError("Missing depot token"); } - const digest = extractImageDigest(body.imageReference); + const digest = body.imageDigest; logger.debug("Pushing image to registry", { id, @@ -121,40 +121,24 @@ export class FinalizeDeploymentV2Service extends BaseService { throw new ServiceValidationError(pushResult.error); } - const fullImage = digest ? `${pushResult.image}@${digest}` : pushResult.image; - logger.debug("Image pushed to registry", { id, deployment, body, - fullImage, + pushedImage: pushResult.image, }); const finalizeService = new FinalizeDeploymentService(); const finalizedDeployment = await finalizeService.call(authenticatedEnv, id, { - imageReference: fullImage, - skipRegistryProxy: true, skipPromotion: body.skipPromotion, + imageDigest: digest, }); return finalizedDeployment; } } -// Extracts the sha256 digest from an image reference -// For example the image ref "registry.depot.dev/gn57tl6chn:8qfjm8w83w@sha256:aa6fd2bdcbbd611556747e72d0b57797f03aa9b39dc910befc83eea2b08a5b85" -// would return "sha256:aa6fd2bdcbbd611556747e72d0b57797f03aa9b39dc910befc83eea2b08a5b85" -function extractImageDigest(image: string) { - const digestIndex = image.lastIndexOf("@"); - - if (digestIndex === -1) { - return; - } - - return image.substring(digestIndex + 1); -} - type ExecutePushToRegistryOptions = { depot: { buildId: string; diff --git a/apps/webapp/app/v3/services/initializeDeployment.server.ts b/apps/webapp/app/v3/services/initializeDeployment.server.ts index 9d5082441a..d4d2c76ea2 100644 --- a/apps/webapp/app/v3/services/initializeDeployment.server.ts +++ b/apps/webapp/app/v3/services/initializeDeployment.server.ts @@ -68,25 +68,19 @@ export class InitializeDeploymentService extends BaseService { }) : undefined; - const sharedImageTag = `${payload.namespace ?? env.DEPLOY_REGISTRY_NAMESPACE}/${ - environment.project.externalRef - }:${nextVersion}.${environment.slug}`; + const imageRefParts = [ + env.DEPLOY_REGISTRY_NAMESPACE, + `${environment.project.externalRef}:${nextVersion}.${environment.slug}`, + ]; - const unmanagedImageParts = []; + const isLocalBuild = !externalBuildData; - if (payload.registryHost) { - unmanagedImageParts.push(payload.registryHost); + // Local builds require the registry host to be able to push the image + if (isLocalBuild) { + imageRefParts.unshift(env.DEPLOY_REGISTRY_HOST); } - if (payload.namespace) { - unmanagedImageParts.push(payload.namespace); - } - unmanagedImageParts.push( - `${environment.project.externalRef}:${nextVersion}.${environment.slug}` - ); - - const unmanagedImageTag = unmanagedImageParts.join("/"); - const isManaged = payload.type === WorkerDeploymentType.MANAGED; + const imageRef = imageRefParts.join("/"); logger.debug("Creating deployment", { environmentId: environment.id, @@ -94,8 +88,7 @@ export class InitializeDeploymentService extends BaseService { version: nextVersion, triggeredById: triggeredBy?.id, type: payload.type, - imageTag: isManaged ? sharedImageTag : unmanagedImageTag, - imageReference: isManaged ? undefined : unmanagedImageTag, + imageRef, }); const deployment = await this._prisma.workerDeployment.create({ @@ -110,7 +103,8 @@ export class InitializeDeploymentService extends BaseService { externalBuildData, triggeredById: triggeredBy?.id, type: payload.type, - imageReference: isManaged ? undefined : unmanagedImageTag, + imageReference: imageRef, + imagePlatform: env.DEPLOY_BUILD_PLATFORM, git: payload.gitMeta ?? undefined, }, }); @@ -124,7 +118,7 @@ export class InitializeDeploymentService extends BaseService { return { deployment, - imageTag: isManaged ? sharedImageTag : unmanagedImageTag, + imageRef, }; }); } diff --git a/apps/webapp/app/v3/services/timeoutDeployment.server.ts b/apps/webapp/app/v3/services/timeoutDeployment.server.ts index 1bb9493a89..ef59c1ddf3 100644 --- a/apps/webapp/app/v3/services/timeoutDeployment.server.ts +++ b/apps/webapp/app/v3/services/timeoutDeployment.server.ts @@ -46,12 +46,13 @@ export class TimeoutDeploymentService extends BaseService { deploymentId: string, fromStatus: string, errorMessage: string, - runAt: Date + runAt: Date, + tx?: PrismaClientOrTransaction ) { await workerQueue.enqueue( "v3.timeoutDeployment", { - deploymentId: deploymentId, + deploymentId, fromStatus, errorMessage, }, @@ -59,6 +60,7 @@ export class TimeoutDeploymentService extends BaseService { runAt, jobKey: `timeoutDeployment:${deploymentId}`, jobKeyMode: "replace", + tx, } ); } diff --git a/internal-packages/database/prisma/schema.prisma b/internal-packages/database/prisma/schema.prisma index 0435c7e779..016bb7ca64 100644 --- a/internal-packages/database/prisma/schema.prisma +++ b/internal-packages/database/prisma/schema.prisma @@ -2858,6 +2858,7 @@ model WorkerDeployment { version String imageReference String? + imagePlatform String @default("linux/amd64") externalBuildData Json? diff --git a/packages/cli-v3/src/commands/deploy.ts b/packages/cli-v3/src/commands/deploy.ts index a858800e74..acbef18582 100644 --- a/packages/cli-v3/src/commands/deploy.ts +++ b/packages/cli-v3/src/commands/deploy.ts @@ -46,10 +46,6 @@ const DeployCommandOptions = CommonCommandOptions.extend({ env: z.enum(["prod", "staging", "preview"]), branch: z.string().optional(), loadImage: z.boolean().default(false), - buildPlatform: z.enum(["linux/amd64", "linux/arm64"]).default("linux/amd64"), - namespace: z.string().optional(), - registry: z.string().optional(), - push: z.boolean().default(false), config: z.string().optional(), projectRef: z.string().optional(), saveLogs: z.boolean().default(false), @@ -57,7 +53,10 @@ const DeployCommandOptions = CommonCommandOptions.extend({ skipPromotion: z.boolean().default(false), noCache: z.boolean().default(false), envFile: z.string().optional(), + // Local build options network: z.enum(["default", "none", "host"]).optional(), + push: z.boolean().default(false), + builder: z.string().default("trigger"), }); type DeployCommandOptions = z.infer; @@ -65,97 +64,80 @@ type DeployCommandOptions = z.infer; type Deployment = InitializeDeploymentResponseBody; export function configureDeployCommand(program: Command) { - return commonOptions( - program - .command("deploy") - .description("Deploy your Trigger.dev v3 project to the cloud.") - .argument("[path]", "The path to the project", ".") - .option( - "-e, --env ", - "Deploy to a specific environment (currently only prod and staging are supported)", - "prod" - ) - .option( - "-b, --branch ", - "The preview branch to deploy to when passing --env preview. If not provided, we'll detect your git branch." - ) - .option("--skip-update-check", "Skip checking for @trigger.dev package updates") - .option("-c, --config ", "The name of the config file, found at [path]") - .option( - "-p, --project-ref ", - "The project ref. Required if there is no config file. This will override the project specified in the config file." - ) - .option( - "--dry-run", - "Do a dry run of the deployment. This will not actually deploy the project, but will show you what would be deployed." + return ( + commonOptions( + program + .command("deploy") + .description("Deploy your Trigger.dev v3 project to the cloud.") + .argument("[path]", "The path to the project", ".") + .option( + "-e, --env ", + "Deploy to a specific environment (currently only prod and staging are supported)", + "prod" + ) + .option( + "-b, --branch ", + "The preview branch to deploy to when passing --env preview. If not provided, we'll detect your git branch." + ) + .option("--skip-update-check", "Skip checking for @trigger.dev package updates") + .option("-c, --config ", "The name of the config file, found at [path]") + .option( + "-p, --project-ref ", + "The project ref. Required if there is no config file. This will override the project specified in the config file." + ) + .option( + "--dry-run", + "Do a dry run of the deployment. This will not actually deploy the project, but will show you what would be deployed." + ) + .option( + "--skip-sync-env-vars", + "Skip syncing environment variables when using the syncEnvVars extension." + ) + .option( + "--env-file ", + "Path to the .env file to load into the CLI process. Defaults to .env in the project directory." + ) + .option( + "--skip-promotion", + "Skip promoting the deployment to the current deployment for the environment." + ) + ) + .addOption( + new CommandOption( + "--no-cache", + "Do not use the cache when building the image. This will slow down the build process but can be useful if you are experiencing issues with the cache." + ).hideHelp() ) - .option( - "--skip-sync-env-vars", - "Skip syncing environment variables when using the syncEnvVars extension." + .addOption( + new CommandOption("--load-image", "Load the built image into your local docker").hideHelp() ) - .option( - "--env-file ", - "Path to the .env file to load into the CLI process. Defaults to .env in the project directory." + .addOption( + new CommandOption( + "--save-logs", + "If provided, will save logs even for successful builds" + ).hideHelp() ) - .option( - "--skip-promotion", - "Skip promoting the deployment to the current deployment for the environment." + // Local build options + .addOption(new CommandOption("--push", "Push the image after local builds").hideHelp()) + .addOption( + new CommandOption( + "--network ", + "The networking mode for RUN instructions when building locally" + ).hideHelp() ) - ) - .addOption( - new CommandOption( - "--no-cache", - "Do not use the cache when building the image. This will slow down the build process but can be useful if you are experiencing issues with the cache." - ).hideHelp() - ) - .addOption( - new CommandOption( - "--push", - "When using the --self-hosted flag, push the image to the default registry. (defaults to false when not using --registry)" - ).hideHelp() - ) - .addOption( - new CommandOption( - "--registry ", - "The registry to push the image to when using --self-hosted" - ).hideHelp() - ) - .addOption( - new CommandOption( - "--tag ", - "(Coming soon) Specify the tag to use when pushing the image to the registry" - ).hideHelp() - ) - .addOption( - new CommandOption( - "--namespace ", - "Specify the namespace to use when pushing the image to the registry" - ).hideHelp() - ) - .addOption( - new CommandOption("--load-image", "Load the built image into your local docker").hideHelp() - ) - .addOption( - new CommandOption( - "--build-platform ", - "The platform to build the deployment image for" + .addOption( + new CommandOption( + "--builder ", + "The builder to use when building locally" + ).hideHelp() ) - .default("linux/amd64") - .hideHelp() - ) - .addOption( - new CommandOption( - "--save-logs", - "If provided, will save logs even for successful builds" - ).hideHelp() - ) - .option("--network ", "The networking mode for RUN instructions when using --self-hosted") - .action(async (path, options) => { - await handleTelemetry(async () => { - await printStandloneInitialBanner(true); - await deployCommand(path, options); - }); - }); + .action(async (path, options) => { + await handleTelemetry(async () => { + await printStandloneInitialBanner(true); + await deployCommand(path, options); + }); + }) + ); } export async function deployCommand(dir: string, options: unknown) { @@ -312,8 +294,6 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) { const deploymentResponse = await projectClient.client.initializeDeployment({ contentHash: buildManifest.contentHash, userId: authorization.userId, - registryHost: options.registry, - namespace: options.namespace, gitMeta, type: features.run_engine_v2 ? "MANAGED" : "V1", }); @@ -398,19 +378,13 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) { } } - const selfHostedRegistryHost = deployment.registryHost ?? options.registry; - const registryHost = selfHostedRegistryHost ?? "registry.trigger.dev"; - const buildResult = await buildImage({ - selfHosted: isLocalBuild, - buildPlatform: options.buildPlatform, + isLocalBuild, noCache: options.noCache, - push: options.push, - registryHost, - registry: options.registry, deploymentId: deployment.id, deploymentVersion: deployment.version, imageTag: deployment.imageTag, + imagePlatform: deployment.imagePlatform, loadImage: options.loadImage, contentHash: deployment.contentHash, externalBuildId: deployment.externalBuildData?.buildId, @@ -424,7 +398,6 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) { authAccessToken: authorization.auth.accessToken, compilationPath: destination.path, buildEnvVars: buildManifest.build.env, - network: options.network, onLog: (logMessage) => { if (isCI) { console.log(logMessage); @@ -437,6 +410,10 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) { $spinner.message(`Building version ${version}: ${logMessage}`); } }, + // Local build options + network: options.network, + builder: options.builder, + push: options.push, }); logger.debug("Build result", buildResult); @@ -498,12 +475,6 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) { throw new SkipLoggingError("Failed to get deployment with worker"); } - const imageReference = isLocalBuild - ? `${selfHostedRegistryHost ? `${selfHostedRegistryHost}/` : ""}${buildResult.image}${ - buildResult.digest ? `@${buildResult.digest}` : "" - }` - : `${buildResult.image}${buildResult.digest ? `@${buildResult.digest}` : ""}`; - if (isCI) { log.step(`Deploying version ${version}\n`); } else { @@ -517,7 +488,7 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) { const finalizeResponse = await projectClient.client.finalizeDeployment( deployment.id, { - imageReference, + imageDigest: buildResult.digest, skipPromotion: options.skipPromotion, }, (logMessage) => { @@ -568,6 +539,11 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) { console.log(rawTestLink); } + if (options.saveLogs) { + const logPath = await saveLogs(deployment.shortCode, buildResult.logs); + console.log(`Full build logs have been saved to ${logPath}`); + } + setGithubActionsOutputAndEnvVars({ envVars: { TRIGGER_DEPLOYMENT_VERSION: version, @@ -630,6 +606,19 @@ async function failDeploy( error.message }. Full build logs have been saved to ${logPath}` ); + + // Display the last few lines of the logs, remove #-prefixed ones + const lastFewLines = logs + .split("\n") + .filter((line) => !line.startsWith("#")) + .filter((line) => line.trim() !== "") + .slice(-5) + .join("\n"); + + if (lastFewLines.trim() !== "") { + console.log("Last few lines of logs:\n"); + console.log(lastFewLines); + } } else { outro(`${chalkError(`${prefix}:`)} ${error.message}.`); } diff --git a/packages/cli-v3/src/commands/workers/build.ts b/packages/cli-v3/src/commands/workers/build.ts index 9daca0ff3c..2c7db5c926 100644 --- a/packages/cli-v3/src/commands/workers/build.ts +++ b/packages/cli-v3/src/commands/workers/build.ts @@ -37,7 +37,6 @@ import { createGitMeta } from "../../utilities/gitMeta.js"; const WorkersBuildCommandOptions = CommonCommandOptions.extend({ // docker build options load: z.boolean().default(false), - platform: z.enum(["linux/amd64", "linux/arm64"]).default("linux/amd64"), network: z.enum(["default", "none", "host"]).optional(), tag: z.string().optional(), push: z.boolean().default(false), @@ -328,12 +327,10 @@ async function _workerBuildCommand(dir: string, options: WorkersBuildCommandOpti } const buildResult = await buildImage({ - selfHosted: local, - buildPlatform: options.platform, + isLocalBuild: local, + imagePlatform: deployment.imagePlatform, noCache: options.noCache, push: options.push, - registryHost: registry, - registry: registry, deploymentId: deployment.id, deploymentVersion: deployment.version, imageTag: deployment.imageTag, @@ -351,6 +348,7 @@ async function _workerBuildCommand(dir: string, options: WorkersBuildCommandOpti compilationPath: destination.path, buildEnvVars: buildManifest.build.env, network: options.network, + builder: "trigger", }); logger.debug("Build result", buildResult); @@ -446,7 +444,7 @@ async function _workerBuildCommand(dir: string, options: WorkersBuildCommandOpti } outro( - `Version ${version} built and ready to deploy: ${buildResult.image} ${ + `Version ${version} built and ready to deploy: ${deployment.imageTag} ${ isLinksSupported ? `| ${deploymentLink} | ${testLink}` : "" }` ); diff --git a/packages/cli-v3/src/deploy/buildImage.ts b/packages/cli-v3/src/deploy/buildImage.ts index 879b66658c..308b6b1d7f 100644 --- a/packages/cli-v3/src/deploy/buildImage.ts +++ b/packages/cli-v3/src/deploy/buildImage.ts @@ -1,26 +1,22 @@ -import { join } from "node:path"; -import { createTempDir, writeJSONFile } from "../utilities/fileSystem.js"; import { logger } from "../utilities/logger.js"; import { depot } from "@depot/cli"; import { x } from "tinyexec"; import { BuildManifest, BuildRuntime } from "@trigger.dev/core/v3/schemas"; +import { networkInterfaces } from "os"; export interface BuildImageOptions { // Common options - selfHosted: boolean; - buildPlatform: string; + isLocalBuild: boolean; + imagePlatform: string; noCache?: boolean; + loadImage?: boolean; - // Self-hosted specific options + // Local build options push: boolean; - registry?: string; network?: string; - - // Non-self-hosted specific options - loadImage?: boolean; + builder: string; // Flattened properties from nested structures - registryHost?: string; authAccessToken: string; imageTag: string; deploymentId: string; @@ -43,15 +39,13 @@ export interface BuildImageOptions { deploymentSpinner?: any; // Replace 'any' with the actual type if known } -export async function buildImage(options: BuildImageOptions) { +export async function buildImage(options: BuildImageOptions): Promise { const { - selfHosted, - buildPlatform, + isLocalBuild, + imagePlatform, noCache, push, - registry, loadImage, - registryHost, authAccessToken, imageTag, deploymentId, @@ -69,22 +63,22 @@ export async function buildImage(options: BuildImageOptions) { branchName, buildEnvVars, network, + builder, onLog, } = options; - if (selfHosted) { - return selfHostedBuildImage({ - registryHost, + if (isLocalBuild) { + return localBuildImage({ imageTag, + imagePlatform, cwd: compilationPath, projectId, deploymentId, deploymentVersion, contentHash, projectRef, - buildPlatform: buildPlatform, - pushImage: push, - selfHostedRegistry: !!registry, + push, + loadImage, noCache, extraCACerts, apiUrl, @@ -92,6 +86,7 @@ export async function buildImage(options: BuildImageOptions) { branchName, buildEnvVars, network, + builder, onLog, }); } @@ -102,16 +97,8 @@ export async function buildImage(options: BuildImageOptions) { ); } - if (!registryHost) { - throw new Error( - "Failed to initialize deployment. The deployment does not have a registry host. To deploy this project, you must use the --self-hosted or --local flag to build and push the image yourself." - ); - } - - return depotBuildImage({ - registryHost, + return remoteBuildImage({ auth: authAccessToken, - imageTag, buildId: externalBuildId, buildToken: externalBuildToken, buildProjectId: externalBuildProjectId, @@ -122,7 +109,7 @@ export async function buildImage(options: BuildImageOptions) { contentHash, projectRef, loadImage, - buildPlatform, + imagePlatform, noCache, extraCACerts, apiUrl, @@ -134,9 +121,7 @@ export async function buildImage(options: BuildImageOptions) { } export interface DepotBuildImageOptions { - registryHost: string; auth: string; - imageTag: string; buildId: string; buildToken: string; buildProjectId: string; @@ -146,7 +131,7 @@ export interface DepotBuildImageOptions { deploymentVersion: string; contentHash: string; projectRef: string; - buildPlatform: string; + imagePlatform: string; apiUrl: string; apiKey: string; branchName?: string; @@ -159,7 +144,6 @@ export interface DepotBuildImageOptions { type BuildImageSuccess = { ok: true; - image: string; imageSizeBytes: number; logs: string; digest?: string; @@ -173,14 +157,7 @@ type BuildImageFailure = { type BuildImageResults = BuildImageSuccess | BuildImageFailure; -async function depotBuildImage(options: DepotBuildImageOptions): Promise { - // Step 3: Ensure we are "logged in" to our registry by writing to $HOME/.docker/config.json - // TODO: make sure this works on windows - const dockerConfigDir = await ensureLoggedIntoDockerRegistry(options.registryHost, { - username: "trigger", - password: options.auth, - }); - +async function remoteBuildImage(options: DepotBuildImageOptions): Promise { const buildArgs = Object.entries(options.buildEnvVars || {}) .filter(([key, value]) => value) .flatMap(([key, value]) => ["--build-arg", `${key}=${value}`]); @@ -191,7 +168,7 @@ async function depotBuildImage(options: DepotBuildImageOptions): Promise; network?: string; + builder: string; + loadImage?: boolean; onLog?: (log: string) => void; } -async function selfHostedBuildImage( - options: SelfHostedBuildImageOptions -): Promise { - const imageRef = `${options.registryHost ? `${options.registryHost}/` : ""}${options.imageTag}`; +async function localBuildImage(options: SelfHostedBuildImageOptions): Promise { + const { builder, imageTag } = options; + + // Ensure multi-platform build is supported on the local machine + let builderExists = false; + const lsLogs: string[] = []; + + // List existing builders + const lsProcess = x("docker", ["buildx", "ls"]); + for await (const line of lsProcess) { + lsLogs.push(line); + logger.debug(line); + options.onLog?.(line); + + if (line.startsWith(builder + " ")) { + builderExists = true; + } + } + if (lsProcess.exitCode !== 0) { + return { + ok: false as const, + 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: string[] = []; + + 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 as const, + 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: string[] = []; + + for await (const line of deleteProcess) { + deleteLogs.push(line); + } + + if (deleteProcess.exitCode !== 0) { + return { + ok: false as const, + 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: string[] = []; + + const args = ( + [ + "buildx", + "create", + "--name", + builder, + "--driver", + "docker-container", + options.network ? `--driver-opt=network=${options.network}` : undefined, + ] satisfies (string | undefined)[] + ).filter(Boolean) as string[]; + + 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 as const, + 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 network = getNetworkArgs(apiUrl, options.network); + const addHost = getAddHost(apiUrl); + + // Don't push if the image tag is a local address, unless the user explicitly wants to push + const shouldPush = options.push + ? true + : imageTag.startsWith("localhost") || + imageTag.startsWith("127.0.0.1") || + imageTag.startsWith("0.0.0.0") + ? false + : true; const args = [ + "buildx", "build", + "--builder", + builder, "-f", "Containerfile", options.noCache ? "--no-cache" : undefined, "--platform", - options.buildPlatform, - ...network, + options.imagePlatform, + options.network ? `--network=${options.network}` : undefined, + addHost ? `--add-host=${addHost}` : undefined, + shouldPush ? "--push" : undefined, + options.loadImage ? "--load" : undefined, "--build-arg", `TRIGGER_PROJECT_ID=${options.projectId}`, "--build-arg", @@ -348,7 +433,7 @@ async function selfHostedBuildImage( "--progress", "plain", "-t", - imageRef, + options.imageTag, ".", // The build context ].filter(Boolean) as string[]; @@ -357,7 +442,6 @@ async function selfHostedBuildImage( }); const errors: string[] = []; - let digest: string | undefined; // Build the image const buildProcess = x("docker", args, { @@ -379,10 +463,10 @@ async function selfHostedBuildImage( }; } - digest = extractImageDigest(errors); + const digest = extractImageDigest(errors); // Get the image size - const sizeProcess = x("docker", ["image", "inspect", imageRef, "--format={{.Size}}"], { + const sizeProcess = x("docker", ["image", "inspect", options.imageTag, "--format={{.Size}}"], { nodeOptions: { cwd: options.cwd }, }); @@ -401,60 +485,14 @@ async function selfHostedBuildImage( options.onLog?.(`Image size: ${(imageSizeBytes / (1024 * 1024)).toFixed(2)} MB`); } - if (options.selfHostedRegistry || options.pushImage) { - const pushArgs = ["push", imageRef].filter(Boolean) as string[]; - - logger.debug(`docker ${pushArgs.join(" ")}`); - - // Push the image - const pushProcess = x("docker", pushArgs, { - nodeOptions: { cwd: options.cwd }, - }); - - for await (const line of pushProcess) { - logger.debug(line); - errors.push(line); - } - - if (pushProcess.exitCode !== 0) { - return { - ok: false as const, - error: "Error pushing image", - logs: extractLogs(errors), - }; - } - } - return { ok: true as const, - image: options.imageTag, imageSizeBytes, digest, logs: extractLogs(errors), }; } -async function ensureLoggedIntoDockerRegistry( - registryHost: string, - auth: { username: string; password: string } -) { - const tmpDir = await createTempDir(); - // Read the current docker config - const dockerConfigPath = join(tmpDir, "config.json"); - - await writeJSONFile(dockerConfigPath, { - auths: { - [registryHost]: { - auth: Buffer.from(`${auth.username}:${auth.password}`).toString("base64"), - }, - }, - }); - - logger.debug(`Writing docker config to ${dockerConfigPath}`); - - return tmpDir; -} - function extractLogs(outputs: string[]) { // Remove empty lines const cleanedOutputs = outputs.map((line) => line.trim()).filter((line) => line !== ""); @@ -462,7 +500,7 @@ function extractLogs(outputs: string[]) { return cleanedOutputs.map((line) => line.trim()).join("\n"); } -function extractImageDigest(outputs: string[]) { +function extractImageDigest(outputs: string[]): string | undefined { const imageDigestRegex = /pushing manifest for .+(?sha256:[a-f0-9]{64})/; for (const line of outputs) { @@ -598,6 +636,10 @@ ENV TRIGGER_PROJECT_ID=\${TRIGGER_PROJECT_ID} \ 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} @@ -706,6 +748,10 @@ ENV TRIGGER_PROJECT_ID=\${TRIGGER_PROJECT_ID} \ 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} @@ -741,28 +787,35 @@ CMD [] `; } -// If apiUrl is something like http://localhost:3030, AND we are on macOS, we need to convert it to http://host.docker.internal:3030 +// 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: string): string { - if (process.platform === "darwin") { - return apiUrl.replace("localhost", "host.docker.internal"); - } - - return apiUrl; + return apiUrl.replace("localhost", "host.docker.internal"); } -function getNetworkArgs(apiUrl: string, network?: string): string[] { - if (network) { - return [`--network`, network]; - } +function getHostIP() { + const interfaces = networkInterfaces(); + + for (const [name, iface] of Object.entries(interfaces)) { + if (!iface) { + continue; + } - if (process.platform === "darwin") { - return []; + for (const net of iface) { + // Skip internal/loopback and non-IPv4 addresses + if (!net.internal && net.family === "IPv4") { + return net.address; + } + } } - if (apiUrl.includes("localhost") || apiUrl.includes("127.0.0.1")) { - return ["--network", "host"]; + return "127.0.0.1"; +} + +function getAddHost(apiUrl: string) { + if (apiUrl.includes("host.docker.internal")) { + return `host.docker.internal:${getHostIP()}`; } - return []; + return; } diff --git a/packages/cli-v3/src/entryPoints/managed-index-controller.ts b/packages/cli-v3/src/entryPoints/managed-index-controller.ts index 26b3332bb1..e3cc53839e 100644 --- a/packages/cli-v3/src/entryPoints/managed-index-controller.ts +++ b/packages/cli-v3/src/entryPoints/managed-index-controller.ts @@ -92,6 +92,9 @@ async function indexDeployment({ const sourceFiles = resolveSourceFiles(buildManifest.sources, workerManifest.tasks); + const buildPlatform = process.env.BUILDPLATFORM; + const targetPlatform = process.env.TARGETPLATFORM; + const backgroundWorkerBody: CreateBackgroundWorkerRequestBody = { localOnly: true, metadata: { @@ -104,9 +107,36 @@ async function indexDeployment({ }, engine: "V2", supportsLazyAttempts: true, + buildPlatform, + targetPlatform, }; - await cliApiClient.createDeploymentBackgroundWorker(deploymentId, backgroundWorkerBody); + const createResponse = await cliApiClient.createDeploymentBackgroundWorker( + deploymentId, + backgroundWorkerBody + ); + + if (!createResponse.success) { + console.error( + JSON.stringify({ + message: "Failed to create background worker", + buildPlatform, + targetPlatform, + error: createResponse.error, + }) + ); + // Do NOT fail the deployment, this may be a multi-platform deployment + return; + } + + console.log( + JSON.stringify({ + message: "Background worker created", + buildPlatform, + targetPlatform, + createResponse: createResponse.data, + }) + ); } catch (error) { const serialiedIndexError = serializeIndexingError(error, stderr.join("\n")); diff --git a/packages/core/src/v3/schemas/api.ts b/packages/core/src/v3/schemas/api.ts index 4e8218ba83..4496463b29 100644 --- a/packages/core/src/v3/schemas/api.ts +++ b/packages/core/src/v3/schemas/api.ts @@ -54,6 +54,8 @@ export const CreateBackgroundWorkerRequestBody = z.object({ metadata: BackgroundWorkerMetadata, engine: RunEngineVersion.optional(), supportsLazyAttempts: z.boolean().optional(), + buildPlatform: z.string().optional(), + targetPlatform: z.string().optional(), }); export type CreateBackgroundWorkerRequestBody = z.infer; @@ -292,11 +294,8 @@ export type StartDeploymentIndexingResponseBody = z.infer< >; export const FinalizeDeploymentRequestBody = z.object({ - imageReference: z.string(), - /** @deprecated This is now determined by the webapp */ - selfHosted: z.boolean().optional(), - skipRegistryProxy: z.boolean().optional(), skipPromotion: z.boolean().optional(), + imageDigest: z.string().optional(), }); export type FinalizeDeploymentRequestBody = z.infer; @@ -329,8 +328,8 @@ export const InitializeDeploymentResponseBody = z.object({ shortCode: z.string(), version: z.string(), imageTag: z.string(), + imagePlatform: z.string(), externalBuildData: ExternalBuildData.optional().nullable(), - registryHost: z.string().optional(), }); export type InitializeDeploymentResponseBody = z.infer; @@ -338,9 +337,11 @@ export type InitializeDeploymentResponseBody = z.infer Date: Fri, 30 May 2025 14:11:25 +0100 Subject: [PATCH 09/28] add worker deployment migration --- .../migration.sql | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 internal-packages/database/prisma/migrations/20250530131049_add_worker_deployment_image_platform/migration.sql diff --git a/internal-packages/database/prisma/migrations/20250530131049_add_worker_deployment_image_platform/migration.sql b/internal-packages/database/prisma/migrations/20250530131049_add_worker_deployment_image_platform/migration.sql new file mode 100644 index 0000000000..8dc7edd90c --- /dev/null +++ b/internal-packages/database/prisma/migrations/20250530131049_add_worker_deployment_image_platform/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "WorkerDeployment" ADD COLUMN "imagePlatform" TEXT NOT NULL DEFAULT 'linux/amd64'; From 2e46524fffeee4ce916fc448534cf709b2a2e9bf Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Fri, 30 May 2025 14:20:26 +0100 Subject: [PATCH 10/28] rename image platform env var --- apps/webapp/app/env.server.ts | 2 +- apps/webapp/app/v3/services/initializeDeployment.server.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/webapp/app/env.server.ts b/apps/webapp/app/env.server.ts index d0ebf16b82..02377169a3 100644 --- a/apps/webapp/app/env.server.ts +++ b/apps/webapp/app/env.server.ts @@ -270,7 +270,7 @@ const EnvironmentSchema = z.object({ DEPLOY_REGISTRY_USERNAME: z.string().optional(), DEPLOY_REGISTRY_PASSWORD: z.string().optional(), DEPLOY_REGISTRY_NAMESPACE: z.string().default("trigger"), - DEPLOY_BUILD_PLATFORM: z.string().default("linux/amd64"), + DEPLOY_IMAGE_PLATFORM: z.string().default("linux/amd64"), DEPLOY_TIMEOUT_MS: z.coerce .number() .int() diff --git a/apps/webapp/app/v3/services/initializeDeployment.server.ts b/apps/webapp/app/v3/services/initializeDeployment.server.ts index d4d2c76ea2..1181cf9ee7 100644 --- a/apps/webapp/app/v3/services/initializeDeployment.server.ts +++ b/apps/webapp/app/v3/services/initializeDeployment.server.ts @@ -104,7 +104,7 @@ export class InitializeDeploymentService extends BaseService { triggeredById: triggeredBy?.id, type: payload.type, imageReference: imageRef, - imagePlatform: env.DEPLOY_BUILD_PLATFORM, + imagePlatform: env.DEPLOY_IMAGE_PLATFORM, git: payload.gitMeta ?? undefined, }, }); From 61357d3ad311c194bebc783d0eb544601bf1e7a3 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Fri, 30 May 2025 17:16:08 +0100 Subject: [PATCH 11/28] only try to sync parent env vars for preview deployments --- ...ojects.$projectRef.envvars.$slug.import.ts | 1 + packages/cli-v3/src/commands/deploy.ts | 21 +++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/apps/webapp/app/routes/api.v1.projects.$projectRef.envvars.$slug.import.ts b/apps/webapp/app/routes/api.v1.projects.$projectRef.envvars.$slug.import.ts index 89ce7ed886..803d99f28e 100644 --- a/apps/webapp/app/routes/api.v1.projects.$projectRef.envvars.$slug.import.ts +++ b/apps/webapp/app/routes/api.v1.projects.$projectRef.envvars.$slug.import.ts @@ -47,6 +47,7 @@ export async function action({ params, request }: ActionFunctionArgs) { })), }); + // Only sync parent variables if this is a branch environment if (environment.parentEnvironmentId && body.parentVariables) { const parentResult = await repository.create(environment.project.id, { override: typeof body.override === "boolean" ? body.override : false, diff --git a/packages/cli-v3/src/commands/deploy.ts b/packages/cli-v3/src/commands/deploy.ts index 047911d08a..a1718adf9c 100644 --- a/packages/cli-v3/src/commands/deploy.ts +++ b/packages/cli-v3/src/commands/deploy.ts @@ -195,6 +195,7 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) { const branch = options.env === "preview" ? getBranch({ specified: options.branch, gitMeta }) : undefined; + if (options.env === "preview" && !branch) { throw new Error( "Didn't auto-detect preview branch, so you need to specify one. Pass --branch ." @@ -318,10 +319,9 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) { } const hasVarsToSync = - buildManifest.deploy.sync && - ((buildManifest.deploy.sync.env && Object.keys(buildManifest.deploy.sync.env).length > 0) || - (buildManifest.deploy.sync.parentEnv && - Object.keys(buildManifest.deploy.sync.parentEnv).length > 0)); + Object.keys(buildManifest.deploy.sync?.env || {}).length > 0 || + // Only sync parent variables if this is a branch environment + (branch && Object.keys(buildManifest.deploy.sync?.parentEnv || {}).length > 0); if (hasVarsToSync) { const childVars = buildManifest.deploy.sync?.env ?? {}; @@ -333,7 +333,8 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) { if (!options.skipSyncEnvVars) { const $spinner = spinner(); $spinner.start(`Syncing ${numberOfEnvVars} env ${vars} with the server`); - const success = await syncEnvVarsWithServer( + + const uploadResult = await syncEnvVarsWithServer( projectClient.client, resolvedConfig.project, options.env, @@ -341,13 +342,13 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) { parentVars ); - if (!success) { + if (!uploadResult.success) { await failDeploy( projectClient.client, deployment, { name: "SyncEnvVarsError", - message: `Failed to sync ${numberOfEnvVars} env ${vars} with the server`, + message: `Failed to sync ${numberOfEnvVars} env ${vars} with the server: ${uploadResult.error}`, }, "", $spinner @@ -587,13 +588,11 @@ export async function syncEnvVarsWithServer( envVars: Record, parentEnvVars?: Record ) { - const uploadResult = await apiClient.importEnvVars(projectRef, environmentSlug, { + return await apiClient.importEnvVars(projectRef, environmentSlug, { variables: envVars, parentVariables: parentEnvVars, override: true, }); - - return uploadResult.success; } async function failDeploy( @@ -637,7 +636,7 @@ async function failDeploy( console.log(lastFewLines); } } else { - outro(`${chalkError(`${prefix}:`)} ${error.message}.`); + outro(`${chalkError(`${prefix}:`)} ${error.message}`); } }; From c51c7f19ff49aff30f48a3f3f88768173d0a1c3a Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Fri, 30 May 2025 17:38:19 +0100 Subject: [PATCH 12/28] add KEEP_TMP_DIRS --- packages/cli-v3/src/utilities/tempDirectories.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli-v3/src/utilities/tempDirectories.ts b/packages/cli-v3/src/utilities/tempDirectories.ts index 7ab28fb11e..8d36d419a5 100644 --- a/packages/cli-v3/src/utilities/tempDirectories.ts +++ b/packages/cli-v3/src/utilities/tempDirectories.ts @@ -37,7 +37,7 @@ export function getTmpDir( // This sometimes fails on Windows with EBUSY } }; - const removeExitListener = keep ? () => {} : onExit(removeDir); + const removeExitListener = keep || process.env.KEEP_TMP_DIRS ? () => {} : onExit(removeDir); return { path: tmpDir, From 07a39884f56e114abd85fb92766ddeaf3a275519 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Sat, 31 May 2025 14:21:22 +0100 Subject: [PATCH 13/28] supervisor: docker api version lock, auth, multi-platform --- apps/supervisor/src/env.ts | 7 ++ apps/supervisor/src/util.ts | 7 +- apps/supervisor/src/workloadManager/docker.ts | 107 ++++++++++++++---- 3 files changed, 97 insertions(+), 24 deletions(-) diff --git a/apps/supervisor/src/env.ts b/apps/supervisor/src/env.ts index a781bf5097..6e7b9e0858 100644 --- a/apps/supervisor/src/env.ts +++ b/apps/supervisor/src/env.ts @@ -41,6 +41,13 @@ const Env = z.object({ */ RUNNER_DOCKER_NETWORKS: z.string().default("host"), + // Docker settings + DOCKER_API_VERSION: z.string().default("v1.41"), + DOCKER_PLATFORM: z.string().optional(), // e.g. linux/amd64, linux/arm64 + DOCKER_REGISTRY_USERNAME: z.string().optional(), + DOCKER_REGISTRY_PASSWORD: z.string().optional(), + DOCKER_REGISTRY_URL: z.string().optional(), // e.g. https://index.docker.io/v1 + // Dequeue settings (provider mode) TRIGGER_DEQUEUE_ENABLED: BoolEnv.default("true"), TRIGGER_DEQUEUE_INTERVAL_MS: z.coerce.number().int().default(250), diff --git a/apps/supervisor/src/util.ts b/apps/supervisor/src/util.ts index ba1bc1b2fd..7cb554cd03 100644 --- a/apps/supervisor/src/util.ts +++ b/apps/supervisor/src/util.ts @@ -1,8 +1,7 @@ -export function getDockerHostDomain() { - const isMacOs = process.platform === "darwin"; - const isWindows = process.platform === "win32"; +import { isMacOS, isWindows } from "std-env"; - return isMacOs || isWindows ? "host.docker.internal" : "localhost"; +export function getDockerHostDomain() { + return isMacOS || isWindows ? "host.docker.internal" : "localhost"; } export function getRunnerId(runId: string, attemptNumber?: number) { diff --git a/apps/supervisor/src/workloadManager/docker.ts b/apps/supervisor/src/workloadManager/docker.ts index 3669e492a9..3fc31e94be 100644 --- a/apps/supervisor/src/workloadManager/docker.ts +++ b/apps/supervisor/src/workloadManager/docker.ts @@ -14,9 +14,13 @@ export class DockerWorkloadManager implements WorkloadManager { private readonly docker: Docker; private readonly runnerNetworks: string[]; + private readonly auth?: Docker.AuthConfig; + private readonly platform?: string; constructor(private opts: WorkloadManagerOptions) { - this.docker = new Docker(); + this.docker = new Docker({ + version: env.DOCKER_API_VERSION, + }); if (opts.workloadApiDomain) { this.logger.warn("⚠️ Custom workload API domain", { @@ -25,6 +29,29 @@ export class DockerWorkloadManager implements WorkloadManager { } this.runnerNetworks = env.RUNNER_DOCKER_NETWORKS.split(","); + + this.platform = env.DOCKER_PLATFORM; + if (this.platform) { + this.logger.info("🖥️ Platform override", { + targetPlatform: this.platform, + hostPlatform: process.arch, + }); + } + + if (env.DOCKER_REGISTRY_USERNAME && env.DOCKER_REGISTRY_PASSWORD && env.DOCKER_REGISTRY_URL) { + this.logger.info("🐋 Using Docker registry credentials", { + username: env.DOCKER_REGISTRY_USERNAME, + url: env.DOCKER_REGISTRY_URL, + }); + + this.auth = { + username: env.DOCKER_REGISTRY_USERNAME, + password: env.DOCKER_REGISTRY_PASSWORD, + serveraddress: env.DOCKER_REGISTRY_URL, + }; + } else { + this.logger.warn("🐋 No Docker registry credentials provided, skipping auth"); + } } async create(opts: WorkloadManagerCreateOptions) { @@ -91,7 +118,6 @@ export class DockerWorkloadManager implements WorkloadManager { } const containerCreateOpts: Docker.ContainerCreateOptions = { - Env: envVars, name: runnerId, Hostname: runnerId, HostConfig: hostConfig, @@ -101,30 +127,63 @@ export class DockerWorkloadManager implements WorkloadManager { AttachStdin: false, }; - try { - // Create container - const container = await this.docker.createContainer(containerCreateOpts); + if (this.platform) { + containerCreateOpts.platform = this.platform; + } + + const logger = this.logger.child({ opts, containerCreateOpts }); - // If there are multiple networks to attach to we need to attach the remaining ones after creation - if (remainingNetworks.length > 0) { - await this.attachContainerToNetworks({ - containerId: container.id, - networkNames: remainingNetworks, - }); - } + // Ensure the image is present + const [createImageError, imageResponseReader] = await tryCatch( + this.docker.createImage(this.auth, { + fromImage: opts.image, + ...(this.platform ? { platform: this.platform } : {}), + }) + ); + if (createImageError) { + logger.error("Failed to pull image", { error: createImageError }); + return; + } - // Start container - const startResult = await container.start(); + const [imageReadError, imageResponse] = await tryCatch(readAllChunks(imageResponseReader)); + if (imageReadError) { + logger.error("failed to read image response", { error: imageReadError }); + return; + } - this.logger.debug("create succeeded", { - opts, - startResult, + logger.debug("pulled image", { image: opts.image, imageResponse }); + + // Create container + const [createContainerError, container] = await tryCatch( + this.docker.createContainer({ + ...containerCreateOpts, + // Add env vars here so they're not logged + Env: envVars, + }) + ); + + if (createContainerError) { + logger.error("Failed to create container", { error: createContainerError }); + return; + } + + // If there are multiple networks to attach to we need to attach the remaining ones after creation + if (remainingNetworks.length > 0) { + await this.attachContainerToNetworks({ containerId: container.id, - containerCreateOpts, + networkNames: remainingNetworks, }); - } catch (error) { - this.logger.error("create failed:", { opts, error, containerCreateOpts }); } + + // Start container + const [startError, startResult] = await tryCatch(container.start()); + + if (startError) { + logger.error("Failed to start container", { error: startError, containerId: container.id }); + return; + } + + logger.debug("create succeeded", { startResult, containerId: container.id }); } private async attachContainerToNetworks({ @@ -173,3 +232,11 @@ export class DockerWorkloadManager implements WorkloadManager { }); } } + +async function readAllChunks(reader: NodeJS.ReadableStream) { + const chunks = []; + for await (const chunk of reader) { + chunks.push(chunk.toString()); + } + return chunks; +} From ffa725f621c78a078fbe6e61cdd5a97b38d4c03c Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Sat, 31 May 2025 19:55:05 +0100 Subject: [PATCH 14/28] set image ref on create, validate digest --- .../v3/services/finalizeDeployment.server.ts | 19 ++++++++++-- .../services/finalizeDeploymentV2.server.ts | 29 +++++++------------ .../services/initializeDeployment.server.ts | 14 ++------- 3 files changed, 30 insertions(+), 32 deletions(-) diff --git a/apps/webapp/app/v3/services/finalizeDeployment.server.ts b/apps/webapp/app/v3/services/finalizeDeployment.server.ts index 8502929820..22de12d438 100644 --- a/apps/webapp/app/v3/services/finalizeDeployment.server.ts +++ b/apps/webapp/app/v3/services/finalizeDeployment.server.ts @@ -60,6 +60,8 @@ export class FinalizeDeploymentService extends BaseService { throw new ServiceValidationError("Worker deployment is not in DEPLOYING status"); } + const imageDigest = validatedImageDigest(body.imageDigest); + // Link the deployment with the background worker const finalizedDeployment = await this._prisma.workerDeployment.update({ where: { @@ -69,9 +71,7 @@ export class FinalizeDeploymentService extends BaseService { status: "DEPLOYED", deployedAt: new Date(), // Only add the digest, if any - imageReference: body.imageDigest - ? `${deployment.imageReference}@${body.imageDigest}` - : undefined, + imageReference: imageDigest ? `${deployment.imageReference}@${imageDigest}` : undefined, }, }); @@ -121,3 +121,16 @@ export class FinalizeDeploymentService extends BaseService { return finalizedDeployment; } } + +function validatedImageDigest(imageDigest?: string): string | undefined { + if (!imageDigest) { + return; + } + + if (!/^sha256:[a-f0-9]{64}$/.test(imageDigest.trim())) { + logger.error("Invalid image digest", { imageDigest }); + return; + } + + return imageDigest.trim(); +} diff --git a/apps/webapp/app/v3/services/finalizeDeploymentV2.server.ts b/apps/webapp/app/v3/services/finalizeDeploymentV2.server.ts index 5c9f969265..810d051680 100644 --- a/apps/webapp/app/v3/services/finalizeDeploymentV2.server.ts +++ b/apps/webapp/app/v3/services/finalizeDeploymentV2.server.ts @@ -34,6 +34,7 @@ export class FinalizeDeploymentV2Service extends BaseService { version: true, externalBuildData: true, environment: true, + imageReference: true, worker: { select: { project: true, @@ -87,13 +88,12 @@ export class FinalizeDeploymentV2Service extends BaseService { throw new ServiceValidationError("Missing depot token"); } - const digest = body.imageDigest; + // All new deployments will set the image reference at creation time + if (!deployment.imageReference) { + throw new ServiceValidationError("Missing image reference"); + } - logger.debug("Pushing image to registry", { - id, - deployment, - digest, - }); + logger.debug("Pushing image to registry", { id, deployment, body }); const pushResult = await executePushToRegistry( { @@ -112,6 +112,7 @@ export class FinalizeDeploymentV2Service extends BaseService { version: deployment.version, environmentSlug: deployment.environment.slug, projectExternalRef: deployment.worker.project.externalRef, + imageReference: deployment.imageReference, }, }, writer @@ -129,11 +130,7 @@ export class FinalizeDeploymentV2Service extends BaseService { }); const finalizeService = new FinalizeDeploymentService(); - - const finalizedDeployment = await finalizeService.call(authenticatedEnv, id, { - skipPromotion: body.skipPromotion, - imageDigest: digest, - }); + const finalizedDeployment = await finalizeService.call(authenticatedEnv, id, body); return finalizedDeployment; } @@ -155,6 +152,7 @@ type ExecutePushToRegistryOptions = { version: string; environmentSlug: string; projectExternalRef: string; + imageReference: string; }; }; @@ -180,11 +178,9 @@ async function executePushToRegistry( password: registry.password, }); - const imageTag = `${registry.host}/${registry.namespace}/${deployment.projectExternalRef}:${deployment.version}.${deployment.environmentSlug}`; + const imageTag = deployment.imageReference; // Step 2: We need to run the depot push command - // DEPOT_TOKEN="" DEPOT_PROJECT_ID="" depot push -t registry.digitalocean.com/trigger-failover/proj_bzhdaqhlymtuhlrcgbqy:20250124.54.prod - // Step 4: Build and push the image const childProcess = execDepot(["push", depot.buildId, "-t", imageTag, "--progress", "plain"], { env: { NODE_ENV: process.env.NODE_ENV, @@ -208,10 +204,7 @@ async function executePushToRegistry( const lines = text.split("\n").filter(Boolean); errors.push(...lines); - logger.debug(text, { - imageTag, - deployment, - }); + logger.debug(text, { deployment }); // Now we can write strings directly if (writer) { diff --git a/apps/webapp/app/v3/services/initializeDeployment.server.ts b/apps/webapp/app/v3/services/initializeDeployment.server.ts index 1181cf9ee7..a120fdb27b 100644 --- a/apps/webapp/app/v3/services/initializeDeployment.server.ts +++ b/apps/webapp/app/v3/services/initializeDeployment.server.ts @@ -68,19 +68,11 @@ export class InitializeDeploymentService extends BaseService { }) : undefined; - const imageRefParts = [ + const imageRef = [ + env.DEPLOY_REGISTRY_HOST, env.DEPLOY_REGISTRY_NAMESPACE, `${environment.project.externalRef}:${nextVersion}.${environment.slug}`, - ]; - - const isLocalBuild = !externalBuildData; - - // Local builds require the registry host to be able to push the image - if (isLocalBuild) { - imageRefParts.unshift(env.DEPLOY_REGISTRY_HOST); - } - - const imageRef = imageRefParts.join("/"); + ].join("/"); logger.debug("Creating deployment", { environmentId: environment.id, From 8c85535df9706dbb8191670b86e5da5e9088bf9e Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Sat, 31 May 2025 19:57:48 +0100 Subject: [PATCH 15/28] use metadata for digest, fix local multi-platform builds --- packages/cli-v3/src/commands/deploy.ts | 6 +- packages/cli-v3/src/deploy/buildImage.ts | 128 ++++++++++++++++---- packages/cli-v3/src/utilities/fileSystem.ts | 2 +- 3 files changed, 107 insertions(+), 29 deletions(-) diff --git a/packages/cli-v3/src/commands/deploy.ts b/packages/cli-v3/src/commands/deploy.ts index a1718adf9c..5ece6b57fd 100644 --- a/packages/cli-v3/src/commands/deploy.ts +++ b/packages/cli-v3/src/commands/deploy.ts @@ -45,7 +45,7 @@ const DeployCommandOptions = CommonCommandOptions.extend({ skipSyncEnvVars: z.boolean().default(false), env: z.enum(["prod", "staging", "preview"]), branch: z.string().optional(), - loadImage: z.boolean().default(false), + load: z.boolean().default(false), config: z.string().optional(), projectRef: z.string().optional(), saveLogs: z.boolean().default(false), @@ -109,7 +109,7 @@ export function configureDeployCommand(program: Command) { ).hideHelp() ) .addOption( - new CommandOption("--load-image", "Load the built image into your local docker").hideHelp() + new CommandOption("--load", "Load the built image into your local docker").hideHelp() ) .addOption( new CommandOption( @@ -392,7 +392,7 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) { deploymentVersion: deployment.version, imageTag: deployment.imageTag, imagePlatform: deployment.imagePlatform, - loadImage: options.loadImage, + loadImage: options.load, contentHash: deployment.contentHash, externalBuildId: deployment.externalBuildData?.buildId, externalBuildToken: deployment.externalBuildData?.buildToken, diff --git a/packages/cli-v3/src/deploy/buildImage.ts b/packages/cli-v3/src/deploy/buildImage.ts index 308b6b1d7f..42d405cb98 100644 --- a/packages/cli-v3/src/deploy/buildImage.ts +++ b/packages/cli-v3/src/deploy/buildImage.ts @@ -3,6 +3,11 @@ import { depot } from "@depot/cli"; import { x } from "tinyexec"; import { BuildManifest, BuildRuntime } from "@trigger.dev/core/v3/schemas"; 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"; export interface BuildImageOptions { // Common options @@ -171,6 +176,8 @@ async function remoteBuildImage(options: DepotBuildImageOptions): Promise line.trim()).join("\n"); } -function extractImageDigest(outputs: string[]): string | undefined { - const imageDigestRegex = /pushing manifest for .+(?sha256:[a-f0-9]{64})/; - - for (const line of outputs) { - const imageDigestMatch = line.match(imageDigestRegex); - - const digest = imageDigestMatch?.groups?.digest; - - if (digest) { - return digest; - } - } - - return; -} - export type GenerateContainerfileOptions = { runtime: BuildRuntime; build: BuildManifest["build"]; @@ -819,3 +841,59 @@ function getAddHost(apiUrl: string) { return; } + +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: string) { + return imagePlatform.split(",").length > 1; +} + +async function ensureQemuRegistered(imagePlatform: string) { + 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: string[] = []; + 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(), + "image.name": z.string().optional(), +}); diff --git a/packages/cli-v3/src/utilities/fileSystem.ts b/packages/cli-v3/src/utilities/fileSystem.ts index f29522d25b..b3957122fb 100644 --- a/packages/cli-v3/src/utilities/fileSystem.ts +++ b/packages/cli-v3/src/utilities/fileSystem.ts @@ -56,7 +56,7 @@ export async function readJSONFile(path: string) { return JSON.parse(fileContents); } -export async function safeFeadJSONFile(path: string) { +export async function safeReadJSONFile(path: string) { try { const fileExists = await pathExists(path); From 4db1de21b90e0926dd1aca8b725ccdde8695e5b6 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Sat, 31 May 2025 19:58:18 +0100 Subject: [PATCH 16/28] print git meta branch before commit --- apps/webapp/app/components/GitMetadata.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/webapp/app/components/GitMetadata.tsx b/apps/webapp/app/components/GitMetadata.tsx index 1ec7d38dba..efe3fb0efb 100644 --- a/apps/webapp/app/components/GitMetadata.tsx +++ b/apps/webapp/app/components/GitMetadata.tsx @@ -8,8 +8,8 @@ export function GitMetadata({ git }: { git?: GitMetaLinks | null }) { return ( <> {git.pullRequestUrl && git.pullRequestNumber && } - {git.shortSha && } {git.branchUrl && } + {git.shortSha && } ); } From 47a1eca0902ecbd21ca103df525da34a2855de00 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Sat, 31 May 2025 20:30:18 +0100 Subject: [PATCH 17/28] improve push and load flag handling --- packages/cli-v3/src/commands/deploy.ts | 15 +++- packages/cli-v3/src/commands/workers/build.ts | 2 +- packages/cli-v3/src/deploy/buildImage.ts | 77 ++++++++++++++----- 3 files changed, 71 insertions(+), 23 deletions(-) diff --git a/packages/cli-v3/src/commands/deploy.ts b/packages/cli-v3/src/commands/deploy.ts index 5ece6b57fd..37632d3e04 100644 --- a/packages/cli-v3/src/commands/deploy.ts +++ b/packages/cli-v3/src/commands/deploy.ts @@ -45,7 +45,7 @@ const DeployCommandOptions = CommonCommandOptions.extend({ skipSyncEnvVars: z.boolean().default(false), env: z.enum(["prod", "staging", "preview"]), branch: z.string().optional(), - load: z.boolean().default(false), + load: z.boolean().optional(), config: z.string().optional(), projectRef: z.string().optional(), saveLogs: z.boolean().default(false), @@ -55,7 +55,7 @@ const DeployCommandOptions = CommonCommandOptions.extend({ envFile: z.string().optional(), // Local build options network: z.enum(["default", "none", "host"]).optional(), - push: z.boolean().default(false), + push: z.boolean().optional(), builder: z.string().default("trigger"), }); @@ -111,6 +111,12 @@ export function configureDeployCommand(program: Command) { .addOption( new CommandOption("--load", "Load the built image into your local docker").hideHelp() ) + .addOption( + new CommandOption( + "--no-load", + "Do not load the built image into your local docker" + ).hideHelp() + ) .addOption( new CommandOption( "--save-logs", @@ -119,6 +125,9 @@ export function configureDeployCommand(program: Command) { ) // Local build options .addOption(new CommandOption("--push", "Push the image after local builds").hideHelp()) + .addOption( + new CommandOption("--no-push", "Do not push the image after local builds").hideHelp() + ) .addOption( new CommandOption( "--network ", @@ -392,7 +401,7 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) { deploymentVersion: deployment.version, imageTag: deployment.imageTag, imagePlatform: deployment.imagePlatform, - loadImage: options.load, + load: options.load, contentHash: deployment.contentHash, externalBuildId: deployment.externalBuildData?.buildId, externalBuildToken: deployment.externalBuildData?.buildToken, diff --git a/packages/cli-v3/src/commands/workers/build.ts b/packages/cli-v3/src/commands/workers/build.ts index 2cc7f9ac06..9d6ff8381a 100644 --- a/packages/cli-v3/src/commands/workers/build.ts +++ b/packages/cli-v3/src/commands/workers/build.ts @@ -335,7 +335,7 @@ async function _workerBuildCommand(dir: string, options: WorkersBuildCommandOpti deploymentId: deployment.id, deploymentVersion: deployment.version, imageTag: deployment.imageTag, - loadImage: options.load, + load: options.load, contentHash: deployment.contentHash, externalBuildId: deployment.externalBuildData?.buildId, externalBuildToken: deployment.externalBuildData?.buildToken, diff --git a/packages/cli-v3/src/deploy/buildImage.ts b/packages/cli-v3/src/deploy/buildImage.ts index 42d405cb98..92d46cd490 100644 --- a/packages/cli-v3/src/deploy/buildImage.ts +++ b/packages/cli-v3/src/deploy/buildImage.ts @@ -8,16 +8,17 @@ 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"; export interface BuildImageOptions { // Common options isLocalBuild: boolean; imagePlatform: string; noCache?: boolean; - loadImage?: boolean; + load?: boolean; // Local build options - push: boolean; + push?: boolean; network?: string; builder: string; @@ -50,7 +51,7 @@ export async function buildImage(options: BuildImageOptions): Promise; @@ -174,6 +175,7 @@ async function remoteBuildImage(options: DepotBuildImageOptions): Promise; network?: string; builder: string; - loadImage?: boolean; + load?: boolean; onLog?: (log: string) => void; } @@ -408,15 +409,13 @@ async function localBuildImage(options: SelfHostedBuildImageOptions): Promise Date: Mon, 2 Jun 2025 10:51:58 +0100 Subject: [PATCH 18/28] make runs after local builds compatible with load and push --- apps/supervisor/src/env.ts | 1 + apps/supervisor/src/workloadManager/docker.ts | 54 ++++++++++++------- packages/cli-v3/src/deploy/buildImage.ts | 3 ++ 3 files changed, 40 insertions(+), 18 deletions(-) diff --git a/apps/supervisor/src/env.ts b/apps/supervisor/src/env.ts index 6e7b9e0858..148458b4a0 100644 --- a/apps/supervisor/src/env.ts +++ b/apps/supervisor/src/env.ts @@ -44,6 +44,7 @@ const Env = z.object({ // Docker settings DOCKER_API_VERSION: z.string().default("v1.41"), DOCKER_PLATFORM: z.string().optional(), // e.g. linux/amd64, linux/arm64 + DOCKER_STRIP_IMAGE_DIGEST: BoolEnv.default(true), DOCKER_REGISTRY_USERNAME: z.string().optional(), DOCKER_REGISTRY_PASSWORD: z.string().optional(), DOCKER_REGISTRY_URL: z.string().optional(), // e.g. https://index.docker.io/v1 diff --git a/apps/supervisor/src/workloadManager/docker.ts b/apps/supervisor/src/workloadManager/docker.ts index 3fc31e94be..09ee8b19d2 100644 --- a/apps/supervisor/src/workloadManager/docker.ts +++ b/apps/supervisor/src/workloadManager/docker.ts @@ -117,11 +117,17 @@ export class DockerWorkloadManager implements WorkloadManager { hostConfig.Memory = opts.machine.memory * 1024 * 1024 * 1024; } + let imageRef = opts.image; + + if (env.DOCKER_STRIP_IMAGE_DIGEST) { + imageRef = opts.image.split("@")[0]!; + } + const containerCreateOpts: Docker.ContainerCreateOptions = { name: runnerId, Hostname: runnerId, HostConfig: hostConfig, - Image: opts.image, + Image: imageRef, AttachStdout: false, AttachStderr: false, AttachStdin: false, @@ -133,25 +139,37 @@ export class DockerWorkloadManager implements WorkloadManager { const logger = this.logger.child({ opts, containerCreateOpts }); - // Ensure the image is present - const [createImageError, imageResponseReader] = await tryCatch( - this.docker.createImage(this.auth, { - fromImage: opts.image, - ...(this.platform ? { platform: this.platform } : {}), - }) - ); - if (createImageError) { - logger.error("Failed to pull image", { error: createImageError }); - return; - } + const [inspectError] = await tryCatch(this.docker.getImage(imageRef).inspect()); - const [imageReadError, imageResponse] = await tryCatch(readAllChunks(imageResponseReader)); - if (imageReadError) { - logger.error("failed to read image response", { error: imageReadError }); - return; - } + // If the image is not present, try to pull it + if (inspectError) { + logger.error("Failed to inspect image, trying to pull", { + error: inspectError, + image: opts.image, + }); - logger.debug("pulled image", { image: opts.image, imageResponse }); + // Ensure the image is present + const [createImageError, imageResponseReader] = await tryCatch( + this.docker.createImage(this.auth, { + fromImage: imageRef, + ...(this.platform ? { platform: this.platform } : {}), + }) + ); + if (createImageError) { + logger.error("Failed to pull image", { error: createImageError }); + return; + } + + const [imageReadError, imageResponse] = await tryCatch(readAllChunks(imageResponseReader)); + if (imageReadError) { + logger.error("failed to read image response", { error: imageReadError }); + return; + } + + logger.debug("pulled image", { image: opts.image, imageResponse }); + } else { + // Image is present, so we can use it to create the container + } // Create container const [createContainerError, container] = await tryCatch( diff --git a/packages/cli-v3/src/deploy/buildImage.ts b/packages/cli-v3/src/deploy/buildImage.ts index 92d46cd490..4c475e5674 100644 --- a/packages/cli-v3/src/deploy/buildImage.ts +++ b/packages/cli-v3/src/deploy/buildImage.ts @@ -499,6 +499,8 @@ async function localBuildImage(options: SelfHostedBuildImageOptions): Promise Date: Mon, 2 Jun 2025 11:14:39 +0100 Subject: [PATCH 19/28] small improvement for platform overrides --- apps/supervisor/src/workloadManager/docker.ts | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/apps/supervisor/src/workloadManager/docker.ts b/apps/supervisor/src/workloadManager/docker.ts index 09ee8b19d2..d36c96e31e 100644 --- a/apps/supervisor/src/workloadManager/docker.ts +++ b/apps/supervisor/src/workloadManager/docker.ts @@ -15,7 +15,7 @@ export class DockerWorkloadManager implements WorkloadManager { private readonly runnerNetworks: string[]; private readonly auth?: Docker.AuthConfig; - private readonly platform?: string; + private readonly platformOverride?: string; constructor(private opts: WorkloadManagerOptions) { this.docker = new Docker({ @@ -30,10 +30,10 @@ export class DockerWorkloadManager implements WorkloadManager { this.runnerNetworks = env.RUNNER_DOCKER_NETWORKS.split(","); - this.platform = env.DOCKER_PLATFORM; - if (this.platform) { + this.platformOverride = env.DOCKER_PLATFORM; + if (this.platformOverride) { this.logger.info("🖥️ Platform override", { - targetPlatform: this.platform, + targetPlatform: this.platformOverride, hostPlatform: process.arch, }); } @@ -133,26 +133,38 @@ export class DockerWorkloadManager implements WorkloadManager { AttachStdin: false, }; - if (this.platform) { - containerCreateOpts.platform = this.platform; + if (this.platformOverride) { + containerCreateOpts.platform = this.platformOverride; } const logger = this.logger.child({ opts, containerCreateOpts }); - const [inspectError] = await tryCatch(this.docker.getImage(imageRef).inspect()); + const [inspectError, inspectResult] = await tryCatch(this.docker.getImage(imageRef).inspect()); + + let shouldPull = !!inspectError; + if (this.platformOverride) { + const imageArchitecture = inspectResult?.Architecture; + + // When the image architecture doesn't match the platform, we need to pull the image + if (imageArchitecture && !this.platformOverride.includes(imageArchitecture)) { + shouldPull = true; + } + } // If the image is not present, try to pull it - if (inspectError) { - logger.error("Failed to inspect image, trying to pull", { + if (shouldPull) { + logger.error("Pulling image", { error: inspectError, image: opts.image, + targetPlatform: this.platformOverride, + imageArchitecture: inspectResult?.Architecture, }); // Ensure the image is present const [createImageError, imageResponseReader] = await tryCatch( this.docker.createImage(this.auth, { fromImage: imageRef, - ...(this.platform ? { platform: this.platform } : {}), + ...(this.platformOverride ? { platform: this.platformOverride } : {}), }) ); if (createImageError) { From 7eaf50b6a052c97efcbd8aeaf724c919e698dd59 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Mon, 2 Jun 2025 11:22:48 +0100 Subject: [PATCH 20/28] add image platform to dequeued message --- internal-packages/run-engine/src/engine/systems/dequeueSystem.ts | 1 + packages/core/src/v3/schemas/runEngine.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/internal-packages/run-engine/src/engine/systems/dequeueSystem.ts b/internal-packages/run-engine/src/engine/systems/dequeueSystem.ts index 3f49f7d982..8ac46d5e89 100644 --- a/internal-packages/run-engine/src/engine/systems/dequeueSystem.ts +++ b/internal-packages/run-engine/src/engine/systems/dequeueSystem.ts @@ -484,6 +484,7 @@ export class DequeueSystem { deployment: { id: result.deployment?.id, friendlyId: result.deployment?.friendlyId, + imagePlatform: result.deployment?.imagePlatform, }, run: { id: lockedTaskRun.id, diff --git a/packages/core/src/v3/schemas/runEngine.ts b/packages/core/src/v3/schemas/runEngine.ts index 2dd75e0ddd..7c3a3d4a00 100644 --- a/packages/core/src/v3/schemas/runEngine.ts +++ b/packages/core/src/v3/schemas/runEngine.ts @@ -239,6 +239,7 @@ export const DequeuedMessage = z.object({ deployment: z.object({ id: z.string().optional(), friendlyId: z.string().optional(), + imagePlatform: z.string().optional(), }), run: z.object({ id: z.string(), From d00ed335b6186eecf408a6987e24191adc398e05 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Mon, 2 Jun 2025 12:10:08 +0100 Subject: [PATCH 21/28] remove deprecated init request body fields --- packages/cli-v3/src/commands/workers/build.ts | 8 -------- packages/core/src/v3/schemas/api.ts | 6 +----- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/packages/cli-v3/src/commands/workers/build.ts b/packages/cli-v3/src/commands/workers/build.ts index 9d6ff8381a..960d94cdde 100644 --- a/packages/cli-v3/src/commands/workers/build.ts +++ b/packages/cli-v3/src/commands/workers/build.ts @@ -234,18 +234,10 @@ async function _workerBuildCommand(dir: string, options: WorkersBuildCommandOpti return; } - const tagParts = parseDockerImageReference(options.tag ?? ""); - - // Account for empty strings to preserve existing behavior - const registry = tagParts.registry ? tagParts.registry : undefined; - const namespace = tagParts.repo ? tagParts.repo : undefined; - const deploymentResponse = await projectClient.client.initializeDeployment({ contentHash: buildManifest.contentHash, userId: authorization.userId, selfHosted: options.local, - registryHost: registry, - namespace: namespace, type: "UNMANAGED", }); diff --git a/packages/core/src/v3/schemas/api.ts b/packages/core/src/v3/schemas/api.ts index 7eec55899b..8701a3aef3 100644 --- a/packages/core/src/v3/schemas/api.ts +++ b/packages/core/src/v3/schemas/api.ts @@ -337,12 +337,8 @@ export type InitializeDeploymentResponseBody = z.infer Date: Mon, 2 Jun 2025 15:38:15 +0100 Subject: [PATCH 22/28] fix fail deployment id param --- apps/webapp/app/v3/services/failDeployment.server.ts | 7 ++++--- apps/webapp/app/v3/services/finalizeDeployment.server.ts | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/apps/webapp/app/v3/services/failDeployment.server.ts b/apps/webapp/app/v3/services/failDeployment.server.ts index 0f4fda1cbb..b486a0f67e 100644 --- a/apps/webapp/app/v3/services/failDeployment.server.ts +++ b/apps/webapp/app/v3/services/failDeployment.server.ts @@ -15,24 +15,25 @@ const FINAL_DEPLOYMENT_STATUSES: WorkerDeploymentStatus[] = [ export class FailDeploymentService extends BaseService { public async call( authenticatedEnv: AuthenticatedEnvironment, - id: string, + friendlyId: string, params: FailDeploymentRequestBody ) { const deployment = await this._prisma.workerDeployment.findFirst({ where: { - friendlyId: id, + friendlyId, environmentId: authenticatedEnv.id, }, }); if (!deployment) { - logger.error("Worker deployment not found", { id }); + logger.error("Worker deployment not found", { friendlyId }); return; } if (FINAL_DEPLOYMENT_STATUSES.includes(deployment.status)) { logger.error("Worker deployment already in final state", { id: deployment.id, + friendlyId, status: deployment.status, }); return; diff --git a/apps/webapp/app/v3/services/finalizeDeployment.server.ts b/apps/webapp/app/v3/services/finalizeDeployment.server.ts index 22de12d438..6e9b0c1da3 100644 --- a/apps/webapp/app/v3/services/finalizeDeployment.server.ts +++ b/apps/webapp/app/v3/services/finalizeDeployment.server.ts @@ -39,7 +39,7 @@ export class FinalizeDeploymentService extends BaseService { logger.error("Worker deployment does not have a worker", { id }); const failService = new FailDeploymentService(); - await failService.call(authenticatedEnv, deployment.id, { + await failService.call(authenticatedEnv, deployment.friendlyId, { error: { name: "MissingWorker", message: "Deployment does not have a worker", From 078c675317e363e01a5df848c473cc905ca90d8d Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Mon, 2 Jun 2025 15:45:31 +0100 Subject: [PATCH 23/28] remove build debug logs --- packages/cli-v3/src/deploy/buildImage.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/cli-v3/src/deploy/buildImage.ts b/packages/cli-v3/src/deploy/buildImage.ts index 4c475e5674..65114cccd5 100644 --- a/packages/cli-v3/src/deploy/buildImage.ts +++ b/packages/cli-v3/src/deploy/buildImage.ts @@ -412,11 +412,6 @@ async function localBuildImage(options: SelfHostedBuildImageOptions): Promise Date: Mon, 2 Jun 2025 20:03:10 +0100 Subject: [PATCH 24/28] pass report merge with no tests --- .github/workflows/unit-tests-internal.yml | 2 +- .github/workflows/unit-tests-packages.yml | 2 +- .github/workflows/unit-tests-webapp.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/unit-tests-internal.yml b/.github/workflows/unit-tests-internal.yml index d1f5f56447..5acac054a6 100644 --- a/.github/workflows/unit-tests-internal.yml +++ b/.github/workflows/unit-tests-internal.yml @@ -127,4 +127,4 @@ jobs: merge-multiple: true - name: Merge reports - run: pnpm dlx vitest@3.1.4 run --merge-reports + run: pnpm dlx vitest@3.1.4 run --merge-reports --pass-with-no-tests diff --git a/.github/workflows/unit-tests-packages.yml b/.github/workflows/unit-tests-packages.yml index edbc4053db..cfa5e88baa 100644 --- a/.github/workflows/unit-tests-packages.yml +++ b/.github/workflows/unit-tests-packages.yml @@ -127,4 +127,4 @@ jobs: merge-multiple: true - name: Merge reports - run: pnpm dlx vitest@3.1.4 run --merge-reports + run: pnpm dlx vitest@3.1.4 run --merge-reports --pass-with-no-tests diff --git a/.github/workflows/unit-tests-webapp.yml b/.github/workflows/unit-tests-webapp.yml index bfaa13447e..4199aff734 100644 --- a/.github/workflows/unit-tests-webapp.yml +++ b/.github/workflows/unit-tests-webapp.yml @@ -133,4 +133,4 @@ jobs: merge-multiple: true - name: Merge reports - run: pnpm dlx vitest@3.1.4 run --merge-reports + run: pnpm dlx vitest@3.1.4 run --merge-reports --pass-with-no-tests From 2e5925c31388ca26d8c769543810122e80414109 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Tue, 3 Jun 2025 11:31:50 +0100 Subject: [PATCH 25/28] structured run debug logs --- apps/supervisor/src/env.ts | 1 + apps/supervisor/src/workloadManager/docker.ts | 1 + packages/cli-v3/src/entryPoints/managed/logger.ts | 5 ++++- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/supervisor/src/env.ts b/apps/supervisor/src/env.ts index 148458b4a0..2c60ebdf1f 100644 --- a/apps/supervisor/src/env.ts +++ b/apps/supervisor/src/env.ts @@ -28,6 +28,7 @@ const Env = z.object({ RUNNER_HEARTBEAT_INTERVAL_SECONDS: z.coerce.number().optional(), RUNNER_SNAPSHOT_POLL_INTERVAL_SECONDS: z.coerce.number().optional(), RUNNER_ADDITIONAL_ENV_VARS: AdditionalEnvVars, // optional (csv) + RUNNER_PRETTY_LOGS: BoolEnv.default(false), RUNNER_DOCKER_AUTOREMOVE: BoolEnv.default(true), /** * Network mode to use for all runners. Supported standard values are: `bridge`, `host`, `none`, and `container:`. diff --git a/apps/supervisor/src/workloadManager/docker.ts b/apps/supervisor/src/workloadManager/docker.ts index d36c96e31e..65d87d9ecf 100644 --- a/apps/supervisor/src/workloadManager/docker.ts +++ b/apps/supervisor/src/workloadManager/docker.ts @@ -72,6 +72,7 @@ export class DockerWorkloadManager implements WorkloadManager { `TRIGGER_WORKER_INSTANCE_NAME=${env.TRIGGER_WORKER_INSTANCE_NAME}`, `OTEL_EXPORTER_OTLP_ENDPOINT=${env.OTEL_EXPORTER_OTLP_ENDPOINT}`, `TRIGGER_RUNNER_ID=${runnerId}`, + `PRETTY_LOGS=${env.RUNNER_PRETTY_LOGS}`, ]; if (this.opts.warmStartUrl) { diff --git a/packages/cli-v3/src/entryPoints/managed/logger.ts b/packages/cli-v3/src/entryPoints/managed/logger.ts index 150d740094..f0f6677732 100644 --- a/packages/cli-v3/src/entryPoints/managed/logger.ts +++ b/packages/cli-v3/src/entryPoints/managed/logger.ts @@ -5,6 +5,7 @@ import { } from "@trigger.dev/core/v3/runEngineWorker"; import { RunnerEnv } from "./env.js"; import { flattenAttributes } from "@trigger.dev/core/v3"; +import { SimpleStructuredLogger } from "@trigger.dev/core/v3/utils/structuredLogger"; export type SendDebugLogOptions = { runId?: string; @@ -26,10 +27,12 @@ export type RunLoggerOptions = { export class ManagedRunLogger implements RunLogger { private readonly httpClient: WorkloadHttpClient; private readonly env: RunnerEnv; + private readonly logger: SimpleStructuredLogger; constructor(opts: RunLoggerOptions) { this.httpClient = opts.httpClient; this.env = opts.env; + this.logger = new SimpleStructuredLogger("managed-run-logger"); } sendDebugLog({ runId, message, date, properties, print = true }: SendDebugLogOptions) { @@ -49,7 +52,7 @@ export class ManagedRunLogger implements RunLogger { }; if (print) { - console.log(message, mergedProperties); + this.logger.log(message, mergedProperties); } const flattenedProperties = flattenAttributes( From 5b7ea14599e43a46d4b89c7be303a6fe3af781ce Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Tue, 3 Jun 2025 11:33:12 +0100 Subject: [PATCH 26/28] add required env var for tests --- .env.example | 2 +- .github/workflows/unit-tests-webapp.yml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index 626b9389dd..c89ed5f012 100644 --- a/.env.example +++ b/.env.example @@ -82,7 +82,7 @@ COORDINATOR_SECRET=coordinator-secret # generate the actual secret with `openssl # DEPOT_ORG_ID= # DEPOT_TOKEN= -# DEPLOY_REGISTRY_HOST=${APP_ORIGIN} # This is the host that the deploy CLI will use to push images to the registry +DEPLOY_REGISTRY_HOST=${APP_ORIGIN} # This is the host that the deploy CLI will use to push images to the registry # DEV_OTEL_EXPORTER_OTLP_ENDPOINT="http://0.0.0.0:4318" # These are needed for the object store (for handling large payloads/outputs) # OBJECT_STORE_BASE_URL="https://{bucket}.{accountId}.r2.cloudflarestorage.com" diff --git a/.github/workflows/unit-tests-webapp.yml b/.github/workflows/unit-tests-webapp.yml index 4199aff734..e96af168c0 100644 --- a/.github/workflows/unit-tests-webapp.yml +++ b/.github/workflows/unit-tests-webapp.yml @@ -86,6 +86,7 @@ jobs: SESSION_SECRET: "secret" MAGIC_LINK_SECRET: "secret" ENCRYPTION_KEY: "secret" + DEPLOY_REGISTRY_HOST: "docker.io" - name: Gather all reports if: ${{ !cancelled() }} From 9c9030136a725ab288412713f6e0c759e282f849 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Tue, 3 Jun 2025 11:56:57 +0100 Subject: [PATCH 27/28] should not be an error log --- apps/supervisor/src/workloadManager/docker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/supervisor/src/workloadManager/docker.ts b/apps/supervisor/src/workloadManager/docker.ts index 65d87d9ecf..d2dc484cc1 100644 --- a/apps/supervisor/src/workloadManager/docker.ts +++ b/apps/supervisor/src/workloadManager/docker.ts @@ -154,7 +154,7 @@ export class DockerWorkloadManager implements WorkloadManager { // If the image is not present, try to pull it if (shouldPull) { - logger.error("Pulling image", { + logger.info("Pulling image", { error: inspectError, image: opts.image, targetPlatform: this.platformOverride, From 71de319a8ed1ae09fb93d5b25824145a2b9c19fd Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Wed, 4 Jun 2025 14:55:24 +0100 Subject: [PATCH 28/28] add changeset --- .changeset/ninety-games-grow.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .changeset/ninety-games-grow.md diff --git a/.changeset/ninety-games-grow.md b/.changeset/ninety-games-grow.md new file mode 100644 index 0000000000..df22eff4ee --- /dev/null +++ b/.changeset/ninety-games-grow.md @@ -0,0 +1,11 @@ +--- +"trigger.dev": patch +"@trigger.dev/core": patch +--- + +- Resolve issue where CLI could get stuck during deploy finalization +- Unify local and remote build logic, with multi-platform build support +- Improve switch command; now accepts profile name as an argument +- Registry configuration is now fully managed by the webapp +- The deploy `--self-hosted` flag is no longer required +- Enhance deployment error reporting and image digest retrieval