chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,316 @@
|
||||
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,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user