Skip to content

Commit 5307cb9

Browse files
authored
Merge branch 'main' into fix/tags_array_infinite_rerenders
2 parents 9706a57 + 7f25e82 commit 5307cb9

File tree

12 files changed

+392
-15
lines changed

12 files changed

+392
-15
lines changed

.changeset/thin-pants-design.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"trigger.dev": patch
3+
"@trigger.dev/core": patch
4+
---
5+
6+
Added support for deployments with local builds.
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { type ActionFunctionArgs, json } from "@remix-run/server-runtime";
2+
import {
3+
type GenerateRegistryCredentialsResponseBody,
4+
ProgressDeploymentRequestBody,
5+
tryCatch,
6+
} from "@trigger.dev/core/v3";
7+
import { z } from "zod";
8+
import { authenticateRequest } from "~/services/apiAuth.server";
9+
import { logger } from "~/services/logger.server";
10+
import { DeploymentService } from "~/v3/services/deployment.server";
11+
12+
const ParamsSchema = z.object({
13+
deploymentId: z.string(),
14+
});
15+
16+
export async function action({ request, params }: ActionFunctionArgs) {
17+
if (request.method.toUpperCase() !== "POST") {
18+
return json({ error: "Method Not Allowed" }, { status: 405 });
19+
}
20+
21+
const parsedParams = ParamsSchema.safeParse(params);
22+
23+
if (!parsedParams.success) {
24+
return json({ error: "Invalid params" }, { status: 400 });
25+
}
26+
27+
const authenticationResult = await authenticateRequest(request, {
28+
apiKey: true,
29+
organizationAccessToken: false,
30+
personalAccessToken: false,
31+
});
32+
33+
if (!authenticationResult || !authenticationResult.result.ok) {
34+
logger.info("Invalid or missing api key", { url: request.url });
35+
return json({ error: "Invalid or Missing API key" }, { status: 401 });
36+
}
37+
38+
const { environment: authenticatedEnv } = authenticationResult.result;
39+
const { deploymentId } = parsedParams.data;
40+
41+
const [, rawBody] = await tryCatch(request.json());
42+
const body = ProgressDeploymentRequestBody.safeParse(rawBody ?? {});
43+
44+
if (!body.success) {
45+
return json({ error: "Invalid request body", issues: body.error.issues }, { status: 400 });
46+
}
47+
48+
const deploymentService = new DeploymentService();
49+
50+
return await deploymentService.generateRegistryCredentials(authenticatedEnv, deploymentId).match(
51+
(result) => {
52+
return json(
53+
{
54+
username: result.username,
55+
password: result.password,
56+
expiresAt: result.expiresAt.toISOString(),
57+
repositoryUri: result.repositoryUri,
58+
} satisfies GenerateRegistryCredentialsResponseBody,
59+
{ status: 200 }
60+
);
61+
},
62+
(error) => {
63+
switch (error.type) {
64+
case "deployment_not_found":
65+
return json({ error: "Deployment not found" }, { status: 404 });
66+
case "deployment_has_no_image_reference":
67+
logger.error(
68+
"Failed to generate registry credentials: deployment_has_no_image_reference",
69+
{ deploymentId }
70+
);
71+
return json({ error: "Deployment has no image reference" }, { status: 409 });
72+
case "deployment_is_already_final":
73+
return json(
74+
{ error: "Failed to generate registry credentials: deployment_is_already_final" },
75+
{ status: 409 }
76+
);
77+
case "missing_registry_credentials":
78+
logger.error("Failed to generate registry credentials: missing_registry_credentials", {
79+
deploymentId,
80+
});
81+
return json({ error: "Missing registry credentials" }, { status: 409 });
82+
case "registry_not_supported":
83+
logger.error("Failed to generate registry credentials: registry_not_supported", {
84+
deploymentId,
85+
});
86+
return json({ error: "Registry not supported" }, { status: 409 });
87+
case "registry_region_not_supported":
88+
logger.error("Failed to generate registry credentials: registry_region_not_supported", {
89+
deploymentId,
90+
});
91+
return json({ error: "Registry region not supported" }, { status: 409 });
92+
case "other":
93+
default:
94+
error.type satisfies "other";
95+
logger.error("Failed to generate registry credentials", { error: error.cause });
96+
return json({ error: "Internal server error" }, { status: 500 });
97+
}
98+
}
99+
);
100+
}

apps/webapp/app/services/platform.v3.server.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,24 @@ export async function setBillingAlert(
511511
return result;
512512
}
513513

514+
export async function generateRegistryCredentials(
515+
projectId: string,
516+
region: "us-east-1" | "eu-central-1"
517+
) {
518+
if (!client) return undefined;
519+
const result = await client.generateRegistryCredentials(projectId, region);
520+
if (!result.success) {
521+
logger.error("Error generating registry credentials", {
522+
error: result.error,
523+
projectId,
524+
region,
525+
});
526+
throw new Error("Failed to generate registry credentials");
527+
}
528+
529+
return result;
530+
}
531+
514532
function isCloud(): boolean {
515533
const acceptableHosts = [
516534
"https://cloud.trigger.dev",

apps/webapp/app/v3/services/deployment.server.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { TimeoutDeploymentService } from "./timeoutDeployment.server";
77
import { env } from "~/env.server";
88
import { createRemoteImageBuild } from "../remoteImageBuilder.server";
99
import { FINAL_DEPLOYMENT_STATUSES } from "./failDeployment.server";
10+
import { generateRegistryCredentials } from "~/services/platform.v3.server";
1011

1112
export class DeploymentService extends BaseService {
1213
/**
@@ -231,4 +232,92 @@ export class DeploymentService extends BaseService {
231232
.andThen(deleteTimeout)
232233
.map(() => undefined);
233234
}
235+
236+
/**
237+
* Generates registry credentials for a deployment. Returns an error if the deployment is in a final state.
238+
*
239+
* Uses the `platform` package, only available in cloud.
240+
*
241+
* @param authenticatedEnv The environment which the deployment belongs to.
242+
* @param friendlyId The friendly deployment ID.
243+
*/
244+
public generateRegistryCredentials(
245+
authenticatedEnv: Pick<AuthenticatedEnvironment, "projectId">,
246+
friendlyId: string
247+
) {
248+
const validateDeployment = (
249+
deployment: Pick<WorkerDeployment, "id" | "status" | "imageReference">
250+
) => {
251+
if (FINAL_DEPLOYMENT_STATUSES.includes(deployment.status)) {
252+
return errAsync({ type: "deployment_is_already_final" as const });
253+
}
254+
return okAsync(deployment);
255+
};
256+
257+
const getDeploymentRegion = (deployment: Pick<WorkerDeployment, "imageReference">) => {
258+
if (!deployment.imageReference) {
259+
return errAsync({ type: "deployment_has_no_image_reference" as const });
260+
}
261+
if (!deployment.imageReference.includes("amazonaws.com")) {
262+
return errAsync({ type: "registry_not_supported" as const });
263+
}
264+
265+
// we should connect the deployment to a region more explicitly in the future
266+
// for now we just use the image reference to determine the region
267+
if (deployment.imageReference.includes("us-east-1")) {
268+
return okAsync({ region: "us-east-1" as const });
269+
}
270+
if (deployment.imageReference.includes("eu-central-1")) {
271+
return okAsync({ region: "eu-central-1" as const });
272+
}
273+
274+
return errAsync({ type: "registry_region_not_supported" as const });
275+
};
276+
277+
const generateCredentials = ({ region }: { region: "us-east-1" | "eu-central-1" }) =>
278+
fromPromise(generateRegistryCredentials(authenticatedEnv.projectId, region), (error) => ({
279+
type: "other" as const,
280+
cause: error,
281+
})).andThen((result) => {
282+
if (!result || !result.success) {
283+
return errAsync({ type: "missing_registry_credentials" as const });
284+
}
285+
return okAsync({
286+
username: result.username,
287+
password: result.password,
288+
expiresAt: new Date(result.expiresAt),
289+
repositoryUri: result.repositoryUri,
290+
});
291+
});
292+
293+
return this.getDeployment(authenticatedEnv.projectId, friendlyId)
294+
.andThen(validateDeployment)
295+
.andThen(getDeploymentRegion)
296+
.andThen(generateCredentials);
297+
}
298+
299+
private getDeployment(projectId: string, friendlyId: string) {
300+
return fromPromise(
301+
this._prisma.workerDeployment.findFirst({
302+
where: {
303+
friendlyId,
304+
projectId,
305+
},
306+
select: {
307+
status: true,
308+
id: true,
309+
imageReference: true,
310+
},
311+
}),
312+
(error) => ({
313+
type: "other" as const,
314+
cause: error,
315+
})
316+
).andThen((deployment) => {
317+
if (!deployment) {
318+
return errAsync({ type: "deployment_not_found" as const });
319+
}
320+
return okAsync(deployment);
321+
});
322+
}
234323
}

apps/webapp/app/v3/services/finalizeDeploymentV2.server.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,15 @@ export class FinalizeDeploymentV2Service extends BaseService {
7171
throw new ServiceValidationError("Worker deployment is not in DEPLOYING status");
7272
}
7373

74+
const finalizeService = new FinalizeDeploymentService();
75+
76+
if (body.skipPushToRegistry) {
77+
logger.debug("Skipping push to registry during deployment finalization", {
78+
deployment,
79+
});
80+
return await finalizeService.call(authenticatedEnv, id, body);
81+
}
82+
7483
const externalBuildData = deployment.externalBuildData
7584
? ExternalBuildData.safeParse(deployment.externalBuildData)
7685
: undefined;
@@ -134,7 +143,6 @@ export class FinalizeDeploymentV2Service extends BaseService {
134143
pushedImage: pushResult.image,
135144
});
136145

137-
const finalizeService = new FinalizeDeploymentService();
138146
const finalizedDeployment = await finalizeService.call(authenticatedEnv, id, body);
139147

140148
return finalizedDeployment;

apps/webapp/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@
117117
"@trigger.dev/core": "workspace:*",
118118
"@trigger.dev/database": "workspace:*",
119119
"@trigger.dev/otlp-importer": "workspace:*",
120-
"@trigger.dev/platform": "1.0.18",
120+
"@trigger.dev/platform": "1.0.19",
121121
"@trigger.dev/redis-worker": "workspace:*",
122122
"@trigger.dev/sdk": "workspace:*",
123123
"@types/pg": "8.6.6",

packages/cli-v3/src/apiClient.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {
3737
GetJWTRequestBody,
3838
GetJWTResponse,
3939
ApiBranchListResponseBody,
40+
GenerateRegistryCredentialsResponseBody,
4041
} from "@trigger.dev/core/v3";
4142
import {
4243
WorkloadDebugLogRequestBody,
@@ -327,6 +328,22 @@ export class CliApiClient {
327328
);
328329
}
329330

331+
async generateRegistryCredentials(deploymentId: string) {
332+
if (!this.accessToken) {
333+
throw new Error("generateRegistryCredentials: No access token");
334+
}
335+
336+
return wrapZodFetch(
337+
GenerateRegistryCredentialsResponseBody,
338+
`${this.apiURL}/api/v1/deployments/${deploymentId}/generate-registry-credentials`,
339+
{
340+
method: "POST",
341+
headers: this.getHeaders(),
342+
body: "{}",
343+
}
344+
);
345+
}
346+
330347
async initializeDeployment(body: InitializeDeploymentRequestBody) {
331348
if (!this.accessToken) {
332349
throw new Error("initializeDeployment: No access token");

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

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ const DeployCommandOptions = CommonCommandOptions.extend({
5757
noCache: z.boolean().default(false),
5858
envFile: z.string().optional(),
5959
// Local build options
60+
forceLocalBuild: z.boolean().optional(),
6061
network: z.enum(["default", "none", "host"]).optional(),
6162
push: z.boolean().optional(),
6263
builder: z.string().default("trigger"),
@@ -127,6 +128,9 @@ export function configureDeployCommand(program: Command) {
127128
).hideHelp()
128129
)
129130
// Local build options
131+
.addOption(
132+
new CommandOption("--force-local-build", "Force a local build of the image").hideHelp()
133+
)
130134
.addOption(new CommandOption("--push", "Push the image after local builds").hideHelp())
131135
.addOption(
132136
new CommandOption("--no-push", "Do not push the image after local builds").hideHelp()
@@ -320,7 +324,9 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) {
320324
},
321325
envVars.TRIGGER_EXISTING_DEPLOYMENT_ID
322326
);
323-
const isLocalBuild = !deployment.externalBuildData;
327+
const isLocalBuild = options.forceLocalBuild || !deployment.externalBuildData;
328+
// Would be best to actually store this separately in the deployment object. This is an okay proxy for now.
329+
const remoteBuildExplicitlySkipped = options.forceLocalBuild && !!deployment.externalBuildData;
324330

325331
// Fail fast if we know local builds will fail
326332
if (isLocalBuild) {
@@ -391,8 +397,10 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) {
391397

392398
const $spinner = spinner();
393399

394-
const buildSuffix = isLocalBuild ? " (local)" : "";
395-
const deploySuffix = isLocalBuild ? " (local build)" : "";
400+
const buildSuffix =
401+
isLocalBuild && !process.env.TRIGGER_LOCAL_BUILD_LABEL_DISABLED ? " (local)" : "";
402+
const deploySuffix =
403+
isLocalBuild && !process.env.TRIGGER_LOCAL_BUILD_LABEL_DISABLED ? " (local build)" : "";
396404

397405
if (isCI) {
398406
log.step(`Building version ${version}\n`);
@@ -420,6 +428,7 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) {
420428
projectRef: resolvedConfig.project,
421429
apiUrl: projectClient.client.apiURL,
422430
apiKey: projectClient.client.accessToken!,
431+
apiClient: projectClient.client,
423432
branchName: branch,
424433
authAccessToken: authorization.auth.accessToken,
425434
compilationPath: destination.path,
@@ -442,6 +451,7 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) {
442451
network: options.network,
443452
builder: options.builder,
444453
push: options.push,
454+
authenticateToRegistry: remoteBuildExplicitlySkipped,
445455
});
446456

447457
logger.debug("Build result", buildResult);
@@ -525,6 +535,7 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) {
525535
{
526536
imageDigest: buildResult.digest,
527537
skipPromotion: options.skipPromotion,
538+
skipPushToRegistry: remoteBuildExplicitlySkipped,
528539
},
529540
(logMessage) => {
530541
if (isCI) {

packages/cli-v3/src/commands/workers/build.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,7 @@ async function _workerBuildCommand(dir: string, options: WorkersBuildCommandOpti
336336
projectRef: resolvedConfig.project,
337337
apiUrl: projectClient.client.apiURL,
338338
apiKey: projectClient.client.accessToken!,
339+
apiClient: projectClient.client,
339340
branchName: branch,
340341
authAccessToken: authorization.auth.accessToken,
341342
compilationPath: destination.path,

0 commit comments

Comments
 (0)