mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2026-03-13 22:12:54 +03:00
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:
2
.github/workflows/astro-build-test.yml
vendored
2
.github/workflows/astro-build-test.yml
vendored
@@ -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: |
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|
||||||
|
|||||||
17
src/lib/repo-eligibility.test.ts
Normal file
17
src/lib/repo-eligibility.test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
6
src/lib/repo-eligibility.ts
Normal file
6
src/lib/repo-eligibility.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import type { GitRepo } from "@/types/Repository";
|
||||||
|
|
||||||
|
export function isMirrorableGitHubRepo(repo: Pick<GitRepo, "isDisabled">): boolean {
|
||||||
|
return repo.isDisabled !== true;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user