create repo if doesn't exist
This commit is contained in:
@@ -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({
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
Generated
+779
-3
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user