import { type BuildServerMetadata, type InitializeDeploymentRequestBody, type ExternalBuildData, } from "@trigger.dev/core/v3"; import { customAlphabet } from "nanoid"; import { env } from "~/env.server"; import { type AuthenticatedEnvironment } from "~/services/apiAuth.server"; import { logger } from "~/services/logger.server"; import { generateFriendlyId } from "../friendlyIdentifiers"; import { createRemoteImageBuild, remoteBuildsEnabled } from "../remoteImageBuilder.server"; import { BaseService, ServiceValidationError } from "./baseService.server"; import { TimeoutDeploymentService } from "./timeoutDeployment.server"; import { getDeploymentImageRef } from "../getDeploymentImageRef.server"; import { tryCatch } from "@trigger.dev/core"; import { getRegistryConfig } from "../registryConfig.server"; import { DeploymentService } from "./deployment.server"; import { createDeploymentWithNextVersion } from "./initializeDeployment/createDeploymentWithNextVersion.server"; import { errAsync } from "neverthrow"; const nanoid = customAlphabet("1234567890abcdefghijklmnopqrstuvwxyz", 8); export class InitializeDeploymentService extends BaseService { public async call( environment: AuthenticatedEnvironment, payload: InitializeDeploymentRequestBody ) { return this.traceWithEnv("call", environment, async () => { if (payload.gitMeta?.commitSha?.startsWith("deployment_")) { // When we introduced automatic deployments via the build server, we slightly changed the deployment flow // mainly in the initialization and starting step: now deployments are first initialized in the `PENDING` status // and updated to `BUILDING` once the build server dequeues the build job. // Newer versions of the `deploy` command in the CLI will automatically attach to the existing deployment // and continue with the build process. For older versions, we can't change the command's client-side behavior, // so we need to handle this case here in the initialization endpoint. As we control the env variables which // the git meta is extracted from in the build server, we can use those to pass the existing deployment ID // to this endpoint. This doesn't affect the git meta on the deployment as it is set prior to this step using the // /start endpoint. It's a rather hacky solution, but it will do for now as it enables us to avoid degrading the // build server experience for users with older CLI versions. We'll eventually be able to remove this workaround // once we stop supporting 3.x CLI versions. const existingDeploymentId = payload.gitMeta.commitSha; const existingDeployment = await this._prisma.workerDeployment.findFirst({ where: { environmentId: environment.id, friendlyId: existingDeploymentId, }, }); if (!existingDeployment) { throw new ServiceValidationError( "Existing deployment not found during deployment initialization" ); } return { deployment: existingDeployment, imageRef: existingDeployment.imageReference ?? "", }; } // v4 CLI versions always send `payload.type` ("MANAGED" or "V1"). v3 CLI // versions never do, so the absence of `type` is a reliable signal that // the request came from a 3.x CLI. Detection always runs (so we can // observe how many deploys are still using v3), enforcement is gated // behind DEPRECATE_V3_CLI_DEPLOYS_ENABLED so it can be rolled out safely. if (!payload.type) { const enforced = env.DEPRECATE_V3_CLI_DEPLOYS_ENABLED === "1"; logger.warn("Detected deploy from deprecated v3 CLI", { environmentId: environment.id, projectId: environment.projectId, organizationId: environment.project.organizationId, enforced, }); if (enforced) { throw new ServiceValidationError( "The trigger.dev CLI v3 is no longer supported for deployments. Please upgrade your project to v4: https://trigger.dev/docs/migrating-from-v3" ); } } if (payload.type === "UNMANAGED") { throw new ServiceValidationError("UNMANAGED deployments are not supported"); } // Upgrade the project to engine "V2" if it's not already. This should cover cases where people deploy to V2 without running dev first. if (payload.type === "MANAGED" && environment.project.engine === "V1") { await this._prisma.project.update({ where: { id: environment.project.id, }, data: { engine: "V2", }, }); } if (payload.selfHosted && remoteBuildsEnabled()) { throw new ServiceValidationError( "Self-hosted deployments are not supported on this instance" ); } // For the `PENDING` initial status, defer the creation of the Depot build until the deployment is started to avoid token expiration issues. // For local and native builds we don't need to generate the Depot tokens. We still need to create an empty object sadly due to a bug in older CLI versions. const generateExternalBuildToken = payload.initialStatus === "PENDING" || payload.isNativeBuild || payload.isLocalBuild; const externalBuildData = generateExternalBuildToken ? ({ projectId: "-", buildToken: "-", buildId: "-", } satisfies ExternalBuildData) : await createRemoteImageBuild(environment.project); const triggeredBy = payload.userId ? await this._prisma.user.findFirst({ where: { id: payload.userId, orgMemberships: { some: { organizationId: environment.project.organizationId, }, }, }, }) : undefined; const isV4Deployment = payload.type === "MANAGED"; const registryConfig = getRegistryConfig(isV4Deployment); const deploymentShortCode = nanoid(8); // We keep using `BUILDING` as the initial status if not explicitly set // to avoid changing the behavior for deployments not created in the build server. // Native builds always start in the `PENDING` status. const initialStatus = payload.initialStatus ?? (payload.isNativeBuild ? "PENDING" : "BUILDING"); const deploymentService = new DeploymentService(); const s2StreamOrFail = await deploymentService .createEventStream(environment.project, { shortCode: deploymentShortCode }) .andThen(({ basin, stream }) => deploymentService.getEventStreamAccessToken(environment.project).map((accessToken) => ({ basin, stream, accessToken, })) ); if (s2StreamOrFail.isErr()) { logger.error( "Failed to create S2 event stream on deployment initialization, continuing without logs stream", { environmentId: environment.id, projectId: environment.projectId, error: s2StreamOrFail.error, } ); } const eventStream = s2StreamOrFail.isOk() ? { s2: { basin: s2StreamOrFail.value.basin, stream: s2StreamOrFail.value.stream, accessToken: s2StreamOrFail.value.accessToken, }, } : undefined; const buildServerMetadata: BuildServerMetadata | undefined = payload.isNativeBuild || payload.buildId ? { buildId: payload.buildId, ...(payload.isNativeBuild ? { isNativeBuild: payload.isNativeBuild, artifactKey: payload.artifactKey, skipPromotion: payload.skipPromotion, configFilePath: payload.configFilePath, skipEnqueue: payload.skipEnqueue, } : {}), } : undefined; // Concurrent deploys to the same environment race on the // `(environmentId, version)` unique constraint. The helper retries on // P2002, recomputing the version (and re-running the image ref call so // the persisted imageReference always matches the persisted version) // each attempt. const deployment = await createDeploymentWithNextVersion( this._prisma, environment.id, async (nextVersion) => { const [imageRefError, imageRefResult] = await tryCatch( getDeploymentImageRef({ registry: registryConfig, projectRef: environment.project.externalRef, nextVersion, environmentType: environment.type, deploymentShortCode, }) ); if (imageRefError) { logger.error("Failed to get deployment image ref", { environmentId: environment.id, projectId: environment.projectId, version: nextVersion, triggeredById: triggeredBy?.id, type: payload.type, cause: imageRefError.message, }); throw new ServiceValidationError("Failed to get deployment image ref"); } const { imageRef, isEcr, repoCreated } = imageRefResult; logger.debug("Creating deployment", { environmentId: environment.id, projectId: environment.projectId, version: nextVersion, triggeredById: triggeredBy?.id, type: payload.type, imageRef, isEcr, repoCreated, initialStatus, artifactKey: payload.isNativeBuild ? payload.artifactKey : undefined, isNativeBuild: payload.isNativeBuild, }); return { // Regenerated per attempt: each attempt is a fresh `create` that // must satisfy `WorkerDeployment.friendlyId @unique`, so reusing a // friendlyId across retries would risk a spurious P2002 on // friendlyId instead of the version collision we're retrying. friendlyId: generateFriendlyId("deployment"), contentHash: payload.contentHash, shortCode: deploymentShortCode, status: initialStatus, projectId: environment.projectId, externalBuildData, buildServerMetadata, triggeredById: triggeredBy?.id, type: payload.type, imageReference: imageRef, imagePlatform: env.DEPLOY_IMAGE_PLATFORM, git: payload.gitMeta ?? undefined, commitSHA: payload.gitMeta?.commitSha ?? undefined, runtime: payload.runtime ?? undefined, triggeredVia: payload.triggeredVia ?? undefined, startedAt: initialStatus === "BUILDING" ? new Date() : undefined, }; } ); const timeoutMs = deployment.status === "PENDING" ? env.DEPLOY_QUEUE_TIMEOUT_MS : env.DEPLOY_TIMEOUT_MS; await TimeoutDeploymentService.enqueue( deployment.id, deployment.status, "Building timed out", new Date(Date.now() + timeoutMs) ); // For github integration there is no artifactKey, hence we skip it here if (payload.isNativeBuild && payload.artifactKey && !payload.skipEnqueue) { const result = await deploymentService .enqueueBuild(environment, deployment, payload.artifactKey, { skipPromotion: payload.skipPromotion, configFilePath: payload.configFilePath, }) .orElse((error) => { logger.error("Failed to enqueue build", { environmentId: environment.id, projectId: environment.projectId, deploymentId: deployment.id, error: error.cause, }); return deploymentService .cancelDeployment(environment, deployment.friendlyId, { canceledReason: "Failed to enqueue build, please try again shortly.", }) .orTee((cancelError) => logger.error("Failed to cancel deployment after failed build enqueue", { environmentId: environment.id, projectId: environment.projectId, deploymentId: deployment.id, error: cancelError, }) ) .andThen(() => errAsync(error)) .orElse(() => errAsync(error)); }); if (result.isErr()) { throw Error("Failed to enqueue build"); } } return { deployment, imageRef: deployment.imageReference ?? "", eventStream, }; }); } }