feat(webapp/deployments): Vercel improvements & fixes (#3037)
This commit is contained in:
Vendored
+9
@@ -31,6 +31,15 @@
|
||||
"cwd": "${workspaceFolder}/apps/webapp",
|
||||
"sourceMaps": true
|
||||
},
|
||||
{
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"name": "Debug opened test file",
|
||||
"command": "pnpm run test -- ./${relativeFile}",
|
||||
"envFile": "${workspaceFolder}/.env",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"sourceMaps": true
|
||||
},
|
||||
{
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
|
||||
@@ -47,6 +47,7 @@ import { type VercelOnboardingData } from "~/presenters/v3/VercelSettingsPresent
|
||||
import { vercelAppInstallPath, v3ProjectSettingsPath, githubAppInstallPath, vercelResourcePath } from "~/utils/pathBuilder";
|
||||
import type { loader } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.vercel";
|
||||
import { useEffect, useState, useCallback, useRef } from "react";
|
||||
import { usePostHogTracking } from "~/hooks/usePostHog";
|
||||
|
||||
function safeRedirectUrl(url: string): string | null {
|
||||
try {
|
||||
@@ -114,6 +115,7 @@ export function VercelOnboardingModal({
|
||||
nextUrl?: string;
|
||||
onDataReload?: (vercelStagingEnvironment?: string) => void;
|
||||
}) {
|
||||
const { capture, startSessionRecording } = usePostHogTracking();
|
||||
const navigation = useNavigation();
|
||||
const fetcher = useTypedFetcher<typeof loader>();
|
||||
const envMappingFetcher = useFetcher();
|
||||
@@ -172,6 +174,33 @@ export function VercelOnboardingModal({
|
||||
prevIsOpenRef.current = isOpen;
|
||||
}, [isOpen, state, computeInitialState]);
|
||||
|
||||
const trackOnboarding = useCallback(
|
||||
(eventName: string, extraProperties?: Record<string, unknown>) => {
|
||||
capture(eventName, {
|
||||
origin: fromMarketplaceContext ? "marketplace" : "dashboard",
|
||||
step: state,
|
||||
organization_slug: organizationSlug,
|
||||
project_slug: projectSlug,
|
||||
...extraProperties,
|
||||
});
|
||||
},
|
||||
[capture, fromMarketplaceContext, state, organizationSlug, projectSlug]
|
||||
);
|
||||
|
||||
const hasTrackedStartRef = useRef(false);
|
||||
const hasTrackedCompletionRef = useRef(false);
|
||||
useEffect(() => {
|
||||
if (isOpen && state === "project-selection" && !hasTrackedStartRef.current) {
|
||||
hasTrackedStartRef.current = true;
|
||||
startSessionRecording();
|
||||
trackOnboarding("vercel onboarding started");
|
||||
}
|
||||
if (!isOpen) {
|
||||
hasTrackedStartRef.current = false;
|
||||
hasTrackedCompletionRef.current = false;
|
||||
}
|
||||
}, [isOpen, state, trackOnboarding, startSessionRecording]);
|
||||
|
||||
const [selectedVercelProject, setSelectedVercelProject] = useState<{
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -337,6 +366,9 @@ export function VercelOnboardingModal({
|
||||
|
||||
useEffect(() => {
|
||||
if (state === "project-selection" && fetcher.data && "success" in fetcher.data && fetcher.data.success && fetcher.state === "idle") {
|
||||
trackOnboarding("vercel onboarding project selected", {
|
||||
vercel_project_name: selectedVercelProject?.name,
|
||||
});
|
||||
setState("loading-env-mapping");
|
||||
if (onDataReload) {
|
||||
onDataReload();
|
||||
@@ -344,7 +376,7 @@ export function VercelOnboardingModal({
|
||||
} else if (fetcher.data && "error" in fetcher.data && typeof fetcher.data.error === "string") {
|
||||
setProjectSelectionError(fetcher.data.error);
|
||||
}
|
||||
}, [state, fetcher.data, fetcher.state, onDataReload]);
|
||||
}, [state, fetcher.data, fetcher.state, onDataReload, trackOnboarding, selectedVercelProject?.name]);
|
||||
|
||||
// For marketplace origin, skip env-mapping step
|
||||
useEffect(() => {
|
||||
@@ -449,14 +481,23 @@ export function VercelOnboardingModal({
|
||||
method: "post",
|
||||
action: actionUrl,
|
||||
});
|
||||
}, [actionUrl, fetcher, onClose, nextUrl, fromMarketplaceContext]);
|
||||
}, [actionUrl, fetcher, onClose, fromMarketplaceContext]);
|
||||
|
||||
const handleSkipEnvMapping = useCallback(() => {
|
||||
trackOnboarding("vercel onboarding env mapping completed", {
|
||||
skipped: true,
|
||||
staging_environment: null,
|
||||
});
|
||||
setVercelStagingEnvironment(null);
|
||||
setState("loading-env-vars");
|
||||
}, []);
|
||||
}, [trackOnboarding]);
|
||||
|
||||
const handleUpdateEnvMapping = useCallback(() => {
|
||||
trackOnboarding("vercel onboarding env mapping completed", {
|
||||
skipped: false,
|
||||
staging_environment: vercelStagingEnvironment?.displayName ?? null,
|
||||
});
|
||||
|
||||
if (!vercelStagingEnvironment) {
|
||||
setState("loading-env-vars");
|
||||
return;
|
||||
@@ -471,9 +512,11 @@ export function VercelOnboardingModal({
|
||||
action: actionUrl,
|
||||
});
|
||||
|
||||
}, [vercelStagingEnvironment, envMappingFetcher, actionUrl]);
|
||||
}, [vercelStagingEnvironment, envMappingFetcher, actionUrl, trackOnboarding]);
|
||||
|
||||
const handleBuildSettingsNext = useCallback(() => {
|
||||
trackOnboarding("vercel onboarding build settings completed");
|
||||
|
||||
if (nextUrl && fromMarketplaceContext && isGitHubConnectedForOnboarding) {
|
||||
setIsRedirecting(true);
|
||||
}
|
||||
@@ -501,7 +544,7 @@ export function VercelOnboardingModal({
|
||||
if (!isGitHubConnectedForOnboarding) {
|
||||
setState("github-connection");
|
||||
}
|
||||
}, [vercelStagingEnvironment, pullEnvVarsBeforeBuild, atomicBuilds, discoverEnvVars, syncEnvVarsMapping, nextUrl, fromMarketplaceContext, isGitHubConnectedForOnboarding, completeOnboardingFetcher, actionUrl]);
|
||||
}, [vercelStagingEnvironment, pullEnvVarsBeforeBuild, atomicBuilds, discoverEnvVars, syncEnvVarsMapping, nextUrl, fromMarketplaceContext, isGitHubConnectedForOnboarding, completeOnboardingFetcher, actionUrl, trackOnboarding]);
|
||||
|
||||
const handleFinishOnboarding = useCallback((e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
@@ -530,10 +573,14 @@ export function VercelOnboardingModal({
|
||||
}, [completeOnboardingFetcher.data, completeOnboardingFetcher.state, state]);
|
||||
|
||||
useEffect(() => {
|
||||
if (state === "completed") {
|
||||
if (state === "completed" && !hasTrackedCompletionRef.current) {
|
||||
hasTrackedCompletionRef.current = true;
|
||||
trackOnboarding("vercel onboarding completed", {
|
||||
github_connected: isGitHubConnectedForOnboarding,
|
||||
});
|
||||
onClose();
|
||||
}
|
||||
}, [state, onClose]);
|
||||
}, [state, onClose, trackOnboarding, isGitHubConnectedForOnboarding]);
|
||||
|
||||
useEffect(() => {
|
||||
if (state === "installing") {
|
||||
@@ -565,6 +612,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;
|
||||
}
|
||||
@@ -578,7 +631,14 @@ export function VercelOnboardingModal({
|
||||
|
||||
if (isLoadingState) {
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={(open) => !open && !fromMarketplaceContext && onClose()}>
|
||||
<Dialog open={isOpen} onOpenChange={(open) => {
|
||||
if (!open && !fromMarketplaceContext) {
|
||||
if (state as string !== "completed") {
|
||||
trackOnboarding("vercel onboarding abandoned");
|
||||
}
|
||||
onClose();
|
||||
}
|
||||
}}>
|
||||
<DialogContent className="max-w-lg">
|
||||
<DialogHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -601,7 +661,14 @@ export function VercelOnboardingModal({
|
||||
const showGitHubConnection = state === "github-connection";
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={(open) => !open && !fromMarketplaceContext && onClose()}>
|
||||
<Dialog open={isOpen} onOpenChange={(open) => {
|
||||
if (!open && !fromMarketplaceContext) {
|
||||
if (state !== "completed") {
|
||||
trackOnboarding("vercel onboarding abandoned");
|
||||
}
|
||||
onClose();
|
||||
}
|
||||
}}>
|
||||
<DialogContent className="max-w-lg">
|
||||
<DialogHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -625,6 +692,7 @@ export function VercelOnboardingModal({
|
||||
</Callout>
|
||||
) : (
|
||||
<Select
|
||||
disabled={availableProjects.length === 1}
|
||||
value={selectedVercelProject?.id || ""}
|
||||
setValue={(value) => {
|
||||
if (!Array.isArray(value)) {
|
||||
@@ -634,6 +702,7 @@ export function VercelOnboardingModal({
|
||||
}
|
||||
}}
|
||||
items={availableProjects}
|
||||
filter={availableProjects.length > 5 ? { keys: ["name"] } : undefined}
|
||||
variant="tertiary/medium"
|
||||
placeholder="Select a Vercel project"
|
||||
dropdownIcon
|
||||
@@ -894,6 +963,10 @@ export function VercelOnboardingModal({
|
||||
<Button
|
||||
variant="primary/medium"
|
||||
onClick={() => {
|
||||
trackOnboarding("vercel onboarding env vars configured", {
|
||||
env_vars_enabled: enabledEnvVars.length,
|
||||
env_vars_total: syncableEnvVars.length,
|
||||
});
|
||||
if (fromMarketplaceContext) {
|
||||
handleBuildSettingsNext();
|
||||
} else {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useLocation } from "@remix-run/react";
|
||||
import posthog from "posthog-js";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { useOrganizationChanged } from "./useOrganizations";
|
||||
import { useOptionalUser, useUserChanged } from "./useUser";
|
||||
import { useProjectChanged } from "./useProject";
|
||||
@@ -68,3 +68,18 @@ export const usePostHog = (apiKey?: string, logging = false, debug = false): voi
|
||||
posthog.capture("$pageview");
|
||||
}, [location, logging]);
|
||||
};
|
||||
|
||||
export function usePostHogTracking() {
|
||||
const capture = useCallback(
|
||||
(eventName: string, properties?: Record<string, unknown>) => {
|
||||
posthog.capture(eventName, properties);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const startSessionRecording = useCallback(() => {
|
||||
posthog.startSessionRecording();
|
||||
}, []);
|
||||
|
||||
return { capture, startSessionRecording };
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
() => {
|
||||
|
||||
@@ -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<Pick<WorkerDeployment, "contentHash" | "runtime"> & { git: GitMeta }>
|
||||
updates: Partial<
|
||||
Pick<WorkerDeployment, "contentHash" | "runtime"> & {
|
||||
git: GitMeta;
|
||||
buildServerMetadata: BuildServerMetadata;
|
||||
}
|
||||
>
|
||||
) {
|
||||
const validateDeployment = (deployment: Pick<WorkerDeployment, "id" | "status"> & { buildServerMetadata?: BuildServerMetadata }) => {
|
||||
const { buildServerMetadata: newBuildServerMetadata, ...restUpdates } = updates;
|
||||
|
||||
const validateDeployment = (
|
||||
deployment: Pick<WorkerDeployment, "id" | "status"> & {
|
||||
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,24 @@ export class DeploymentService extends BaseService {
|
||||
return okAsync(deployment);
|
||||
};
|
||||
|
||||
const progressToInstalling = (deployment: Pick<WorkerDeployment, "id">) =>
|
||||
fromPromise(
|
||||
const progressToInstalling = (
|
||||
deployment: Pick<WorkerDeployment, "id"> & { buildServerMetadata?: BuildServerMetadata }
|
||||
) => {
|
||||
const existingBuildServerMetadata = deployment.buildServerMetadata;
|
||||
|
||||
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 +102,7 @@ export class DeploymentService extends BaseService {
|
||||
}
|
||||
return okAsync({ id: deployment.id, status: "INSTALLING" as const });
|
||||
});
|
||||
};
|
||||
|
||||
const progressToBuilding = (
|
||||
deployment: Pick<WorkerDeployment, "id"> & { buildServerMetadata?: BuildServerMetadata }
|
||||
@@ -85,13 +114,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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { InitializeDeploymentRequestBody } from "./api.js";
|
||||
import type { InitializeDeploymentRequestBody as InitializeDeploymentRequestBodyType } from "./api.js";
|
||||
|
||||
describe("InitializeDeploymentRequestBody", () => {
|
||||
const base = { contentHash: "abc123" };
|
||||
|
||||
describe("non-native build variant (isNativeBuild omitted or false)", () => {
|
||||
it("parses with only required fields", () => {
|
||||
const result = InitializeDeploymentRequestBody.safeParse(base);
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.isNativeBuild).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("parses with isNativeBuild explicitly false", () => {
|
||||
const result = InitializeDeploymentRequestBody.safeParse({ ...base, isNativeBuild: false });
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("parses with optional base fields", () => {
|
||||
const result = InitializeDeploymentRequestBody.safeParse({
|
||||
...base,
|
||||
userId: "user_1",
|
||||
type: "MANAGED",
|
||||
runtime: "node",
|
||||
initialStatus: "PENDING",
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("strips native-only fields when isNativeBuild is false", () => {
|
||||
const result = InitializeDeploymentRequestBody.safeParse({
|
||||
...base,
|
||||
isNativeBuild: false,
|
||||
skipPromotion: true,
|
||||
});
|
||||
// Zod discriminatedUnion matches the non-native branch and strips unknown keys
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect("skipPromotion" in result.data).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("native build variant (isNativeBuild: true)", () => {
|
||||
it("parses with isNativeBuild true", () => {
|
||||
const result = InitializeDeploymentRequestBody.safeParse({
|
||||
...base,
|
||||
isNativeBuild: true,
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.isNativeBuild).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("parses with native-specific optional fields", () => {
|
||||
const result = InitializeDeploymentRequestBody.safeParse({
|
||||
...base,
|
||||
isNativeBuild: true,
|
||||
skipPromotion: true,
|
||||
artifactKey: "artifact_abc",
|
||||
configFilePath: "trigger.config.ts",
|
||||
skipEnqueue: true,
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.skipPromotion).toBe(true);
|
||||
expect(result.data.artifactKey).toBe("artifact_abc");
|
||||
expect(result.data.configFilePath).toBe("trigger.config.ts");
|
||||
expect(result.data.skipEnqueue).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("skipEnqueue defaults to false when omitted", () => {
|
||||
const result = InitializeDeploymentRequestBody.safeParse({
|
||||
...base,
|
||||
isNativeBuild: true,
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.skipEnqueue).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("rejects invalid inputs", () => {
|
||||
it("rejects missing contentHash", () => {
|
||||
const result = InitializeDeploymentRequestBody.safeParse({});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects invalid type enum value", () => {
|
||||
const result = InitializeDeploymentRequestBody.safeParse({
|
||||
...base,
|
||||
type: "INVALID",
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects invalid initialStatus enum value", () => {
|
||||
const result = InitializeDeploymentRequestBody.safeParse({
|
||||
...base,
|
||||
initialStatus: "RUNNING",
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("type-level checks", () => {
|
||||
it("native variant exposes native-specific fields", () => {
|
||||
const result = InitializeDeploymentRequestBody.parse({
|
||||
...base,
|
||||
isNativeBuild: true,
|
||||
skipPromotion: true,
|
||||
});
|
||||
|
||||
if (result.isNativeBuild === true) {
|
||||
const _skipPromotion: boolean | undefined = result.skipPromotion;
|
||||
const _artifactKey: string | undefined = result.artifactKey;
|
||||
const _configFilePath: string | undefined = result.configFilePath;
|
||||
expect(_skipPromotion).toBe(true);
|
||||
expect(_artifactKey).toBeUndefined();
|
||||
expect(_configFilePath).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
it("non-native variant narrows correctly", () => {
|
||||
const result: InitializeDeploymentRequestBodyType =
|
||||
InitializeDeploymentRequestBody.parse(base);
|
||||
|
||||
if (!result.isNativeBuild) {
|
||||
// Should only have base fields — native-specific fields should not exist
|
||||
const narrowed: { isNativeBuild?: false; contentHash: string } = result;
|
||||
expect(narrowed.contentHash).toBe("abc123");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -485,10 +485,22 @@ export const FinalizeDeploymentRequestBody = z.object({
|
||||
|
||||
export type FinalizeDeploymentRequestBody = z.infer<typeof FinalizeDeploymentRequestBody>;
|
||||
|
||||
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<typeof BuildServerMetadata>;
|
||||
|
||||
export const ProgressDeploymentRequestBody = z.object({
|
||||
contentHash: z.string().optional(),
|
||||
gitMeta: GitMeta.optional(),
|
||||
runtime: z.string().optional(),
|
||||
buildServerMetadata: BuildServerMetadata.optional(),
|
||||
});
|
||||
|
||||
export type ProgressDeploymentRequestBody = z.infer<typeof ProgressDeploymentRequestBody>;
|
||||
@@ -528,16 +540,6 @@ export const DeploymentTriggeredVia = z
|
||||
|
||||
export type DeploymentTriggeredVia = z.infer<typeof DeploymentTriggeredVia>;
|
||||
|
||||
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<typeof BuildServerMetadata>;
|
||||
|
||||
export const UpsertBranchRequestBody = z.object({
|
||||
git: GitMeta.optional(),
|
||||
env: z.enum(["preview"]),
|
||||
@@ -590,41 +592,53 @@ export const InitializeDeploymentResponseBody = z.object({
|
||||
|
||||
export type InitializeDeploymentResponseBody = z.infer<typeof InitializeDeploymentResponseBody>;
|
||||
|
||||
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(),
|
||||
})
|
||||
.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),
|
||||
}),
|
||||
])
|
||||
)
|
||||
);
|
||||
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()
|
||||
});
|
||||
type BaseOutput = z.output<typeof InitializeDeploymentRequestBodyBase>;
|
||||
|
||||
type NativeBuildOutput = BaseOutput & {
|
||||
isNativeBuild: true;
|
||||
skipPromotion?: boolean;
|
||||
artifactKey?: string;
|
||||
configFilePath?: string;
|
||||
skipEnqueue?: boolean;
|
||||
};
|
||||
|
||||
type NonNativeBuildOutput = BaseOutput & {
|
||||
isNativeBuild: false;
|
||||
skipPromotion?: never;
|
||||
artifactKey?: never;
|
||||
configFilePath?: never;
|
||||
skipEnqueue?: never;
|
||||
};
|
||||
|
||||
const InitializeDeploymentRequestBodyFull = InitializeDeploymentRequestBodyBase.extend({
|
||||
isNativeBuild: z.boolean().default(false),
|
||||
skipPromotion: z.boolean().optional(),
|
||||
artifactKey: z.string().optional(),
|
||||
configFilePath: z.string().optional(),
|
||||
skipEnqueue: z.boolean().optional().default(false),
|
||||
});
|
||||
|
||||
export const InitializeDeploymentRequestBody = InitializeDeploymentRequestBodyFull.transform(
|
||||
(data): NativeBuildOutput | NonNativeBuildOutput => {
|
||||
if (data.isNativeBuild) {
|
||||
return { ...data, isNativeBuild: true as const };
|
||||
}
|
||||
const { skipPromotion, artifactKey, configFilePath, skipEnqueue, ...rest } = data;
|
||||
return { ...rest, isNativeBuild: false as const };
|
||||
}
|
||||
);
|
||||
|
||||
export type InitializeDeploymentRequestBody = z.infer<typeof InitializeDeploymentRequestBody>;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user