From 08da526ddde2e0320747963f54bfcedd928079e3 Mon Sep 17 00:00:00 2001 From: ARUNAVO RAY Date: Thu, 26 Feb 2026 10:19:28 +0530 Subject: [PATCH] fix(github): keep disabled repos from cleanup while skipping new imports (#191) * fix: preserve disabled repos while skipping new imports * ci: upgrade bun to 1.3.6 for test workflow --- .github/workflows/astro-build-test.yml | 2 +- src/lib/github.ts | 5 ++++- src/lib/repo-eligibility.test.ts | 17 +++++++++++++++++ src/lib/repo-eligibility.ts | 6 ++++++ src/lib/repository-cleanup-service.ts | 22 +++++++++++++++------- src/lib/scheduler-service.ts | 15 +++++++++++++-- src/pages/api/sync/index.ts | 5 ++++- src/pages/api/sync/organization.ts | 3 ++- src/types/Repository.ts | 1 + 9 files changed, 63 insertions(+), 13 deletions(-) create mode 100644 src/lib/repo-eligibility.test.ts create mode 100644 src/lib/repo-eligibility.ts diff --git a/.github/workflows/astro-build-test.yml b/.github/workflows/astro-build-test.yml index 4905931..0dc8b03 100644 --- a/.github/workflows/astro-build-test.yml +++ b/.github/workflows/astro-build-test.yml @@ -28,7 +28,7 @@ jobs: - name: Setup Bun uses: oven-sh/setup-bun@v1 with: - bun-version: '1.2.16' + bun-version: '1.3.6' - name: Check lockfile and install dependencies run: | diff --git a/src/lib/github.ts b/src/lib/github.ts index d79cd1b..052ce57 100644 --- a/src/lib/github.ts +++ b/src/lib/github.ts @@ -254,6 +254,7 @@ export async function getGithubRepositories({ visibility: (repo.visibility ?? "public") as GitRepo["visibility"], status: "imported", + isDisabled: repo.disabled ?? false, lastMirrored: undefined, errorMessage: undefined, @@ -275,7 +276,7 @@ export async function getGithubStarredRepositories({ }: { octokit: Octokit; config: Partial; -}) { +}): Promise { try { const starredRepos = await octokit.paginate( octokit.activity.listReposStarredByAuthenticatedUser, @@ -314,6 +315,7 @@ export async function getGithubStarredRepositories({ visibility: (repo.visibility ?? "public") as GitRepo["visibility"], status: "imported", + isDisabled: repo.disabled ?? false, lastMirrored: undefined, errorMessage: undefined, @@ -438,6 +440,7 @@ export async function getGithubOrganizationRepositories({ visibility: (repo.visibility ?? "public") as GitRepo["visibility"], status: "imported", + isDisabled: repo.disabled ?? false, lastMirrored: undefined, errorMessage: undefined, diff --git a/src/lib/repo-eligibility.test.ts b/src/lib/repo-eligibility.test.ts new file mode 100644 index 0000000..376ceee --- /dev/null +++ b/src/lib/repo-eligibility.test.ts @@ -0,0 +1,17 @@ +import { describe, expect, it } from "bun:test"; +import { isMirrorableGitHubRepo } from "@/lib/repo-eligibility"; + +describe("isMirrorableGitHubRepo", () => { + it("returns false for disabled repos", () => { + expect(isMirrorableGitHubRepo({ isDisabled: true })).toBe(false); + }); + + it("returns true for enabled repos", () => { + expect(isMirrorableGitHubRepo({ isDisabled: false })).toBe(true); + }); + + it("returns true when disabled flag is absent", () => { + expect(isMirrorableGitHubRepo({})).toBe(true); + }); +}); + diff --git a/src/lib/repo-eligibility.ts b/src/lib/repo-eligibility.ts new file mode 100644 index 0000000..acdc011 --- /dev/null +++ b/src/lib/repo-eligibility.ts @@ -0,0 +1,6 @@ +import type { GitRepo } from "@/types/Repository"; + +export function isMirrorableGitHubRepo(repo: Pick): boolean { + return repo.isDisabled !== true; +} + diff --git a/src/lib/repository-cleanup-service.ts b/src/lib/repository-cleanup-service.ts index 4c6215c..549a0a1 100644 --- a/src/lib/repository-cleanup-service.ts +++ b/src/lib/repository-cleanup-service.ts @@ -10,6 +10,7 @@ import { createGitHubClient, getGithubRepositories, getGithubStarredRepositories import { createGiteaClient, deleteGiteaRepo, archiveGiteaRepo, getGiteaRepoOwnerAsync, checkRepoLocation } from '@/lib/gitea'; import { getDecryptedGitHubToken, getDecryptedGiteaToken } from '@/lib/utils/config-encryption'; import { publishEvent } from '@/lib/events'; +import { isMirrorableGitHubRepo } from '@/lib/repo-eligibility'; let cleanupInterval: NodeJS.Timeout | null = null; let isCleanupRunning = false; @@ -59,7 +60,9 @@ async function identifyOrphanedRepositories(config: any): Promise { return []; } - const githubRepoFullNames = new Set(allGithubRepos.map(repo => repo.fullName)); + const githubReposByFullName = new Map( + allGithubRepos.map((repo) => [repo.fullName, repo] as const) + ); // Get all repositories from our database const dbRepos = await db @@ -70,18 +73,23 @@ async function identifyOrphanedRepositories(config: any): Promise { // Only identify repositories as orphaned if we successfully accessed GitHub // This prevents false positives when GitHub is down or account is inaccessible const orphanedRepos = dbRepos.filter(repo => { - const isOrphaned = !githubRepoFullNames.has(repo.fullName); - if (!isOrphaned) { - return false; - } - // Skip repositories we've already archived/preserved if (repo.status === 'archived' || repo.isArchived) { console.log(`[Repository Cleanup] Skipping ${repo.fullName} - already archived`); return false; } - return true; + const githubRepo = githubReposByFullName.get(repo.fullName); + if (!githubRepo) { + return true; + } + + if (!isMirrorableGitHubRepo(githubRepo)) { + console.log(`[Repository Cleanup] Preserving ${repo.fullName} - repository is disabled on GitHub`); + return false; + } + + return false; }); if (orphanedRepos.length > 0) { diff --git a/src/lib/scheduler-service.ts b/src/lib/scheduler-service.ts index 5ce5ae7..8fe45d2 100644 --- a/src/lib/scheduler-service.ts +++ b/src/lib/scheduler-service.ts @@ -12,6 +12,7 @@ import { parseInterval, formatDuration } from '@/lib/utils/duration-parser'; import type { Repository } from '@/lib/db/schema'; import { repoStatusEnum, repositoryVisibilityEnum } from '@/types/Repository'; import { mergeGitReposPreferStarred, normalizeGitRepoToInsert, calcBatchSizeForInsert } from '@/lib/repo-utils'; +import { isMirrorableGitHubRepo } from '@/lib/repo-eligibility'; let schedulerInterval: NodeJS.Timeout | null = null; let isSchedulerRunning = false; @@ -96,6 +97,7 @@ async function runScheduledSync(config: any): Promise { : Promise.resolve([]), ]); const allGithubRepos = mergeGitReposPreferStarred(basicAndForkedRepos, starredRepos); + const mirrorableGithubRepos = allGithubRepos.filter(isMirrorableGitHubRepo); // Check for new repositories const existingRepos = await db @@ -104,7 +106,7 @@ async function runScheduledSync(config: any): Promise { .where(eq(repositories.userId, userId)); const existingRepoNames = new Set(existingRepos.map(r => r.normalizedFullName)); - const newRepos = allGithubRepos.filter(r => !existingRepoNames.has(r.fullName.toLowerCase())); + const newRepos = mirrorableGithubRepos.filter(r => !existingRepoNames.has(r.fullName.toLowerCase())); if (newRepos.length > 0) { console.log(`[Scheduler] Found ${newRepos.length} new repositories for user ${userId}`); @@ -129,6 +131,10 @@ async function runScheduledSync(config: any): Promise { } else { console.log(`[Scheduler] No new repositories found for user ${userId}`); } + const skippedDisabledCount = allGithubRepos.length - mirrorableGithubRepos.length; + if (skippedDisabledCount > 0) { + console.log(`[Scheduler] Skipped ${skippedDisabledCount} disabled GitHub repositories for user ${userId}`); + } } catch (error) { console.error(`[Scheduler] Failed to auto-import repositories for user ${userId}:`, error); } @@ -429,6 +435,7 @@ async function performInitialAutoStart(): Promise { : Promise.resolve([]), ]); const allGithubRepos = mergeGitReposPreferStarred(basicAndForkedRepos, starredRepos); + const mirrorableGithubRepos = allGithubRepos.filter(isMirrorableGitHubRepo); // Check for new repositories const existingRepos = await db @@ -437,7 +444,7 @@ async function performInitialAutoStart(): Promise { .where(eq(repositories.userId, config.userId)); const existingRepoNames = new Set(existingRepos.map(r => r.normalizedFullName)); - const reposToImport = allGithubRepos.filter(r => !existingRepoNames.has(r.fullName.toLowerCase())); + const reposToImport = mirrorableGithubRepos.filter(r => !existingRepoNames.has(r.fullName.toLowerCase())); if (reposToImport.length > 0) { console.log(`[Scheduler] Importing ${reposToImport.length} repositories for user ${config.userId}...`); @@ -462,6 +469,10 @@ async function performInitialAutoStart(): Promise { } else { console.log(`[Scheduler] No new repositories to import for user ${config.userId}`); } + const skippedDisabledCount = allGithubRepos.length - mirrorableGithubRepos.length; + if (skippedDisabledCount > 0) { + console.log(`[Scheduler] Skipped ${skippedDisabledCount} disabled GitHub repositories for user ${config.userId}`); + } // Check if we already have mirrored repositories (indicating this isn't first run) const mirroredRepos = await db diff --git a/src/pages/api/sync/index.ts b/src/pages/api/sync/index.ts index 4a28f90..c014c4f 100644 --- a/src/pages/api/sync/index.ts +++ b/src/pages/api/sync/index.ts @@ -13,6 +13,7 @@ import { jsonResponse, createSecureErrorResponse } from "@/lib/utils"; import { mergeGitReposPreferStarred, calcBatchSizeForInsert } from "@/lib/repo-utils"; import { getDecryptedGitHubToken } from "@/lib/utils/config-encryption"; import { requireAuthenticatedUserId } from "@/lib/auth-guards"; +import { isMirrorableGitHubRepo } from "@/lib/repo-eligibility"; export const POST: APIRoute = async ({ request, locals }) => { const authResult = await requireAuthenticatedUserId({ request, locals }); @@ -56,9 +57,10 @@ export const POST: APIRoute = async ({ request, locals }) => { // Merge and de-duplicate by fullName, preferring starred variant when duplicated const allGithubRepos = mergeGitReposPreferStarred(basicAndForkedRepos, starredRepos); + const mirrorableGithubRepos = allGithubRepos.filter(isMirrorableGitHubRepo); // Prepare full list of repos and orgs - const newRepos = allGithubRepos.map((repo) => ({ + const newRepos = mirrorableGithubRepos.map((repo) => ({ id: uuidv4(), userId, configId: config.id, @@ -186,6 +188,7 @@ export const POST: APIRoute = async ({ request, locals }) => { message: "Repositories and organizations synced successfully", newRepositories: insertedRepos.length, newOrganizations: insertedOrgs.length, + skippedDisabledRepositories: allGithubRepos.length - mirrorableGithubRepos.length, }, }); } catch (error) { diff --git a/src/pages/api/sync/organization.ts b/src/pages/api/sync/organization.ts index 5f60b16..96fbc81 100644 --- a/src/pages/api/sync/organization.ts +++ b/src/pages/api/sync/organization.ts @@ -150,9 +150,10 @@ export const POST: APIRoute = async ({ request, locals }) => { const existingIds = new Set(allRepos.map(r => r.id)); const uniqueMemberRepos = memberRepos.filter(r => !existingIds.has(r.id)); allRepos.push(...uniqueMemberRepos); + const mirrorableRepos = allRepos.filter((repo) => !repo.disabled); // Insert repositories - const repoRecords = allRepos.map((repo) => { + const repoRecords = mirrorableRepos.map((repo) => { const normalizedOwner = repo.owner.login.trim().toLowerCase(); const normalizedRepoName = repo.name.trim().toLowerCase(); diff --git a/src/types/Repository.ts b/src/types/Repository.ts index ab8f2ba..fd9a712 100644 --- a/src/types/Repository.ts +++ b/src/types/Repository.ts @@ -70,6 +70,7 @@ export interface GitRepo { visibility: RepositoryVisibility; status: RepoStatus; + isDisabled?: boolean; lastMirrored?: Date; errorMessage?: string;