import { BuildServerMetadata, DeploymentErrorData, ExternalBuildData, prepareDeploymentError, } from "@trigger.dev/core/v3"; import { type RuntimeEnvironment, type WorkerDeployment } from "@trigger.dev/database"; import { type PrismaClient, prisma } from "~/db.server"; import { type Organization } from "~/models/organization.server"; import { type Project } from "~/models/project.server"; import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server"; import { type User } from "~/models/user.server"; import { getUsername } from "~/utils/username"; import { processGitMetadata } from "./BranchesPresenter.server"; import { VercelProjectIntegrationDataSchema } from "~/v3/vercel/vercelProjectIntegrationSchema"; import { S2 } from "@s2-dev/streamstore"; import { env } from "~/env.server"; import { createRedisClient } from "~/redis.server"; import { tryCatch } from "@trigger.dev/core"; import { logger } from "~/services/logger.server"; const S2_TOKEN_KEY_PREFIX = "s2-token:project:"; const s2TokenRedis = createRedisClient("s2-token-cache", { host: env.CACHE_REDIS_HOST, port: env.CACHE_REDIS_PORT, username: env.CACHE_REDIS_USERNAME, password: env.CACHE_REDIS_PASSWORD, tlsDisabled: env.CACHE_REDIS_TLS_DISABLED === "true", clusterMode: env.CACHE_REDIS_CLUSTER_MODE_ENABLED === "1", }); const s2 = env.S2_ENABLED === "1" ? new S2({ accessToken: env.S2_ACCESS_TOKEN }) : undefined; export type ErrorData = { name: string; message: string; stack?: string; stderr?: string; }; export class DeploymentPresenter { #prismaClient: PrismaClient; constructor(prismaClient: PrismaClient = prisma) { this.#prismaClient = prismaClient; } public async call({ userId, projectSlug, organizationSlug, environmentSlug, deploymentShortCode, }: { userId: User["id"]; projectSlug: Project["slug"]; organizationSlug: Organization["slug"]; environmentSlug: RuntimeEnvironment["slug"]; deploymentShortCode: WorkerDeployment["shortCode"]; }) { const project = await this.#prismaClient.project.findFirstOrThrow({ select: { id: true, organizationId: true, externalRef: true, }, where: { slug: projectSlug, organization: { slug: organizationSlug, members: { some: { userId, }, }, }, }, }); const environment = await findEnvironmentBySlug(project.id, environmentSlug, userId); if (!environment) { throw new Error(`Environment not found`); } const deployment = await this.#prismaClient.workerDeployment.findFirstOrThrow({ where: { projectId: project.id, shortCode: deploymentShortCode, environmentId: environment.id, }, select: { id: true, shortCode: true, version: true, runtime: true, runtimeVersion: true, errorData: true, imageReference: true, imagePlatform: true, externalBuildData: true, projectId: true, type: true, environment: { select: { id: true, type: true, slug: true, orgMember: { select: { user: { select: { id: true, name: true, displayName: true, }, }, }, }, }, }, status: true, builtAt: true, deployedAt: true, createdAt: true, startedAt: true, installedAt: true, canceledAt: true, canceledReason: true, git: true, promotions: { select: { label: true, }, }, worker: { select: { tasks: { select: { slug: true, filePath: true, }, orderBy: { slug: "asc", }, }, sdkVersion: true, cliVersion: true, }, }, triggeredBy: { select: { id: true, name: true, displayName: true, avatarUrl: true, }, }, buildServerMetadata: true, triggeredVia: true, }, }); const gitMetadata = processGitMetadata(deployment.git); // Look up Vercel integration data to construct a deployment URL let vercelDeploymentUrl: string | undefined; const vercelProjectIntegration = await this.#prismaClient.organizationProjectIntegration.findFirst({ where: { projectId: project.id, deletedAt: null, organizationIntegration: { service: "VERCEL", deletedAt: null, }, }, select: { integrationData: true, }, }); if (vercelProjectIntegration) { const parsed = VercelProjectIntegrationDataSchema.safeParse( vercelProjectIntegration.integrationData ); if (parsed.success && parsed.data.vercelTeamSlug) { const integrationDeployment = await this.#prismaClient.integrationDeployment.findFirst({ where: { deploymentId: deployment.id, integrationName: "vercel", }, select: { integrationDeploymentId: true, }, orderBy: { createdAt: "desc", }, }); if (integrationDeployment) { const vercelId = integrationDeployment.integrationDeploymentId; vercelDeploymentUrl = `https://vercel.com/${parsed.data.vercelTeamSlug}/${parsed.data.vercelProjectName}/${vercelId}`; } } } const externalBuildData = deployment.externalBuildData ? ExternalBuildData.safeParse(deployment.externalBuildData) : undefined; const buildServerMetadata = deployment.buildServerMetadata ? BuildServerMetadata.safeParse(deployment.buildServerMetadata) : undefined; let eventStream = undefined; if ( env.S2_ENABLED === "1" && (buildServerMetadata || gitMetadata?.source === "trigger_github_app" || env.S2_DEPLOYMENT_STREAMS_LOCAL === "1") ) { const [error, accessToken] = await tryCatch(this.getS2AccessToken(project.externalRef)); if (error) { logger.error("Failed getting S2 access token", { error }); } else { eventStream = { s2: { basin: env.S2_DEPLOYMENT_LOGS_BASIN_NAME, stream: `projects/${project.externalRef}/deployments/${deployment.shortCode}`, accessToken, }, }; } } return { eventStream, deployment: { id: deployment.id, shortCode: deployment.shortCode, version: deployment.version, status: deployment.status, createdAt: deployment.createdAt, startedAt: deployment.startedAt, installedAt: deployment.installedAt, builtAt: deployment.builtAt, deployedAt: deployment.deployedAt, canceledAt: deployment.canceledAt, canceledReason: deployment.canceledReason, tasks: deployment.worker?.tasks, label: deployment.promotions?.[0]?.label, environment: { id: deployment.environment.id, type: deployment.environment.type, slug: deployment.environment.slug, userId: deployment.environment.orgMember?.user.id, userName: getUsername(deployment.environment.orgMember?.user), }, deployedBy: deployment.triggeredBy, sdkVersion: deployment.worker?.sdkVersion, cliVersion: deployment.worker?.cliVersion, runtime: deployment.runtime, runtimeVersion: deployment.runtimeVersion, imageReference: deployment.imageReference, imagePlatform: deployment.imagePlatform, externalBuildData: externalBuildData && externalBuildData.success ? externalBuildData.data : undefined, projectId: deployment.projectId, organizationId: project.organizationId, errorData: DeploymentPresenter.prepareErrorData(deployment.errorData), isBuilt: !!deployment.builtAt, type: deployment.type, git: gitMetadata, triggeredVia: deployment.triggeredVia, vercelDeploymentUrl, }, }; } private async getS2AccessToken(projectRef: string): Promise { if (env.S2_ENABLED !== "1" || !s2) { throw new Error("Failed getting S2 access token: S2 is not enabled"); } const redisKey = `${S2_TOKEN_KEY_PREFIX}${projectRef}`; const cachedToken = await s2TokenRedis.get(redisKey); if (cachedToken) { return cachedToken; } const { accessToken } = await s2.accessTokens.issue({ id: `${projectRef}-${new Date().getTime()}`, expiresAt: new Date(Date.now() + 60 * 60 * 1000), // 1 hour scope: { ops: ["read"], basins: { exact: env.S2_DEPLOYMENT_LOGS_BASIN_NAME, }, streams: { prefix: `projects/${projectRef}/deployments/`, }, }, }); await s2TokenRedis.setex( redisKey, 59 * 60, // slightly shorter than the token validity period accessToken ); return accessToken; } public static prepareErrorData(errorData: WorkerDeployment["errorData"]): ErrorData | undefined { if (!errorData) { return; } const deploymentError = DeploymentErrorData.safeParse(errorData); if (!deploymentError.success) { return; } return prepareDeploymentError(deploymentError.data); } }