GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — feat(webapp): redeploy --local-bundle deployments from the dashboard · triggerdotdev/trigger.dev@ecf3220

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
  • .server-changes
    • redeploy-local-bundle-deployments.md
  • apps/webapp/app
    • presenters/v3
      • DeploymentListPresenter.server.ts
    • routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments
      • route.tsx
    • v3
      • redeploy.ts
      • services
        • initializeDeployment.server.ts
        • initializeDeployment
          • buildEnvVarsEnvelope.server.test.ts
          • buildEnvVarsEnvelope.server.ts
        • redeployDeployment.server.test.ts
        • redeployDeployment.server.ts
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎.server-changes/redeploy-local-bundle-deployments.md‎

Copy file name to clipboard
+6Lines changed: 6 additions & 0 deletions
  • Display the source diff
  • Display the rich diff
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: feature
4+
---
5+
6+
Recent deployments made with `--local-bundle` can now be redeployed from the deployments list, rebuilding the same bundle with its build-time environment variables.
Collapse file

‎apps/webapp/app/presenters/v3/DeploymentListPresenter.server.ts‎

Copy file name to clipboardExpand all lines: apps/webapp/app/presenters/v3/DeploymentListPresenter.server.ts
+7-1Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ export class DeploymentListPresenter {
158158
triggeredVia: string | null;
159159
git: Prisma.JsonValue | null;
160160
buildServerMetadata: Prisma.JsonValue | null;
161+
hasBuildEnvVars: boolean;
161162
createdAt: Date;
162163
integrationDeploymentId: string | null;
163164
}[]
@@ -182,6 +183,7 @@ export class DeploymentListPresenter {
182183
wd."triggeredVia",
183184
wd."git",
184185
wd."buildServerMetadata",
186+
wd."buildEnvVars" IS NOT NULL AS "hasBuildEnvVars",
185187
wd."createdAt"
186188
${vercelSelect}
187189
FROM
@@ -229,6 +231,7 @@ LIMIT ${pageSize} OFFSET ${pageSize * (page - 1)};`;
229231
const label = labeledDeployments.find(
230232
(labeledDeployment) => labeledDeployment.deploymentId === deployment.id
231233
);
234+
const redeploySource = resolveRedeploySource(deployment);
232235

233236
let vercelDeploymentUrl: string | null = null;
234237
if (
@@ -277,7 +280,10 @@ LIMIT ${pageSize} OFFSET ${pageSize * (page - 1)};`;
277280
triggeredVia: deployment.triggeredVia,
278281
git: processGitMetadata(deployment.git),
279282
createdAt: deployment.createdAt,
280-
redeploySource: resolveRedeploySource(deployment)?.kind ?? null,
283+
redeploySource:
284+
redeploySource?.kind === "artifact" && redeploySource.fromBundle
285+
? ("bundle" as const)
286+
: (redeploySource?.kind ?? null),
281287
vercelDeploymentUrl,
282288
};
283289
}),
Collapse file

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

Copy file name to clipboardExpand all lines: apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments/route.tsx
+6Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -760,6 +760,12 @@ function RedeployDeploymentDialog({
760760
<InlineCode variant="extra-small">{deployment.git.shortSha}</InlineCode> from branch{" "}
761761
<InlineCode variant="extra-small">{deployment.git.branchName}</InlineCode>.
762762
</>
763+
) : deployment.redeploySource === "bundle" ? (
764+
<>
765+
A new deployment will be created from the bundle uploaded for deployment{" "}
766+
<InlineCode variant="extra-small">{deployment.shortCode}</InlineCode>, with the
767+
build-time environment variables it was deployed with.
768+
</>
763769
) : (
764770
<>
765771
A new deployment will be created from the source files of deployment{" "}
Collapse file

‎apps/webapp/app/v3/redeploy.ts‎

Copy file name to clipboardExpand all lines: apps/webapp/app/v3/redeploy.ts
+16-8Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,20 @@ export function redeployOfShortCode(buildServerMetadata: unknown): string | null
88

99
export type RedeploySource =
1010
| { kind: "github"; commitSha: string; commitRef: string; git: GitMeta }
11-
| { kind: "artifact"; artifactKey: string; configFilePath?: string; git?: GitMeta };
11+
| {
12+
kind: "artifact";
13+
artifactKey: string;
14+
configFilePath?: string;
15+
git?: GitMeta;
16+
fromBundle: boolean;
17+
};
1218

13-
/** Which build source a deployment can be rebuilt from. Bundle uploads are not redeployable yet. */
19+
/** Which build source a deployment can be rebuilt from. A bundle also needs its stored build env vars. */
1420
export function resolveRedeploySource(deployment: {
1521
triggeredVia: string | null;
1622
git: unknown;
1723
buildServerMetadata: unknown;
24+
hasBuildEnvVars: boolean;
1825
}): RedeploySource | null {
1926
const git = GitMeta.safeParse(deployment.git);
2027

@@ -31,17 +38,18 @@ export function resolveRedeploySource(deployment: {
3138
}
3239

3340
const metadata = BuildServerMetadata.safeParse(deployment.buildServerMetadata);
34-
if (
35-
metadata.success &&
36-
metadata.data.isNativeBuild &&
37-
metadata.data.artifactKey &&
38-
!metadata.data.fromBundle
39-
) {
41+
if (metadata.success && metadata.data.isNativeBuild && metadata.data.artifactKey) {
42+
// The key prefix still marks a bundle when the metadata flag was lost to schema skew
43+
const fromBundle =
44+
metadata.data.fromBundle === true || metadata.data.artifactKey.startsWith("bundles/");
45+
if (fromBundle && !deployment.hasBuildEnvVars) return null;
46+
4047
return {
4148
kind: "artifact",
4249
artifactKey: metadata.data.artifactKey,
4350
configFilePath: metadata.data.configFilePath,
4451
git: git.success ? git.data : undefined,
52+
fromBundle,
4553
};
4654
}
4755

Collapse file

‎apps/webapp/app/v3/services/initializeDeployment.server.ts‎

Copy file name to clipboardExpand all lines: apps/webapp/app/v3/services/initializeDeployment.server.ts
+11-34Lines changed: 11 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { resolveBuildRuntime } from "@trigger.dev/core/v3/build";
88
import { customAlphabet } from "nanoid";
99
import { env } from "~/env.server";
1010
import { type AuthenticatedEnvironment } from "~/services/apiAuth.server";
11-
import { encryptSecret } from "~/services/secrets/secretStore.server";
11+
import { type EncryptedSecretValue } from "~/services/secrets/secretStore.server";
1212
import { logger } from "~/services/logger.server";
1313
import { generateFriendlyId } from "../friendlyIdentifiers";
1414
import { createRemoteImageBuild, remoteBuildsEnabled } from "../remoteImageBuilder.server";
@@ -20,6 +20,7 @@ import { tryCatch } from "@trigger.dev/core";
2020
import { getRegistryConfig } from "../registryConfig.server";
2121
import { DeploymentService } from "./deployment.server";
2222
import { recordDeploymentInitialized } from "./recordDeploymentFinished.server";
23+
import { buildEnvVarsEnvelope } from "./initializeDeployment/buildEnvVarsEnvelope.server";
2324
import { createDeploymentWithNextVersion } from "./initializeDeployment/createDeploymentWithNextVersion.server";
2425
import {
2526
cancelSupersededDeployments,
@@ -61,7 +62,7 @@ export class InitializeDeploymentService extends BaseService {
6162
public async call(
6263
environment: AuthenticatedEnvironment,
6364
payload: InitializeDeploymentRequestBody,
64-
options?: { cliVersion?: string; redeployOf?: string }
65+
options?: { cliVersion?: string; redeployOf?: string; buildEnvVars?: EncryptedSecretValue }
6566
): Promise<InitializeDeploymentResult> {
6667
return this.traceWithEnv("call", environment, async (span) => {
6768
let runtime: BuildRuntime;
@@ -223,6 +224,14 @@ export class InitializeDeploymentService extends BaseService {
223224

224225
span.setAttribute("outcome", "created");
225226

227+
// Before cancelling superseded deployments, so an invalid payload leaves them running
228+
const encryptedBuildEnvVars = await buildEnvVarsEnvelope(payload, {
229+
copyFrom: options?.buildEnvVars,
230+
encryptionKey: env.ENCRYPTION_KEY,
231+
maxKeys: env.DEPLOYMENT_BUILD_ENV_VARS_MAX_KEYS,
232+
maxBytes: env.DEPLOYMENT_BUILD_ENV_VARS_SIZE_LIMIT_BYTES,
233+
});
234+
226235
const canceledDeployments =
227236
reuse.action === "cancel-then-build"
228237
? await cancelSupersededDeployments({
@@ -303,38 +312,6 @@ export class InitializeDeploymentService extends BaseService {
303312
}
304313
: undefined;
305314

306-
let encryptedBuildEnvVars: Awaited<ReturnType<typeof encryptSecret>> | undefined;
307-
308-
if (
309-
payload.isNativeBuild &&
310-
payload.fromBundle &&
311-
payload.buildEnvVars &&
312-
Object.keys(payload.buildEnvVars).length > 0
313-
) {
314-
const buildEnvVars = payload.buildEnvVars;
315-
316-
const keyCount = Object.keys(buildEnvVars).length;
317-
if (keyCount > env.DEPLOYMENT_BUILD_ENV_VARS_MAX_KEYS) {
318-
throw new ServiceValidationError(
319-
`Build environment variable count (${keyCount}) exceeds the allowed limit of ${env.DEPLOYMENT_BUILD_ENV_VARS_MAX_KEYS}. Reach out to us if you are seeing this error consistently.`
320-
);
321-
}
322-
323-
const serialized = JSON.stringify(buildEnvVars);
324-
const serializedBytes = Buffer.byteLength(serialized, "utf8");
325-
if (serializedBytes > env.DEPLOYMENT_BUILD_ENV_VARS_SIZE_LIMIT_BYTES) {
326-
const sizeKB = parseFloat((serializedBytes / 1024).toFixed(1));
327-
const limitKB = parseFloat(
328-
(env.DEPLOYMENT_BUILD_ENV_VARS_SIZE_LIMIT_BYTES / 1024).toFixed(1)
329-
);
330-
throw new ServiceValidationError(
331-
`Build environment variables size (${sizeKB} KB) exceeds the allowed limit of ${limitKB} KB. Reach out to us if you are seeing this error consistently.`
332-
);
333-
}
334-
335-
encryptedBuildEnvVars = await encryptSecret(env.ENCRYPTION_KEY, serialized);
336-
}
337-
338315
const buildServerMetadata: BuildServerMetadata | undefined =
339316
payload.isNativeBuild || payload.buildId || options?.redeployOf
340317
? {
Collapse file

‎apps/webapp/app/v3/services/initializeDeployment/buildEnvVarsEnvelope.server.test.ts‎

Copy file name to clipboard
+55Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { InitializeDeploymentRequestBody } from "@trigger.dev/core/v3";
2+
import { describe, expect, it } from "vitest";
3+
import { decryptSecret } from "~/services/secrets/secretStore.server";
4+
import { buildEnvVarsEnvelope } from "./buildEnvVarsEnvelope.server";
5+
6+
const encryptionKey = "0123456789abcdef0123456789abcdef";
7+
const limits = { encryptionKey, maxKeys: 3, maxBytes: 128 };
8+
9+
function payload(overrides: Record<string, unknown> = {}) {
10+
return InitializeDeploymentRequestBody.parse({
11+
contentHash: "hash",
12+
type: "MANAGED",
13+
isNativeBuild: true,
14+
artifactKey: "bundles/proj_ref/prod/abc.tar.gz",
15+
fromBundle: true,
16+
...overrides,
17+
});
18+
}
19+
20+
async function decrypted(envelope: Awaited<ReturnType<typeof buildEnvVarsEnvelope>>) {
21+
expect(envelope).toBeDefined();
22+
return JSON.parse(await decryptSecret(encryptionKey, envelope!));
23+
}
24+
25+
describe("buildEnvVarsEnvelope", () => {
26+
it("encrypts a bundle deploy's build env vars", async () => {
27+
const vars = { DATABASE_URL: "postgres://build" };
28+
expect(
29+
await decrypted(await buildEnvVarsEnvelope(payload({ buildEnvVars: vars }), limits))
30+
).toEqual(vars);
31+
});
32+
33+
it("stores an empty envelope for a bundle deploy without build env vars", async () => {
34+
expect(await decrypted(await buildEnvVarsEnvelope(payload(), limits))).toEqual({});
35+
});
36+
37+
it("stores a redeploy source's envelope verbatim", async () => {
38+
const copyFrom = { nonce: "n", ciphertext: "c", tag: "t" };
39+
expect(
40+
await buildEnvVarsEnvelope(payload({ buildEnvVars: { IGNORED: "x" } }), {
41+
...limits,
42+
copyFrom,
43+
})
44+
).toBe(copyFrom);
45+
});
46+
47+
it("stores nothing for a source upload", async () => {
48+
expect(
49+
await buildEnvVarsEnvelope(
50+
payload({ fromBundle: undefined, buildEnvVars: { A: "1" } }),
51+
limits
52+
)
53+
).toBeUndefined();
54+
});
55+
});
Collapse file

‎apps/webapp/app/v3/services/initializeDeployment/buildEnvVarsEnvelope.server.ts‎

Copy file name to clipboard
+39Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { type InitializeDeploymentRequestBody } from "@trigger.dev/core/v3";
2+
import { type EncryptedSecretValue, encryptSecret } from "~/services/secrets/secretStore.server";
3+
import { ServiceValidationError } from "../baseService.server";
4+
5+
/** The envelope stored on a bundle deployment: the redeploy source's copy, or the payload's vars encrypted. */
6+
export async function buildEnvVarsEnvelope(
7+
payload: InitializeDeploymentRequestBody,
8+
options: {
9+
copyFrom?: EncryptedSecretValue;
10+
encryptionKey: string;
11+
maxKeys: number;
12+
maxBytes: number;
13+
}
14+
): Promise<EncryptedSecretValue | undefined> {
15+
if (!payload.isNativeBuild || !payload.fromBundle) return undefined;
16+
if (options.copyFrom) return options.copyFrom;
17+
18+
// Stored even when empty: redeploying a bundle requires its envelope
19+
const buildEnvVars = payload.buildEnvVars ?? {};
20+
21+
const keyCount = Object.keys(buildEnvVars).length;
22+
if (keyCount > options.maxKeys) {
23+
throw new ServiceValidationError(
24+
`Build environment variable count (${keyCount}) exceeds the allowed limit of ${options.maxKeys}. Reach out to us if you are seeing this error consistently.`
25+
);
26+
}
27+
28+
const serialized = JSON.stringify(buildEnvVars);
29+
const serializedBytes = Buffer.byteLength(serialized, "utf8");
30+
if (serializedBytes > options.maxBytes) {
31+
const sizeKB = parseFloat((serializedBytes / 1024).toFixed(1));
32+
const limitKB = parseFloat((options.maxBytes / 1024).toFixed(1));
33+
throw new ServiceValidationError(
34+
`Build environment variables size (${sizeKB} KB) exceeds the allowed limit of ${limitKB} KB. Reach out to us if you are seeing this error consistently.`
35+
);
36+
}
37+
38+
return encryptSecret(options.encryptionKey, serialized);
39+
}
Collapse file

‎apps/webapp/app/v3/services/redeployDeployment.server.test.ts‎

Copy file name to clipboardExpand all lines: apps/webapp/app/v3/services/redeployDeployment.server.test.ts
+62-5Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,22 @@ function artifactDeployment(overrides: Record<string, unknown> = {}) {
8787
});
8888
}
8989

90+
const envelope = { nonce: "n", ciphertext: "c", tag: "t" };
91+
92+
function bundleDeployment(overrides: Record<string, unknown> = {}) {
93+
return artifactDeployment({
94+
buildServerMetadata: {
95+
isNativeBuild: true,
96+
artifactKey: "bundles/proj_ref/prod/abc.tar.gz",
97+
configFilePath: "trigger.config.ts",
98+
fromBundle: true,
99+
},
100+
runtime: "bun",
101+
buildEnvVars: envelope,
102+
...overrides,
103+
});
104+
}
105+
90106
// findFirst resolves the source deployment, then the in-flight probe.
91107
function mockLookups(source: unknown, inFlight: unknown = null) {
92108
deps.findFirst.mockReset().mockResolvedValueOnce(source).mockResolvedValueOnce(inFlight);
@@ -136,10 +152,12 @@ describe("RedeployDeploymentService", () => {
136152
["a GitHub deployment missing the commit ref", githubDeployment({ git: { commitSha: sha } })],
137153
["a CLI deployment without an artifact", githubDeployment({ triggeredVia: "cli:manual" })],
138154
[
139-
"a bundle deployment",
140-
artifactDeployment({
141-
buildServerMetadata: { isNativeBuild: true, artifactKey: "bundles/x", fromBundle: true },
142-
}),
155+
"a bundle deployment whose build env vars were cleared",
156+
bundleDeployment({ buildEnvVars: null }),
157+
],
158+
[
159+
"a bundle deployment with an unreadable envelope",
160+
bundleDeployment({ buildEnvVars: { ciphertext: "c" } }),
143161
],
144162
[
145163
"a legacy build deployment",
@@ -268,15 +286,54 @@ describe("RedeployDeploymentService", () => {
268286
expect(payload.externalId).toBeUndefined();
269287
expect(payload.force).toBeUndefined();
270288

289+
expect(deps.initialize.mock.calls[0]![2]).toEqual({ redeployOf: "abcd" });
271290
expect(deps.enqueueArtifact).toHaveBeenCalledWith(
272291
"proj_1",
273292
"deployment_new",
274293
"deployments/proj_ref/prod/abc.tar.gz",
275-
{ configFilePath: "trigger.config.ts", skipPromotion: false }
294+
{ configFilePath: "trigger.config.ts", skipPromotion: false, fromBundle: false }
276295
);
277296
expect(deps.enqueueGithub).not.toHaveBeenCalled();
278297
});
279298

299+
it("rebuilds a bundle deployment and carries its build env vars over", async () => {
300+
mockLookups(bundleDeployment());
301+
expect(await callOk()).toEqual({ version: "20260930.2", shortCode: "efgh" });
302+
303+
const [, payload, options] = deps.initialize.mock.calls[0]!;
304+
expect(payload).toMatchObject({
305+
isNativeBuild: true,
306+
artifactKey: "bundles/proj_ref/prod/abc.tar.gz",
307+
configFilePath: "trigger.config.ts",
308+
fromBundle: true,
309+
runtime: "bun",
310+
skipEnqueue: true,
311+
triggeredVia: "dashboard",
312+
});
313+
expect(payload.buildEnvVars).toBeUndefined();
314+
expect(options).toEqual({ redeployOf: "abcd", buildEnvVars: envelope });
315+
316+
expect(deps.enqueueArtifact).toHaveBeenCalledWith(
317+
"proj_1",
318+
"deployment_new",
319+
"bundles/proj_ref/prod/abc.tar.gz",
320+
{ configFilePath: "trigger.config.ts", skipPromotion: false, fromBundle: true }
321+
);
322+
});
323+
324+
it("treats a bundles/ artifact key as a bundle when the metadata flag is missing", async () => {
325+
mockLookups(
326+
bundleDeployment({
327+
buildServerMetadata: {
328+
isNativeBuild: true,
329+
artifactKey: "bundles/proj_ref/prod/abc.tar.gz",
330+
},
331+
})
332+
);
333+
expect(await callOk()).toMatchObject({ shortCode: "efgh" });
334+
expect(deps.enqueueArtifact.mock.calls[0]![3]).toMatchObject({ fromBundle: true });
335+
});
336+
280337
it("passes skipPromotion through to the deployment and the build", async () => {
281338
expect(await callOk(undefined, true)).toMatchObject({ shortCode: "efgh" });
282339
expect(deps.initialize.mock.calls[0]![1]).toMatchObject({ skipPromotion: true });

0 commit comments

Comments
0 (0)
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.