49728b5a5f
Depot builds have short-lived tokens and their TTL is not exposed in the SDK. As queued deployments can stay in the queue for an arbitrary amount of time, deferring the remote build creation helps avoid expired Depot token issues.
192 lines
7.2 KiB
TypeScript
192 lines
7.2 KiB
TypeScript
import { type InitializeDeploymentRequestBody } 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 { calculateNextBuildVersion } from "../utils/calculateNextBuildVersion";
|
|
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";
|
|
|
|
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 ?? "",
|
|
};
|
|
}
|
|
|
|
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",
|
|
},
|
|
});
|
|
}
|
|
|
|
const latestDeployment = await this._prisma.workerDeployment.findFirst({
|
|
where: {
|
|
environmentId: environment.id,
|
|
},
|
|
orderBy: {
|
|
createdAt: "desc",
|
|
},
|
|
take: 1,
|
|
});
|
|
|
|
const nextVersion = calculateNextBuildVersion(latestDeployment?.version);
|
|
|
|
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.
|
|
// This helps avoid Depot token expiration issues.
|
|
const externalBuildData =
|
|
payload.initialStatus === "PENDING"
|
|
? undefined
|
|
: 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);
|
|
|
|
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;
|
|
|
|
// 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
|
|
const initialStatus = payload.initialStatus ?? "BUILDING";
|
|
|
|
logger.debug("Creating deployment", {
|
|
environmentId: environment.id,
|
|
projectId: environment.projectId,
|
|
version: nextVersion,
|
|
triggeredById: triggeredBy?.id,
|
|
type: payload.type,
|
|
imageRef,
|
|
isEcr,
|
|
repoCreated,
|
|
initialStatus,
|
|
});
|
|
|
|
const deployment = await this._prisma.workerDeployment.create({
|
|
data: {
|
|
friendlyId: generateFriendlyId("deployment"),
|
|
contentHash: payload.contentHash,
|
|
shortCode: deploymentShortCode,
|
|
version: nextVersion,
|
|
status: initialStatus,
|
|
environmentId: environment.id,
|
|
projectId: environment.projectId,
|
|
externalBuildData,
|
|
triggeredById: triggeredBy?.id,
|
|
type: payload.type,
|
|
imageReference: imageRef,
|
|
imagePlatform: env.DEPLOY_IMAGE_PLATFORM,
|
|
git: payload.gitMeta ?? undefined,
|
|
runtime: payload.runtime ?? 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)
|
|
);
|
|
|
|
return {
|
|
deployment,
|
|
imageRef,
|
|
};
|
|
});
|
|
}
|
|
}
|