Skip to content

Commit cd07aae

Browse files
committed
fix(webapp): prevent duplicate preview env image tags
1 parent ed23615 commit cd07aae

File tree

3 files changed

+122
-60
lines changed

3 files changed

+122
-60
lines changed

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ 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";
15+
import { customAlphabet } from "nanoid";
16+
17+
const nanoid = customAlphabet("1234567890abcdefghijklmnopqrstuvwxyz", 8);
1418

1519
// Optional configuration for cross-account access
1620
export type AssumeRoleConfig = {
@@ -101,19 +105,21 @@ export async function getDeploymentImageRef({
101105
registry,
102106
projectRef,
103107
nextVersion,
104-
environmentSlug,
108+
environmentType,
105109
}: {
106110
registry: RegistryConfig;
107111
projectRef: string;
108112
nextVersion: string;
109-
environmentSlug: string;
113+
environmentType: EnvironmentType;
110114
}): Promise<{
111115
imageRef: string;
112116
isEcr: boolean;
113117
repoCreated: boolean;
114118
}> {
115119
const repositoryName = `${registry.namespace}/${projectRef}`;
116-
const imageRef = `${registry.host}/${repositoryName}:${nextVersion}.${environmentSlug}`;
120+
const envType = environmentType.toLowerCase();
121+
const randomSuffix = nanoid();
122+
const imageRef = `${registry.host}/${repositoryName}:${nextVersion}.${envType}.${randomSuffix}`;
117123

118124
if (!isEcrRegistry(registry.host)) {
119125
return {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export class InitializeDeploymentService extends BaseService {
7878
registry: registryConfig,
7979
projectRef: environment.project.externalRef,
8080
nextVersion,
81-
environmentSlug: environment.slug,
81+
environmentType: environment.type,
8282
})
8383
);
8484

apps/webapp/test/getDeploymentImageRef.test.ts

Lines changed: 112 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ 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+
const escapeHostForRegex = (host: string) => host.replace(/\./g, "\\.");
12+
13+
describe("getDeploymentImageRef", () => {
1214
const testHost =
1315
process.env.DEPLOY_REGISTRY_HOST || "123456789012.dkr.ecr.us-east-1.amazonaws.com";
1416
const testNamespace = process.env.DEPLOY_REGISTRY_NAMESPACE || "test-namespace";
@@ -25,7 +27,7 @@ describe.skipIf(process.env.RUN_REGISTRY_TESTS !== "1")("getDeploymentImageRef",
2527

2628
// Clean up test repository after tests
2729
afterAll(async () => {
28-
if (process.env.KEEP_TEST_REPO === "1") {
30+
if (process.env.KEEP_TEST_REPO === "1" || process.env.RUN_ECR_TESTS !== "1") {
2931
return;
3032
}
3133

@@ -57,7 +59,7 @@ describe.skipIf(process.env.RUN_REGISTRY_TESTS !== "1")("getDeploymentImageRef",
5759
it("should return the correct image ref for non-ECR registry", async () => {
5860
const imageRef = await getDeploymentImageRef({
5961
registry: {
60-
host: "registry.digitalocean.com",
62+
host: "registry.example.com",
6163
namespace: testNamespace,
6264
username: "test-user",
6365
password: "test-pass",
@@ -67,17 +69,78 @@ describe.skipIf(process.env.RUN_REGISTRY_TESTS !== "1")("getDeploymentImageRef",
6769
},
6870
projectRef: testProjectRef,
6971
nextVersion: "20250630.1",
70-
environmentSlug: "test",
72+
environmentType: "DEVELOPMENT",
7173
});
7274

73-
expect(imageRef.imageRef).toBe(
74-
`registry.digitalocean.com/${testNamespace}/${testProjectRef}:20250630.1.test`
75+
// Check the image ref structure and that it contains expected parts
76+
expect(imageRef.imageRef).toMatch(
77+
new RegExp(
78+
`^${escapeHostForRegex(
79+
"registry.example.com"
80+
)}/${testNamespace}/${testProjectRef}:20250630\\.1\\.development\\.[a-z0-9]{8}$`
81+
)
7582
);
7683
expect(imageRef.isEcr).toBe(false);
7784
});
7885

79-
it("should create ECR repository and return correct image ref", async () => {
80-
const imageRef1 = await getDeploymentImageRef({
86+
it.skipIf(process.env.RUN_ECR_TESTS !== "1")(
87+
"should create ECR repository and return correct image ref",
88+
async () => {
89+
const imageRef1 = await getDeploymentImageRef({
90+
registry: {
91+
host: testHost,
92+
namespace: testNamespace,
93+
username: "test-user",
94+
password: "test-pass",
95+
ecrTags: registryTags,
96+
ecrAssumeRoleArn: roleArn,
97+
ecrAssumeRoleExternalId: externalId,
98+
},
99+
projectRef: testProjectRef2,
100+
nextVersion: "20250630.1",
101+
environmentType: "DEVELOPMENT",
102+
});
103+
104+
expect(imageRef1.imageRef).toMatch(
105+
new RegExp(
106+
`^${escapeHostForRegex(
107+
testHost
108+
)}/${testNamespace}/${testProjectRef2}:20250630\\.1\\.development\\.[a-z0-9]{8}$`
109+
)
110+
);
111+
expect(imageRef1.isEcr).toBe(true);
112+
expect(imageRef1.repoCreated).toBe(true);
113+
114+
const imageRef2 = await getDeploymentImageRef({
115+
registry: {
116+
host: testHost,
117+
namespace: testNamespace,
118+
username: "test-user",
119+
password: "test-pass",
120+
ecrTags: registryTags,
121+
ecrAssumeRoleArn: roleArn,
122+
ecrAssumeRoleExternalId: externalId,
123+
},
124+
projectRef: testProjectRef2,
125+
nextVersion: "20250630.2",
126+
environmentType: "DEVELOPMENT",
127+
});
128+
129+
expect(imageRef2.imageRef).toMatch(
130+
new RegExp(
131+
`^${escapeHostForRegex(
132+
testHost
133+
)}/${testNamespace}/${testProjectRef2}:20250630\\.2\\.development\\.[a-z0-9]{8}$`
134+
)
135+
);
136+
expect(imageRef2.isEcr).toBe(true);
137+
expect(imageRef2.repoCreated).toBe(false);
138+
}
139+
);
140+
141+
it.skipIf(process.env.RUN_ECR_TESTS !== "1")("should reuse existing ECR repository", async () => {
142+
// This should use the repository created in the previous test
143+
const imageRef = await getDeploymentImageRef({
81144
registry: {
82145
host: testHost,
83146
namespace: testNamespace,
@@ -87,44 +150,44 @@ describe.skipIf(process.env.RUN_REGISTRY_TESTS !== "1")("getDeploymentImageRef",
87150
ecrAssumeRoleArn: roleArn,
88151
ecrAssumeRoleExternalId: externalId,
89152
},
90-
projectRef: testProjectRef2,
91-
nextVersion: "20250630.1",
92-
environmentSlug: "test",
153+
projectRef: testProjectRef,
154+
nextVersion: "20250630.2",
155+
environmentType: "PRODUCTION",
93156
});
94157

95-
expect(imageRef1.imageRef).toBe(
96-
`${testHost}/${testNamespace}/${testProjectRef2}:20250630.1.test`
158+
expect(imageRef.imageRef).toMatch(
159+
new RegExp(
160+
`^${escapeHostForRegex(
161+
testHost
162+
)}/${testNamespace}/${testProjectRef}:20250630\\.2\\.production\\.[a-z0-9]{8}$`
163+
)
97164
);
98-
expect(imageRef1.isEcr).toBe(true);
99-
expect(imageRef1.repoCreated).toBe(true);
165+
expect(imageRef.isEcr).toBe(true);
166+
});
100167

101-
const imageRef2 = await getDeploymentImageRef({
168+
it("should generate unique image tags for different deployments with same environment type", async () => {
169+
// Simulates the scenario where multiple deployments happen to the same environment type
170+
const sameEnvironmentType = "PREVIEW";
171+
const sameVersion = "20250630.1";
172+
173+
const firstImageRef = await getDeploymentImageRef({
102174
registry: {
103-
host: testHost,
175+
host: "registry.example.com",
104176
namespace: testNamespace,
105177
username: "test-user",
106178
password: "test-pass",
107179
ecrTags: registryTags,
108180
ecrAssumeRoleArn: roleArn,
109181
ecrAssumeRoleExternalId: externalId,
110182
},
111-
projectRef: testProjectRef2,
112-
nextVersion: "20250630.2",
113-
environmentSlug: "test",
183+
projectRef: testProjectRef,
184+
nextVersion: sameVersion,
185+
environmentType: sameEnvironmentType,
114186
});
115187

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({
188+
const secondImageRef = await getDeploymentImageRef({
126189
registry: {
127-
host: testHost,
190+
host: "registry.example.com",
128191
namespace: testNamespace,
129192
username: "test-user",
130193
password: "test-pass",
@@ -133,37 +196,30 @@ describe.skipIf(process.env.RUN_REGISTRY_TESTS !== "1")("getDeploymentImageRef",
133196
ecrAssumeRoleExternalId: externalId,
134197
},
135198
projectRef: testProjectRef,
136-
nextVersion: "20250630.2",
137-
environmentSlug: "prod",
199+
nextVersion: sameVersion,
200+
environmentType: sameEnvironmentType,
138201
});
139202

140-
expect(imageRef.imageRef).toBe(
141-
`${testHost}/${testNamespace}/${testProjectRef}:20250630.2.prod`
203+
// Even with the same environment type and version, the image refs should be different due to random suffix
204+
expect(firstImageRef.imageRef).toMatch(
205+
new RegExp(
206+
`^${escapeHostForRegex(
207+
"registry.example.com"
208+
)}/${testNamespace}/${testProjectRef}:${sameVersion}\\.preview\\.[a-z0-9]{8}$`
209+
)
142210
);
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");
211+
expect(secondImageRef.imageRef).toMatch(
212+
new RegExp(
213+
`^${escapeHostForRegex(
214+
"registry.example.com"
215+
)}/${testNamespace}/${testProjectRef}:${sameVersion}\\.preview\\.[a-z0-9]{8}$`
216+
)
217+
);
218+
expect(firstImageRef.imageRef).not.toBe(secondImageRef.imageRef);
163219
});
164220
});
165221

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

0 commit comments

Comments
 (0)