From 8dc50f7ebfa2a46c6cb4bdccda73a4e647413d19 Mon Sep 17 00:00:00 2001 From: Arunavo Ray Date: Sat, 9 Aug 2025 10:10:08 +0530 Subject: [PATCH] Address Issue #68 and #69 --- docker-entrypoint.sh | 22 +++ package.json | 1 + scripts/startup-env-config.ts | 52 +++++ src/lib/db/schema.ts | 1 + src/lib/env-config-loader.ts | 264 +++++++++++++++++++++++++ src/lib/gitea.ts | 341 ++++++++++++++++++++++++++++++++- src/lib/utils/config-mapper.ts | 5 +- src/middleware.ts | 13 ++ 8 files changed, 691 insertions(+), 8 deletions(-) create mode 100644 scripts/startup-env-config.ts create mode 100644 src/lib/env-config-loader.ts diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 6015c11..c114006 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -280,6 +280,28 @@ fi +# Initialize configuration from environment variables if provided +echo "Checking for environment configuration..." +if [ -f "dist/scripts/startup-env-config.js" ]; then + echo "Loading configuration from environment variables..." + bun dist/scripts/startup-env-config.js + ENV_CONFIG_EXIT_CODE=$? +elif [ -f "scripts/startup-env-config.ts" ]; then + echo "Loading configuration from environment variables..." + bun scripts/startup-env-config.ts + ENV_CONFIG_EXIT_CODE=$? +else + echo "Environment configuration script not found. Skipping." + ENV_CONFIG_EXIT_CODE=0 +fi + +# Log environment config result +if [ $ENV_CONFIG_EXIT_CODE -eq 0 ]; then + echo "✅ Environment configuration loaded successfully" +else + echo "⚠️ Environment configuration loading completed with warnings" +fi + # Run startup recovery to handle any interrupted jobs echo "Running startup recovery..." if [ -f "dist/scripts/startup-recovery.js" ]; then diff --git a/package.json b/package.json index 7384353..771ccaf 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "db:studio": "bun drizzle-kit studio", "startup-recovery": "bun scripts/startup-recovery.ts", "startup-recovery-force": "bun scripts/startup-recovery.ts --force", + "startup-env-config": "bun scripts/startup-env-config.ts", "test-recovery": "bun scripts/test-recovery.ts", "test-recovery-cleanup": "bun scripts/test-recovery.ts --cleanup", "test-shutdown": "bun scripts/test-graceful-shutdown.ts", diff --git a/scripts/startup-env-config.ts b/scripts/startup-env-config.ts new file mode 100644 index 0000000..b9b1636 --- /dev/null +++ b/scripts/startup-env-config.ts @@ -0,0 +1,52 @@ +#!/usr/bin/env bun +/** + * Startup environment configuration script + * This script loads configuration from environment variables before the application starts + * It ensures that Docker environment variables are properly populated in the database + * + * Usage: + * bun scripts/startup-env-config.ts + */ + +import { initializeConfigFromEnv } from "../src/lib/env-config-loader"; + +async function runEnvConfigInitialization() { + console.log('=== Gitea Mirror Environment Configuration ==='); + console.log('Loading configuration from environment variables...'); + console.log(''); + + const startTime = Date.now(); + + try { + await initializeConfigFromEnv(); + + const endTime = Date.now(); + const duration = endTime - startTime; + + console.log(`✅ Environment configuration loaded successfully in ${duration}ms`); + process.exit(0); + } catch (error) { + const endTime = Date.now(); + const duration = endTime - startTime; + + console.error(`❌ Failed to load environment configuration after ${duration}ms:`, error); + console.error('Application will start anyway, but environment configuration was not loaded.'); + + // Exit with error code but allow startup to continue + process.exit(1); + } +} + +// Handle process signals gracefully +process.on('SIGINT', () => { + console.log('\n⚠️ Configuration loading interrupted by SIGINT'); + process.exit(130); +}); + +process.on('SIGTERM', () => { + console.log('\n⚠️ Configuration loading interrupted by SIGTERM'); + process.exit(143); +}); + +// Run the environment configuration initialization +runEnvConfigInitialization(); \ No newline at end of file diff --git a/src/lib/db/schema.ts b/src/lib/db/schema.ts index f3630f2..4ae8eb2 100644 --- a/src/lib/db/schema.ts +++ b/src/lib/db/schema.ts @@ -26,6 +26,7 @@ export const githubConfigSchema = z.object({ starredReposOrg: z.string().optional(), mirrorStrategy: z.enum(["preserve", "single-org", "flat-user", "mixed"]).default("preserve"), defaultOrg: z.string().optional(), + skipStarredIssues: z.boolean().default(false), }); export const giteaConfigSchema = z.object({ diff --git a/src/lib/env-config-loader.ts b/src/lib/env-config-loader.ts new file mode 100644 index 0000000..28c126c --- /dev/null +++ b/src/lib/env-config-loader.ts @@ -0,0 +1,264 @@ +/** + * Environment variable configuration loader + * Loads configuration from environment variables and populates the database + */ + +import { db, configs, users } from '@/lib/db'; +import { eq, and } from 'drizzle-orm'; +import { v4 as uuidv4 } from 'uuid'; +import { encrypt } from '@/lib/utils/encryption'; + +interface EnvConfig { + github: { + username?: string; + token?: string; + privateRepositories?: boolean; + mirrorStarred?: boolean; + skipForks?: boolean; + mirrorOrganizations?: boolean; + preserveOrgStructure?: boolean; + onlyMirrorOrgs?: boolean; + skipStarredIssues?: boolean; + }; + gitea: { + url?: string; + username?: string; + token?: string; + organization?: string; + visibility?: 'public' | 'private' | 'limited'; + }; + mirror: { + mirrorIssues?: boolean; + mirrorWiki?: boolean; + mirrorReleases?: boolean; + mirrorPullRequests?: boolean; + mirrorLabels?: boolean; + mirrorMilestones?: boolean; + }; + schedule: { + delay?: number; + enabled?: boolean; + }; + cleanup: { + enabled?: boolean; + retentionDays?: number; + }; +} + +/** + * Parse environment variables into configuration object + */ +function parseEnvConfig(): EnvConfig { + return { + github: { + username: process.env.GITHUB_USERNAME, + token: process.env.GITHUB_TOKEN, + privateRepositories: process.env.PRIVATE_REPOSITORIES === 'true', + mirrorStarred: process.env.MIRROR_STARRED === 'true', + skipForks: process.env.SKIP_FORKS === 'true', + mirrorOrganizations: process.env.MIRROR_ORGANIZATIONS === 'true', + preserveOrgStructure: process.env.PRESERVE_ORG_STRUCTURE === 'true', + onlyMirrorOrgs: process.env.ONLY_MIRROR_ORGS === 'true', + skipStarredIssues: process.env.SKIP_STARRED_ISSUES === 'true', + }, + gitea: { + url: process.env.GITEA_URL, + username: process.env.GITEA_USERNAME, + token: process.env.GITEA_TOKEN, + organization: process.env.GITEA_ORGANIZATION, + visibility: process.env.GITEA_ORG_VISIBILITY as 'public' | 'private' | 'limited', + }, + mirror: { + mirrorIssues: process.env.MIRROR_ISSUES === 'true', + mirrorWiki: process.env.MIRROR_WIKI === 'true', + mirrorReleases: process.env.MIRROR_RELEASES === 'true', + mirrorPullRequests: process.env.MIRROR_PULL_REQUESTS === 'true', + mirrorLabels: process.env.MIRROR_LABELS === 'true', + mirrorMilestones: process.env.MIRROR_MILESTONES === 'true', + }, + schedule: { + delay: process.env.DELAY ? parseInt(process.env.DELAY, 10) : undefined, + enabled: process.env.SCHEDULE_ENABLED === 'true', + }, + cleanup: { + enabled: process.env.CLEANUP_ENABLED === 'true', + retentionDays: process.env.CLEANUP_RETENTION_DAYS ? parseInt(process.env.CLEANUP_RETENTION_DAYS, 10) : undefined, + }, + }; +} + +/** + * Check if environment configuration is available + */ +function hasEnvConfig(envConfig: EnvConfig): boolean { + // Check if any GitHub or Gitea config is provided + return !!( + envConfig.github.username || + envConfig.github.token || + envConfig.gitea.url || + envConfig.gitea.username || + envConfig.gitea.token + ); +} + +/** + * Initialize configuration from environment variables + * This function runs on application startup and populates the database + * with configuration from environment variables if available + */ +export async function initializeConfigFromEnv(): Promise { + try { + const envConfig = parseEnvConfig(); + + // Skip if no environment config is provided + if (!hasEnvConfig(envConfig)) { + console.log('[ENV Config Loader] No environment configuration found, skipping initialization'); + return; + } + + console.log('[ENV Config Loader] Found environment configuration, initializing...'); + + // Get the first user (admin user) + const firstUser = await db + .select() + .from(users) + .limit(1); + + if (firstUser.length === 0) { + console.log('[ENV Config Loader] No users found, skipping configuration initialization'); + return; + } + + const userId = firstUser[0].id; + + // Check if config already exists for this user + const existingConfig = await db + .select() + .from(configs) + .where(eq(configs.userId, userId)) + .limit(1); + + // Determine mirror strategy based on environment variables + let mirrorStrategy: 'preserve' | 'single-org' | 'flat-user' | 'mixed' = 'preserve'; + if (envConfig.github.preserveOrgStructure === false && envConfig.gitea.organization) { + mirrorStrategy = 'single-org'; + } else if (envConfig.github.preserveOrgStructure === true) { + mirrorStrategy = 'preserve'; + } + + // Build GitHub config + const githubConfig = { + owner: envConfig.github.username || existingConfig?.[0]?.githubConfig?.owner || '', + type: 'personal' as const, + token: envConfig.github.token ? encrypt(envConfig.github.token) : existingConfig?.[0]?.githubConfig?.token || '', + includeStarred: envConfig.github.mirrorStarred ?? existingConfig?.[0]?.githubConfig?.includeStarred ?? false, + includeForks: !(envConfig.github.skipForks ?? false), + includeArchived: existingConfig?.[0]?.githubConfig?.includeArchived ?? false, + includePrivate: envConfig.github.privateRepositories ?? existingConfig?.[0]?.githubConfig?.includePrivate ?? false, + includePublic: existingConfig?.[0]?.githubConfig?.includePublic ?? true, + includeOrganizations: envConfig.github.mirrorOrganizations ? [] : (existingConfig?.[0]?.githubConfig?.includeOrganizations ?? []), + starredReposOrg: 'starred', + mirrorStrategy, + defaultOrg: envConfig.gitea.organization || 'github-mirrors', + skipStarredIssues: envConfig.github.skipStarredIssues ?? existingConfig?.[0]?.githubConfig?.skipStarredIssues ?? false, + }; + + // Build Gitea config + const giteaConfig = { + url: envConfig.gitea.url || existingConfig?.[0]?.giteaConfig?.url || '', + token: envConfig.gitea.token ? encrypt(envConfig.gitea.token) : existingConfig?.[0]?.giteaConfig?.token || '', + defaultOwner: envConfig.gitea.username || existingConfig?.[0]?.giteaConfig?.defaultOwner || '', + mirrorInterval: existingConfig?.[0]?.giteaConfig?.mirrorInterval || '8h', + lfs: existingConfig?.[0]?.giteaConfig?.lfs ?? false, + wiki: envConfig.mirror.mirrorWiki ?? existingConfig?.[0]?.giteaConfig?.wiki ?? false, + visibility: envConfig.gitea.visibility || existingConfig?.[0]?.giteaConfig?.visibility || 'public', + createOrg: true, + addTopics: existingConfig?.[0]?.giteaConfig?.addTopics ?? true, + preserveVisibility: existingConfig?.[0]?.giteaConfig?.preserveVisibility ?? false, + forkStrategy: existingConfig?.[0]?.giteaConfig?.forkStrategy || 'reference', + mirrorReleases: envConfig.mirror.mirrorReleases ?? existingConfig?.[0]?.giteaConfig?.mirrorReleases ?? false, + mirrorMetadata: (envConfig.mirror.mirrorIssues || envConfig.mirror.mirrorPullRequests || envConfig.mirror.mirrorLabels || envConfig.mirror.mirrorMilestones) ?? existingConfig?.[0]?.giteaConfig?.mirrorMetadata ?? false, + mirrorIssues: envConfig.mirror.mirrorIssues ?? existingConfig?.[0]?.giteaConfig?.mirrorIssues ?? false, + mirrorPullRequests: envConfig.mirror.mirrorPullRequests ?? existingConfig?.[0]?.giteaConfig?.mirrorPullRequests ?? false, + mirrorLabels: envConfig.mirror.mirrorLabels ?? existingConfig?.[0]?.giteaConfig?.mirrorLabels ?? false, + mirrorMilestones: envConfig.mirror.mirrorMilestones ?? existingConfig?.[0]?.giteaConfig?.mirrorMilestones ?? false, + }; + + // Build schedule config + const scheduleConfig = { + enabled: envConfig.schedule.enabled ?? existingConfig?.[0]?.scheduleConfig?.enabled ?? false, + interval: envConfig.schedule.delay ? String(envConfig.schedule.delay) : existingConfig?.[0]?.scheduleConfig?.interval || '3600', + concurrent: existingConfig?.[0]?.scheduleConfig?.concurrent ?? false, + batchSize: existingConfig?.[0]?.scheduleConfig?.batchSize ?? 10, + pauseBetweenBatches: existingConfig?.[0]?.scheduleConfig?.pauseBetweenBatches ?? 5000, + retryAttempts: existingConfig?.[0]?.scheduleConfig?.retryAttempts ?? 3, + retryDelay: existingConfig?.[0]?.scheduleConfig?.retryDelay ?? 60000, + timeout: existingConfig?.[0]?.scheduleConfig?.timeout ?? 3600000, + autoRetry: existingConfig?.[0]?.scheduleConfig?.autoRetry ?? true, + cleanupBeforeMirror: existingConfig?.[0]?.scheduleConfig?.cleanupBeforeMirror ?? false, + notifyOnFailure: existingConfig?.[0]?.scheduleConfig?.notifyOnFailure ?? true, + notifyOnSuccess: existingConfig?.[0]?.scheduleConfig?.notifyOnSuccess ?? false, + logLevel: existingConfig?.[0]?.scheduleConfig?.logLevel || 'info', + timezone: existingConfig?.[0]?.scheduleConfig?.timezone || 'UTC', + onlyMirrorUpdated: existingConfig?.[0]?.scheduleConfig?.onlyMirrorUpdated ?? false, + updateInterval: existingConfig?.[0]?.scheduleConfig?.updateInterval ?? 86400000, + skipRecentlyMirrored: existingConfig?.[0]?.scheduleConfig?.skipRecentlyMirrored ?? true, + recentThreshold: existingConfig?.[0]?.scheduleConfig?.recentThreshold ?? 3600000, + lastRun: existingConfig?.[0]?.scheduleConfig?.lastRun || null, + nextRun: existingConfig?.[0]?.scheduleConfig?.nextRun || null, + }; + + // Build cleanup config + const cleanupConfig = { + enabled: envConfig.cleanup.enabled ?? existingConfig?.[0]?.cleanupConfig?.enabled ?? false, + retentionDays: envConfig.cleanup.retentionDays ? envConfig.cleanup.retentionDays * 86400 : existingConfig?.[0]?.cleanupConfig?.retentionDays ?? 604800, // Convert days to seconds + deleteFromGitea: existingConfig?.[0]?.cleanupConfig?.deleteFromGitea ?? false, + deleteIfNotInGitHub: existingConfig?.[0]?.cleanupConfig?.deleteIfNotInGitHub ?? true, + protectedRepos: existingConfig?.[0]?.cleanupConfig?.protectedRepos ?? [], + dryRun: existingConfig?.[0]?.cleanupConfig?.dryRun ?? true, + orphanedRepoAction: existingConfig?.[0]?.cleanupConfig?.orphanedRepoAction || 'archive', + batchSize: existingConfig?.[0]?.cleanupConfig?.batchSize ?? 10, + pauseBetweenDeletes: existingConfig?.[0]?.cleanupConfig?.pauseBetweenDeletes ?? 2000, + lastRun: existingConfig?.[0]?.cleanupConfig?.lastRun || null, + nextRun: existingConfig?.[0]?.cleanupConfig?.nextRun || null, + }; + + if (existingConfig.length > 0) { + // Update existing config + console.log('[ENV Config Loader] Updating existing configuration with environment variables'); + await db + .update(configs) + .set({ + githubConfig, + giteaConfig, + scheduleConfig, + cleanupConfig, + updatedAt: new Date(), + }) + .where(eq(configs.id, existingConfig[0].id)); + } else { + // Create new config + console.log('[ENV Config Loader] Creating new configuration from environment variables'); + const configId = uuidv4(); + await db.insert(configs).values({ + id: configId, + userId, + name: 'Environment Configuration', + isActive: true, + githubConfig, + giteaConfig, + include: [], + exclude: [], + scheduleConfig, + cleanupConfig, + createdAt: new Date(), + updatedAt: new Date(), + }); + } + + console.log('[ENV Config Loader] Configuration initialized successfully from environment variables'); + } catch (error) { + console.error('[ENV Config Loader] Failed to initialize configuration from environment:', error); + // Don't throw - this is a non-critical initialization + } +} \ No newline at end of file diff --git a/src/lib/gitea.ts b/src/lib/gitea.ts index 8658134..54d5581 100644 --- a/src/lib/gitea.ts +++ b/src/lib/gitea.ts @@ -390,7 +390,7 @@ export const mirrorGithubRepoToGitea = async ({ clone_addr: cloneAddress, repo_name: repository.name, mirror: true, - wiki: config.githubConfig.mirrorWiki || false, // will mirror wiki if it exists + wiki: config.giteaConfig?.wiki || false, // will mirror wiki if it exists private: repository.isPrivate, repo_owner: repoOwner, description: "", @@ -402,7 +402,7 @@ export const mirrorGithubRepoToGitea = async ({ ); //mirror releases - if (config.githubConfig?.mirrorReleases) { + if (config.giteaConfig?.mirrorReleases) { await mirrorGitHubReleasesToGitea({ config, octokit, @@ -412,8 +412,8 @@ export const mirrorGithubRepoToGitea = async ({ // clone issues // Skip issues for starred repos if skipStarredIssues is enabled - const shouldMirrorIssues = config.githubConfig.mirrorIssues && - !(repository.isStarred && config.githubConfig.skipStarredIssues); + const shouldMirrorIssues = config.giteaConfig?.mirrorIssues && + !(repository.isStarred && config.githubConfig?.skipStarredIssues); if (shouldMirrorIssues) { await mirrorGitRepoIssuesToGitea({ @@ -424,6 +424,36 @@ export const mirrorGithubRepoToGitea = async ({ }); } + // Mirror pull requests if enabled + if (config.giteaConfig?.mirrorPullRequests) { + await mirrorGitRepoPullRequestsToGitea({ + config, + octokit, + repository, + giteaOwner: repoOwner, + }); + } + + // Mirror labels if enabled (and not already done via issues) + if (config.giteaConfig?.mirrorLabels && !shouldMirrorIssues) { + await mirrorGitRepoLabelsToGitea({ + config, + octokit, + repository, + giteaOwner: repoOwner, + }); + } + + // Mirror milestones if enabled + if (config.giteaConfig?.mirrorMilestones) { + await mirrorGitRepoMilestonesToGitea({ + config, + octokit, + repository, + giteaOwner: repoOwner, + }); + } + console.log(`Repository ${repository.name} mirrored successfully`); // Mark repos as "mirrored" in DB @@ -617,7 +647,7 @@ export async function mirrorGitHubRepoToGiteaOrg({ uid: giteaOrgId, repo_name: repository.name, mirror: true, - wiki: config.githubConfig?.mirrorWiki || false, // will mirror wiki if it exists + wiki: config.giteaConfig?.wiki || false, // will mirror wiki if it exists private: repository.isPrivate, }, { @@ -626,7 +656,7 @@ export async function mirrorGitHubRepoToGiteaOrg({ ); //mirror releases - if (config.githubConfig?.mirrorReleases) { + if (config.giteaConfig?.mirrorReleases) { await mirrorGitHubReleasesToGitea({ config, octokit, @@ -636,7 +666,7 @@ export async function mirrorGitHubRepoToGiteaOrg({ // Clone issues // Skip issues for starred repos if skipStarredIssues is enabled - const shouldMirrorIssues = config.githubConfig?.mirrorIssues && + const shouldMirrorIssues = config.giteaConfig?.mirrorIssues && !(repository.isStarred && config.githubConfig?.skipStarredIssues); if (shouldMirrorIssues) { @@ -648,6 +678,36 @@ export async function mirrorGitHubRepoToGiteaOrg({ }); } + // Mirror pull requests if enabled + if (config.giteaConfig?.mirrorPullRequests) { + await mirrorGitRepoPullRequestsToGitea({ + config, + octokit, + repository, + giteaOwner: orgName, + }); + } + + // Mirror labels if enabled (and not already done via issues) + if (config.giteaConfig?.mirrorLabels && !shouldMirrorIssues) { + await mirrorGitRepoLabelsToGitea({ + config, + octokit, + repository, + giteaOwner: orgName, + }); + } + + // Mirror milestones if enabled + if (config.giteaConfig?.mirrorMilestones) { + await mirrorGitRepoMilestonesToGitea({ + config, + octokit, + repository, + giteaOwner: orgName, + }); + } + console.log( `Repository ${repository.name} mirrored successfully to organization ${orgName}` ); @@ -1228,4 +1288,271 @@ export async function mirrorGitHubReleasesToGitea({ } console.log(`✅ Mirrored ${releases.data.length} GitHub releases to Gitea`); +} + +export async function mirrorGitRepoPullRequestsToGitea({ + config, + octokit, + repository, + giteaOwner, +}: { + config: Partial; + octokit: Octokit; + repository: Repository; + giteaOwner: string; +}) { + if ( + !config.githubConfig?.token || + !config.giteaConfig?.token || + !config.giteaConfig?.url || + !config.giteaConfig?.username + ) { + throw new Error("Missing GitHub or Gitea configuration."); + } + + // Decrypt config tokens for API usage + const decryptedConfig = decryptConfigTokens(config as Config); + + const [owner, repo] = repository.fullName.split("/"); + + // Fetch GitHub pull requests + const pullRequests = await octokit.paginate( + octokit.rest.pulls.list, + { + owner, + repo, + state: "all", + per_page: 100, + }, + (res) => res.data + ); + + console.log( + `Mirroring ${pullRequests.length} pull requests from ${repository.fullName}` + ); + + if (pullRequests.length === 0) { + console.log(`No pull requests to mirror for ${repository.fullName}`); + return; + } + + // Note: Gitea doesn't have a direct API to create pull requests from external sources + // Pull requests are typically created through Git operations + // For now, we'll create them as issues with a special label + + // Get or create a PR label + try { + await httpPost( + `${config.giteaConfig.url}/api/v1/repos/${giteaOwner}/${repository.name}/labels`, + { + name: "pull-request", + color: "#0366d6", + description: "Mirrored from GitHub Pull Request" + }, + { + Authorization: `token ${decryptedConfig.giteaConfig.token}`, + } + ); + } catch (error) { + // Label might already exist, continue + } + + const { processWithRetry } = await import("@/lib/utils/concurrency"); + + await processWithRetry( + pullRequests, + async (pr) => { + const issueData = { + title: `[PR #${pr.number}] ${pr.title}`, + body: `**Original Pull Request:** ${pr.html_url}\n\n**State:** ${pr.state}\n**Merged:** ${pr.merged_at ? 'Yes' : 'No'}\n\n---\n\n${pr.body || 'No description provided'}`, + labels: [{ name: "pull-request" }], + state: pr.state === "closed" ? "closed" : "open", + }; + + try { + await httpPost( + `${config.giteaConfig!.url}/api/v1/repos/${giteaOwner}/${repository.name}/issues`, + issueData, + { + Authorization: `token ${decryptedConfig.giteaConfig!.token}`, + } + ); + } catch (error) { + console.error( + `Failed to mirror PR #${pr.number}: ${error instanceof Error ? error.message : String(error)}` + ); + } + }, + { + maxConcurrency: 5, + retryAttempts: 3, + retryDelay: 1000, + } + ); + + console.log(`✅ Mirrored ${pullRequests.length} pull requests to Gitea`); +} + +export async function mirrorGitRepoLabelsToGitea({ + config, + octokit, + repository, + giteaOwner, +}: { + config: Partial; + octokit: Octokit; + repository: Repository; + giteaOwner: string; +}) { + if ( + !config.githubConfig?.token || + !config.giteaConfig?.token || + !config.giteaConfig?.url + ) { + throw new Error("Missing GitHub or Gitea configuration."); + } + + // Decrypt config tokens for API usage + const decryptedConfig = decryptConfigTokens(config as Config); + + const [owner, repo] = repository.fullName.split("/"); + + // Fetch GitHub labels + const labels = await octokit.paginate( + octokit.rest.issues.listLabelsForRepo, + { + owner, + repo, + per_page: 100, + }, + (res) => res.data + ); + + console.log(`Mirroring ${labels.length} labels from ${repository.fullName}`); + + if (labels.length === 0) { + console.log(`No labels to mirror for ${repository.fullName}`); + return; + } + + // Get existing labels from Gitea + const giteaLabelsRes = await httpGet( + `${config.giteaConfig.url}/api/v1/repos/${giteaOwner}/${repository.name}/labels`, + { + Authorization: `token ${decryptedConfig.giteaConfig.token}`, + } + ); + + const existingLabels = new Set( + giteaLabelsRes.data.map((label: any) => label.name) + ); + + let mirroredCount = 0; + for (const label of labels) { + if (!existingLabels.has(label.name)) { + try { + await httpPost( + `${config.giteaConfig.url}/api/v1/repos/${giteaOwner}/${repository.name}/labels`, + { + name: label.name, + color: `#${label.color}`, + description: label.description || "", + }, + { + Authorization: `token ${decryptedConfig.giteaConfig.token}`, + } + ); + mirroredCount++; + } catch (error) { + console.error( + `Failed to mirror label "${label.name}": ${error instanceof Error ? error.message : String(error)}` + ); + } + } + } + + console.log(`✅ Mirrored ${mirroredCount} new labels to Gitea`); +} + +export async function mirrorGitRepoMilestonesToGitea({ + config, + octokit, + repository, + giteaOwner, +}: { + config: Partial; + octokit: Octokit; + repository: Repository; + giteaOwner: string; +}) { + if ( + !config.githubConfig?.token || + !config.giteaConfig?.token || + !config.giteaConfig?.url + ) { + throw new Error("Missing GitHub or Gitea configuration."); + } + + // Decrypt config tokens for API usage + const decryptedConfig = decryptConfigTokens(config as Config); + + const [owner, repo] = repository.fullName.split("/"); + + // Fetch GitHub milestones + const milestones = await octokit.paginate( + octokit.rest.issues.listMilestones, + { + owner, + repo, + state: "all", + per_page: 100, + }, + (res) => res.data + ); + + console.log(`Mirroring ${milestones.length} milestones from ${repository.fullName}`); + + if (milestones.length === 0) { + console.log(`No milestones to mirror for ${repository.fullName}`); + return; + } + + // Get existing milestones from Gitea + const giteaMilestonesRes = await httpGet( + `${config.giteaConfig.url}/api/v1/repos/${giteaOwner}/${repository.name}/milestones`, + { + Authorization: `token ${decryptedConfig.giteaConfig.token}`, + } + ); + + const existingMilestones = new Set( + giteaMilestonesRes.data.map((milestone: any) => milestone.title) + ); + + let mirroredCount = 0; + for (const milestone of milestones) { + if (!existingMilestones.has(milestone.title)) { + try { + await httpPost( + `${config.giteaConfig.url}/api/v1/repos/${giteaOwner}/${repository.name}/milestones`, + { + title: milestone.title, + description: milestone.description || "", + due_on: milestone.due_on, + state: milestone.state, + }, + { + Authorization: `token ${decryptedConfig.giteaConfig.token}`, + } + ); + mirroredCount++; + } catch (error) { + console.error( + `Failed to mirror milestone "${milestone.title}": ${error instanceof Error ? error.message : String(error)}` + ); + } + } + } + + console.log(`✅ Mirrored ${mirroredCount} new milestones to Gitea`); } \ No newline at end of file diff --git a/src/lib/utils/config-mapper.ts b/src/lib/utils/config-mapper.ts index 40ecdb2..22fe9ab 100644 --- a/src/lib/utils/config-mapper.ts +++ b/src/lib/utils/config-mapper.ts @@ -50,6 +50,9 @@ export function mapUiToDbConfig( // Mirror strategy mirrorStrategy: giteaConfig.mirrorStrategy || "preserve", defaultOrg: giteaConfig.organization, + + // Advanced options + skipStarredIssues: advancedOptions.skipStarredIssues, }; // Map Gitea config to match database schema @@ -142,7 +145,7 @@ export function mapDbToUiConfig(dbConfig: any): { // Map advanced options const advancedOptions: AdvancedOptions = { skipForks: !(dbConfig.githubConfig?.includeForks ?? true), // Invert includeForks to get skipForks - skipStarredIssues: false, // Not stored in current schema + skipStarredIssues: dbConfig.githubConfig?.skipStarredIssues || false, }; return { diff --git a/src/middleware.ts b/src/middleware.ts index d02dbca..fe1f66e 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -5,12 +5,14 @@ import { initializeShutdownManager, registerShutdownCallback } from './lib/shutd import { setupSignalHandlers } from './lib/signal-handlers'; import { auth } from './lib/auth'; import { isHeaderAuthEnabled, authenticateWithHeaders } from './lib/auth-header'; +import { initializeConfigFromEnv } from './lib/env-config-loader'; // Flag to track if recovery has been initialized let recoveryInitialized = false; let recoveryAttempted = false; let cleanupServiceStarted = false; let shutdownManagerInitialized = false; +let envConfigInitialized = false; export const onRequest = defineMiddleware(async (context, next) => { // First, try Better Auth session (cookie-based) @@ -73,6 +75,17 @@ export const onRequest = defineMiddleware(async (context, next) => { } } + // Initialize configuration from environment variables (only once) + if (!envConfigInitialized) { + envConfigInitialized = true; + try { + await initializeConfigFromEnv(); + } catch (error) { + console.error('⚠️ Failed to initialize configuration from environment:', error); + // Continue anyway - environment config is optional + } + } + // Initialize recovery system only once when the server starts // This is a fallback in case the startup script didn't run if (!recoveryInitialized && !recoveryAttempted) {