Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 6 additions & 20 deletions apps/webapp/app/components/integrations/VercelOnboardingModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,11 @@ export function VercelOnboardingModal({
}
return "project-selection";
}
// If onboarding was already completed but GitHub is not connected,
// go directly to the github-connection step (e.g., returning from GitHub App installation)
if (onboardingData?.isOnboardingComplete && !onboardingData?.isGitHubConnected) {
return "github-connection";
}
// For marketplace origin, skip env-mapping step and go directly to env-var-sync
if (!fromMarketplaceContext) {
const customEnvs = (onboardingData?.customEnvironments?.length ?? 0) > 0 && hasStagingEnvironment;
Expand Down Expand Up @@ -1159,26 +1164,7 @@ export function VercelOnboardingModal({
>
Complete
</Button>
) : (
<Button
variant="tertiary/medium"
onClick={() => {
trackOnboarding("vercel onboarding github skipped");
setState("completed");
if (fromMarketplaceContext && nextUrl) {
const validUrl = safeRedirectUrl(nextUrl);
if (validUrl) {
window.location.href = validUrl;
}
}
}}
>
Skip for now
</Button>
)
}
cancelButton={
isGitHubConnectedForOnboarding && fromMarketplaceContext && nextUrl ? (
) : !fromMarketplaceContext ? (
<Button
variant="tertiary/medium"
onClick={() => {
Expand Down
30 changes: 17 additions & 13 deletions apps/webapp/app/routes/login._index/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
showGoogleAuth: isGoogleAuthSupported,
lastAuthMethod,
authError: null,
isVercelMarketplace: redirectTo.startsWith("/vercel/callback"),
},
{
headers: {
Expand All @@ -106,6 +107,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
showGoogleAuth: isGoogleAuthSupported,
lastAuthMethod,
authError,
isVercelMarketplace: false,
});
}
}
Expand Down Expand Up @@ -164,19 +166,21 @@ export default function LoginPage() {
</Form>
</div>
)}
<div className="relative w-full">
{data.lastAuthMethod === "email" && <LastUsedBadge />}
<LinkButton
to={data.redirectTo ? `/login/magic?redirectTo=${encodeURIComponent(data.redirectTo)}` : "/login/magic"}
variant="secondary/extra-large"
fullWidth
data-action="continue with email"
className="text-text-bright"
>
<EnvelopeIcon className="mr-2 size-5 text-text-bright" />
Continue with Email
</LinkButton>
</div>
{!data.isVercelMarketplace && (
<div className="relative w-full">
{data.lastAuthMethod === "email" && <LastUsedBadge />}
<LinkButton
to={data.redirectTo ? `/login/magic?redirectTo=${encodeURIComponent(data.redirectTo)}` : "/login/magic"}
variant="secondary/extra-large"
fullWidth
data-action="continue with email"
className="text-text-bright"
>
<EnvelopeIcon className="mr-2 size-5 text-text-bright" />
Continue with Email
</LinkButton>
</div>
)}
{data.authError && <FormError>{data.authError}</FormError>}
</div>
<Paragraph variant="extra-small" className="mt-2 text-center">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server";
import { ProjectSettingsService } from "~/services/projectSettings.server";
import { logger } from "~/services/logger.server";
import { triggerInitialDeployment } from "~/services/platform.v3.server";
import { VercelIntegrationService } from "~/services/vercelIntegration.server";
import { requireUserId } from "~/services/session.server";
import {
githubAppInstallPath,
Expand Down Expand Up @@ -210,16 +211,22 @@ export async function action({ request, params }: ActionFunctionArgs) {
);

if (resultOrFail.isOk()) {
// Trigger initial deployment for marketplace flows now that GitHub is connected
if (redirectUrl) {
try {
if (redirectUrl.includes("origin=marketplace")) {
await triggerInitialDeployment(projectId, { environment: "prod" });
}
} catch (error) {
logger.error("Invalid redirect URL, skipping initial deployment trigger", { redirectUrl, error });
// Invalid redirectUrl, skip initial deployment check
// Trigger initial deployment for marketplace flows now that GitHub is connected.
// We check the persisted onboardingOrigin on the Vercel integration rather than
// the redirectUrl, because the redirect URL loses the marketplace context when
// the user installs the GitHub App for the first time (full-page redirect cycle).
try {
const vercelService = new VercelIntegrationService();
const vercelIntegration = await vercelService.getVercelProjectIntegration(projectId);
if (
vercelIntegration?.parsedIntegrationData.onboardingCompleted &&
vercelIntegration.parsedIntegrationData.onboardingOrigin === "marketplace"
) {
logger.info("Marketplace flow detected, triggering initial deployment", { projectId });
await triggerInitialDeployment(projectId, { environment: "prod" });
Comment on lines +214 to +226
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

This will retrigger the “initial” deployment on later GitHub reconnects.

onboardingCompleted and onboardingOrigin === "marketplace" are durable integration fields. Once a marketplace project has been onboarded, every future connect-repo action still satisfies this branch, so disconnecting/reconnecting GitHub later will queue another initial deployment. This needs a one-shot pending flag that is consumed after the first successful trigger, or the onboarding marker needs to be cleared immediately after it is used.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/webapp/app/routes/resources.orgs`.$organizationSlug.projects.$projectParam.env.$envParam.github.tsx
around lines 214 - 226, The current logic in
VercelIntegrationService.getVercelProjectIntegration and the branch checking
vercelIntegration.parsedIntegrationData.onboardingCompleted && onboardingOrigin
=== "marketplace" will retrigger initial deployments on any future reconnect;
add a one-shot marker (e.g., parsedIntegrationData.pendingInitialDeployment or
use a method like VercelIntegrationService.clearOnboardingMarker) and update the
integration record so that after you call triggerInitialDeployment(projectId, {
environment: "prod" }) you persistently consume that flag (set
pendingInitialDeployment = false or clear onboardingOrigin/onboardingCompleted)
via VercelIntegrationService (add/update methods as needed) so the branch only
fires once.

}
} catch (error) {
logger.error("Failed to check Vercel integration or trigger initial deployment", { projectId, error });
}

return redirectWithMessage(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@ export async function action({ request, params }: ActionFunctionArgs) {
syncEnvVarsMapping,
next,
skipRedirect,
origin,
} = submission.value;

const parsedStagingEnv = parseVercelStagingEnvironment(vercelStagingEnvironment);
Expand All @@ -306,6 +307,7 @@ export async function action({ request, params }: ActionFunctionArgs) {
atomicBuilds,
discoverEnvVars,
syncEnvVarsMapping: parsedSyncEnvVarsMapping,
origin: origin === "marketplace" ? "marketplace" : "dashboard",
});

if (result) {
Expand Down
4 changes: 3 additions & 1 deletion apps/webapp/app/services/vercelIntegration.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,7 @@ export class VercelIntegrationService {
atomicBuilds?: EnvSlug[] | null;
discoverEnvVars?: EnvSlug[] | null;
syncEnvVarsMapping?: SyncEnvVarsMapping;
origin?: "marketplace" | "dashboard";
}
): Promise<VercelProjectIntegrationWithParsedData | null> {
const existing = await this.getVercelProjectIntegration(projectId);
Expand All @@ -544,8 +545,9 @@ export class VercelIntegrationService {
vercelStagingEnvironment: params.vercelStagingEnvironment ?? null,
},
//This is intentionally not updated here, in case of resetting the onboarding it should not override the existing mapping with an empty one
syncEnvVarsMapping: existing.parsedIntegrationData.syncEnvVarsMapping,
syncEnvVarsMapping: existing.parsedIntegrationData.syncEnvVarsMapping,
onboardingCompleted: true,
onboardingOrigin: params.origin ?? existing.parsedIntegrationData.onboardingOrigin,
};

const updated = await this.#prismaClient.organizationProjectIntegration.update({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export const VercelProjectIntegrationDataSchema = z.object({
vercelTeamSlug: z.string().optional(),
vercelProjectId: z.string(),
onboardingCompleted: z.boolean().optional(),
onboardingOrigin: z.enum(["marketplace", "dashboard"]).optional(),
});

export type VercelProjectIntegrationData = z.infer<typeof VercelProjectIntegrationDataSchema>;
Expand Down
Loading