diff --git a/demo/src/demo.ts b/demo/src/demo.ts index 02072c2..2cd6df2 100644 --- a/demo/src/demo.ts +++ b/demo/src/demo.ts @@ -1,7 +1,8 @@ -import { logger, main } from "gitlab-webhook-listener-bot"; +import { logger, main, GitlabClient } from "gitlab-webhook-listener-bot"; import { RenovateRebase } from "./handlers/RenovateRebase"; import { EventLogger } from "./handlers/EventLogger"; import { ProjectCreateEvent } from "./handlers/ProjectCreateEvent"; +import { RegistryCleanup } from "./handlers/RegistryCleanup"; main({ logger, @@ -9,6 +10,7 @@ main({ new EventLogger(logger), new ProjectCreateEvent(logger), new RenovateRebase(logger), + new RegistryCleanup(new GitlabClient(process.env.GITLAB_URL || "", process.env.GITLAB_TOKEN || ""), logger), ], async livenessProbe() { logger.debug("Called livenessProbe"); diff --git a/demo/src/handlers/RegistryCleanup.ts b/demo/src/handlers/RegistryCleanup.ts new file mode 100644 index 0000000..c231c47 --- /dev/null +++ b/demo/src/handlers/RegistryCleanup.ts @@ -0,0 +1,67 @@ +import { + GitlabClient, + LoggerInterface, + MergeRequestHandler, + MergeRequestPayload, + WebhookEvent, + slugify, +} from "gitlab-webhook-listener-bot"; + +/** + * Delete docker build images when merge request is closed or merged. + */ +export class RegistryCleanup extends MergeRequestHandler { + public constructor( + private readonly gitlab: GitlabClient, + logger: LoggerInterface, + ) { + super(logger); + } + + public isValid({ payload }: WebhookEvent): boolean { + const { + object_attributes: { + action, + }, + } = payload; + + return ( + ["close", "merge"].includes(action) + ); + } + + public async handle({ payload }: WebhookEvent): Promise { + const { + object_attributes: { + iid: mr_id, + title, + action, + source_branch, + }, + project: { + id: project_id, + path_with_namespace: project_path, + }, + } = payload; + + this.logger.info(`Deleting docker images related to the merge request ${source_branch} (action: ${action}): ${project_path}!${mr_id}: ${title}`); + + await this.cleanupAutoDevopsImages(project_id, slugify(source_branch)); + } + + /** + * Delete auto-devops images + */ + private async cleanupAutoDevopsImages(project_id: number, source_branch: string): Promise { + const repository = await this.gitlab.getRegistryRepositoryByName(project_id, source_branch); + + this.logger.debug(`Found auto-devops repository: ${JSON.stringify(repository)}`); + + if (!repository) { + return; + } + + this.logger.info("Deleting repository", repository); + await this.gitlab.removeRegistryRepositoryById(project_id, repository.id); + } +} diff --git a/package.json b/package.json index 81a18a2..30a5831 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "ngrok": "ngrok --config ngrok.yml start app", "prepublishOnly": "set -e; pnpm install; pnpm build", "set-version": "pnpm version --new-version $APP_VERSION --no-git-tag-version", - "start": "cd demo; pnpm install; pnpm start" + "start": "set -e; pnpm build; cd demo; pnpm install; pnpm start" }, "engines": { "node": ">=18" diff --git a/src/gitlab/GitlabClient.ts b/src/gitlab/GitlabClient.ts index de825aa..62f5a06 100644 --- a/src/gitlab/GitlabClient.ts +++ b/src/gitlab/GitlabClient.ts @@ -1,5 +1,7 @@ import { Gitlab } from "@gitbeaker/rest"; +type ProjectId = string | number; + export class GitlabClient { protected readonly api: InstanceType>; @@ -12,4 +14,30 @@ export class GitlabClient { token, }); } + + async getRegistryRepositoryByName(projectId: ProjectId, name: string) { + // NOTE: There doesn't appear to be a method that would not involve fetching all repositories + let repositories; + + try { + repositories = await this.api.ContainerRegistry.allRepositories({ projectId }); + } catch (e: any) { + // forbidden in case feature not enabled + if (e.cause.description === "403 Forbidden") { + return null; + } + + throw e; + } + + return repositories.filter(repository => repository.name === name)[0]; + } + + async removeRegistryRepositoryById(projectId: ProjectId, repositoryId: number) { + const code = await this.api.ContainerRegistry.removeRepository(projectId, repositoryId); + + if ((code as any as number) !== 202) { + throw new Error(`Unexpected code: ${code}`); + } + } }