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
This commit is contained in:
ARUNAVO RAY
2026-02-26 10:19:28 +05:30
committed by GitHub
parent 2395e14382
commit 08da526ddd
9 changed files with 63 additions and 13 deletions

View File

@@ -28,7 +28,7 @@ jobs:
- name: Setup Bun - name: Setup Bun
uses: oven-sh/setup-bun@v1 uses: oven-sh/setup-bun@v1
with: with:
bun-version: '1.2.16' bun-version: '1.3.6'
- name: Check lockfile and install dependencies - name: Check lockfile and install dependencies
run: | run: |

View File

@@ -254,6 +254,7 @@ export async function getGithubRepositories({
visibility: (repo.visibility ?? "public") as GitRepo["visibility"], visibility: (repo.visibility ?? "public") as GitRepo["visibility"],
status: "imported", status: "imported",
isDisabled: repo.disabled ?? false,
lastMirrored: undefined, lastMirrored: undefined,
errorMessage: undefined, errorMessage: undefined,
@@ -275,7 +276,7 @@ export async function getGithubStarredRepositories({
}: { }: {
octokit: Octokit; octokit: Octokit;
config: Partial<Config>; config: Partial<Config>;
}) { }): Promise<GitRepo[]> {
try { try {
const starredRepos = await octokit.paginate( const starredRepos = await octokit.paginate(
octokit.activity.listReposStarredByAuthenticatedUser, octokit.activity.listReposStarredByAuthenticatedUser,
@@ -314,6 +315,7 @@ export async function getGithubStarredRepositories({
visibility: (repo.visibility ?? "public") as GitRepo["visibility"], visibility: (repo.visibility ?? "public") as GitRepo["visibility"],
status: "imported", status: "imported",
isDisabled: repo.disabled ?? false,
lastMirrored: undefined, lastMirrored: undefined,
errorMessage: undefined, errorMessage: undefined,
@@ -438,6 +440,7 @@ export async function getGithubOrganizationRepositories({
visibility: (repo.visibility ?? "public") as GitRepo["visibility"], visibility: (repo.visibility ?? "public") as GitRepo["visibility"],
status: "imported", status: "imported",
isDisabled: repo.disabled ?? false,
lastMirrored: undefined, lastMirrored: undefined,
errorMessage: undefined, errorMessage: undefined,

View File

@@ -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);
});
});

View File

@@ -0,0 +1,6 @@
import type { GitRepo } from "@/types/Repository";
export function isMirrorableGitHubRepo(repo: Pick<GitRepo, "isDisabled">): boolean {
return repo.isDisabled !== true;
}

View File

@@ -10,6 +10,7 @@ import { createGitHubClient, getGithubRepositories, getGithubStarredRepositories
import { createGiteaClient, deleteGiteaRepo, archiveGiteaRepo, getGiteaRepoOwnerAsync, checkRepoLocation } from '@/lib/gitea'; import { createGiteaClient, deleteGiteaRepo, archiveGiteaRepo, getGiteaRepoOwnerAsync, checkRepoLocation } from '@/lib/gitea';
import { getDecryptedGitHubToken, getDecryptedGiteaToken } from '@/lib/utils/config-encryption'; import { getDecryptedGitHubToken, getDecryptedGiteaToken } from '@/lib/utils/config-encryption';
import { publishEvent } from '@/lib/events'; import { publishEvent } from '@/lib/events';
import { isMirrorableGitHubRepo } from '@/lib/repo-eligibility';
let cleanupInterval: NodeJS.Timeout | null = null; let cleanupInterval: NodeJS.Timeout | null = null;
let isCleanupRunning = false; let isCleanupRunning = false;
@@ -59,7 +60,9 @@ async function identifyOrphanedRepositories(config: any): Promise<any[]> {
return []; 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 // Get all repositories from our database
const dbRepos = await db const dbRepos = await db
@@ -70,18 +73,23 @@ async function identifyOrphanedRepositories(config: any): Promise<any[]> {
// Only identify repositories as orphaned if we successfully accessed GitHub // Only identify repositories as orphaned if we successfully accessed GitHub
// This prevents false positives when GitHub is down or account is inaccessible // This prevents false positives when GitHub is down or account is inaccessible
const orphanedRepos = dbRepos.filter(repo => { const orphanedRepos = dbRepos.filter(repo => {
const isOrphaned = !githubRepoFullNames.has(repo.fullName);
if (!isOrphaned) {
return false;
}
// Skip repositories we've already archived/preserved // Skip repositories we've already archived/preserved
if (repo.status === 'archived' || repo.isArchived) { if (repo.status === 'archived' || repo.isArchived) {
console.log(`[Repository Cleanup] Skipping ${repo.fullName} - already archived`); console.log(`[Repository Cleanup] Skipping ${repo.fullName} - already archived`);
return false; 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) { if (orphanedRepos.length > 0) {

View File

@@ -12,6 +12,7 @@ import { parseInterval, formatDuration } from '@/lib/utils/duration-parser';
import type { Repository } from '@/lib/db/schema'; import type { Repository } from '@/lib/db/schema';
import { repoStatusEnum, repositoryVisibilityEnum } from '@/types/Repository'; import { repoStatusEnum, repositoryVisibilityEnum } from '@/types/Repository';
import { mergeGitReposPreferStarred, normalizeGitRepoToInsert, calcBatchSizeForInsert } from '@/lib/repo-utils'; import { mergeGitReposPreferStarred, normalizeGitRepoToInsert, calcBatchSizeForInsert } from '@/lib/repo-utils';
import { isMirrorableGitHubRepo } from '@/lib/repo-eligibility';
let schedulerInterval: NodeJS.Timeout | null = null; let schedulerInterval: NodeJS.Timeout | null = null;
let isSchedulerRunning = false; let isSchedulerRunning = false;
@@ -96,6 +97,7 @@ async function runScheduledSync(config: any): Promise<void> {
: Promise.resolve([]), : Promise.resolve([]),
]); ]);
const allGithubRepos = mergeGitReposPreferStarred(basicAndForkedRepos, starredRepos); const allGithubRepos = mergeGitReposPreferStarred(basicAndForkedRepos, starredRepos);
const mirrorableGithubRepos = allGithubRepos.filter(isMirrorableGitHubRepo);
// Check for new repositories // Check for new repositories
const existingRepos = await db const existingRepos = await db
@@ -104,7 +106,7 @@ async function runScheduledSync(config: any): Promise<void> {
.where(eq(repositories.userId, userId)); .where(eq(repositories.userId, userId));
const existingRepoNames = new Set(existingRepos.map(r => r.normalizedFullName)); 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) { if (newRepos.length > 0) {
console.log(`[Scheduler] Found ${newRepos.length} new repositories for user ${userId}`); console.log(`[Scheduler] Found ${newRepos.length} new repositories for user ${userId}`);
@@ -129,6 +131,10 @@ async function runScheduledSync(config: any): Promise<void> {
} else { } else {
console.log(`[Scheduler] No new repositories found for user ${userId}`); 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) { } catch (error) {
console.error(`[Scheduler] Failed to auto-import repositories for user ${userId}:`, error); console.error(`[Scheduler] Failed to auto-import repositories for user ${userId}:`, error);
} }
@@ -429,6 +435,7 @@ async function performInitialAutoStart(): Promise<void> {
: Promise.resolve([]), : Promise.resolve([]),
]); ]);
const allGithubRepos = mergeGitReposPreferStarred(basicAndForkedRepos, starredRepos); const allGithubRepos = mergeGitReposPreferStarred(basicAndForkedRepos, starredRepos);
const mirrorableGithubRepos = allGithubRepos.filter(isMirrorableGitHubRepo);
// Check for new repositories // Check for new repositories
const existingRepos = await db const existingRepos = await db
@@ -437,7 +444,7 @@ async function performInitialAutoStart(): Promise<void> {
.where(eq(repositories.userId, config.userId)); .where(eq(repositories.userId, config.userId));
const existingRepoNames = new Set(existingRepos.map(r => r.normalizedFullName)); 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) { if (reposToImport.length > 0) {
console.log(`[Scheduler] Importing ${reposToImport.length} repositories for user ${config.userId}...`); console.log(`[Scheduler] Importing ${reposToImport.length} repositories for user ${config.userId}...`);
@@ -462,6 +469,10 @@ async function performInitialAutoStart(): Promise<void> {
} else { } else {
console.log(`[Scheduler] No new repositories to import for user ${config.userId}`); 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) // Check if we already have mirrored repositories (indicating this isn't first run)
const mirroredRepos = await db const mirroredRepos = await db

View File

@@ -13,6 +13,7 @@ import { jsonResponse, createSecureErrorResponse } from "@/lib/utils";
import { mergeGitReposPreferStarred, calcBatchSizeForInsert } from "@/lib/repo-utils"; import { mergeGitReposPreferStarred, calcBatchSizeForInsert } from "@/lib/repo-utils";
import { getDecryptedGitHubToken } from "@/lib/utils/config-encryption"; import { getDecryptedGitHubToken } from "@/lib/utils/config-encryption";
import { requireAuthenticatedUserId } from "@/lib/auth-guards"; import { requireAuthenticatedUserId } from "@/lib/auth-guards";
import { isMirrorableGitHubRepo } from "@/lib/repo-eligibility";
export const POST: APIRoute = async ({ request, locals }) => { export const POST: APIRoute = async ({ request, locals }) => {
const authResult = await requireAuthenticatedUserId({ 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 // Merge and de-duplicate by fullName, preferring starred variant when duplicated
const allGithubRepos = mergeGitReposPreferStarred(basicAndForkedRepos, starredRepos); const allGithubRepos = mergeGitReposPreferStarred(basicAndForkedRepos, starredRepos);
const mirrorableGithubRepos = allGithubRepos.filter(isMirrorableGitHubRepo);
// Prepare full list of repos and orgs // Prepare full list of repos and orgs
const newRepos = allGithubRepos.map((repo) => ({ const newRepos = mirrorableGithubRepos.map((repo) => ({
id: uuidv4(), id: uuidv4(),
userId, userId,
configId: config.id, configId: config.id,
@@ -186,6 +188,7 @@ export const POST: APIRoute = async ({ request, locals }) => {
message: "Repositories and organizations synced successfully", message: "Repositories and organizations synced successfully",
newRepositories: insertedRepos.length, newRepositories: insertedRepos.length,
newOrganizations: insertedOrgs.length, newOrganizations: insertedOrgs.length,
skippedDisabledRepositories: allGithubRepos.length - mirrorableGithubRepos.length,
}, },
}); });
} catch (error) { } catch (error) {

View File

@@ -150,9 +150,10 @@ export const POST: APIRoute = async ({ request, locals }) => {
const existingIds = new Set(allRepos.map(r => r.id)); const existingIds = new Set(allRepos.map(r => r.id));
const uniqueMemberRepos = memberRepos.filter(r => !existingIds.has(r.id)); const uniqueMemberRepos = memberRepos.filter(r => !existingIds.has(r.id));
allRepos.push(...uniqueMemberRepos); allRepos.push(...uniqueMemberRepos);
const mirrorableRepos = allRepos.filter((repo) => !repo.disabled);
// Insert repositories // Insert repositories
const repoRecords = allRepos.map((repo) => { const repoRecords = mirrorableRepos.map((repo) => {
const normalizedOwner = repo.owner.login.trim().toLowerCase(); const normalizedOwner = repo.owner.login.trim().toLowerCase();
const normalizedRepoName = repo.name.trim().toLowerCase(); const normalizedRepoName = repo.name.trim().toLowerCase();

View File

@@ -70,6 +70,7 @@ export interface GitRepo {
visibility: RepositoryVisibility; visibility: RepositoryVisibility;
status: RepoStatus; status: RepoStatus;
isDisabled?: boolean;
lastMirrored?: Date; lastMirrored?: Date;
errorMessage?: string; errorMessage?: string;