Skip to content

Commit 59934bd

Browse files
committed
Support for preview branches
1 parent bd0ff35 commit 59934bd

File tree

8 files changed

+235
-47
lines changed

8 files changed

+235
-47
lines changed

apps/webapp/app/routes/api.v1.projects.$projectRef.$env.jwt.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,29 @@ export async function action({ request, params }: LoaderFunctionArgs) {
7474
);
7575
}
7676

77+
const triggerBranch = request.headers.get("x-trigger-branch") ?? undefined;
78+
79+
let previewBranchEnvironmentId: string | undefined;
80+
81+
if (triggerBranch) {
82+
const previewBranch = await prisma.runtimeEnvironment.findFirst({
83+
where: {
84+
projectId: project.id,
85+
branchName: triggerBranch,
86+
parentEnvironmentId: runtimeEnv.id,
87+
archivedAt: null,
88+
},
89+
});
90+
91+
if (previewBranch) {
92+
previewBranchEnvironmentId = previewBranch.id;
93+
} else {
94+
return json({ error: `Preview branch ${triggerBranch} not found` }, { status: 404 });
95+
}
96+
}
97+
7798
const claims = {
78-
sub: runtimeEnv.id,
99+
sub: previewBranchEnvironmentId ?? runtimeEnv.id,
79100
pub: true,
80101
...parsedBody.data.claims,
81102
};

apps/webapp/app/routes/api.v1.projects.$projectRef.branches.ts

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { json, type ActionFunctionArgs } from "@remix-run/server-runtime";
1+
import { json, LoaderFunctionArgs, type ActionFunctionArgs } from "@remix-run/server-runtime";
22
import { tryCatch, UpsertBranchRequestBody } from "@trigger.dev/core/v3";
33
import { z } from "zod";
44
import { prisma } from "~/db.server";
@@ -93,3 +93,82 @@ export async function action({ request, params }: ActionFunctionArgs) {
9393

9494
return json(result.branch);
9595
}
96+
97+
export async function loader({ request, params }: LoaderFunctionArgs) {
98+
const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request);
99+
if (!authenticationResult) {
100+
return json({ error: "Invalid or Missing Access Token" }, { status: 401 });
101+
}
102+
103+
const parsedParams = ParamsSchema.safeParse(params);
104+
105+
if (!parsedParams.success) {
106+
return json({ error: "Invalid Params" }, { status: 400 });
107+
}
108+
109+
const { projectRef } = parsedParams.data;
110+
111+
const project = await prisma.project.findFirst({
112+
select: {
113+
id: true,
114+
},
115+
where: {
116+
externalRef: projectRef,
117+
organization: {
118+
members: {
119+
some: {
120+
userId: authenticationResult.userId,
121+
},
122+
},
123+
},
124+
},
125+
});
126+
127+
if (!project) {
128+
return json({ error: "Project not found" }, { status: 404 });
129+
}
130+
131+
const previewEnvironment = await prisma.runtimeEnvironment.findFirst({
132+
select: {
133+
id: true,
134+
},
135+
where: {
136+
projectId: project.id,
137+
slug: "preview",
138+
},
139+
});
140+
141+
if (!previewEnvironment) {
142+
return json(
143+
{ error: "You don't have preview branches setup. Go to the dashboard to enable them." },
144+
{ status: 400 }
145+
);
146+
}
147+
148+
const branches = await prisma.runtimeEnvironment.findMany({
149+
where: {
150+
projectId: project.id,
151+
parentEnvironmentId: previewEnvironment.id,
152+
archivedAt: null,
153+
},
154+
select: {
155+
id: true,
156+
branchName: true,
157+
createdAt: true,
158+
updatedAt: true,
159+
git: true,
160+
paused: true,
161+
},
162+
});
163+
164+
return json({
165+
branches: branches.map((branch) => ({
166+
id: branch.id,
167+
name: branch.branchName ?? "main",
168+
createdAt: branch.createdAt,
169+
updatedAt: branch.updatedAt,
170+
git: branch.git ?? undefined,
171+
isPaused: branch.paused,
172+
})),
173+
});
174+
}

apps/webapp/app/routes/api.v1.runs.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
ApiRunListPresenter,
44
ApiRunListSearchParams,
55
} from "~/presenters/v3/ApiRunListPresenter.server";
6+
import { logger } from "~/services/logger.server";
67
import { createLoaderApiRoute } from "~/services/routeBuilders/apiBuilder.server";
78

89
export const loader = createLoaderApiRoute(
@@ -17,7 +18,9 @@ export const loader = createLoaderApiRoute(
1718
},
1819
findResource: async () => 1, // This is a dummy function, we don't need to find a resource
1920
},
20-
async ({ searchParams, authentication, apiVersion }) => {
21+
async ({ searchParams, authentication, apiVersion, headers }) => {
22+
logger.info("api.v1.runs.loader", { searchParams, authentication, apiVersion, headers });
23+
2124
const presenter = new ApiRunListPresenter();
2225
const result = await presenter.call(
2326
authentication.environment.project,

packages/cli-v3/src/apiClient.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {
3636
GetWorkerByTagResponse,
3737
GetJWTRequestBody,
3838
GetJWTResponse,
39+
ApiBranchListResponseBody,
3940
} from "@trigger.dev/core/v3";
4041
import {
4142
WorkloadDebugLogRequestBody,
@@ -278,6 +279,20 @@ export class CliApiClient {
278279
);
279280
}
280281

282+
async listBranches(projectRef: string) {
283+
if (!this.accessToken) {
284+
throw new Error("listBranches: No access token");
285+
}
286+
287+
return wrapZodFetch(
288+
ApiBranchListResponseBody,
289+
`${this.apiURL}/api/v1/projects/${projectRef}/branches`,
290+
{
291+
headers: this.getHeaders(),
292+
}
293+
);
294+
}
295+
281296
async getEnvironmentVariables(projectRef: string) {
282297
if (!this.accessToken) {
283298
throw new Error("getEnvironmentVariables: No access token");

packages/cli-v3/src/commands/mcp.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
registerListRunsTool,
2323
registerSearchDocsTool,
2424
registerTriggerTaskTool,
25+
registerListPreviewBranchesTool,
2526
} from "../mcp/tools.js";
2627
import { printStandloneInitialBanner } from "../utilities/initialBanner.js";
2728
import { logger } from "../utilities/logger.js";
@@ -114,6 +115,7 @@ export async function mcpCommand(options: McpCommandOptions) {
114115
registerCreateProjectTool(context);
115116
registerDeployTool(context);
116117
registerListDeploymentsTool(context);
118+
registerListPreviewBranchesTool(context);
117119

118120
await server.connect(transport);
119121
}

packages/cli-v3/src/mcp/auth.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,9 +183,10 @@ export async function createApiClientWithPublicJWT(
183183
auth: LoginResultOk,
184184
projectRef: string,
185185
envName: string,
186-
scopes: string[]
186+
scopes: string[],
187+
previewBranch?: string
187188
) {
188-
const cliApiClient = new CliApiClient(auth.auth.apiUrl, auth.auth.accessToken);
189+
const cliApiClient = new CliApiClient(auth.auth.apiUrl, auth.auth.accessToken, previewBranch);
189190

190191
const jwt = await cliApiClient.getJWT(projectRef, envName, {
191192
claims: {

packages/cli-v3/src/mcp/tools.ts

Lines changed: 94 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,7 @@ export function registerGetTasksTool(context: McpContext) {
329329
return respondWithError(auth.error);
330330
}
331331

332-
const cliApiClient = new CliApiClient(auth.auth.apiUrl, auth.auth.accessToken);
332+
const cliApiClient = new CliApiClient(auth.auth.apiUrl, auth.auth.accessToken, branch);
333333

334334
// TODO: support other tags and preview branches
335335
const worker = await cliApiClient.getWorkerByTag($projectRef, environment, "current");
@@ -471,9 +471,13 @@ export function registerTriggerTaskTool(context: McpContext) {
471471
return respondWithError(auth.error);
472472
}
473473

474-
const apiClient = await createApiClientWithPublicJWT(auth, $projectRef, environment, [
475-
"write:tasks",
476-
]);
474+
const apiClient = await createApiClientWithPublicJWT(
475+
auth,
476+
$projectRef,
477+
environment,
478+
["write:tasks"],
479+
branch
480+
);
477481

478482
if (!apiClient) {
479483
return respondWithError("Failed to create API client with public JWT");
@@ -583,9 +587,13 @@ export function registerGetRunDetailsTool(context: McpContext) {
583587
return respondWithError(auth.error);
584588
}
585589

586-
const apiClient = await createApiClientWithPublicJWT(auth, $projectRef, environment, [
587-
`read:runs:${runId}`,
588-
]);
590+
const apiClient = await createApiClientWithPublicJWT(
591+
auth,
592+
$projectRef,
593+
environment,
594+
[`read:runs:${runId}`],
595+
branch
596+
);
589597

590598
if (!apiClient) {
591599
return respondWithError("Failed to create API client with public JWT");
@@ -680,10 +688,13 @@ export function registerCancelRunTool(context: McpContext) {
680688
return respondWithError(auth.error);
681689
}
682690

683-
const apiClient = await createApiClientWithPublicJWT(auth, $projectRef, environment, [
684-
`write:runs:${runId}`,
685-
`read:runs:${runId}`,
686-
]);
691+
const apiClient = await createApiClientWithPublicJWT(
692+
auth,
693+
$projectRef,
694+
environment,
695+
[`write:runs:${runId}`, `read:runs:${runId}`],
696+
branch
697+
);
687698

688699
if (!apiClient) {
689700
return respondWithError("Failed to create API client with public JWT");
@@ -823,9 +834,13 @@ export function registerListRunsTool(context: McpContext) {
823834
return respondWithError(auth.error);
824835
}
825836

826-
const apiClient = await createApiClientWithPublicJWT(auth, $projectRef, environment, [
827-
"read:runs",
828-
]);
837+
const apiClient = await createApiClientWithPublicJWT(
838+
auth,
839+
$projectRef,
840+
environment,
841+
["read:runs"],
842+
branch
843+
);
829844

830845
if (!apiClient) {
831846
return respondWithError("Failed to create API client with public JWT");
@@ -1065,9 +1080,13 @@ export function registerListDeploymentsTool(context: McpContext) {
10651080
return respondWithError(auth.error);
10661081
}
10671082

1068-
const apiClient = await createApiClientWithPublicJWT(auth, $projectRef, environment, [
1069-
"read:deployments",
1070-
]);
1083+
const apiClient = await createApiClientWithPublicJWT(
1084+
auth,
1085+
$projectRef,
1086+
environment,
1087+
["read:deployments"],
1088+
branch
1089+
);
10711090

10721091
if (!apiClient) {
10731092
return respondWithError("Failed to create API client with public JWT");
@@ -1089,6 +1108,64 @@ export function registerListDeploymentsTool(context: McpContext) {
10891108
);
10901109
}
10911110

1111+
export function registerListPreviewBranchesTool(context: McpContext) {
1112+
context.server.registerTool(
1113+
"list_preview_branches",
1114+
{
1115+
description: "List all preview branches in the project",
1116+
inputSchema: {
1117+
projectRef: ProjectRefSchema,
1118+
configPath: z
1119+
.string()
1120+
.describe(
1121+
"The path to the trigger.config.ts file. Only used when the trigger.config.ts file is not at the root dir (like in a monorepo setup). If not provided, we will try to find the config file in the current working directory"
1122+
)
1123+
.optional(),
1124+
},
1125+
},
1126+
async ({ projectRef, configPath }) => {
1127+
context.logger?.log("calling list_preview_branches", { projectRef, configPath });
1128+
1129+
if (context.options.devOnly) {
1130+
return respondWithError(`This MCP server is only available for the dev environment. `);
1131+
}
1132+
1133+
const projectRefResult = await resolveExistingProjectRef(context, projectRef, configPath);
1134+
1135+
if (projectRefResult.status === "error") {
1136+
return respondWithError(projectRefResult.error);
1137+
}
1138+
1139+
const $projectRef = projectRefResult.projectRef;
1140+
1141+
context.logger?.log("list_preview_branches projectRefResult", { projectRefResult });
1142+
1143+
const auth = await mcpAuth({
1144+
server: context.server,
1145+
defaultApiUrl: context.options.apiUrl,
1146+
profile: context.options.profile,
1147+
context,
1148+
});
1149+
1150+
if (!auth.ok) {
1151+
return respondWithError(auth.error);
1152+
}
1153+
1154+
const cliApiClient = new CliApiClient(auth.auth.apiUrl, auth.auth.accessToken);
1155+
1156+
const branches = await cliApiClient.listBranches($projectRef);
1157+
1158+
if (!branches.success) {
1159+
return respondWithError(branches.error);
1160+
}
1161+
1162+
return {
1163+
content: [{ type: "text", text: JSON.stringify(branches.data, null, 2) }],
1164+
};
1165+
}
1166+
);
1167+
}
1168+
10921169
async function resolveCLIExec(context: McpContext, cwd: string): Promise<[string, string]> {
10931170
// Lets first try to get the version of the CLI package
10941171
const installedCLI = await tryResolveTriggerCLIPath(context, cwd);
@@ -1415,28 +1492,3 @@ To view the project dashboard, visit: ${auth.dashboardUrl}/projects/v3/${project
14151492
14161493
${text}`;
14171494
}
1418-
1419-
async function getWritingTasksGuide(prompt: string) {
1420-
const urls = [
1421-
"https://trigger.dev/docs/tasks/overview.md",
1422-
"https://trigger.dev/docs/tasks/schemaTask.md",
1423-
"https://trigger.dev/docs/tasks/scheduled.md",
1424-
"https://trigger.dev/docs/triggering.md",
1425-
"https://trigger.dev/docs/writing-tasks-introduction.md",
1426-
];
1427-
1428-
const responses = await Promise.all(urls.map((url) => fetch(url)));
1429-
const texts = await Promise.all(responses.map((response) => response.text()));
1430-
1431-
const text = texts.join("\n\n");
1432-
1433-
return `
1434-
## Trigger.dev Task Writing Guide:
1435-
1436-
${text}
1437-
1438-
## Now please write the tasks based on the following prompt:
1439-
1440-
${prompt}
1441-
`;
1442-
}

0 commit comments

Comments
 (0)