Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/project-environments-endpoint.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@trigger.dev/core": patch
---

Add `GetProjectEnvironmentsResponseBody` and `ProjectEnvironment` schemas for the new `GET /api/v1/projects/{projectRef}/environments` endpoint, which lists the parent environments (dev, staging, preview, prod) a personal access token can access for a project. Dev is scoped to the token owner and branch (preview child) environments are excluded.
71 changes: 71 additions & 0 deletions apps/webapp/app/routes/api.v1.projects.$projectRef.environments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { json } from "@remix-run/server-runtime";
import { type GetProjectEnvironmentsResponseBody } from "@trigger.dev/core/v3";
import { z } from "zod";
import { $replica } from "~/db.server";
import { findProjectByRef } from "~/models/project.server";
import { createLoaderPATApiRoute } from "~/services/routeBuilders/apiBuilder.server";
import { sortEnvironments } from "~/utils/environmentSort";

const ParamsSchema = z.object({
projectRef: z.string(),
});

export const loader = createLoaderPATApiRoute(
{
params: ParamsSchema,
corsStrategy: "all",
// Resolve projectRef → org so the PAT plugin can ground its role-floor
// calculation. Membership is enforced by the plugin (`authenticatePat`
// rejects users who aren't members of the target org) and again by
// `findProjectByRef` below.
context: async (params) => {
const project = await $replica.project.findFirst({
where: { externalRef: params.projectRef },
select: { organizationId: true },
});
return project ? { organizationId: project.organizationId } : {};
},
authorization: { action: "read", resource: () => ({ type: "environments" }) },
},
async ({ params, authentication }) => {
const project = await findProjectByRef(params.projectRef, authentication.userId);

if (!project) {
return json({ error: "Project not found" }, { status: 404 });
}

const environments = await $replica.runtimeEnvironment.findMany({
where: {
projectId: project.id,
// Only base/parent environments. Branch children (preview branches)
// are excluded — syncs target the parent and branches override elsewhere.
parentEnvironmentId: null,
archivedAt: null,
OR: [
{ type: { in: ["STAGING", "PRODUCTION", "PREVIEW"] } },
// dev is per-user: only return the caller's own dev environment
{ type: "DEVELOPMENT", orgMember: { userId: authentication.userId } },
],
},
select: {
id: true,
slug: true,
type: true,
isBranchableEnvironment: true,
branchName: true,
paused: true,
},
});

const result: GetProjectEnvironmentsResponseBody = sortEnvironments(environments).map((env) => ({
id: env.id,
slug: env.slug,
type: env.type,
isBranchableEnvironment: env.isBranchableEnvironment,
branchName: env.branchName,
paused: env.paused,
}));

return json(result);
}
);
17 changes: 17 additions & 0 deletions packages/core/src/v3/schemas/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,23 @@ export const GetProjectEnvResponse = z.object({

export type GetProjectEnvResponse = z.infer<typeof GetProjectEnvResponse>;

export const ProjectEnvironment = z.object({
id: z.string(),
/// The slug used as the environment identifier in env var endpoints (e.g. "dev", "stg", "prod", "preview")
slug: z.string(),
type: z.enum(["DEVELOPMENT", "STAGING", "PREVIEW", "PRODUCTION"]),
/// Whether this is the branchable parent (preview); individual branches are not returned
isBranchableEnvironment: z.boolean(),
branchName: z.string().nullable(),
paused: z.boolean(),
});

export type ProjectEnvironment = z.infer<typeof ProjectEnvironment>;

export const GetProjectEnvironmentsResponseBody = z.array(ProjectEnvironment);

export type GetProjectEnvironmentsResponseBody = z.infer<typeof GetProjectEnvironmentsResponseBody>;

// Zod schema for the response body type
export const GetWorkerTaskResponse = z.object({
id: z.string(),
Expand Down
Loading