fix: Complete Issue #72 - Fix automatic mirroring and repository cleanup

Major fixes for Docker environment variable issues and cleanup functionality:

🔧 **Duration Parser & Scheduler Fixes**
- Add comprehensive duration parser supporting "8h", "30m", "24h" formats
- Fix GITEA_MIRROR_INTERVAL environment variable mapping to scheduler
- Auto-enable scheduler when GITEA_MIRROR_INTERVAL is set
- Improve scheduler logging to clarify timing behavior (from last run, not startup)

🧹 **Repository Cleanup Service**
- Complete repository cleanup service for orphaned repos (unstarred, deleted)
- Fix cleanup configuration logic - now works with CLEANUP_DELETE_IF_NOT_IN_GITHUB=true
- Auto-enable cleanup when deleteIfNotInGitHub is enabled
- Add manual cleanup trigger API endpoint (/api/cleanup/trigger)
- Support archive/delete actions with dry-run mode and protected repos

🐛 **Environment Variable Integration**
- Fix scheduler not recognizing GITEA_MIRROR_INTERVAL=8h
- Fix cleanup requiring both CLEANUP_DELETE_FROM_GITEA and CLEANUP_DELETE_IF_NOT_IN_GITHUB
- Auto-enable services when relevant environment variables are set
- Better error logging and debugging information

📚 **Documentation Updates**
- Update .env.example with auto-enabling behavior notes
- Update ENVIRONMENT_VARIABLES.md with clarified functionality
- Add comprehensive tests for duration parsing

This resolves the core issues where:
1. GITEA_MIRROR_INTERVAL=8h was not working for automatic mirroring
2. Repository cleanup was not working despite CLEANUP_DELETE_IF_NOT_IN_GITHUB=true
3. Users had no visibility into why scheduling/cleanup wasn't working

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Arunavo Ray
2025-08-20 11:06:21 +05:30
parent 0fb5f9e190
commit 698eb0b507
10 changed files with 1254 additions and 9 deletions

View File

@@ -0,0 +1,130 @@
import type { APIRoute } from 'astro';
import { auth } from '@/lib/auth';
import { createSecureErrorResponse } from '@/lib/utils/error-handler';
import { triggerRepositoryCleanup } from '@/lib/repository-cleanup-service';
/**
* Manually trigger repository cleanup for the current user
* This can be called when repositories are updated or when immediate cleanup is needed
*/
export const POST: APIRoute = async ({ request }) => {
try {
// Get user session
const session = await auth.api.getSession({
headers: request.headers,
});
if (!session?.user?.id) {
return new Response(
JSON.stringify({ error: 'Unauthorized' }),
{
status: 401,
headers: { 'Content-Type': 'application/json' },
}
);
}
console.log(`[Cleanup API] Manual cleanup triggered for user ${session.user.id}`);
// Trigger immediate cleanup for this user
const results = await triggerRepositoryCleanup(session.user.id);
console.log(`[Cleanup API] Cleanup completed: ${results.processedCount}/${results.orphanedCount} repositories processed, ${results.errors.length} errors`);
return new Response(
JSON.stringify({
success: true,
message: 'Repository cleanup completed',
results: {
orphanedCount: results.orphanedCount,
processedCount: results.processedCount,
errorCount: results.errors.length,
errors: results.errors,
},
}),
{
status: 200,
headers: { 'Content-Type': 'application/json' },
}
);
} catch (error) {
console.error('[Cleanup API] Error during manual cleanup:', error);
return createSecureErrorResponse(error);
}
};
/**
* Get cleanup status and configuration for the current user
*/
export const GET: APIRoute = async ({ request }) => {
try {
// Get user session
const session = await auth.api.getSession({
headers: request.headers,
});
if (!session?.user?.id) {
return new Response(
JSON.stringify({ error: 'Unauthorized' }),
{
status: 401,
headers: { 'Content-Type': 'application/json' },
}
);
}
// Import inside the function to avoid import issues
const { db, configs } = await import('@/lib/db');
const { eq, and } = await import('drizzle-orm');
// Get user's cleanup configuration
const [config] = await db
.select()
.from(configs)
.where(and(
eq(configs.userId, session.user.id),
eq(configs.isActive, true)
))
.limit(1);
if (!config) {
return new Response(
JSON.stringify({
success: false,
message: 'No active configuration found',
cleanupEnabled: false,
}),
{
status: 200,
headers: { 'Content-Type': 'application/json' },
}
);
}
const cleanupConfig = config.cleanupConfig || {};
const isCleanupEnabled = cleanupConfig.enabled || cleanupConfig.deleteIfNotInGitHub;
return new Response(
JSON.stringify({
success: true,
cleanupEnabled: isCleanupEnabled,
configuration: {
enabled: cleanupConfig.enabled,
deleteFromGitea: cleanupConfig.deleteFromGitea,
deleteIfNotInGitHub: cleanupConfig.deleteIfNotInGitHub,
dryRun: cleanupConfig.dryRun,
orphanedRepoAction: cleanupConfig.orphanedRepoAction || 'archive',
lastRun: cleanupConfig.lastRun,
nextRun: cleanupConfig.nextRun,
},
}),
{
status: 200,
headers: { 'Content-Type': 'application/json' },
}
);
} catch (error) {
console.error('[Cleanup API] Error getting cleanup status:', error);
return createSecureErrorResponse(error);
}
};