From 7b296ce2fb080db71ac97faf8292ce0c55f16653 Mon Sep 17 00:00:00 2001 From: Oskar Otwinowski Date: Thu, 12 Feb 2026 17:24:43 +0100 Subject: [PATCH 1/5] feat(deployments): propagate build metadata and conditionally enqueue Include build server metadata in progress API and deployment handling, add skipEnqueue flag, and avoid enque builds for GitHub-integrated ments without artifact keys. - Add BuildServerMetadata schema and include it in ProgressDeploymentRequestBody. - Thread buildServerMetadata through API handler into deployment service. - Make artifactKey optional for native builds and add skipEnqueue with default false to request validation. - When progressing deployments, merge incoming buildServerMetadata and validate against existing metadata. - Only enqueue native builds when artifactKey is present and skipEnqueue is false. This prevents enqueue attempts for GitHub integrated builds that lack an artifactKey. --- ...i.v1.deployments.$deploymentId.progress.ts | 1 + .../app/v3/services/deployment.server.ts | 59 ++++++++++++-- .../services/initializeDeployment.server.ts | 4 +- packages/core/src/v3/schemas/api.ts | 80 +++++++++---------- 4 files changed, 92 insertions(+), 52 deletions(-) diff --git a/apps/webapp/app/routes/api.v1.deployments.$deploymentId.progress.ts b/apps/webapp/app/routes/api.v1.deployments.$deploymentId.progress.ts index 64f20f4f64..2c78c59f55 100644 --- a/apps/webapp/app/routes/api.v1.deployments.$deploymentId.progress.ts +++ b/apps/webapp/app/routes/api.v1.deployments.$deploymentId.progress.ts @@ -48,6 +48,7 @@ export async function action({ request, params }: ActionFunctionArgs) { contentHash: body.data.contentHash, git: body.data.gitMeta, runtime: body.data.runtime, + buildServerMetadata: body.data.buildServerMetadata, }) .match( () => { diff --git a/apps/webapp/app/v3/services/deployment.server.ts b/apps/webapp/app/v3/services/deployment.server.ts index 848c06c453..176b1947af 100644 --- a/apps/webapp/app/v3/services/deployment.server.ts +++ b/apps/webapp/app/v3/services/deployment.server.ts @@ -2,7 +2,12 @@ import { type AuthenticatedEnvironment } from "~/services/apiAuth.server"; import { BaseService } from "./baseService.server"; import { errAsync, fromPromise, okAsync, type ResultAsync } from "neverthrow"; import { type WorkerDeployment, type Project } from "@trigger.dev/database"; -import { BuildServerMetadata, logger, type GitMeta, type DeploymentEvent } from "@trigger.dev/core/v3"; +import { + BuildServerMetadata, + logger, + type GitMeta, + type DeploymentEvent, +} from "@trigger.dev/core/v3"; import { TimeoutDeploymentService } from "./timeoutDeployment.server"; import { env } from "~/env.server"; import { createRemoteImageBuild } from "../remoteImageBuilder.server"; @@ -38,9 +43,20 @@ export class DeploymentService extends BaseService { public progressDeployment( authenticatedEnv: AuthenticatedEnvironment, friendlyId: string, - updates: Partial & { git: GitMeta }> + updates: Partial< + Pick & { + git: GitMeta; + buildServerMetadata: BuildServerMetadata; + } + > ) { - const validateDeployment = (deployment: Pick & { buildServerMetadata?: BuildServerMetadata }) => { + const { buildServerMetadata: newBuildServerMetadata, ...restUpdates } = updates; + + const validateDeployment = ( + deployment: Pick & { + buildServerMetadata?: BuildServerMetadata; + } + ) => { if (deployment.status !== "PENDING" && deployment.status !== "INSTALLING") { logger.warn( "Attempted progressing deployment that is not in PENDING or INSTALLING status", @@ -54,12 +70,27 @@ export class DeploymentService extends BaseService { return okAsync(deployment); }; - const progressToInstalling = (deployment: Pick) => - fromPromise( + const progressToInstalling = ( + deployment: Pick & { buildServerMetadata?: BuildServerMetadata } + ) => { + const existingBuildServerMetadata = deployment.buildServerMetadata as + | BuildServerMetadata + | null + | undefined; + + return fromPromise( this._prisma.workerDeployment.updateMany({ where: { id: deployment.id, status: "PENDING" }, // status could've changed in the meantime, we're not locking the row data: { - ...updates, + ...restUpdates, + ...(newBuildServerMetadata + ? { + buildServerMetadata: { + ...(existingBuildServerMetadata ?? {}), + ...newBuildServerMetadata, + }, + } + : {}), status: "INSTALLING", startedAt: new Date(), }, @@ -74,6 +105,7 @@ export class DeploymentService extends BaseService { } return okAsync({ id: deployment.id, status: "INSTALLING" as const }); }); + }; const progressToBuilding = ( deployment: Pick & { buildServerMetadata?: BuildServerMetadata } @@ -85,13 +117,26 @@ export class DeploymentService extends BaseService { cause: error, })); + const existingBuildServerMetadata = deployment.buildServerMetadata as + | BuildServerMetadata + | null + | undefined; + return createRemoteBuildIfNeeded .andThen((externalBuildData) => fromPromise( this._prisma.workerDeployment.updateMany({ where: { id: deployment.id, status: "INSTALLING" }, // status could've changed in the meantime, we're not locking the row data: { - ...updates, + ...restUpdates, + ...(newBuildServerMetadata + ? { + buildServerMetadata: { + ...(existingBuildServerMetadata ?? {}), + ...newBuildServerMetadata, + }, + } + : {}), externalBuildData, status: "BUILDING", installedAt: new Date(), diff --git a/apps/webapp/app/v3/services/initializeDeployment.server.ts b/apps/webapp/app/v3/services/initializeDeployment.server.ts index 96439d94d6..939cfd7e6f 100644 --- a/apps/webapp/app/v3/services/initializeDeployment.server.ts +++ b/apps/webapp/app/v3/services/initializeDeployment.server.ts @@ -200,6 +200,7 @@ export class InitializeDeploymentService extends BaseService { artifactKey: payload.artifactKey, skipPromotion: payload.skipPromotion, configFilePath: payload.configFilePath, + skipEnqueue: payload.skipEnqueue, } : {}), } @@ -238,7 +239,8 @@ export class InitializeDeploymentService extends BaseService { new Date(Date.now() + timeoutMs) ); - if (payload.isNativeBuild) { + // 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, diff --git a/packages/core/src/v3/schemas/api.ts b/packages/core/src/v3/schemas/api.ts index 4cb5c96503..5591760879 100644 --- a/packages/core/src/v3/schemas/api.ts +++ b/packages/core/src/v3/schemas/api.ts @@ -485,10 +485,22 @@ export const FinalizeDeploymentRequestBody = z.object({ export type FinalizeDeploymentRequestBody = z.infer; +export const BuildServerMetadata = z.object({ + buildId: z.string().optional(), + isNativeBuild: z.boolean().optional(), + artifactKey: z.string().optional(), + skipPromotion: z.boolean().optional(), + configFilePath: z.string().optional(), + skipEnqueue: z.boolean().optional(), +}); + +export type BuildServerMetadata = z.infer; + export const ProgressDeploymentRequestBody = z.object({ contentHash: z.string().optional(), gitMeta: GitMeta.optional(), runtime: z.string().optional(), + buildServerMetadata: BuildServerMetadata.optional(), }); export type ProgressDeploymentRequestBody = z.infer; @@ -528,16 +540,6 @@ export const DeploymentTriggeredVia = z export type DeploymentTriggeredVia = z.infer; -export const BuildServerMetadata = z.object({ - buildId: z.string().optional(), - isNativeBuild: z.boolean().optional(), - artifactKey: z.string().optional(), - skipPromotion: z.boolean().optional(), - configFilePath: z.string().optional(), -}); - -export type BuildServerMetadata = z.infer; - export const UpsertBranchRequestBody = z.object({ git: GitMeta.optional(), env: z.enum(["preview"]), @@ -590,41 +592,31 @@ export const InitializeDeploymentResponseBody = z.object({ export type InitializeDeploymentResponseBody = z.infer; -export const InitializeDeploymentRequestBody = z - .object({ - contentHash: z.string(), - userId: z.string().optional(), - /** @deprecated This is now determined by the webapp. This is only used to warn users with old CLI versions. */ - selfHosted: z.boolean().optional(), - gitMeta: GitMeta.optional(), - type: z.enum(["MANAGED", "UNMANAGED", "V1"]).optional(), - runtime: z.string().optional(), - initialStatus: z.enum(["PENDING", "BUILDING"]).optional(), - triggeredVia: DeploymentTriggeredVia.optional(), - buildId: z.string().optional(), +const InitializeDeploymentRequestBodyBase = z.object({ + contentHash: z.string(), + userId: z.string().optional(), + /** @deprecated This is now determined by the webapp. This is only used to warn users with old CLI versions. */ + selfHosted: z.boolean().optional(), + gitMeta: GitMeta.optional(), + type: z.enum(["MANAGED", "UNMANAGED", "V1"]).optional(), + runtime: z.string().optional(), + initialStatus: z.enum(["PENDING", "BUILDING"]).optional(), + triggeredVia: DeploymentTriggeredVia.optional(), + buildId: z.string().optional(), + isNativeBuild: z.boolean().default(false).optional() +}) +export const InitializeDeploymentRequestBody = z.discriminatedUnion("isNativeBuild", [ + InitializeDeploymentRequestBodyBase.extend({ + isNativeBuild: z.literal(true), + skipPromotion: z.boolean().optional(), + artifactKey: z.string().optional(), + configFilePath: z.string().optional(), + skipEnqueue: z.boolean().default(false).optional(), + }), + InitializeDeploymentRequestBodyBase.extend({ + isNativeBuild: z.literal(false).optional(), }) - .and( - z.preprocess( - (val) => { - const obj = val as any; - if (!obj || !obj.isNativeBuild) { - return { ...obj, isNativeBuild: false }; - } - return obj; - }, - z.discriminatedUnion("isNativeBuild", [ - z.object({ - isNativeBuild: z.literal(true), - skipPromotion: z.boolean(), - artifactKey: z.string(), - configFilePath: z.string().optional(), - }), - z.object({ - isNativeBuild: z.literal(false), - }), - ]) - ) - ); +]); export type InitializeDeploymentRequestBody = z.infer; From 349feb011f1a509dc9c39112a1df75994250074a Mon Sep 17 00:00:00 2001 From: Oskar Otwinowski Date: Thu, 12 Feb 2026 17:25:22 +0100 Subject: [PATCH 2/5] feat(webapp): auto-select and optimize project selector Automatically set the Vercel project when entering project-selection if there is at least one available project and none selected. This prevents an empty selection state and reduces manual clicks for the common case of a single or first project. Disable the Select when there is exactly one available project to make the intent explicit and avoid unnecessary interaction. Add a filter prop to the Select only when there are more than five projects to keep the dropdown performant and avoid enabling filtering for small lists. Adjust the effect deps to trigger selection only on relevant state and project list changes. --- .../app/components/integrations/VercelOnboardingModal.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/apps/webapp/app/components/integrations/VercelOnboardingModal.tsx b/apps/webapp/app/components/integrations/VercelOnboardingModal.tsx index c2a5bfec43..ec541b6647 100644 --- a/apps/webapp/app/components/integrations/VercelOnboardingModal.tsx +++ b/apps/webapp/app/components/integrations/VercelOnboardingModal.tsx @@ -565,6 +565,12 @@ export function VercelOnboardingModal({ } }, [state, customEnvironments, vercelStagingEnvironment]); + useEffect(() => { + if (state === "project-selection" && availableProjects.length > 0 && !selectedVercelProject) { + setSelectedVercelProject(availableProjects[0]); + } + }, [state, availableProjects, selectedVercelProject]); + if (!isOpen || onboardingData?.authInvalid) { return null; } @@ -625,6 +631,7 @@ export function VercelOnboardingModal({ ) : (