diff --git a/packages/das/src/api/admin.controller.ts b/packages/das/src/api/admin.controller.ts index fb6437b..0766f55 100644 --- a/packages/das/src/api/admin.controller.ts +++ b/packages/das/src/api/admin.controller.ts @@ -14,6 +14,7 @@ import { ApiTags, ApiOperation, ApiSecurity, ApiBody } from "@nestjs/swagger"; import { RequireApiKeyGuard } from "./require-api-key.guard"; import { Repo } from "../entities"; import { FETCH_QUEUE, FETCH_JOBS } from "../queue/constants"; +import { validateRepoFullName } from "../utils/repo-full-name"; interface BackfillBody { repoFullName: string; @@ -24,18 +25,6 @@ interface RegisterBody { repoFullName: string; } -// GitHub owner/repo pattern: alphanum + `.`, `_`, `-`, length reasonable. -const REPO_FULL_NAME_PATTERN = /^[\w.-]{1,100}\/[\w.-]{1,100}$/; - -function validateRepoFullName(value: unknown): string { - if (typeof value !== "string" || !REPO_FULL_NAME_PATTERN.test(value)) { - throw new BadRequestException( - 'repoFullName must match "owner/repo" (alphanumerics, dot, dash, underscore)', - ); - } - return value; -} - function validateDays(value: unknown): number | undefined { if (value === undefined || value === null) return undefined; if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) { diff --git a/packages/das/src/utils/repo-full-name.ts b/packages/das/src/utils/repo-full-name.ts new file mode 100644 index 0000000..6cadc44 --- /dev/null +++ b/packages/das/src/utils/repo-full-name.ts @@ -0,0 +1,13 @@ +import { BadRequestException } from "@nestjs/common"; + +/** GitHub owner/repo pattern: alphanum + `.`, `_`, `-`, length reasonable. */ +export const REPO_FULL_NAME_PATTERN = /^[\w.-]{1,100}\/[\w.-]{1,100}$/; + +export function validateRepoFullName(value: unknown): string { + if (typeof value !== "string" || !REPO_FULL_NAME_PATTERN.test(value)) { + throw new BadRequestException( + 'repoFullName must match "owner/repo" (alphanumerics, dot, dash, underscore)', + ); + } + return value; +} diff --git a/packages/das/src/webhook/handlers/installation.handler.ts b/packages/das/src/webhook/handlers/installation.handler.ts index 8d019bf..1c7eec0 100644 --- a/packages/das/src/webhook/handlers/installation.handler.ts +++ b/packages/das/src/webhook/handlers/installation.handler.ts @@ -38,6 +38,7 @@ export class InstallationHandler { payload.repositories ?? payload.repositories_added ?? []; for (const repo of repos) { + const repoFullName = String(repo.full_name); // Atomic upsert: insert with addedAt on first encounter; on conflict only // update installationId so addedAt is never overwritten on re-fires. await this.repoRepo @@ -45,23 +46,37 @@ export class InstallationHandler { .insert() .into(Repo) .values({ - repoFullName: repo.full_name, + repoFullName, installationId: String(installationId), addedAt: new Date().toISOString(), }) .orUpdate(["installation_id"], ["repo_full_name"]) .execute(); - this.logger.log(`Tracking repo: ${repo.full_name}`); + this.logger.log(`Tracking repo: ${repoFullName}`); } // installation_repositories.removed — soft clear, preserve historical data. + // Match case-insensitively so removal events clear rows even if earlier + // registration paths preserved a different Owner/Repo casing (#120). const removed: any[] = payload.repositories_removed ?? []; for (const repo of removed) { - await this.repoRepo.update(repo.full_name, { - installationId: null, - registered: false, - }); - this.logger.log(`Stopped tracking repo: ${repo.full_name}`); + const repoFullName = String(repo.full_name); + const result = await this.repoRepo + .createQueryBuilder() + .update() + .set({ installationId: null, registered: false }) + .where("LOWER(repo_full_name) = LOWER(:repoFullName)", { + repoFullName, + }) + .execute(); + + if (!result.affected) { + this.logger.warn( + `No repo row matched removal for ${repo.full_name} (canonical: ${repoFullName})`, + ); + } else { + this.logger.log(`Stopped tracking repo: ${repoFullName}`); + } } } }