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..9805a8f --- /dev/null +++ b/packages/das/src/utils/repo-full-name.ts @@ -0,0 +1,18 @@ +import { BadRequestException } from "@nestjs/common"; + +/** GitHub owner/repo pattern: alphanum + `.`, `_`, `-`, length reasonable. */ +export const REPO_FULL_NAME_PATTERN = /^[\w.-]{1,100}\/[\w.-]{1,100}$/; + +/** GitHub treats repository identity as case-insensitive; canonical form is lowercase. */ +export function normalizeRepoFullName(value: string): string { + return value.toLowerCase(); +} + +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 normalizeRepoFullName(value); +} diff --git a/packages/das/src/webhook/handlers/installation.handler.ts b/packages/das/src/webhook/handlers/installation.handler.ts index 7eaf630..5cf2c19 100644 --- a/packages/das/src/webhook/handlers/installation.handler.ts +++ b/packages/das/src/webhook/handlers/installation.handler.ts @@ -3,6 +3,7 @@ import { Injectable, Logger } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; import { Repository } from "typeorm"; import { Repo } from "../../entities"; +import { normalizeRepoFullName } from "../../utils/repo-full-name"; @Injectable() export class InstallationHandler { @@ -38,6 +39,7 @@ export class InstallationHandler { payload.repositories ?? payload.repositories_added ?? []; for (const repo of repos) { + const repoFullName = normalizeRepoFullName(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 +47,35 @@ export class InstallationHandler { .insert() .into(Repo) .values({ - repoFullName: repo.full_name, + repoFullName, installationId: String(installationId), addedAt: new Date().toISOString(), }) .orUpdate(["installationId"], ["repoFullName"]) .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 rows stored via admin API (lowercase) or legacy + // webhook casing (GitHub's raw Owner/Repo) are all cleared (#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 = normalizeRepoFullName(String(repo.full_name)); + const result = await this.repoRepo + .createQueryBuilder() + .update() + .set({ installationId: null, registered: false }) + .where("LOWER(repo_full_name) = :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}`); + } } } }