Skip to content

Commit fd8bf06

Browse files
committed
fix delete preview branches and api url fallback
1 parent e4e134a commit fd8bf06

3 files changed

Lines changed: 106 additions & 27 deletions

File tree

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

Lines changed: 60 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { type ActionFunctionArgs, json } from "@remix-run/server-runtime";
22
import { tryCatch } from "@trigger.dev/core";
33
import { z } from "zod";
44
import { prisma } from "~/db.server";
5-
import { authenticateRequest } from "~/services/apiAuth.server";
5+
import { authenticateRequestWithScopedApiKey } from "~/services/apiAuth.server";
66
import { ArchiveBranchService } from "~/services/archiveBranch.server";
77
import { logger } from "~/services/logger.server";
88
import { toBranchableEnvironmentType } from "~/utils/branchableEnvironment";
@@ -24,15 +24,25 @@ export async function action({ request, params }: ActionFunctionArgs) {
2424

2525
logger.info("Archive branch", { url: request.url, params });
2626

27-
const authenticationResult = await authenticateRequest(request, {
27+
const authentication = await authenticateRequestWithScopedApiKey(request, {
2828
personalAccessToken: true,
2929
organizationAccessToken: true,
30-
apiKey: false,
30+
apiKey: {
31+
action: "write",
32+
resource: { type: "branches" },
33+
allowPreviewParent: true,
34+
},
3135
});
3236

33-
if (!authenticationResult) {
34-
return json({ error: "Invalid or Missing Access Token" }, { status: 401 });
37+
if (!authentication.ok) {
38+
return json({ error: authentication.error }, { status: authentication.status });
3539
}
40+
const authenticationResult = authentication.authentication;
41+
42+
const apiKeyEnvironment =
43+
authenticationResult.type === "apiKey" && authenticationResult.result.ok
44+
? authenticationResult.result.environment
45+
: undefined;
3646

3747
const parsedParams = ParamsSchema.safeParse(params);
3848

@@ -54,25 +64,44 @@ export async function action({ request, params }: ActionFunctionArgs) {
5464

5565
const { env, branch } = parsed.data;
5666

67+
// API keys can only archive Preview branches
68+
if (
69+
authenticationResult.type === "apiKey" &&
70+
(!apiKeyEnvironment ||
71+
apiKeyEnvironment.type !== "PREVIEW" ||
72+
apiKeyEnvironment.parentEnvironmentId !== null ||
73+
env !== "preview")
74+
) {
75+
return json(
76+
{ error: "API keys must belong to the parent Preview environment." },
77+
{ status: 403 }
78+
);
79+
}
80+
5781
const environmentType = toBranchableEnvironmentType(env);
82+
83+
const organizationFilter =
84+
authenticationResult.type === "organizationAccessToken"
85+
? { id: authenticationResult.result.organizationId }
86+
: authenticationResult.type === "apiKey"
87+
? { id: apiKeyEnvironment!.organizationId }
88+
: {
89+
members: {
90+
some: {
91+
userId: authenticationResult.result.userId,
92+
},
93+
},
94+
};
95+
5896
const environments = await prisma.runtimeEnvironment.findMany({
5997
select: {
6098
id: true,
6199
archivedAt: true,
62100
},
63101
where: {
64-
organization:
65-
authenticationResult.type === "organizationAccessToken"
66-
? { id: authenticationResult.result.organizationId }
67-
: {
68-
members: {
69-
some: {
70-
userId: authenticationResult.result.userId,
71-
},
72-
},
73-
},
102+
organization: organizationFilter,
74103
// Dev branches are per-org-member: only the owner may archive their own.
75-
...(authenticationResult.type !== "organizationAccessToken" &&
104+
...(authenticationResult.type === "personalAccessToken" &&
76105
environmentType === "DEVELOPMENT"
77106
? { orgMember: { userId: authenticationResult.result.userId } }
78107
: {}),
@@ -91,7 +120,7 @@ export async function action({ request, params }: ActionFunctionArgs) {
91120
const activeEnvironments = environments.filter((env) => env.archivedAt === null);
92121

93122
if (
94-
authenticationResult.type === "organizationAccessToken" &&
123+
authenticationResult.type !== "personalAccessToken" &&
95124
environmentType === "DEVELOPMENT" &&
96125
activeEnvironments.length > 1
97126
) {
@@ -110,15 +139,21 @@ export async function action({ request, params }: ActionFunctionArgs) {
110139
return json({ error: "Branch already archived" }, { status: 400 });
111140
}
112141

142+
let orgFilter:
143+
| { type: "userMembership"; userId: string }
144+
| { type: "orgId"; organizationId: string };
145+
if (authenticationResult.type === "personalAccessToken") {
146+
orgFilter = { type: "userMembership", userId: authenticationResult.result.userId };
147+
} else if (authenticationResult.type === "organizationAccessToken") {
148+
orgFilter = { type: "orgId", organizationId: authenticationResult.result.organizationId };
149+
} else {
150+
orgFilter = { type: "orgId", organizationId: apiKeyEnvironment!.organizationId };
151+
}
152+
113153
const service = new ArchiveBranchService();
114-
const result = await service.call(
115-
authenticationResult.type === "organizationAccessToken"
116-
? { type: "orgId", organizationId: authenticationResult.result.organizationId }
117-
: { type: "userMembership", userId: authenticationResult.result.userId },
118-
{
119-
environmentId: environment.id,
120-
}
121-
);
154+
const result = await service.call(orgFilter, {
155+
environmentId: environment.id,
156+
});
122157

123158
if (result.success) {
124159
return json(result);

packages/cli-v3/src/deploy/auth.test.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1-
import { describe, expect, it } from "vitest";
1+
import { describe, expect, it, vi } from "vitest";
2+
3+
vi.mock("../utilities/configFiles.js", () => ({
4+
readAuthConfigProfile: vi.fn(() => undefined),
5+
}));
6+
27
import { authenticateForDeploy, userIdForDeploy } from "./auth.js";
8+
import { readAuthConfigProfile } from "../utilities/configFiles.js";
39

410
describe("authenticateForDeploy", () => {
511
it("gives TRIGGER_SECRET_KEY precedence without logging in", async () => {
@@ -58,6 +64,42 @@ describe("authenticateForDeploy", () => {
5864
});
5965
});
6066

67+
it("falls back to the saved profile's API URL for self-hosted instances", async () => {
68+
vi.mocked(readAuthConfigProfile).mockReturnValueOnce({
69+
apiUrl: "https://trigger.internal.example.com",
70+
});
71+
72+
const result = await authenticateForDeploy({
73+
secretKey: "tr_prod_sk_deploy",
74+
profile: "selfhosted",
75+
silent: true,
76+
login: async () => ({ ok: false, error: "should not be called" }),
77+
});
78+
79+
expect(result).toMatchObject({
80+
dashboardUrl: "https://trigger.internal.example.com",
81+
auth: { apiUrl: "https://trigger.internal.example.com" },
82+
});
83+
});
84+
85+
it("prefers an explicit API URL over the saved profile", async () => {
86+
vi.mocked(readAuthConfigProfile).mockReturnValueOnce({
87+
apiUrl: "https://trigger.internal.example.com",
88+
});
89+
90+
const result = await authenticateForDeploy({
91+
secretKey: "tr_prod_sk_deploy",
92+
apiUrl: "https://api.trigger.dev",
93+
profile: "selfhosted",
94+
silent: true,
95+
login: async () => ({ ok: false, error: "should not be called" }),
96+
});
97+
98+
expect(result).toMatchObject({
99+
auth: { apiUrl: "https://api.trigger.dev" },
100+
});
101+
});
102+
61103
it("keeps login authentication when no secret key is set", async () => {
62104
let loginOptions: unknown;
63105
const result = await authenticateForDeploy({

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { CLOUD_API_URL, CLOUD_WEB_URL } from "../consts.js";
2+
import { readAuthConfigProfile } from "../utilities/configFiles.js";
23
import type { LoginResult, LoginResultOk } from "../utilities/session.js";
34

45
const personalTokenPrefix = "tr_pat_";
@@ -38,7 +39,8 @@ export async function authenticateForDeploy({
3839
silent: boolean;
3940
login: LoginForDeploy;
4041
}): Promise<LoginResult | DeployAuthorization> {
41-
const resolvedApiUrl = apiUrl ?? CLOUD_API_URL;
42+
const authConfig = readAuthConfigProfile(profile);
43+
const resolvedApiUrl = apiUrl ?? authConfig?.apiUrl ?? CLOUD_API_URL;
4244

4345
if (!secretKey) {
4446
return login({

0 commit comments

Comments
 (0)