create repo if doesn't exist

This commit is contained in:
nicktrn
2025-06-30 16:36:58 +01:00
parent aa24a9aae6
commit e16c43ca9f
6 changed files with 1111 additions and 9 deletions
+2
View File
@@ -232,6 +232,8 @@ const EnvironmentSchema = z.object({
DEPLOY_REGISTRY_USERNAME: z.string().optional(),
DEPLOY_REGISTRY_PASSWORD: z.string().optional(),
DEPLOY_REGISTRY_NAMESPACE: z.string().min(1).default("trigger"),
DEPLOY_REGISTRY_ID: z.string().optional(),
DEPLOY_REGISTRY_TAGS: z.string().optional(), // csv, for example: "key1=value1,key2=value2"
DEPLOY_IMAGE_PLATFORM: z.string().default("linux/amd64"),
DEPLOY_TIMEOUT_MS: z.coerce
.number()
@@ -0,0 +1,198 @@
import {
ECRClient,
CreateRepositoryCommand,
DescribeRepositoriesCommand,
type Repository,
type Tag,
RepositoryNotFoundException,
} from "@aws-sdk/client-ecr";
import { tryCatch } from "@trigger.dev/core";
import { logger } from "~/services/logger.server";
export async function getDeploymentImageRef({
host,
namespace,
projectRef,
nextVersion,
environmentSlug,
registryId,
registryTags,
}: {
host: string;
namespace: string;
projectRef: string;
nextVersion: string;
environmentSlug: string;
registryId?: string;
registryTags?: string;
}): Promise<{
imageRef: string;
isEcr: boolean;
}> {
const repositoryName = `${namespace}/${projectRef}`;
const imageRef = `${host}/${repositoryName}:${nextVersion}.${environmentSlug}`;
if (!isEcrRegistry(host)) {
return {
imageRef,
isEcr: false,
};
}
const [ecrRepoError] = await tryCatch(
ensureEcrRepositoryExists({ repositoryName, registryHost: host, registryId, registryTags })
);
if (ecrRepoError) {
logger.error("Failed to ensure ECR repository exists", {
repositoryName,
host,
ecrRepoError: ecrRepoError.message,
});
throw ecrRepoError;
}
return {
imageRef,
isEcr: true,
};
}
function isEcrRegistry(registryHost: string) {
return registryHost.includes("amazonaws.com");
}
function parseRegistryTags(tags: string): Tag[] {
return tags.split(",").map((tag) => {
const [key, value] = tag.split("=");
return { Key: key, Value: value };
});
}
async function createEcrRepository({
repositoryName,
region,
registryId,
registryTags,
}: {
repositoryName: string;
region: string;
registryId?: string;
registryTags?: string;
}): Promise<Repository> {
const ecr = new ECRClient({ region });
const result = await ecr.send(
new CreateRepositoryCommand({
repositoryName,
imageTagMutability: "IMMUTABLE",
encryptionConfiguration: {
encryptionType: "AES256",
},
registryId,
tags: registryTags ? parseRegistryTags(registryTags) : undefined,
})
);
if (!result.repository) {
logger.error("Failed to create ECR repository", { repositoryName, result });
throw new Error(`Failed to create ECR repository: ${repositoryName}`);
}
return result.repository;
}
async function getEcrRepository({
repositoryName,
region,
registryId,
}: {
repositoryName: string;
region: string;
registryId?: string;
}): Promise<Repository | undefined> {
const ecr = new ECRClient({ region });
try {
const result = await ecr.send(
new DescribeRepositoriesCommand({
repositoryNames: [repositoryName],
registryId,
})
);
if (!result.repositories || result.repositories.length === 0) {
logger.debug("ECR repository not found", { repositoryName, region, result });
return undefined;
}
return result.repositories[0];
} catch (error) {
if (error instanceof RepositoryNotFoundException) {
logger.debug("ECR repository not found: RepositoryNotFoundException", {
repositoryName,
region,
});
return undefined;
}
throw error;
}
}
export function getEcrRegion(registryHost: string): string | undefined {
const parts = registryHost.split(".");
if (parts.length !== 6 || parts[1] !== "dkr" || parts[2] !== "ecr") {
return undefined;
}
return parts[3];
}
async function ensureEcrRepositoryExists({
repositoryName,
registryHost,
registryId,
registryTags,
}: {
repositoryName: string;
registryHost: string;
registryId?: string;
registryTags?: string;
}): Promise<Repository> {
const region = getEcrRegion(registryHost);
if (!region) {
throw new Error(`Invalid ECR registry host: ${registryHost}`);
}
const [getRepoError, existingRepo] = await tryCatch(
getEcrRepository({ repositoryName, region, registryId })
);
if (getRepoError) {
logger.error("Failed to get ECR repository", { repositoryName, region, getRepoError });
throw getRepoError;
}
if (existingRepo) {
logger.debug("ECR repository already exists", { repositoryName, region, existingRepo });
return existingRepo;
}
const [createRepoError, newRepo] = await tryCatch(
createEcrRepository({ repositoryName, region, registryId, registryTags })
);
if (createRepoError) {
logger.error("Failed to create ECR repository", { repositoryName, region, createRepoError });
throw createRepoError;
}
if (newRepo.repositoryName !== repositoryName) {
logger.error("ECR repository name mismatch", { repositoryName, region, newRepo });
throw new Error(
`ECR repository name mismatch: ${repositoryName} !== ${newRepo.repositoryName}`
);
}
return newRepo;
}
@@ -1,5 +1,4 @@
import { type InitializeDeploymentRequestBody } from "@trigger.dev/core/v3";
import { WorkerDeploymentType } from "@trigger.dev/database";
import { customAlphabet } from "nanoid";
import { env } from "~/env.server";
import { type AuthenticatedEnvironment } from "~/services/apiAuth.server";
@@ -9,6 +8,8 @@ import { createRemoteImageBuild, remoteBuildsEnabled } from "../remoteImageBuild
import { calculateNextBuildVersion } from "../utils/calculateNextBuildVersion";
import { BaseService, ServiceValidationError } from "./baseService.server";
import { TimeoutDeploymentService } from "./timeoutDeployment.server";
import { getDeploymentImageRef } from "../getDeploymentImageRef.server";
import { tryCatch } from "@trigger.dev/core";
const nanoid = customAlphabet("1234567890abcdefghijklmnopqrstuvwxyz", 8);
@@ -68,11 +69,31 @@ export class InitializeDeploymentService extends BaseService {
})
: undefined;
const imageRef = [
env.DEPLOY_REGISTRY_HOST,
env.DEPLOY_REGISTRY_NAMESPACE,
`${environment.project.externalRef}:${nextVersion}.${environment.slug}`,
].join("/");
const [imageRefError, imageRefResult] = await tryCatch(
getDeploymentImageRef({
host: env.DEPLOY_REGISTRY_HOST,
namespace: env.DEPLOY_REGISTRY_NAMESPACE,
projectRef: environment.project.externalRef,
nextVersion,
environmentSlug: environment.slug,
registryId: env.DEPLOY_REGISTRY_ID,
registryTags: env.DEPLOY_REGISTRY_TAGS,
})
);
if (imageRefError) {
logger.error("Failed to get deployment image ref", {
environmentId: environment.id,
projectId: environment.projectId,
version: nextVersion,
triggeredById: triggeredBy?.id,
type: payload.type,
cause: imageRefError.message,
});
throw new ServiceValidationError("Failed to get deployment image ref");
}
const { imageRef, isEcr } = imageRefResult;
logger.debug("Creating deployment", {
environmentId: environment.id,
@@ -81,6 +102,7 @@ export class InitializeDeploymentService extends BaseService {
triggeredById: triggeredBy?.id,
type: payload.type,
imageRef,
isEcr,
});
const deployment = await this._prisma.workerDeployment.create({
+1
View File
@@ -33,6 +33,7 @@
"dependencies": {
"@ariakit/react": "^0.4.6",
"@ariakit/react-core": "^0.4.6",
"@aws-sdk/client-ecr": "^3.839.0",
"@aws-sdk/client-sqs": "^3.445.0",
"@codemirror/autocomplete": "^6.3.1",
"@codemirror/commands": "^6.1.2",
@@ -0,0 +1,103 @@
import { describe, expect, it } from "vitest";
import { getDeploymentImageRef, getEcrRegion } from "../app/v3/getDeploymentImageRef.server";
import { ECRClient, DeleteRepositoryCommand } from "@aws-sdk/client-ecr";
describe.skipIf(process.env.RUN_REGISTRY_TESTS !== "1")("getDeploymentImageRef", () => {
const testHost = "123456789012.dkr.ecr.us-east-1.amazonaws.com";
const testNamespace = "test-namespace";
const testProjectRef = "test-project-" + Math.random().toString(36).substring(7);
const registryId = process.env.DEPLOY_REGISTRY_ID;
const registryTags = "test=test,test2=test2";
// Clean up test repository after tests
afterAll(async () => {
if (!registryId) {
return;
}
if (process.env.KEEP_TEST_REPO === "1") {
return;
}
try {
const region = getEcrRegion(testHost);
const ecr = new ECRClient({ region });
await ecr.send(
new DeleteRepositoryCommand({
repositoryName: `${testNamespace}/${testProjectRef}`,
registryId,
force: true,
})
);
} catch (error) {
console.warn("Failed to delete test repository:", error);
}
});
it("should return the correct image ref for non-ECR registry", async () => {
const imageRef = await getDeploymentImageRef({
host: "registry.digitalocean.com",
namespace: testNamespace,
projectRef: testProjectRef,
nextVersion: "20250630.1",
environmentSlug: "test",
registryId,
registryTags,
});
expect(imageRef.imageRef).toBe(
`registry.digitalocean.com/${testNamespace}/${testProjectRef}:20250630.1.test`
);
expect(imageRef.isEcr).toBe(false);
});
it("should create ECR repository and return correct image ref", async () => {
const imageRef = await getDeploymentImageRef({
host: testHost,
namespace: testNamespace,
projectRef: testProjectRef,
nextVersion: "20250630.1",
environmentSlug: "test",
registryId,
registryTags,
});
expect(imageRef.imageRef).toBe(
`${testHost}/${testNamespace}/${testProjectRef}:20250630.1.test`
);
expect(imageRef.isEcr).toBe(true);
});
it("should reuse existing ECR repository", async () => {
// This should use the repository created in the previous test
const imageRef = await getDeploymentImageRef({
host: testHost,
namespace: testNamespace,
projectRef: testProjectRef,
nextVersion: "20250630.2",
environmentSlug: "prod",
registryId,
registryTags,
});
expect(imageRef.imageRef).toBe(
`${testHost}/${testNamespace}/${testProjectRef}:20250630.2.prod`
);
expect(imageRef.isEcr).toBe(true);
});
it("should throw error for invalid ECR host", async () => {
await expect(
getDeploymentImageRef({
host: "invalid.ecr.amazonaws.com",
namespace: testNamespace,
projectRef: testProjectRef,
nextVersion: "20250630.1",
environmentSlug: "test",
registryId,
registryTags,
})
).rejects.toThrow("Invalid ECR registry host: invalid.ecr.amazonaws.com");
});
});
+779 -3
View File
File diff suppressed because it is too large Load Diff