Skip to content

Commit 416dbcd

Browse files
authored
fix(webapp): disable gh-triggered preview deployments if the preview env is disabled (#2595)
* Show hint if preview branches are disabled in the project * Enable preview deployments only if the preview environemtn is enabled * Fix prisma reference
1 parent 679b41d commit 416dbcd

File tree

3 files changed

+149
-89
lines changed

3 files changed

+149
-89
lines changed

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings/route.tsx

Lines changed: 48 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ import {
6060
githubAppInstallPath,
6161
EnvironmentParamSchema,
6262
v3ProjectSettingsPath,
63+
docsPath,
64+
v3BillingPath,
6365
} from "~/utils/pathBuilder";
6466
import React, { useEffect, useState } from "react";
6567
import { Select, SelectItem } from "~/components/primitives/Select";
@@ -77,6 +79,7 @@ import { TextLink } from "~/components/primitives/TextLink";
7779
import { cn } from "~/utils/cn";
7880
import { ProjectSettingsPresenter } from "~/services/projectSettingsPresenter.server";
7981
import { type BuildSettings } from "~/v3/buildSettings";
82+
import { InfoIconTooltip } from "~/components/primitives/Tooltip";
8083

8184
export const meta: MetaFunction = () => {
8285
return [
@@ -126,6 +129,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
126129
githubAppEnabled: gitHubApp.enabled,
127130
githubAppInstallations: gitHubApp.installations,
128131
connectedGithubRepository: gitHubApp.connectedRepository,
132+
isPreviewEnvironmentEnabled: gitHubApp.isPreviewEnvironmentEnabled,
129133
buildSettings,
130134
});
131135
};
@@ -433,8 +437,13 @@ export const action: ActionFunction = async ({ request, params }) => {
433437
};
434438

435439
export default function Page() {
436-
const { githubAppInstallations, connectedGithubRepository, githubAppEnabled, buildSettings } =
437-
useTypedLoaderData<typeof loader>();
440+
const {
441+
githubAppInstallations,
442+
connectedGithubRepository,
443+
githubAppEnabled,
444+
buildSettings,
445+
isPreviewEnvironmentEnabled,
446+
} = useTypedLoaderData<typeof loader>();
438447
const project = useProject();
439448
const organization = useOrganization();
440449
const environment = useEnvironment();
@@ -561,7 +570,10 @@ export default function Page() {
561570
<Header2 spacing>Git settings</Header2>
562571
<div className="w-full rounded-sm border border-grid-dimmed p-4">
563572
{connectedGithubRepository ? (
564-
<ConnectedGitHubRepoForm connectedGitHubRepo={connectedGithubRepository} />
573+
<ConnectedGitHubRepoForm
574+
connectedGitHubRepo={connectedGithubRepository}
575+
previewEnvironmentEnabled={isPreviewEnvironmentEnabled}
576+
/>
565577
) : (
566578
<GitHubConnectionPrompt
567579
gitHubAppInstallations={githubAppInstallations ?? []}
@@ -903,11 +915,14 @@ type ConnectedGitHubRepo = {
903915

904916
function ConnectedGitHubRepoForm({
905917
connectedGitHubRepo,
918+
previewEnvironmentEnabled,
906919
}: {
907920
connectedGitHubRepo: ConnectedGitHubRepo;
921+
previewEnvironmentEnabled?: boolean;
908922
}) {
909923
const lastSubmission = useActionData() as any;
910924
const navigation = useNavigation();
925+
const organization = useOrganization();
911926

912927
const [hasGitSettingsChanges, setHasGitSettingsChanges] = useState(false);
913928
const [gitSettingsValues, setGitSettingsValues] = useState({
@@ -1003,10 +1018,10 @@ function ConnectedGitHubRepoForm({
10031018
<Fieldset>
10041019
<InputGroup fullWidth>
10051020
<Hint>
1006-
Every commit on the selected tracking branch creates a deployment in the corresponding
1021+
Every push to the selected tracking branch creates a deployment in the corresponding
10071022
environment.
10081023
</Hint>
1009-
<div className="grid grid-cols-[120px_1fr] gap-3">
1024+
<div className="mt-1 grid grid-cols-[120px_1fr] gap-3">
10101025
<div className="flex items-center gap-1.5">
10111026
<EnvironmentIcon environment={{ type: "PRODUCTION" }} className="size-4" />
10121027
<span className={`text-sm ${environmentTextClassName({ type: "PRODUCTION" })}`}>
@@ -1054,19 +1069,34 @@ function ConnectedGitHubRepoForm({
10541069
{environmentFullTitle({ type: "PREVIEW" })}
10551070
</span>
10561071
</div>
1057-
<Switch
1058-
name="previewDeploymentsEnabled"
1059-
defaultChecked={connectedGitHubRepo.previewDeploymentsEnabled}
1060-
variant="small"
1061-
label="create preview deployments for pull requests"
1062-
labelPosition="right"
1063-
onCheckedChange={(checked) => {
1064-
setGitSettingsValues((prev) => ({
1065-
...prev,
1066-
previewDeploymentsEnabled: checked,
1067-
}));
1068-
}}
1069-
/>
1072+
<div className="flex items-center gap-1.5">
1073+
<Switch
1074+
name="previewDeploymentsEnabled"
1075+
disabled={!previewEnvironmentEnabled}
1076+
defaultChecked={
1077+
connectedGitHubRepo.previewDeploymentsEnabled && previewEnvironmentEnabled
1078+
}
1079+
variant="small"
1080+
label="Create preview deployments for pull requests"
1081+
labelPosition="right"
1082+
onCheckedChange={(checked) => {
1083+
setGitSettingsValues((prev) => ({
1084+
...prev,
1085+
previewDeploymentsEnabled: checked,
1086+
}));
1087+
}}
1088+
/>
1089+
{!previewEnvironmentEnabled && (
1090+
<InfoIconTooltip
1091+
content={
1092+
<span className="text-xs">
1093+
<TextLink to={v3BillingPath(organization)}>Upgrade</TextLink> your plan to
1094+
enable preview branches
1095+
</span>
1096+
}
1097+
/>
1098+
)}
1099+
</div>
10701100
</div>
10711101
<FormError>{fields.productionBranch?.error}</FormError>
10721102
<FormError>{fields.stagingBranch?.error}</FormError>

apps/webapp/app/services/projectSettings.server.ts

Lines changed: 45 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { DeleteProjectService } from "~/services/deleteProject.server";
44
import { BranchTrackingConfigSchema, type BranchTrackingConfig } from "~/v3/github";
55
import { checkGitHubBranchExists } from "~/services/gitHub.server";
66
import { errAsync, fromPromise, okAsync, ResultAsync } from "neverthrow";
7-
import { BuildSettings } from "~/v3/buildSettings";
7+
import { type BuildSettings } from "~/v3/buildSettings";
88

99
export class ProjectSettingsService {
1010
#prismaClient: PrismaClient;
@@ -82,7 +82,7 @@ export class ProjectSettingsService {
8282
(error) => ({ type: "other" as const, cause: error })
8383
);
8484

85-
const createConnectedRepo = (defaultBranch: string) =>
85+
const createConnectedRepo = (defaultBranch: string, previewDeploymentsEnabled: boolean) =>
8686
fromPromise(
8787
this.#prismaClient.connectedGithubRepository.create({
8888
data: {
@@ -92,21 +92,23 @@ export class ProjectSettingsService {
9292
prod: { branch: defaultBranch },
9393
staging: {},
9494
} satisfies BranchTrackingConfig,
95-
previewDeploymentsEnabled: true,
95+
previewDeploymentsEnabled,
9696
},
9797
}),
9898
(error) => ({ type: "other" as const, cause: error })
9999
);
100100

101-
return ResultAsync.combine([getRepository(), findExistingConnection()]).andThen(
102-
([repository, existingConnection]) => {
103-
if (existingConnection) {
104-
return errAsync({ type: "project_already_has_connected_repository" as const });
105-
}
106-
107-
return createConnectedRepo(repository.defaultBranch);
101+
return ResultAsync.combine([
102+
getRepository(),
103+
findExistingConnection(),
104+
this.isPreviewEnvironmentEnabled(projectId),
105+
]).andThen(([repository, existingConnection, previewEnvironmentEnabled]) => {
106+
if (existingConnection) {
107+
return errAsync({ type: "project_already_has_connected_repository" as const });
108108
}
109-
);
109+
110+
return createConnectedRepo(repository.defaultBranch, previewEnvironmentEnabled);
111+
});
110112
}
111113

112114
disconnectGitHubRepo(projectId: string) {
@@ -208,18 +210,22 @@ export class ProjectSettingsService {
208210
return okAsync(stagingBranch);
209211
};
210212

211-
const updateConnectedRepo = () =>
213+
const updateConnectedRepo = (data: {
214+
productionBranch: string | undefined;
215+
stagingBranch: string | undefined;
216+
previewDeploymentsEnabled: boolean | undefined;
217+
}) =>
212218
fromPromise(
213219
this.#prismaClient.connectedGithubRepository.update({
214220
where: {
215221
projectId: projectId,
216222
},
217223
data: {
218224
branchTracking: {
219-
prod: productionBranch ? { branch: productionBranch } : {},
220-
staging: stagingBranch ? { branch: stagingBranch } : {},
225+
prod: data.productionBranch ? { branch: data.productionBranch } : {},
226+
staging: data.stagingBranch ? { branch: data.stagingBranch } : {},
221227
} satisfies BranchTrackingConfig,
222-
previewDeploymentsEnabled: previewDeploymentsEnabled,
228+
previewDeploymentsEnabled: data.previewDeploymentsEnabled,
223229
},
224230
}),
225231
(error) => ({ type: "other" as const, cause: error })
@@ -240,8 +246,14 @@ export class ProjectSettingsService {
240246
fullRepoName: connectedRepo.repository.fullName,
241247
oldStagingBranch: connectedRepo.branchTracking?.staging?.branch,
242248
}),
249+
this.isPreviewEnvironmentEnabled(projectId),
243250
]);
244251
})
252+
.map(([productionBranch, stagingBranch, previewEnvironmentEnabled]) => ({
253+
productionBranch,
254+
stagingBranch,
255+
previewDeploymentsEnabled: previewDeploymentsEnabled && previewEnvironmentEnabled,
256+
}))
245257
.andThen(updateConnectedRepo);
246258
}
247259

@@ -296,4 +308,22 @@ export class ProjectSettingsService {
296308
});
297309
});
298310
}
311+
312+
private isPreviewEnvironmentEnabled(projectId: string) {
313+
return fromPromise(
314+
this.#prismaClient.runtimeEnvironment.findFirst({
315+
select: {
316+
id: true,
317+
},
318+
where: {
319+
projectId: projectId,
320+
slug: "preview",
321+
},
322+
}),
323+
(error) => ({
324+
type: "other" as const,
325+
cause: error,
326+
})
327+
).map((previewEnvironment) => previewEnvironment !== null);
328+
}
299329
}

apps/webapp/app/services/projectSettingsPresenter.server.ts

Lines changed: 56 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { prisma } from "~/db.server";
33
import { BranchTrackingConfigSchema } from "~/v3/github";
44
import { env } from "~/env.server";
55
import { findProjectBySlug } from "~/models/project.server";
6-
import { err, fromPromise, ok, okAsync } from "neverthrow";
6+
import { err, fromPromise, ok, ResultAsync } from "neverthrow";
77
import { BuildSettingsSchema } from "~/v3/buildSettings";
88

99
export class ProjectSettingsPresenter {
@@ -20,33 +20,31 @@ export class ProjectSettingsPresenter {
2020
fromPromise(findProjectBySlug(organizationSlug, projectSlug, userId), (error) => ({
2121
type: "other" as const,
2222
cause: error,
23-
})).andThen((project) => {
24-
if (!project) {
25-
return err({ type: "project_not_found" as const });
26-
}
27-
return ok(project);
28-
});
23+
}))
24+
.andThen((project) => {
25+
if (!project) {
26+
return err({ type: "project_not_found" as const });
27+
}
28+
return ok(project);
29+
})
30+
.map((project) => {
31+
const buildSettingsOrFailure = BuildSettingsSchema.safeParse(project.buildSettings);
32+
const buildSettings = buildSettingsOrFailure.success
33+
? buildSettingsOrFailure.data
34+
: undefined;
35+
return { ...project, buildSettings };
36+
});
2937

3038
if (!githubAppEnabled) {
31-
return getProject().andThen((project) => {
32-
if (!project) {
33-
return err({ type: "project_not_found" as const });
34-
}
35-
36-
const buildSettingsOrFailure = BuildSettingsSchema.safeParse(project.buildSettings);
37-
const buildSettings = buildSettingsOrFailure.success
38-
? buildSettingsOrFailure.data
39-
: undefined;
40-
41-
return ok({
42-
gitHubApp: {
43-
enabled: false,
44-
connectedRepository: undefined,
45-
installations: undefined,
46-
},
47-
buildSettings,
48-
});
49-
});
39+
return getProject().map(({ buildSettings }) => ({
40+
gitHubApp: {
41+
enabled: false,
42+
connectedRepository: undefined,
43+
installations: undefined,
44+
isPreviewEnvironmentEnabled: undefined,
45+
},
46+
buildSettings,
47+
}));
5048
}
5149

5250
const findConnectedGithubRepository = (projectId: string) =>
@@ -136,37 +134,39 @@ export class ProjectSettingsPresenter {
136134
})
137135
);
138136

139-
return getProject().andThen((project) =>
140-
findConnectedGithubRepository(project.id).andThen((connectedGithubRepository) => {
141-
const buildSettingsOrFailure = BuildSettingsSchema.safeParse(project.buildSettings);
142-
const buildSettings = buildSettingsOrFailure.success
143-
? buildSettingsOrFailure.data
144-
: undefined;
145-
146-
if (connectedGithubRepository) {
147-
return okAsync({
148-
gitHubApp: {
149-
enabled: true,
150-
connectedRepository: connectedGithubRepository,
151-
// skip loading installations if there is a connected repository
152-
// a project can have only a single connected repository
153-
installations: undefined,
154-
},
155-
buildSettings,
156-
});
157-
}
137+
const isPreviewEnvironmentEnabled = (projectId: string) =>
138+
fromPromise(
139+
this.#prismaClient.runtimeEnvironment.findFirst({
140+
select: {
141+
id: true,
142+
},
143+
where: {
144+
projectId: projectId,
145+
slug: "preview",
146+
},
147+
}),
148+
(error) => ({
149+
type: "other" as const,
150+
cause: error,
151+
})
152+
).map((previewEnvironment) => previewEnvironment !== null);
158153

159-
return listGithubAppInstallations(project.organizationId).map((githubAppInstallations) => {
160-
return {
161-
gitHubApp: {
162-
enabled: true,
163-
connectedRepository: undefined,
164-
installations: githubAppInstallations,
165-
},
166-
buildSettings,
167-
};
168-
});
169-
})
154+
return getProject().andThen((project) =>
155+
ResultAsync.combine([
156+
isPreviewEnvironmentEnabled(project.id),
157+
findConnectedGithubRepository(project.id),
158+
listGithubAppInstallations(project.organizationId),
159+
]).map(
160+
([isPreviewEnvironmentEnabled, connectedGithubRepository, githubAppInstallations]) => ({
161+
gitHubApp: {
162+
enabled: true,
163+
connectedRepository: connectedGithubRepository,
164+
installations: githubAppInstallations,
165+
isPreviewEnvironmentEnabled,
166+
},
167+
buildSettings: project.buildSettings,
168+
})
169+
)
170170
);
171171
}
172172
}

0 commit comments

Comments
 (0)