Skip to content

Commit a1e9738

Browse files
authored
fix(webapp): prevent duplicate preview env image tags (#2475)
* fix(webapp): prevent duplicate preview env image tags * use deploy shortcode instead of new nanoid * replace regexps in tests
1 parent 59c17e0 commit a1e9738

File tree

3 files changed

+104
-62
lines changed

3 files changed

+104
-62
lines changed

apps/webapp/app/v3/getDeploymentImageRef.server.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { STSClient, AssumeRoleCommand } from "@aws-sdk/client-sts";
1111
import { tryCatch } from "@trigger.dev/core";
1212
import { logger } from "~/services/logger.server";
1313
import { type RegistryConfig } from "./registryConfig.server";
14+
import type { EnvironmentType } from "@trigger.dev/core/v3";
1415

1516
// Optional configuration for cross-account access
1617
export type AssumeRoleConfig = {
@@ -101,19 +102,22 @@ export async function getDeploymentImageRef({
101102
registry,
102103
projectRef,
103104
nextVersion,
104-
environmentSlug,
105+
environmentType,
106+
deploymentShortCode,
105107
}: {
106108
registry: RegistryConfig;
107109
projectRef: string;
108110
nextVersion: string;
109-
environmentSlug: string;
111+
environmentType: EnvironmentType;
112+
deploymentShortCode: string;
110113
}): Promise<{
111114
imageRef: string;
112115
isEcr: boolean;
113116
repoCreated: boolean;
114117
}> {
115118
const repositoryName = `${registry.namespace}/${projectRef}`;
116-
const imageRef = `${registry.host}/${repositoryName}:${nextVersion}.${environmentSlug}`;
119+
const envType = environmentType.toLowerCase();
120+
const imageRef = `${registry.host}/${repositoryName}:${nextVersion}.${envType}.${deploymentShortCode}`;
117121

118122
if (!isEcrRegistry(registry.host)) {
119123
return {

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,12 +73,15 @@ export class InitializeDeploymentService extends BaseService {
7373
const isV4Deployment = payload.type === "MANAGED";
7474
const registryConfig = getRegistryConfig(isV4Deployment);
7575

76+
const deploymentShortCode = nanoid(8);
77+
7678
const [imageRefError, imageRefResult] = await tryCatch(
7779
getDeploymentImageRef({
7880
registry: registryConfig,
7981
projectRef: environment.project.externalRef,
8082
nextVersion,
81-
environmentSlug: environment.slug,
83+
environmentType: environment.type,
84+
deploymentShortCode,
8285
})
8386
);
8487

@@ -111,7 +114,7 @@ export class InitializeDeploymentService extends BaseService {
111114
data: {
112115
friendlyId: generateFriendlyId("deployment"),
113116
contentHash: payload.contentHash,
114-
shortCode: nanoid(8),
117+
shortCode: deploymentShortCode,
115118
version: nextVersion,
116119
status: "BUILDING",
117120
environmentId: environment.id,

apps/webapp/test/getDeploymentImageRef.test.ts

Lines changed: 92 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
} from "../app/v3/getDeploymentImageRef.server";
99
import { DeleteRepositoryCommand } from "@aws-sdk/client-ecr";
1010

11-
describe.skipIf(process.env.RUN_REGISTRY_TESTS !== "1")("getDeploymentImageRef", () => {
11+
describe("getDeploymentImageRef", () => {
1212
const testHost =
1313
process.env.DEPLOY_REGISTRY_HOST || "123456789012.dkr.ecr.us-east-1.amazonaws.com";
1414
const testNamespace = process.env.DEPLOY_REGISTRY_NAMESPACE || "test-namespace";
@@ -25,7 +25,7 @@ describe.skipIf(process.env.RUN_REGISTRY_TESTS !== "1")("getDeploymentImageRef",
2525

2626
// Clean up test repository after tests
2727
afterAll(async () => {
28-
if (process.env.KEEP_TEST_REPO === "1") {
28+
if (process.env.KEEP_TEST_REPO === "1" || process.env.RUN_ECR_TESTS !== "1") {
2929
return;
3030
}
3131

@@ -57,7 +57,7 @@ describe.skipIf(process.env.RUN_REGISTRY_TESTS !== "1")("getDeploymentImageRef",
5757
it("should return the correct image ref for non-ECR registry", async () => {
5858
const imageRef = await getDeploymentImageRef({
5959
registry: {
60-
host: "registry.digitalocean.com",
60+
host: "registry.example.com",
6161
namespace: testNamespace,
6262
username: "test-user",
6363
password: "test-pass",
@@ -67,17 +67,69 @@ describe.skipIf(process.env.RUN_REGISTRY_TESTS !== "1")("getDeploymentImageRef",
6767
},
6868
projectRef: testProjectRef,
6969
nextVersion: "20250630.1",
70-
environmentSlug: "test",
70+
environmentType: "DEVELOPMENT",
71+
deploymentShortCode: "test1234",
7172
});
7273

74+
// Check the image ref structure and that it contains expected parts
7375
expect(imageRef.imageRef).toBe(
74-
`registry.digitalocean.com/${testNamespace}/${testProjectRef}:20250630.1.test`
76+
`registry.example.com/${testNamespace}/${testProjectRef}:20250630.1.development.test1234`
7577
);
7678
expect(imageRef.isEcr).toBe(false);
7779
});
7880

79-
it("should create ECR repository and return correct image ref", async () => {
80-
const imageRef1 = await getDeploymentImageRef({
81+
it.skipIf(process.env.RUN_ECR_TESTS !== "1")(
82+
"should create ECR repository and return correct image ref",
83+
async () => {
84+
const imageRef1 = await getDeploymentImageRef({
85+
registry: {
86+
host: testHost,
87+
namespace: testNamespace,
88+
username: "test-user",
89+
password: "test-pass",
90+
ecrTags: registryTags,
91+
ecrAssumeRoleArn: roleArn,
92+
ecrAssumeRoleExternalId: externalId,
93+
},
94+
projectRef: testProjectRef2,
95+
nextVersion: "20250630.1",
96+
environmentType: "DEVELOPMENT",
97+
deploymentShortCode: "test1234",
98+
});
99+
100+
expect(imageRef1.imageRef).toBe(
101+
`${testHost}/${testNamespace}/${testProjectRef2}:20250630.1.development.test1234`
102+
);
103+
expect(imageRef1.isEcr).toBe(true);
104+
expect(imageRef1.repoCreated).toBe(true);
105+
106+
const imageRef2 = await getDeploymentImageRef({
107+
registry: {
108+
host: testHost,
109+
namespace: testNamespace,
110+
username: "test-user",
111+
password: "test-pass",
112+
ecrTags: registryTags,
113+
ecrAssumeRoleArn: roleArn,
114+
ecrAssumeRoleExternalId: externalId,
115+
},
116+
projectRef: testProjectRef2,
117+
nextVersion: "20250630.2",
118+
environmentType: "DEVELOPMENT",
119+
deploymentShortCode: "test1234",
120+
});
121+
122+
expect(imageRef2.imageRef).toBe(
123+
`${testHost}/${testNamespace}/${testProjectRef2}:20250630.2.development.test1234`
124+
);
125+
expect(imageRef2.isEcr).toBe(true);
126+
expect(imageRef2.repoCreated).toBe(false);
127+
}
128+
);
129+
130+
it.skipIf(process.env.RUN_ECR_TESTS !== "1")("should reuse existing ECR repository", async () => {
131+
// This should use the repository created in the previous test
132+
const imageRef = await getDeploymentImageRef({
81133
registry: {
82134
host: testHost,
83135
namespace: testNamespace,
@@ -87,44 +139,42 @@ describe.skipIf(process.env.RUN_REGISTRY_TESTS !== "1")("getDeploymentImageRef",
87139
ecrAssumeRoleArn: roleArn,
88140
ecrAssumeRoleExternalId: externalId,
89141
},
90-
projectRef: testProjectRef2,
91-
nextVersion: "20250630.1",
92-
environmentSlug: "test",
142+
projectRef: testProjectRef,
143+
nextVersion: "20250630.2",
144+
environmentType: "PRODUCTION",
145+
deploymentShortCode: "test1234",
93146
});
94147

95-
expect(imageRef1.imageRef).toBe(
96-
`${testHost}/${testNamespace}/${testProjectRef2}:20250630.1.test`
148+
expect(imageRef.imageRef).toBe(
149+
`${testHost}/${testNamespace}/${testProjectRef}:20250630.2.production.test1234`
97150
);
98-
expect(imageRef1.isEcr).toBe(true);
99-
expect(imageRef1.repoCreated).toBe(true);
151+
expect(imageRef.isEcr).toBe(true);
152+
});
153+
154+
it("should generate unique image tags for different deployments with same environment type", async () => {
155+
// Simulates the scenario where multiple deployments happen to the same environment type
156+
const sameEnvironmentType = "PREVIEW";
157+
const sameVersion = "20250630.1";
100158

101-
const imageRef2 = await getDeploymentImageRef({
159+
const firstImageRef = await getDeploymentImageRef({
102160
registry: {
103-
host: testHost,
161+
host: "registry.example.com",
104162
namespace: testNamespace,
105163
username: "test-user",
106164
password: "test-pass",
107165
ecrTags: registryTags,
108166
ecrAssumeRoleArn: roleArn,
109167
ecrAssumeRoleExternalId: externalId,
110168
},
111-
projectRef: testProjectRef2,
112-
nextVersion: "20250630.2",
113-
environmentSlug: "test",
169+
projectRef: testProjectRef,
170+
nextVersion: sameVersion,
171+
environmentType: sameEnvironmentType,
172+
deploymentShortCode: "test1234",
114173
});
115174

116-
expect(imageRef2.imageRef).toBe(
117-
`${testHost}/${testNamespace}/${testProjectRef2}:20250630.2.test`
118-
);
119-
expect(imageRef2.isEcr).toBe(true);
120-
expect(imageRef2.repoCreated).toBe(false);
121-
});
122-
123-
it("should reuse existing ECR repository", async () => {
124-
// This should use the repository created in the previous test
125-
const imageRef = await getDeploymentImageRef({
175+
const secondImageRef = await getDeploymentImageRef({
126176
registry: {
127-
host: testHost,
177+
host: "registry.example.com",
128178
namespace: testNamespace,
129179
username: "test-user",
130180
password: "test-pass",
@@ -133,37 +183,23 @@ describe.skipIf(process.env.RUN_REGISTRY_TESTS !== "1")("getDeploymentImageRef",
133183
ecrAssumeRoleExternalId: externalId,
134184
},
135185
projectRef: testProjectRef,
136-
nextVersion: "20250630.2",
137-
environmentSlug: "prod",
186+
nextVersion: sameVersion,
187+
environmentType: sameEnvironmentType,
188+
deploymentShortCode: "test4321",
138189
});
139190

140-
expect(imageRef.imageRef).toBe(
141-
`${testHost}/${testNamespace}/${testProjectRef}:20250630.2.prod`
191+
// Even with the same environment type and version, the image refs should be different due to deployment short codes
192+
expect(firstImageRef.imageRef).toBe(
193+
`registry.example.com/${testNamespace}/${testProjectRef}:${sameVersion}.preview.test1234`
142194
);
143-
expect(imageRef.isEcr).toBe(true);
144-
});
145-
146-
it("should throw error for invalid ECR host", async () => {
147-
await expect(
148-
getDeploymentImageRef({
149-
registry: {
150-
host: "invalid.ecr.amazonaws.com",
151-
namespace: testNamespace,
152-
username: "test-user",
153-
password: "test-pass",
154-
ecrTags: registryTags,
155-
ecrAssumeRoleArn: roleArn,
156-
ecrAssumeRoleExternalId: externalId,
157-
},
158-
projectRef: testProjectRef,
159-
nextVersion: "20250630.1",
160-
environmentSlug: "test",
161-
})
162-
).rejects.toThrow("Invalid ECR registry host: invalid.ecr.amazonaws.com");
195+
expect(secondImageRef.imageRef).toBe(
196+
`registry.example.com/${testNamespace}/${testProjectRef}:${sameVersion}.preview.test4321`
197+
);
198+
expect(firstImageRef.imageRef).not.toBe(secondImageRef.imageRef);
163199
});
164200
});
165201

166-
describe.skipIf(process.env.RUN_REGISTRY_AUTH_TESTS !== "1")("getEcrAuthToken", () => {
202+
describe.skipIf(process.env.RUN_ECR_TESTS !== "1")("getEcrAuthToken", () => {
167203
const testHost =
168204
process.env.DEPLOY_REGISTRY_HOST || "123456789012.dkr.ecr.us-east-1.amazonaws.com";
169205

@@ -188,8 +224,7 @@ describe.skipIf(process.env.RUN_REGISTRY_AUTH_TESTS !== "1")("getEcrAuthToken",
188224
expect(auth.password.length).toBeGreaterThan(0);
189225

190226
// Verify the token format (should be a base64-encoded string)
191-
const base64Regex = /^[A-Za-z0-9+/=]+$/;
192-
expect(base64Regex.test(auth.password)).toBe(true);
227+
expect(auth.password).toMatch(/^[A-Za-z0-9+/=]+$/);
193228
});
194229

195230
it("should throw error for invalid region", async () => {

0 commit comments

Comments
 (0)