Compare commits

..

7 Commits

Author SHA1 Message Date
Arunavo Ray
afaac70bb8 chore: bump version to v3.2.2
Patch release including:
- Fix for issues #68 and #69
- Hero redesign improvements
- Mobile hero image support
2025-08-09 10:25:38 +05:30
ARUNAVO RAY
da95c1d5fd Merge pull request #70 from RayLabsHQ/Fixes
Address Issue #68 and #69
2025-08-09 10:23:57 +05:30
Arunavo Ray
8dc50f7ebf Address Issue #68 and #69 2025-08-09 10:10:08 +05:30
ARUNAVO RAY
eafc44d112 Merge pull request #67 from abhrajitray77/hero-redesign
🐧 Added hero image for mobile
2025-08-07 22:04:42 +05:30
abhrajitray77
25cff6fe8e 🐧 Added hero image for mobile 2025-08-07 22:03:15 +05:30
ARUNAVO RAY
29fe7ba895 Merge pull request #66 from abhrajitray77/hero-redesign
🔥 Added shader hero component
2025-08-07 20:12:03 +05:30
abhrajitray77
fbcedc404a 🔥 Added shader hero component 2025-08-07 20:10:46 +05:30
12 changed files with 853 additions and 24 deletions

View File

@@ -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

View File

@@ -1,7 +1,7 @@
{
"name": "gitea-mirror",
"type": "module",
"version": "3.2.1",
"version": "3.2.2",
"engines": {
"bun": ">=1.2.9"
},
@@ -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",

View File

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

View File

@@ -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({

View File

@@ -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<void> {
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
}
}

View File

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

View File

@@ -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 {

View File

@@ -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) {

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

View File

@@ -9,27 +9,29 @@ export function Hero() {
return (
<section className="relative min-h-[100vh] pt-20 pb-10 flex flex-col items-center justify-center px-4 sm:px-6 lg:px-8 overflow-hidden">
{/* Elegant gradient background */}
<div className="absolute inset-0 -z-10 overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-br from-primary/5 via-transparent to-accent/5"></div>
<div className="absolute -top-1/2 -left-1/2 w-full h-full bg-gradient-radial from-primary/10 to-transparent blur-3xl"></div>
<div className="absolute -bottom-1/2 -right-1/2 w-full h-full bg-gradient-radial from-accent/10 to-transparent blur-3xl"></div>
</div>
{/* spline object */}
<div className="spline-object absolute inset-0 max-lg:-z-10 max-h-[40rem] -translate-y-16 md:max-h-[50rem] lg:max-h-[60%] xl:max-h-[70%] 2xl:max-h-[80%] md:-translate-y-24 lg:-translate-y-28 flex items-center justify-center">
<div className="absolute right-2 bottom-4 h-20 w-40 bg-[#f8fbfb] dark:bg-[#010708]"/>
<div className="block md:hidden w-[80%]">
<img
src="/assets/hero_logo.webp"
alt="Gitea Mirror hero image"
className="w-full h-full object-contain"
/>
</div>
<div className="absolute right-2 bottom-4 h-20 w-40 bg-background hidden md:block"/>
<Suspense fallback={
<div className="w-full h-full flex items-center justify-center">
<div className="w-full h-full md:flex items-center justify-center hidden">
<img
src="/assets/logo.png"
alt="Gitea Mirror Logo"
src="/assets/hero_logo.webp"
alt="Gitea Mirror hero logo"
className="w-[200px] h-[160px] md:w-[280px] md:h-[240px] lg:w-[360px] lg:h-[320px] xl:w-[420px] xl:h-[380px] 2xl:w-[480px] 2xl:h-[420px] object-contain"
/>
</div>
}>
<Spline
scene="https://prod.spline.design/jl0aKWbdH9vHQnYV/scene.splinecode"
className="hidden md:block"
/>
</Suspense>
</div>
@@ -44,13 +46,13 @@ export function Hero() {
</span>
</h1>
<p className="mt-4 sm:mt-6 text-base sm:text-lg md:text-xl text-muted-foreground max-w-3xl mx-auto px-4">
<p className="mt-4 sm:mt-6 text-base sm:text-lg md:text-xl text-muted-foreground max-w-3xl mx-auto px-4 z-20">
Automatically mirror your GitHub repositories to self-hosted Gitea.
Never lose access to your code with continuous backup and
synchronization.
</p>
<div className="mt-6 sm:mt-8 flex flex-wrap items-center justify-center gap-3 text-xs sm:text-sm text-muted-foreground px-4">
<div className="mt-6 sm:mt-8 flex flex-wrap items-center justify-center gap-3 text-xs sm:text-sm text-muted-foreground px-4 z-20">
<div className="flex items-center gap-2 px-3 py-1 rounded-full bg-primary/10 text-primary">
<Shield className="w-3 h-3 sm:w-4 sm:h-4" />
<span className="font-medium">Self-Hosted</span>
@@ -66,7 +68,7 @@ export function Hero() {
</div>
{/* Call to action buttons */}
<div className="mt-8 sm:mt-10 flex flex-col sm:flex-row items-center justify-center gap-3 sm:gap-4 px-4">
<div className="mt-8 sm:mt-10 flex flex-col sm:flex-row items-center justify-center gap-3 sm:gap-4 px-4 z-20">
<Button
size="lg"
className="relative group w-full sm:w-auto min-h-[48px] text-base bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90 shadow-lg shadow-primary/25 hover:shadow-xl hover:shadow-primary/30 transition-all duration-300"

View File

@@ -0,0 +1,140 @@
---
---
<canvas id="shader-canvas" class="hidden lg:block absolute inset-0 w-full h-full dark:opacity-90 z-10 pointer-events-none"></canvas>
<script>
const canvas = document.getElementById('shader-canvas') as HTMLCanvasElement | null;
if (!canvas) {
console.error('Canvas element not found');
} else {
const gl = canvas.getContext('webgl', { alpha: true });
if (!gl) {
console.error('WebGL not supported!');
} else {
const vertexShaderSource = `
attribute vec2 a_position;
void main() {
gl_Position = vec4(a_position, 0.0, 1.0);
}
`;
const fragmentShaderSource = `
precision mediump float;
uniform vec2 iResolution;
uniform float iTime;
uniform vec3 u_color1;
uniform vec3 u_color2;
#define iterations 2
void main() {
vec2 uv = gl_FragCoord.xy / iResolution.xy;
vec2 mirrored_uv = uv;
if (mirrored_uv.x > 0.5) {
mirrored_uv.x = 1.0 - mirrored_uv.x;
}
float res = 1.0;
for (int i = 0; i < iterations; i++) {
res += cos(mirrored_uv.y * 12.345 - iTime * 1.0 + cos(res * 12.234) * 0.2 + cos(mirrored_uv.x * 32.2345 + cos(mirrored_uv.y * 17.234))) + cos(mirrored_uv.x * 12.345);
}
vec3 c = mix(u_color1, u_color2, cos(res + cos(mirrored_uv.y * 24.3214) * 0.1 + cos(mirrored_uv.x * 6.324 + iTime * 1.0) + iTime) * 0.5 + 0.5);
float vignette = clamp((length(mirrored_uv - 0.1 + cos(iTime * 0.9 + mirrored_uv.yx * 4.34 + mirrored_uv.xy * res) * 0.2) * 5.0 - 0.4), 0.0, 1.0);
vec3 final_color = mix(c, vec3(0.0), vignette);
gl_FragColor = vec4(final_color, length(final_color));
}
`;
function createShader(context: WebGLRenderingContext, type: number, source: string): WebGLShader | null {
const shader = context.createShader(type);
if (!shader) return null;
context.shaderSource(shader, source);
context.compileShader(shader);
if (!context.getShaderParameter(shader, context.COMPILE_STATUS)) {
console.error('Shader compilation error:', context.getShaderInfoLog(shader));
context.deleteShader(shader);
return null;
}
return shader;
}
const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
if (vertexShader && fragmentShader) {
const program = gl.createProgram();
if (program) {
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.error('Program linking error:', gl.getProgramInfoLog(program));
} else {
gl.useProgram(program);
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
const positions = [-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
const positionAttributeLocation = gl.getAttribLocation(program, "a_position");
gl.enableVertexAttribArray(positionAttributeLocation);
gl.vertexAttribPointer(positionAttributeLocation, 2, gl.FLOAT, false, 0, 0);
const resolutionLocation = gl.getUniformLocation(program, "iResolution");
const timeLocation = gl.getUniformLocation(program, "iTime");
const color1Location = gl.getUniformLocation(program, "u_color1");
const color2Location = gl.getUniformLocation(program, "u_color2");
const color1 = [0.122, 0.502, 0.122]; // #1f801f
const color2 = [0.059, 0.251, 0.059]; // #0f400f
function render(time: number): void {
if (!gl || !program || !canvas) return;
time *= 0.001;
const displayWidth = canvas.clientWidth;
const displayHeight = canvas.clientHeight;
if (canvas.width !== displayWidth || canvas.height !== displayHeight) {
canvas.width = displayWidth;
canvas.height = displayHeight;
gl.viewport(0, 0, canvas.width, canvas.height);
}
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.useProgram(program);
if (resolutionLocation !== null) {
gl.uniform2f(resolutionLocation, canvas.width, canvas.height);
}
if (timeLocation !== null) {
gl.uniform1f(timeLocation, time);
}
if (color1Location !== null) {
gl.uniform3fv(color1Location, color1);
}
if (color2Location !== null) {
gl.uniform3fv(color2Location, color2);
}
gl.drawArrays(gl.TRIANGLES, 0, 6);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
}
}
}
}
}
</script>

View File

@@ -2,6 +2,7 @@
import '../styles/global.css';
import { Header } from '../components/Header';
import { Hero } from '../components/Hero';
import ShaderBackground from '../components/ShaderBackground.astro';
import Features from '../components/Features.astro';
import Screenshots from '../components/Screenshots.astro';
import { Installation } from '../components/Installation';
@@ -117,7 +118,10 @@ const structuredData = {
<Header client:load />
<main>
<Hero client:load />
<div class="relative">
<ShaderBackground />
<Hero client:load />
</div>
<Features />
<Screenshots />
<Installation client:load />