From 6d611ee6ac17bc702ad51495e562d89e17fd1450 Mon Sep 17 00:00:00 2001 From: myftija Date: Wed, 8 Oct 2025 15:21:41 +0200 Subject: [PATCH 1/3] Show hint if preview branches are disabled in the project --- .../route.tsx | 66 ++++++++--- .../projectSettingsPresenter.server.ts | 112 +++++++++--------- 2 files changed, 104 insertions(+), 74 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings/route.tsx index 1ba5f26407..45da9a87b3 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings/route.tsx @@ -60,6 +60,8 @@ import { githubAppInstallPath, EnvironmentParamSchema, v3ProjectSettingsPath, + docsPath, + v3BillingPath, } from "~/utils/pathBuilder"; import React, { useEffect, useState } from "react"; import { Select, SelectItem } from "~/components/primitives/Select"; @@ -77,6 +79,7 @@ import { TextLink } from "~/components/primitives/TextLink"; import { cn } from "~/utils/cn"; import { ProjectSettingsPresenter } from "~/services/projectSettingsPresenter.server"; import { type BuildSettings } from "~/v3/buildSettings"; +import { InfoIconTooltip } from "~/components/primitives/Tooltip"; export const meta: MetaFunction = () => { return [ @@ -126,6 +129,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { githubAppEnabled: gitHubApp.enabled, githubAppInstallations: gitHubApp.installations, connectedGithubRepository: gitHubApp.connectedRepository, + isPreviewEnvironmentEnabled: gitHubApp.isPreviewEnvironmentEnabled, buildSettings, }); }; @@ -433,8 +437,13 @@ export const action: ActionFunction = async ({ request, params }) => { }; export default function Page() { - const { githubAppInstallations, connectedGithubRepository, githubAppEnabled, buildSettings } = - useTypedLoaderData(); + const { + githubAppInstallations, + connectedGithubRepository, + githubAppEnabled, + buildSettings, + isPreviewEnvironmentEnabled, + } = useTypedLoaderData(); const project = useProject(); const organization = useOrganization(); const environment = useEnvironment(); @@ -561,7 +570,10 @@ export default function Page() { Git settings
{connectedGithubRepository ? ( - + ) : ( - Every commit on the selected tracking branch creates a deployment in the corresponding + Every push to the selected tracking branch creates a deployment in the corresponding environment. -
+
@@ -1054,19 +1069,34 @@ function ConnectedGitHubRepoForm({ {environmentFullTitle({ type: "PREVIEW" })}
- { - setGitSettingsValues((prev) => ({ - ...prev, - previewDeploymentsEnabled: checked, - })); - }} - /> +
+ { + setGitSettingsValues((prev) => ({ + ...prev, + previewDeploymentsEnabled: checked, + })); + }} + /> + {!previewEnvironmentEnabled && ( + + Upgrade your plan to + enable preview branches + + } + /> + )} +
{fields.productionBranch?.error} {fields.stagingBranch?.error} diff --git a/apps/webapp/app/services/projectSettingsPresenter.server.ts b/apps/webapp/app/services/projectSettingsPresenter.server.ts index 20c197d855..bb671c5610 100644 --- a/apps/webapp/app/services/projectSettingsPresenter.server.ts +++ b/apps/webapp/app/services/projectSettingsPresenter.server.ts @@ -3,7 +3,7 @@ import { prisma } from "~/db.server"; import { BranchTrackingConfigSchema } from "~/v3/github"; import { env } from "~/env.server"; import { findProjectBySlug } from "~/models/project.server"; -import { err, fromPromise, ok, okAsync } from "neverthrow"; +import { err, fromPromise, ok, ResultAsync } from "neverthrow"; import { BuildSettingsSchema } from "~/v3/buildSettings"; export class ProjectSettingsPresenter { @@ -20,33 +20,31 @@ export class ProjectSettingsPresenter { fromPromise(findProjectBySlug(organizationSlug, projectSlug, userId), (error) => ({ type: "other" as const, cause: error, - })).andThen((project) => { - if (!project) { - return err({ type: "project_not_found" as const }); - } - return ok(project); - }); + })) + .andThen((project) => { + if (!project) { + return err({ type: "project_not_found" as const }); + } + return ok(project); + }) + .map((project) => { + const buildSettingsOrFailure = BuildSettingsSchema.safeParse(project.buildSettings); + const buildSettings = buildSettingsOrFailure.success + ? buildSettingsOrFailure.data + : undefined; + return { ...project, buildSettings }; + }); if (!githubAppEnabled) { - return getProject().andThen((project) => { - if (!project) { - return err({ type: "project_not_found" as const }); - } - - const buildSettingsOrFailure = BuildSettingsSchema.safeParse(project.buildSettings); - const buildSettings = buildSettingsOrFailure.success - ? buildSettingsOrFailure.data - : undefined; - - return ok({ - gitHubApp: { - enabled: false, - connectedRepository: undefined, - installations: undefined, - }, - buildSettings, - }); - }); + return getProject().map(({ buildSettings }) => ({ + gitHubApp: { + enabled: false, + connectedRepository: undefined, + installations: undefined, + isPreviewEnvironmentEnabled: undefined, + }, + buildSettings, + })); } const findConnectedGithubRepository = (projectId: string) => @@ -136,37 +134,39 @@ export class ProjectSettingsPresenter { }) ); - return getProject().andThen((project) => - findConnectedGithubRepository(project.id).andThen((connectedGithubRepository) => { - const buildSettingsOrFailure = BuildSettingsSchema.safeParse(project.buildSettings); - const buildSettings = buildSettingsOrFailure.success - ? buildSettingsOrFailure.data - : undefined; - - if (connectedGithubRepository) { - return okAsync({ - gitHubApp: { - enabled: true, - connectedRepository: connectedGithubRepository, - // skip loading installations if there is a connected repository - // a project can have only a single connected repository - installations: undefined, - }, - buildSettings, - }); - } + const isPreviewEnvironmentEnabled = (projectId: string) => + fromPromise( + prisma.runtimeEnvironment.findFirst({ + select: { + id: true, + }, + where: { + projectId: projectId, + slug: "preview", + }, + }), + (error) => ({ + type: "other" as const, + cause: error, + }) + ).map((previewEnvironment) => previewEnvironment !== null); - return listGithubAppInstallations(project.organizationId).map((githubAppInstallations) => { - return { - gitHubApp: { - enabled: true, - connectedRepository: undefined, - installations: githubAppInstallations, - }, - buildSettings, - }; - }); - }) + return getProject().andThen((project) => + ResultAsync.combine([ + isPreviewEnvironmentEnabled(project.id), + findConnectedGithubRepository(project.id), + listGithubAppInstallations(project.organizationId), + ]).map( + ([isPreviewEnvironmentEnabled, connectedGithubRepository, githubAppInstallations]) => ({ + gitHubApp: { + enabled: true, + connectedRepository: connectedGithubRepository, + installations: githubAppInstallations, + isPreviewEnvironmentEnabled, + }, + buildSettings: project.buildSettings, + }) + ) ); } } From 273874eeb1c58c90e62c8f5de9e8cc16aa98d57d Mon Sep 17 00:00:00 2001 From: myftija Date: Wed, 8 Oct 2025 15:22:08 +0200 Subject: [PATCH 2/3] Enable preview deployments only if the preview environemtn is enabled --- .../app/services/projectSettings.server.ts | 60 ++++++++++++++----- 1 file changed, 45 insertions(+), 15 deletions(-) diff --git a/apps/webapp/app/services/projectSettings.server.ts b/apps/webapp/app/services/projectSettings.server.ts index 93d05c1f89..ae035d5300 100644 --- a/apps/webapp/app/services/projectSettings.server.ts +++ b/apps/webapp/app/services/projectSettings.server.ts @@ -4,7 +4,7 @@ import { DeleteProjectService } from "~/services/deleteProject.server"; import { BranchTrackingConfigSchema, type BranchTrackingConfig } from "~/v3/github"; import { checkGitHubBranchExists } from "~/services/gitHub.server"; import { errAsync, fromPromise, okAsync, ResultAsync } from "neverthrow"; -import { BuildSettings } from "~/v3/buildSettings"; +import { type BuildSettings } from "~/v3/buildSettings"; export class ProjectSettingsService { #prismaClient: PrismaClient; @@ -82,7 +82,7 @@ export class ProjectSettingsService { (error) => ({ type: "other" as const, cause: error }) ); - const createConnectedRepo = (defaultBranch: string) => + const createConnectedRepo = (defaultBranch: string, previewDeploymentsEnabled: boolean) => fromPromise( this.#prismaClient.connectedGithubRepository.create({ data: { @@ -92,21 +92,23 @@ export class ProjectSettingsService { prod: { branch: defaultBranch }, staging: {}, } satisfies BranchTrackingConfig, - previewDeploymentsEnabled: true, + previewDeploymentsEnabled, }, }), (error) => ({ type: "other" as const, cause: error }) ); - return ResultAsync.combine([getRepository(), findExistingConnection()]).andThen( - ([repository, existingConnection]) => { - if (existingConnection) { - return errAsync({ type: "project_already_has_connected_repository" as const }); - } - - return createConnectedRepo(repository.defaultBranch); + return ResultAsync.combine([ + getRepository(), + findExistingConnection(), + this.isPreviewEnvironmentEnabled(projectId), + ]).andThen(([repository, existingConnection, previewEnvironmentEnabled]) => { + if (existingConnection) { + return errAsync({ type: "project_already_has_connected_repository" as const }); } - ); + + return createConnectedRepo(repository.defaultBranch, previewEnvironmentEnabled); + }); } disconnectGitHubRepo(projectId: string) { @@ -208,7 +210,11 @@ export class ProjectSettingsService { return okAsync(stagingBranch); }; - const updateConnectedRepo = () => + const updateConnectedRepo = (data: { + productionBranch: string | undefined; + stagingBranch: string | undefined; + previewDeploymentsEnabled: boolean | undefined; + }) => fromPromise( this.#prismaClient.connectedGithubRepository.update({ where: { @@ -216,10 +222,10 @@ export class ProjectSettingsService { }, data: { branchTracking: { - prod: productionBranch ? { branch: productionBranch } : {}, - staging: stagingBranch ? { branch: stagingBranch } : {}, + prod: data.productionBranch ? { branch: data.productionBranch } : {}, + staging: data.stagingBranch ? { branch: data.stagingBranch } : {}, } satisfies BranchTrackingConfig, - previewDeploymentsEnabled: previewDeploymentsEnabled, + previewDeploymentsEnabled: data.previewDeploymentsEnabled, }, }), (error) => ({ type: "other" as const, cause: error }) @@ -240,8 +246,14 @@ export class ProjectSettingsService { fullRepoName: connectedRepo.repository.fullName, oldStagingBranch: connectedRepo.branchTracking?.staging?.branch, }), + this.isPreviewEnvironmentEnabled(projectId), ]); }) + .map(([productionBranch, stagingBranch, previewEnvironmentEnabled]) => ({ + productionBranch, + stagingBranch, + previewDeploymentsEnabled: previewDeploymentsEnabled && previewEnvironmentEnabled, + })) .andThen(updateConnectedRepo); } @@ -296,4 +308,22 @@ export class ProjectSettingsService { }); }); } + + private isPreviewEnvironmentEnabled(projectId: string) { + return fromPromise( + this.#prismaClient.runtimeEnvironment.findFirst({ + select: { + id: true, + }, + where: { + projectId: projectId, + slug: "preview", + }, + }), + (error) => ({ + type: "other" as const, + cause: error, + }) + ).map((previewEnvironment) => previewEnvironment !== null); + } } From 1f15cff35a8d74a52f1b7d2299c734fb9696ff8d Mon Sep 17 00:00:00 2001 From: myftija Date: Wed, 8 Oct 2025 16:30:32 +0200 Subject: [PATCH 3/3] Fix prisma reference --- apps/webapp/app/services/projectSettingsPresenter.server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/webapp/app/services/projectSettingsPresenter.server.ts b/apps/webapp/app/services/projectSettingsPresenter.server.ts index bb671c5610..6108dd164e 100644 --- a/apps/webapp/app/services/projectSettingsPresenter.server.ts +++ b/apps/webapp/app/services/projectSettingsPresenter.server.ts @@ -136,7 +136,7 @@ export class ProjectSettingsPresenter { const isPreviewEnvironmentEnabled = (projectId: string) => fromPromise( - prisma.runtimeEnvironment.findFirst({ + this.#prismaClient.runtimeEnvironment.findFirst({ select: { id: true, },