Skip to content

Commit d86288f

Browse files
committed
feat: expose project build settings
1 parent dc42ae7 commit d86288f

File tree

6 files changed

+239
-27
lines changed

6 files changed

+239
-27
lines changed

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

Lines changed: 178 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ import {
6262
EnvironmentParamSchema,
6363
v3ProjectSettingsPath,
6464
} from "~/utils/pathBuilder";
65-
import { useEffect, useState } from "react";
65+
import React, { useEffect, useState } from "react";
6666
import { Select, SelectItem } from "~/components/primitives/Select";
6767
import { Switch } from "~/components/primitives/Switch";
6868
import { type BranchTrackingConfig } from "~/v3/github";
@@ -120,7 +120,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
120120
}
121121
}
122122

123-
const { gitHubApp } = resultOrFail.value;
123+
const { gitHubApp, buildSettings } = resultOrFail.value;
124124

125125
const session = await getSession(request.headers.get("Cookie"));
126126
const openGitHubRepoConnectionModal = session.get("gitHubAppInstalled") === true;
@@ -134,6 +134,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
134134
githubAppInstallations: gitHubApp.installations,
135135
connectedGithubRepository: gitHubApp.connectedRepository,
136136
openGitHubRepoConnectionModal,
137+
buildSettings,
137138
},
138139
{ headers }
139140
);
@@ -155,6 +156,13 @@ const UpdateGitSettingsFormSchema = z.object({
155156
.transform((val) => val === "on"),
156157
});
157158

159+
const UpdateBuildSettingsFormSchema = z.object({
160+
action: z.literal("update-build-settings"),
161+
rootDirectory: z.string().trim().optional(),
162+
installCommand: z.string().trim().optional(),
163+
triggerConfigFile: z.string().trim().optional(),
164+
});
165+
158166
export function createSchema(
159167
constraints: {
160168
getSlugMatch?: (slug: string) => { isMatch: boolean; projectSlug: string };
@@ -188,6 +196,7 @@ export function createSchema(
188196
}),
189197
ConnectGitHubRepoFormSchema,
190198
UpdateGitSettingsFormSchema,
199+
UpdateBuildSettingsFormSchema,
191200
z.object({
192201
action: z.literal("disconnect-repo"),
193202
}),
@@ -376,6 +385,31 @@ export const action: ActionFunction = async ({ request, params }) => {
376385
success: true,
377386
});
378387
}
388+
case "update-build-settings": {
389+
const { rootDirectory, installCommand, triggerConfigFile } = submission.value;
390+
391+
const resultOrFail = await projectSettingsService.updateBuildSettings(projectId, {
392+
rootDirectory: rootDirectory || undefined,
393+
installCommand: installCommand || undefined,
394+
triggerConfigFile: triggerConfigFile || undefined,
395+
});
396+
397+
if (resultOrFail.isErr()) {
398+
switch (resultOrFail.error.type) {
399+
case "other":
400+
default: {
401+
resultOrFail.error.type satisfies "other";
402+
403+
logger.error("Failed to update build settings", {
404+
error: resultOrFail.error,
405+
});
406+
return redirectBackWithErrorMessage(request, "Failed to update build settings");
407+
}
408+
}
409+
}
410+
411+
return redirectBackWithSuccessMessage(request, "Build settings updated successfully");
412+
}
379413
default: {
380414
submission.value satisfies never;
381415
return redirectBackWithErrorMessage(request, "Failed to process request");
@@ -389,6 +423,7 @@ export default function Page() {
389423
connectedGithubRepository,
390424
githubAppEnabled,
391425
openGitHubRepoConnectionModal,
426+
buildSettings,
392427
} = useTypedLoaderData<typeof loader>();
393428
const project = useProject();
394429
const organization = useOrganization();
@@ -511,22 +546,31 @@ export default function Page() {
511546
</div>
512547

513548
{githubAppEnabled && (
514-
<div>
515-
<Header2 spacing>Git settings</Header2>
516-
<div className="w-full rounded-sm border border-grid-dimmed p-4">
517-
{connectedGithubRepository ? (
518-
<ConnectedGitHubRepoForm connectedGitHubRepo={connectedGithubRepository} />
519-
) : (
520-
<GitHubConnectionPrompt
521-
gitHubAppInstallations={githubAppInstallations ?? []}
522-
organizationSlug={organization.slug}
523-
projectSlug={project.slug}
524-
environmentSlug={environment.slug}
525-
openGitHubRepoConnectionModal={openGitHubRepoConnectionModal}
526-
/>
527-
)}
549+
<React.Fragment>
550+
<div>
551+
<Header2 spacing>Git settings</Header2>
552+
<div className="w-full rounded-sm border border-grid-dimmed p-4">
553+
{connectedGithubRepository ? (
554+
<ConnectedGitHubRepoForm connectedGitHubRepo={connectedGithubRepository} />
555+
) : (
556+
<GitHubConnectionPrompt
557+
gitHubAppInstallations={githubAppInstallations ?? []}
558+
organizationSlug={organization.slug}
559+
projectSlug={project.slug}
560+
environmentSlug={environment.slug}
561+
openGitHubRepoConnectionModal={openGitHubRepoConnectionModal}
562+
/>
563+
)}
564+
</div>
528565
</div>
529-
</div>
566+
567+
<div>
568+
<Header2 spacing>Build settings</Header2>
569+
<div className="w-full rounded-sm border border-grid-dimmed p-4">
570+
<BuildSettingsForm buildSettings={buildSettings ?? {}} />
571+
</div>
572+
</div>
573+
</React.Fragment>
530574
)}
531575

532576
<div>
@@ -1033,3 +1077,120 @@ function ConnectedGitHubRepoForm({
10331077
</>
10341078
);
10351079
}
1080+
1081+
type BuildSettings = {
1082+
rootDirectory?: string;
1083+
installCommand?: string;
1084+
triggerConfigFile?: string;
1085+
};
1086+
1087+
function BuildSettingsForm({ buildSettings }: { buildSettings: BuildSettings }) {
1088+
const lastSubmission = useActionData() as any;
1089+
const navigation = useNavigation();
1090+
1091+
const [hasBuildSettingsChanges, setHasBuildSettingsChanges] = useState(false);
1092+
const [buildSettingsValues, setBuildSettingsValues] = useState({
1093+
rootDirectory: buildSettings?.rootDirectory || "",
1094+
installCommand: buildSettings?.installCommand || "",
1095+
triggerConfigFile: buildSettings?.triggerConfigFile || "",
1096+
});
1097+
1098+
useEffect(() => {
1099+
const hasChanges =
1100+
buildSettingsValues.rootDirectory !== (buildSettings?.rootDirectory || "") ||
1101+
buildSettingsValues.installCommand !== (buildSettings?.installCommand || "") ||
1102+
buildSettingsValues.triggerConfigFile !== (buildSettings?.triggerConfigFile || "");
1103+
setHasBuildSettingsChanges(hasChanges);
1104+
}, [buildSettingsValues, buildSettings]);
1105+
1106+
const [buildSettingsForm, fields] = useForm({
1107+
id: "update-build-settings",
1108+
lastSubmission: lastSubmission,
1109+
shouldRevalidate: "onSubmit",
1110+
onValidate({ formData }) {
1111+
return parse(formData, {
1112+
schema: UpdateBuildSettingsFormSchema,
1113+
});
1114+
},
1115+
});
1116+
1117+
const isBuildSettingsLoading =
1118+
navigation.formData?.get("action") === "update-build-settings" &&
1119+
(navigation.state === "submitting" || navigation.state === "loading");
1120+
1121+
return (
1122+
<Form method="post" {...buildSettingsForm.props}>
1123+
<Fieldset>
1124+
<InputGroup fullWidth>
1125+
<Label htmlFor={fields.rootDirectory.id}>Root directory</Label>
1126+
<Input
1127+
{...conform.input(fields.rootDirectory, { type: "text" })}
1128+
defaultValue={buildSettings?.rootDirectory || ""}
1129+
placeholder="/"
1130+
onChange={(e) => {
1131+
setBuildSettingsValues((prev) => ({
1132+
...prev,
1133+
rootDirectory: e.target.value,
1134+
}));
1135+
}}
1136+
/>
1137+
<Hint>The directory that contains your code.</Hint>
1138+
<FormError id={fields.rootDirectory.errorId}>{fields.rootDirectory.error}</FormError>
1139+
</InputGroup>
1140+
<InputGroup fullWidth>
1141+
<Label htmlFor={fields.installCommand.id}>Install command</Label>
1142+
<Input
1143+
{...conform.input(fields.installCommand, { type: "text" })}
1144+
defaultValue={buildSettings?.installCommand || ""}
1145+
placeholder="e.g., `npm install`, or `bun install`"
1146+
onChange={(e) => {
1147+
setBuildSettingsValues((prev) => ({
1148+
...prev,
1149+
installCommand: e.target.value,
1150+
}));
1151+
}}
1152+
/>
1153+
<Hint>
1154+
Command to install your project dependencies. This is auto-detected by default.
1155+
</Hint>
1156+
<FormError id={fields.installCommand.errorId}>{fields.installCommand.error}</FormError>
1157+
</InputGroup>
1158+
<InputGroup fullWidth>
1159+
<Label htmlFor={fields.triggerConfigFile.id}>Trigger config file</Label>
1160+
<Input
1161+
{...conform.input(fields.triggerConfigFile, { type: "text" })}
1162+
defaultValue={buildSettings?.triggerConfigFile || ""}
1163+
placeholder="trigger.config.ts"
1164+
onChange={(e) => {
1165+
setBuildSettingsValues((prev) => ({
1166+
...prev,
1167+
triggerConfigFile: e.target.value,
1168+
}));
1169+
}}
1170+
/>
1171+
<Hint>
1172+
Path to your trigger configuration file, relative to the specified root directory.
1173+
</Hint>
1174+
<FormError id={fields.triggerConfigFile.errorId}>
1175+
{fields.triggerConfigFile.error}
1176+
</FormError>
1177+
</InputGroup>
1178+
<FormError>{buildSettingsForm.error}</FormError>
1179+
<FormButtons
1180+
confirmButton={
1181+
<Button
1182+
type="submit"
1183+
name="action"
1184+
value="update-build-settings"
1185+
variant="secondary/small"
1186+
disabled={isBuildSettingsLoading || !hasBuildSettingsChanges}
1187+
LeadingIcon={isBuildSettingsLoading ? SpinnerWhite : undefined}
1188+
>
1189+
Save
1190+
</Button>
1191+
}
1192+
/>
1193+
</Fieldset>
1194+
</Form>
1195+
);
1196+
}

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +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";
78

89
export class ProjectSettingsService {
910
#prismaClient: PrismaClient;
@@ -244,6 +245,23 @@ export class ProjectSettingsService {
244245
.andThen(updateConnectedRepo);
245246
}
246247

248+
updateBuildSettings(projectId: string, buildSettings: BuildSettings) {
249+
return fromPromise(
250+
this.#prismaClient.project.update({
251+
where: {
252+
id: projectId,
253+
},
254+
data: {
255+
buildSettings: buildSettings,
256+
},
257+
}),
258+
(error) => ({
259+
type: "other" as const,
260+
cause: error,
261+
})
262+
);
263+
}
264+
247265
verifyProjectMembership(organizationSlug: string, projectSlug: string, userId: string) {
248266
const findProject = () =>
249267
fromPromise(

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

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { BranchTrackingConfigSchema } from "~/v3/github";
44
import { env } from "~/env.server";
55
import { findProjectBySlug } from "~/models/project.server";
66
import { err, fromPromise, ok, okAsync } from "neverthrow";
7+
import { BuildSettingsSchema } from "~/v3/buildSettings";
78

89
export class ProjectSettingsPresenter {
910
#prismaClient: PrismaClient;
@@ -15,16 +16,6 @@ export class ProjectSettingsPresenter {
1516
getProjectSettings(organizationSlug: string, projectSlug: string, userId: string) {
1617
const githubAppEnabled = env.GITHUB_APP_ENABLED === "1";
1718

18-
if (!githubAppEnabled) {
19-
return okAsync({
20-
gitHubApp: {
21-
enabled: false,
22-
connectedRepository: undefined,
23-
installations: undefined,
24-
},
25-
});
26-
}
27-
2819
const getProject = () =>
2920
fromPromise(findProjectBySlug(organizationSlug, projectSlug, userId), (error) => ({
3021
type: "other" as const,
@@ -36,6 +27,28 @@ export class ProjectSettingsPresenter {
3627
return ok(project);
3728
});
3829

30+
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+
});
50+
}
51+
3952
const findConnectedGithubRepository = (projectId: string) =>
4053
fromPromise(
4154
this.#prismaClient.connectedGithubRepository.findFirst({
@@ -119,6 +132,11 @@ export class ProjectSettingsPresenter {
119132

120133
return getProject().andThen((project) =>
121134
findConnectedGithubRepository(project.id).andThen((connectedGithubRepository) => {
135+
const buildSettingsOrFailure = BuildSettingsSchema.safeParse(project.buildSettings);
136+
const buildSettings = buildSettingsOrFailure.success
137+
? buildSettingsOrFailure.data
138+
: undefined;
139+
122140
if (connectedGithubRepository) {
123141
return okAsync({
124142
gitHubApp: {
@@ -128,6 +146,7 @@ export class ProjectSettingsPresenter {
128146
// a project can have only a single connected repository
129147
installations: undefined,
130148
},
149+
buildSettings,
131150
});
132151
}
133152

@@ -138,6 +157,7 @@ export class ProjectSettingsPresenter {
138157
connectedRepository: undefined,
139158
installations: githubAppInstallations,
140159
},
160+
buildSettings,
141161
};
142162
});
143163
})
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { z } from "zod";
2+
3+
export const BuildSettingsSchema = z.object({
4+
rootDirectory: z.string().optional(),
5+
installCommand: z.string().optional(),
6+
triggerConfigFile: z.string().optional(),
7+
});
8+
9+
export type BuildSettings = z.infer<typeof BuildSettingsSchema>;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
ALTER TABLE "public"."Project" ADD COLUMN "buildSettings" JSONB;
2+

internal-packages/database/prisma/schema.prisma

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,8 @@ model Project {
394394
executionSnapshots TaskRunExecutionSnapshot[]
395395
waitpointTags WaitpointTag[]
396396
connectedGithubRepository ConnectedGithubRepository?
397+
398+
buildSettings Json?
397399
}
398400

399401
enum ProjectVersion {

0 commit comments

Comments
 (0)