Compare commits

..

1 Commits

Author SHA1 Message Date
Arunavo Ray
491546a97c added basic nix pack 2025-10-31 09:00:18 +05:30
17 changed files with 1069 additions and 536 deletions

1
.envrc Normal file
View File

@@ -0,0 +1 @@
use flake

5
.gitignore vendored
View File

@@ -32,3 +32,8 @@ certs/*.pem
certs/*.cer
!certs/README.md
# Nix build artifacts
result
result-*
.direnv/

153
NIX.md Normal file
View File

@@ -0,0 +1,153 @@
# Nix Deployment Quick Reference
## TL;DR
```bash
# Just run it - zero configuration needed!
nix run .#gitea-mirror
```
Secrets auto-generate, database auto-initializes, and the web UI starts at http://localhost:4321.
---
## Installation Options
### 1. Run Without Installing
```bash
nix run .#gitea-mirror
```
### 2. Install to Profile
```bash
nix profile install .#gitea-mirror
gitea-mirror
```
### 3. NixOS System Service
```nix
# configuration.nix
{
inputs.gitea-mirror.url = "github:RayLabsHQ/gitea-mirror";
services.gitea-mirror = {
enable = true;
betterAuthUrl = "https://mirror.example.com"; # For production
openFirewall = true;
};
}
```
### 4. Development
```bash
nix develop
# or
direnv allow
```
---
## What Gets Auto-Generated?
On first run, the wrapper automatically:
1. Creates `~/.local/share/gitea-mirror/` (or `$DATA_DIR`)
2. Generates `BETTER_AUTH_SECRET``.better_auth_secret`
3. Generates `ENCRYPTION_SECRET``.encryption_secret`
4. Initializes SQLite database
5. Runs startup recovery and repair scripts
6. Starts the application
---
## Key Commands
```bash
# Database management
gitea-mirror-db init # Initialize database
gitea-mirror-db check # Health check
gitea-mirror-db fix # Fix issues
# Development
nix develop # Enter dev shell
nix build # Build package
nix flake check # Validate flake
```
---
## Environment Variables
All vars from `docker-compose.alt.yml` are supported:
```bash
DATA_DIR="$HOME/.local/share/gitea-mirror"
PORT=4321
HOST="0.0.0.0"
BETTER_AUTH_URL="http://localhost:4321"
# Secrets (auto-generated if not set)
BETTER_AUTH_SECRET=auto-generated
ENCRYPTION_SECRET=auto-generated
# Concurrency (for perfect ordering, set both to 1)
MIRROR_ISSUE_CONCURRENCY=3
MIRROR_PULL_REQUEST_CONCURRENCY=5
```
---
## NixOS Module Options
```nix
services.gitea-mirror = {
enable = true;
package = ...; # Override package
dataDir = "/var/lib/gitea-mirror"; # Data location
user = "gitea-mirror"; # Service user
group = "gitea-mirror"; # Service group
host = "0.0.0.0"; # Bind address
port = 4321; # Listen port
betterAuthUrl = "http://..."; # External URL
betterAuthTrustedOrigins = "..."; # CORS origins
mirrorIssueConcurrency = 3; # Concurrency
mirrorPullRequestConcurrency = 5; # Concurrency
environmentFile = null; # Optional secrets file
openFirewall = true; # Open firewall
};
```
---
## Comparison: Docker vs Nix
| Feature | Docker | Nix |
|---------|--------|-----|
| **Config Required** | BETTER_AUTH_SECRET | None (auto-generated) |
| **Startup** | `docker-compose up` | `nix run .#gitea-mirror` |
| **Service** | Docker daemon | systemd (NixOS) |
| **Updates** | `docker pull` | `nix flake update` |
| **Reproducible** | Image-based | Hash-based |
---
## Full Documentation
See [docs/NIX_DEPLOYMENT.md](docs/NIX_DEPLOYMENT.md) for:
- Complete NixOS module configuration
- Home Manager integration
- Production deployment examples
- Migration from Docker
- Troubleshooting guide
---
## Key Features
- **Zero-config deployment** - Runs immediately without setup
- **Auto-secret generation** - Secure secrets created and persisted
- **Startup recovery** - Handles interrupted jobs automatically
- **Graceful shutdown** - Proper signal handling
- **Health checks** - Built-in monitoring support
- **Security hardening** - NixOS module includes systemd protections
- **Docker parity** - Same behavior as `docker-compose.alt.yml`

View File

@@ -150,6 +150,34 @@ bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/Proxmo
See the [Proxmox VE Community Scripts](https://community-scripts.github.io/ProxmoxVE/scripts?id=gitea-mirror) for more details.
### Nix/NixOS
Zero-configuration deployment with Nix:
```bash
# Run immediately - no setup needed!
nix run github:RayLabsHQ/gitea-mirror
# Or install to profile
nix profile install github:RayLabsHQ/gitea-mirror
gitea-mirror
```
**NixOS users** - add to your configuration:
```nix
{
inputs.gitea-mirror.url = "github:RayLabsHQ/gitea-mirror";
services.gitea-mirror = {
enable = true;
betterAuthUrl = "https://mirror.example.com";
openFirewall = true;
};
}
```
Secrets auto-generate, database auto-initializes. See [NIX.md](NIX.md) for quick reference or [docs/NIX_DEPLOYMENT.md](docs/NIX_DEPLOYMENT.md) for full documentation.
### Manual Installation
```bash

View File

@@ -1,496 +0,0 @@
Date: 2025-11-07Application: Gitea Mirror v3.9.0Branch: bun-v1.3.1Scope: Full application security review
---
Executive Summary
A comprehensive security audit was conducted across all components of the Gitea Mirror application, including authentication, authorization,
encryption, input validation, API endpoints, database operations, UI components, and external integrations. The review identified 23 security
findings across multiple severity levels.
Critical Findings: 2
High Severity: 9
Medium Severity: 10
Low Severity: 2
Overall Security Posture: The application demonstrates strong foundational security practices (AES-256-GCM encryption, Drizzle ORM parameterized
queries, React auto-escaping) but has critical authorization flaws that require immediate remediation.
---
CRITICAL SEVERITY VULNERABILITIES
Vuln 1: Broken Object Level Authorization (BOLA) - Multiple Endpoints
Severity: CRITICALConfidence: 1.0Category: Authorization Bypass
Affected Files:
- src/pages/api/config/index.ts:18
- src/pages/api/job/mirror-repo.ts:16
- src/pages/api/github/repositories.ts:11
- src/pages/api/dashboard/index.ts:9
- src/pages/api/activities/index.ts:8
- src/pages/api/sync/repository.ts:15
- src/pages/api/sync/organization.ts:14
- src/pages/api/job/mirror-org.ts:14
- src/pages/api/job/retry-repo.ts:18
- src/pages/api/job/sync-repo.ts:11
- src/pages/api/job/schedule-sync-repo.ts:13
Description: Multiple API endpoints accept a userId parameter from client requests without validating that the authenticated user matches the
requested userId. This allows any authenticated user to access and manipulate any other user's data.
Exploit Scenario:
POST /api/config/index
Content-Type: application/json
Cookie: better-auth-session=attacker-session
{
"userId": "victim-user-id",
"giteaConfig": {
"token": "attacker-controlled-token"
}
}
Result: Attacker modifies victim's configuration, steals encrypted tokens, triggers mirror operations, or accesses activity logs.
Recommendation:
export const POST: APIRoute = async (context) => {
// 1. Validate authentication
const { user, response } = await requireAuth(context);
if (response) return response;
// 2. Use authenticated userId from session, NOT from request body
const authenticatedUserId = user!.id;
// 3. Don't accept userId from client
const body = await request.json();
const { githubConfig, giteaConfig } = body;
// 4. Use session userId for all operations
const config = await db
.select()
.from(configs)
.where(eq(configs.userId, authenticatedUserId));
}
---
Vuln 2: Header Authentication Spoofing - src/lib/auth-header.ts:48
Severity: CRITICALConfidence: 0.95Category: Authentication Bypass
Description: The header authentication mechanism trusts HTTP headers (X-Authentik-Username, X-Authentik-Email) without proper validation. If the
application is accessible without a properly configured reverse proxy, attackers can send these headers directly to impersonate any user.
Exploit Scenario:
GET /api/config/index?userId=admin
Host: gitea-mirror.example.com
X-Authentik-Username: admin
X-Authentik-Email: admin@example.com
Result: Attacker gains admin access without authentication.
Recommendation:
// 1. Add trusted proxy IP validation
const TRUSTED_PROXY_IPS = process.env.TRUSTED_PROXY_IPS?.split(',') || [];
export function extractUserFromHeaders(headers: Headers, remoteIp: string): UserInfo | null {
// Validate request comes from trusted reverse proxy
if (TRUSTED_PROXY_IPS.length > 0 && !TRUSTED_PROXY_IPS.includes(remoteIp)) {
console.warn(`Header auth rejected: untrusted IP ${remoteIp}`);
return null;
}
// 2. Add shared secret validation
const authSecret = headers.get('X-Auth-Secret');
if (authSecret !== process.env.HEADER_AUTH_SECRET) {
console.warn('Header auth rejected: invalid secret');
return null;
}
// Continue with header extraction...
}
Additional Requirements:
- Document that header auth MUST only be used behind a reverse proxy
- Reverse proxy MUST strip X-Authentik-* headers from untrusted requests
- Direct access MUST be blocked via firewall rules
---
HIGH SEVERITY VULNERABILITIES
Vuln 3: Missing Authentication on Critical Endpoints
Severity: HIGHConfidence: 1.0Category: Authentication Bypass
Affected Endpoints:
- /api/github/test-connection - Tests GitHub connection with user's token
- /api/gitea/test-connection - Tests Gitea connection with user's token
- /api/rate-limit/index - Exposes rate limit info
- /api/events/index - SSE endpoint for events
- /api/activities/cleanup - Deletes activities
Description: Several sensitive endpoints lack authentication checks entirely.
Exploit Scenario:
POST /api/github/test-connection
{"token": "stolen-token", "userId": "victim"}
Recommendation: Add requireAuth to all sensitive endpoints.
---
Vuln 4: Token Exposure Through Config API - src/pages/api/config/index.ts:220
Severity: HIGHConfidence: 0.9Category: Sensitive Data Exposure
Description: The GET /api/config/index endpoint decrypts and returns GitHub/Gitea API tokens in plaintext. Combined with BOLA, any user can retrieve
any other user's tokens.
Exploit Scenario:
1. Attacker authenticates as User A
2. Calls /api/config/index?userId=admin
3. Receives admin's decrypted GitHub Personal Access Token
4. Uses stolen token to access admin's repositories
Recommendation:
// Never send full tokens - use masked versions
if (githubConfig.token) {
const decrypted = decrypt(githubConfig.token);
githubConfig.token = maskToken(decrypted); // "ghp_****...last4"
githubConfig.tokenSet = true;
}
---
Vuln 5: Static Salt in Key Derivation - src/lib/utils/encryption.ts:21
Severity: HIGHConfidence: 0.95Category: Cryptographic Weakness
Description: The key derivation function uses a static, hardcoded salt that reduces protection against precomputation attacks.
Exploit Scenario: If ENCRYPTION_SECRET is weak or compromised, the static salt makes brute-force attacks more efficient. Attackers can build targeted
rainbow tables.
Recommendation:
// Generate and store unique salt per installation
const SALT_FILE = '/app/data/.encryption_salt';
let installationSalt: Buffer;
if (fs.existsSync(SALT_FILE)) {
installationSalt = Buffer.from(fs.readFileSync(SALT_FILE, 'utf8'), 'hex');
} else {
installationSalt = crypto.randomBytes(32);
fs.writeFileSync(SALT_FILE, installationSalt.toString('hex'));
fs.chmodSync(SALT_FILE, 0o600);
}
return crypto.pbkdf2Sync(secret, installationSalt, ITERATIONS, KEY_LENGTH, 'sha256');
---
Vuln 6: Weak Default Secret - src/lib/config.ts:22
Severity: HIGHConfidence: 0.90Category: Hardcoded Credentials
Description: The application uses a hardcoded default for BETTER_AUTH_SECRET when not set.
Exploit Scenario: Non-Docker deployments may use the weak default. Attackers can forge authentication tokens using the known default secret.
Recommendation:
BETTER_AUTH_SECRET: (() => {
const secret = process.env.BETTER_AUTH_SECRET;
const weakDefaults = ["your-secret-key-change-this-in-production"];
if (!secret || weakDefaults.includes(secret)) {
if (process.env.NODE_ENV === 'production') {
throw new Error("BETTER_AUTH_SECRET required. Generate with: openssl rand -base64 32");
}
return "dev-secret-minimum-32-chars";
}
if (secret.length < 32) {
throw new Error("BETTER_AUTH_SECRET must be at least 32 characters");
}
return secret;
})()
---
Vuln 7: Unencrypted OAuth Client Secrets - src/lib/db/schema.ts:569
Severity: HIGHConfidence: 0.92Category: Sensitive Data Exposure
Description: OAuth application client secrets and SSO provider credentials are stored as plaintext, inconsistent with encrypted GitHub/Gitea tokens.
Exploit Scenario: Database leak exposes OAuth client secrets, allowing attackers to impersonate the application to external OAuth providers.
Recommendation:
// Encrypt before storage
import { encrypt } from "@/lib/utils/encryption";
await db.insert(oauthApplications).values({
clientSecret: encrypt(clientSecret),
// ...
});
// Add decryption helper
export function decryptOAuthClientSecret(encrypted: string): string {
return decrypt(encrypted);
}
---
Vuln 8: SSRF in OIDC Discovery - src/pages/api/sso/discover.ts:48
Severity: HIGHConfidence: 0.95Category: Server-Side Request Forgery
Description: The endpoint accepts user-provided issuer URL and fetches ${issuer}/.well-known/openid-configuration without validating against internal
networks.
Exploit Scenario:
POST /api/sso/discover
{"issuer": "http://169.254.169.254/latest/meta-data"}
Result: Access to AWS metadata service, internal APIs, or port scanning.
Recommendation:
// Block private IPs and localhost
const ALLOWED_PROTOCOLS = ['https:'];
const hostname = parsedIssuer.hostname.toLowerCase();
if (
!ALLOWED_PROTOCOLS.includes(parsedIssuer.protocol) ||
hostname === 'localhost' ||
hostname.match(/^10\./) ||
hostname.match(/^192\.168\./) ||
hostname.match(/^172\.(1[6-9]|2[0-9]|3[0-1])\./) ||
hostname.match(/^169\.254\./) ||
hostname.match(/^127\./)
) {
return new Response(JSON.stringify({ error: "Invalid issuer" }), { status: 400 });
}
---
Vuln 9: SSRF in Gitea Connection Test - src/pages/api/gitea/test-connection.ts:29
Severity: HIGHConfidence: 0.95Category: SSRF / Input Validation
Description: Accepts user-provided url and makes HTTP request without validation.
Exploit Scenario: Same as Vuln 8 - internal network scanning, metadata service access.
Recommendation: Apply same URL validation as Vuln 8.
---
Vuln 10: Command Injection in Docker Build - scripts/build-docker.sh:72
Severity: HIGHConfidence: 0.95Category: Command Injection
Description: Uses eval with dynamically constructed Docker command including environment variables.
Exploit Scenario:
DOCKER_IMAGE='test; rm -rf /; #'
./scripts/build-docker.sh
Recommendation:
# Replace eval with direct execution
if docker buildx build --platform linux/amd64,linux/arm64 \
-t "$FULL_IMAGE_NAME" \
${LOAD:+--load} \
${PUSH:+--push} \
.; then
echo "Success"
fi
---
Vuln 11: Path Traversal in Gitea API - src/lib/gitea.ts:193
Severity: MEDIUM-HIGHConfidence: 0.85Category: Path Traversal / SSRF
Description: Repository owner/name interpolated into URLs without encoding.
Exploit Scenario:
owner = "../../admin"
repoName = "users/../../../config"
// Results in: /api/v1/repos/../../admin/users/../../../config
Recommendation:
const response = await fetch(
`${config.url}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repoName)}`,
// ...
);
Apply to all URL constructions in gitea.ts and gitea-enhanced.ts.
---
MEDIUM SEVERITY VULNERABILITIES
Vuln 12: Weak Session Configuration - src/lib/auth.ts:105
Severity: MEDIUMConfidence: 0.85
Description: 30-day session expiration is excessive; missing explicit security flags.
Recommendation:
session: {
expiresIn: 60 * 60 * 24 * 7, // 7 days max
cookieOptions: {
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
httpOnly: true,
},
}
---
Vuln 13: Missing CSRF Protection
Severity: MEDIUMConfidence: 0.80
Description: No visible CSRF token validation on state-changing operations.
Recommendation: Verify Better Auth CSRF protection is enabled; implement explicit tokens if needed.
---
Vuln 14: Weak Random Number Generation - src/lib/utils.ts:12
Severity: MEDIUMConfidence: 0.85
Description: generateRandomString() uses Math.random() (not cryptographically secure) for OAuth client credentials.
Recommendation:
// Use existing secure function
const clientId = `client_${generateSecureToken(16)}`;
const clientSecret = `secret_${generateSecureToken(24)}`;
---
Vuln 15-18: Input Validation Issues
Severity: MEDIUMConfidence: 0.85
Affected:
- src/pages/api/repositories/[id].ts:24 - destinationOrg not validated
- src/pages/api/organizations/[id].ts:24 - destinationOrg not validated
- src/pages/api/job/mirror-repo.ts:19 - repositoryIds array not validated
- src/lib/helpers.ts:49 - Repository names in messages not sanitized
Recommendation: Use Zod schemas for all user inputs:
const updateRepoSchema = z.object({
destinationOrg: z.string()
.min(1)
.max(100)
.regex(/^[a-zA-Z0-9\-_\.]+$/)
.nullable()
.optional()
});
---
LOW SEVERITY VULNERABILITIES
Vuln 19: XSS via Astro set:html - src/pages/docs/quickstart.astro:102
Severity: LOWConfidence: 0.85
Description: Uses set:html with static data; risky pattern if copied with dynamic content.
Recommendation:
<p class="text-sm" set:text={item.text}></p>
---
Vuln 20: Open Redirect in OAuth - src/components/oauth/ConsentPage.tsx:118
Severity: LOWConfidence: 0.90
Description: OAuth redirect uses window.location.href without explicit protocol validation.
Recommendation:
if (!['http:', 'https:'].includes(url.protocol)) {
throw new Error('Invalid protocol');
}
window.location.assign(url.toString());
---
POSITIVE SECURITY FINDINGS ✅
The codebase demonstrates excellent security practices:
1. SQL Injection Protection - Drizzle ORM with parameterized queries throughout
2. Token Encryption - AES-256-GCM for GitHub/Gitea tokens
3. XSS Protection - React auto-escaping, no dangerouslySetInnerHTML found
4. Password Hashing - bcrypt via Better Auth
5. Secure Random - crypto.randomBytes() in encryption functions
6. Input Validation - Comprehensive Zod schemas
7. Safe Path Operations - path.join() with proper base directories
8. No Command Execution - TypeScript codebase doesn't use child_process
9. Authentication Framework - Better Auth with session management
10. Type Safety - TypeScript strict mode
---
REMEDIATION PRIORITY
Phase 1: EMERGENCY (Deploy Today)
1. Fix BOLA vulnerabilities - Add authentication and userId validation (Vuln 1)
2. Disable or secure header auth - Add IP whitelist validation (Vuln 2)
3. Add authentication checks - Protect unprotected endpoints (Vuln 3)
Phase 2: URGENT (This Week)
1. Implement token masking - Don't return decrypted tokens (Vuln 4)
2. Fix SSRF vulnerabilities - Add URL validation (Vuln 8, 9)
3. Fix command injection - Remove eval from build script (Vuln 10)
4. Add URL encoding - Path traversal fix (Vuln 11)
Phase 3: HIGH (This Sprint)
1. Fix KDF salt - Generate unique salt per installation (Vuln 5)
2. Enforce strong secrets - Fail if weak defaults used (Vuln 6)
3. Encrypt OAuth secrets - Match GitHub/Gitea token encryption (Vuln 7)
4. Reduce session expiration - 7 days maximum (Vuln 12)
5. Add CSRF protection - Explicit tokens (Vuln 13)
Phase 4: MEDIUM (Next Sprint)
1. Input validation - Zod schemas for all endpoints (Vuln 14-18)
2. Fix weak RNG - Use crypto.randomBytes (Vuln 14)
Phase 5: LOW (Backlog)
1. Remove set:html - Use safer alternatives (Vuln 19)
2. OAuth redirect validation - Add protocol check (Vuln 20)
---
TESTING RECOMMENDATIONS
Proof of Concept Tests
# Test BOLA
curl -X POST http://localhost:4321/api/config/index \
-H "Content-Type: application/json" \
-H "Cookie: better-auth-session=<attacker-session>" \
-d '{"userId":"victim-id","githubConfig":{...}}'
# Test header spoofing
curl http://localhost:4321/api/config/index?userId=admin \
-H "X-Authentik-Username: admin" \
-H "X-Authentik-Email: admin@example.com"
# Test SSRF
curl -X POST http://localhost:4321/api/sso/discover \
-H "Content-Type: application/json" \
-d '{"issuer":"http://169.254.169.254/latest/meta-data"}'
---
SECURITY GRADE
Overall Security Grade: C+
- Would be B+ after Phase 1-2 fixes
- Would be A- after all fixes
Critical Issues: 2 (authorization bypass, auth spoofing)Risk Level: HIGH due to BOLA allowing complete account takeover
---
CONCLUSION
The Gitea Mirror application has a strong security foundation with proper encryption, ORM usage, and XSS protection. However, critical authorization
flaws in API endpoints allow authenticated users to access any other user's data. These must be fixed immediately before production deployment.
The development team has clearly prioritized security (encryption, input validation, type safety), and the identified issues are fixable with
moderate effort. After remediation, this will be a secure, production-ready application.

475
docs/NIX_DEPLOYMENT.md Normal file
View File

@@ -0,0 +1,475 @@
# Nix Deployment Guide
This guide covers deploying Gitea Mirror using Nix flakes. The Nix deployment follows the same minimal configuration philosophy as `docker-compose.alt.yml` - secrets are auto-generated, and everything else can be configured via the web UI.
## Prerequisites
- Nix with flakes enabled (Nix 2.4+)
- For NixOS module: NixOS 23.05+
To enable flakes, add to `/etc/nix/nix.conf` or `~/.config/nix/nix.conf`:
```
experimental-features = nix-command flakes
```
## Quick Start (Zero Configuration!)
### Run Immediately - No Setup Required
```bash
# Run directly from the flake
nix run .#gitea-mirror
# Or from GitHub (once published)
nix run github:RayLabsHQ/gitea-mirror
```
That's it! On first run:
- Secrets (`BETTER_AUTH_SECRET` and `ENCRYPTION_SECRET`) are auto-generated
- Database is automatically created and initialized
- Startup recovery and repair scripts run automatically
- Access the web UI at http://localhost:4321
Everything else (GitHub credentials, Gitea settings, mirror options) is configured through the web interface after signup.
### Development Environment
```bash
# Enter development shell with all dependencies
nix develop
# Or use direnv for automatic environment loading
echo "use flake" > .envrc
direnv allow
```
### Build and Install
```bash
# Build the package
nix build
# Run the built package
./result/bin/gitea-mirror
# Install to your profile
nix profile install .#gitea-mirror
```
## What Happens on First Run?
Following the same pattern as the Docker deployment, the Nix package automatically:
1. **Creates data directory**: `~/.local/share/gitea-mirror` (or `$DATA_DIR`)
2. **Generates secrets** (stored securely in data directory):
- `BETTER_AUTH_SECRET` - Session authentication (32-char hex)
- `ENCRYPTION_SECRET` - Token encryption (48-char base64)
3. **Initializes database**: SQLite database with Drizzle migrations
4. **Runs startup scripts**:
- Environment configuration loader
- Crash recovery for interrupted jobs
- Repository status repair
5. **Starts the application** with graceful shutdown handling
## NixOS Module - Minimal Deployment
### Simplest Possible Configuration
Add to your NixOS configuration (`/etc/nixos/configuration.nix`):
```nix
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
gitea-mirror.url = "github:RayLabsHQ/gitea-mirror";
};
outputs = { nixpkgs, gitea-mirror, ... }: {
nixosConfigurations.your-hostname = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
gitea-mirror.nixosModules.default
{
# That's it! Just enable the service
services.gitea-mirror.enable = true;
}
];
};
};
}
```
Apply with:
```bash
sudo nixos-rebuild switch
```
Access at http://localhost:4321, sign up (first user is admin), and configure everything via the web UI.
### Production Configuration
For production with custom domain and firewall:
```nix
{
services.gitea-mirror = {
enable = true;
host = "0.0.0.0";
port = 4321;
betterAuthUrl = "https://mirror.example.com";
betterAuthTrustedOrigins = "https://mirror.example.com";
openFirewall = true;
};
# Optional: Use with nginx reverse proxy
services.nginx = {
enable = true;
virtualHosts."mirror.example.com" = {
locations."/" = {
proxyPass = "http://127.0.0.1:4321";
proxyWebsockets = true;
};
enableACME = true;
forceSSL = true;
};
};
}
```
### Advanced: Manual Secret Management
If you prefer to manage secrets manually (e.g., with sops-nix or agenix):
1. Create a secrets file:
```bash
# /var/lib/gitea-mirror/secrets.env
BETTER_AUTH_SECRET=your-32-character-minimum-secret-key-here
ENCRYPTION_SECRET=your-encryption-secret-here
```
2. Reference it in your configuration:
```nix
{
services.gitea-mirror = {
enable = true;
environmentFile = "/var/lib/gitea-mirror/secrets.env";
};
}
```
### Full Configuration Options
```nix
{
services.gitea-mirror = {
enable = true;
package = gitea-mirror.packages.x86_64-linux.default; # Override package
dataDir = "/var/lib/gitea-mirror";
user = "gitea-mirror";
group = "gitea-mirror";
host = "0.0.0.0";
port = 4321;
betterAuthUrl = "https://mirror.example.com";
betterAuthTrustedOrigins = "https://mirror.example.com";
# Concurrency controls (match docker-compose.alt.yml)
mirrorIssueConcurrency = 3; # Set to 1 for perfect chronological order
mirrorPullRequestConcurrency = 5; # Set to 1 for perfect chronological order
environmentFile = null; # Optional secrets file
openFirewall = true;
};
}
```
## Service Management (NixOS)
```bash
# Start the service
sudo systemctl start gitea-mirror
# Stop the service
sudo systemctl stop gitea-mirror
# Restart the service
sudo systemctl restart gitea-mirror
# Check status
sudo systemctl status gitea-mirror
# View logs
sudo journalctl -u gitea-mirror -f
# Health check
curl http://localhost:4321/api/health
```
## Environment Variables
All variables from `docker-compose.alt.yml` are supported:
```bash
# === AUTO-GENERATED (Don't set unless you want specific values) ===
BETTER_AUTH_SECRET # Auto-generated, stored in data dir
ENCRYPTION_SECRET # Auto-generated, stored in data dir
# === CORE SETTINGS (Have good defaults) ===
DATA_DIR="$HOME/.local/share/gitea-mirror"
DATABASE_URL="file:$DATA_DIR/gitea-mirror.db"
HOST="0.0.0.0"
PORT="4321"
NODE_ENV="production"
# === BETTER AUTH (Override for custom domains) ===
BETTER_AUTH_URL="http://localhost:4321"
BETTER_AUTH_TRUSTED_ORIGINS="http://localhost:4321"
PUBLIC_BETTER_AUTH_URL="http://localhost:4321"
# === CONCURRENCY CONTROLS ===
MIRROR_ISSUE_CONCURRENCY=3 # Default: 3 (set to 1 for perfect order)
MIRROR_PULL_REQUEST_CONCURRENCY=5 # Default: 5 (set to 1 for perfect order)
# === CONFIGURE VIA WEB UI (Not needed at startup) ===
# GitHub credentials, Gitea settings, mirror options, scheduling, etc.
# All configured after signup through the web interface
```
## Database Management
The Nix package includes a database management helper:
```bash
# Initialize database (done automatically on first run)
gitea-mirror-db init
# Check database health
gitea-mirror-db check
# Fix database issues
gitea-mirror-db fix
# Reset users
gitea-mirror-db reset-users
```
## Home Manager Integration
For single-user deployments:
```nix
{ config, pkgs, ... }:
let
gitea-mirror = (import (fetchTarball "https://github.com/RayLabsHQ/gitea-mirror/archive/main.tar.gz")).packages.${pkgs.system}.default;
in {
home.packages = [ gitea-mirror ];
# Optional: Run as user service
systemd.user.services.gitea-mirror = {
Unit = {
Description = "Gitea Mirror Service";
After = [ "network.target" ];
};
Service = {
Type = "simple";
ExecStart = "${gitea-mirror}/bin/gitea-mirror";
Restart = "always";
Environment = [
"DATA_DIR=%h/.local/share/gitea-mirror"
"HOST=127.0.0.1"
"PORT=4321"
];
};
Install = {
WantedBy = [ "default.target" ];
};
};
}
```
## Docker Image from Nix (Optional)
You can also use Nix to create a Docker image:
```nix
# Add to flake.nix packages section
dockerImage = pkgs.dockerTools.buildLayeredImage {
name = "gitea-mirror";
tag = "latest";
contents = [ self.packages.${system}.default pkgs.cacert pkgs.openssl ];
config = {
Cmd = [ "${self.packages.${system}.default}/bin/gitea-mirror" ];
ExposedPorts = { "4321/tcp" = {}; };
Env = [
"DATA_DIR=/data"
"DATABASE_URL=file:/data/gitea-mirror.db"
];
Volumes = { "/data" = {}; };
};
};
```
Build and load:
```bash
nix build .#dockerImage
docker load < result
docker run -p 4321:4321 -v gitea-mirror-data:/data gitea-mirror:latest
```
## Comparison: Docker vs Nix
Both deployment methods follow the same philosophy:
| Feature | Docker Compose | Nix |
|---------|---------------|-----|
| **Configuration** | Minimal (only BETTER_AUTH_SECRET) | Zero config (auto-generated) |
| **Secret Generation** | Auto-generated & persisted | Auto-generated & persisted |
| **Database Init** | Automatic on first run | Automatic on first run |
| **Startup Scripts** | Runs recovery/repair/env-config | Runs recovery/repair/env-config |
| **Graceful Shutdown** | Signal handling in entrypoint | Signal handling in wrapper |
| **Health Check** | Docker healthcheck | systemd timer (optional) |
| **Updates** | `docker pull` | `nix flake update && nixos-rebuild` |
## Troubleshooting
### Check Auto-Generated Secrets
```bash
# For standalone
cat ~/.local/share/gitea-mirror/.better_auth_secret
cat ~/.local/share/gitea-mirror/.encryption_secret
# For NixOS service
sudo cat /var/lib/gitea-mirror/.better_auth_secret
sudo cat /var/lib/gitea-mirror/.encryption_secret
```
### Database Issues
```bash
# Check if database exists
ls -la ~/.local/share/gitea-mirror/gitea-mirror.db
# Reinitialize (deletes all data!)
rm ~/.local/share/gitea-mirror/gitea-mirror.db
gitea-mirror-db init
```
### Permission Issues (NixOS)
```bash
sudo chown -R gitea-mirror:gitea-mirror /var/lib/gitea-mirror
sudo chmod 700 /var/lib/gitea-mirror
```
### Port Already in Use
```bash
# Change port
export PORT=8080
gitea-mirror
# Or in NixOS config
services.gitea-mirror.port = 8080;
```
### View Startup Logs
```bash
# Standalone (verbose output on console)
gitea-mirror
# NixOS service
sudo journalctl -u gitea-mirror -f --since "5 minutes ago"
```
## Updating
### Standalone Installation
```bash
# Update flake lock
nix flake update
# Rebuild
nix build
# Or update profile
nix profile upgrade gitea-mirror
```
### NixOS
```bash
# Update input
sudo nix flake lock --update-input gitea-mirror
# Rebuild system
sudo nixos-rebuild switch
```
## Migration from Docker
To migrate from Docker to Nix while keeping your data:
1. **Stop Docker container:**
```bash
docker-compose -f docker-compose.alt.yml down
```
2. **Copy data directory:**
```bash
# For standalone
cp -r ./data ~/.local/share/gitea-mirror
# For NixOS
sudo cp -r ./data /var/lib/gitea-mirror
sudo chown -R gitea-mirror:gitea-mirror /var/lib/gitea-mirror
```
3. **Copy secrets (if you want to keep them):**
```bash
# Extract from Docker volume
docker run --rm -v gitea-mirror_data:/data alpine \
cat /data/.better_auth_secret > better_auth_secret
docker run --rm -v gitea-mirror_data:/data alpine \
cat /data/.encryption_secret > encryption_secret
# Copy to new location
cp better_auth_secret ~/.local/share/gitea-mirror/.better_auth_secret
cp encryption_secret ~/.local/share/gitea-mirror/.encryption_secret
chmod 600 ~/.local/share/gitea-mirror/.*_secret
```
4. **Start Nix version:**
```bash
gitea-mirror
```
## CI/CD Integration
Example GitHub Actions workflow:
```yaml
name: Build with Nix
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: cachix/install-nix-action@v24
with:
extra_nix_config: |
experimental-features = nix-command flakes
- uses: cachix/cachix-action@v12
with:
name: gitea-mirror
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
- run: nix build
- run: nix flake check
```
## Resources
- [Nix Manual](https://nixos.org/manual/nix/stable/)
- [NixOS Options Search](https://search.nixos.org/options)
- [Nix Pills Tutorial](https://nixos.org/guides/nix-pills/)
- [Project Documentation](../README.md)
- [Docker Deployment](../docker-compose.alt.yml) - Equivalent minimal config

395
flake.nix Normal file
View File

@@ -0,0 +1,395 @@
{
description = "Gitea Mirror - Self-hosted GitHub to Gitea mirroring service";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
# Build the application
gitea-mirror = pkgs.stdenv.mkDerivation {
pname = "gitea-mirror";
version = "3.8.11";
src = ./.;
nativeBuildInputs = with pkgs; [
bun
];
buildInputs = with pkgs; [
sqlite
openssl
];
configurePhase = ''
export HOME=$TMPDIR
export BUN_INSTALL=$TMPDIR/.bun
export PATH=$BUN_INSTALL/bin:$PATH
'';
buildPhase = ''
# Install dependencies
bun install --frozen-lockfile --no-progress
# Build the application
bun run build
'';
installPhase = ''
mkdir -p $out/lib/gitea-mirror
mkdir -p $out/bin
# Copy the built application
cp -r dist $out/lib/gitea-mirror/
cp -r node_modules $out/lib/gitea-mirror/
cp -r scripts $out/lib/gitea-mirror/
cp package.json $out/lib/gitea-mirror/
# Create entrypoint script that matches Docker behavior
cat > $out/bin/gitea-mirror <<'EOF'
#!/usr/bin/env bash
set -e
# === DEFAULT CONFIGURATION ===
# These match docker-compose.alt.yml defaults
export DATA_DIR=''${DATA_DIR:-"$HOME/.local/share/gitea-mirror"}
export DATABASE_URL=''${DATABASE_URL:-"file:$DATA_DIR/gitea-mirror.db"}
export HOST=''${HOST:-"0.0.0.0"}
export PORT=''${PORT:-"4321"}
export NODE_ENV=''${NODE_ENV:-"production"}
# Better Auth configuration
export BETTER_AUTH_URL=''${BETTER_AUTH_URL:-"http://localhost:4321"}
export BETTER_AUTH_TRUSTED_ORIGINS=''${BETTER_AUTH_TRUSTED_ORIGINS:-"http://localhost:4321"}
export PUBLIC_BETTER_AUTH_URL=''${PUBLIC_BETTER_AUTH_URL:-"http://localhost:4321"}
# Concurrency settings (match docker-compose.alt.yml)
export MIRROR_ISSUE_CONCURRENCY=''${MIRROR_ISSUE_CONCURRENCY:-3}
export MIRROR_PULL_REQUEST_CONCURRENCY=''${MIRROR_PULL_REQUEST_CONCURRENCY:-5}
# Create data directory
mkdir -p "$DATA_DIR"
cd $out/lib/gitea-mirror
# === AUTO-GENERATE SECRETS ===
BETTER_AUTH_SECRET_FILE="$DATA_DIR/.better_auth_secret"
ENCRYPTION_SECRET_FILE="$DATA_DIR/.encryption_secret"
# Generate BETTER_AUTH_SECRET if not provided
if [ -z "$BETTER_AUTH_SECRET" ]; then
if [ -f "$BETTER_AUTH_SECRET_FILE" ]; then
echo "Using previously generated BETTER_AUTH_SECRET"
export BETTER_AUTH_SECRET=$(cat "$BETTER_AUTH_SECRET_FILE")
else
echo "Generating a secure random BETTER_AUTH_SECRET"
GENERATED_SECRET=$(${pkgs.openssl}/bin/openssl rand -hex 32)
export BETTER_AUTH_SECRET="$GENERATED_SECRET"
echo "$GENERATED_SECRET" > "$BETTER_AUTH_SECRET_FILE"
chmod 600 "$BETTER_AUTH_SECRET_FILE"
echo " BETTER_AUTH_SECRET generated and saved to $BETTER_AUTH_SECRET_FILE"
fi
fi
# Generate ENCRYPTION_SECRET if not provided
if [ -z "$ENCRYPTION_SECRET" ]; then
if [ -f "$ENCRYPTION_SECRET_FILE" ]; then
echo "Using previously generated ENCRYPTION_SECRET"
export ENCRYPTION_SECRET=$(cat "$ENCRYPTION_SECRET_FILE")
else
echo "Generating a secure random ENCRYPTION_SECRET"
GENERATED_ENCRYPTION_SECRET=$(${pkgs.openssl}/bin/openssl rand -base64 36)
export ENCRYPTION_SECRET="$GENERATED_ENCRYPTION_SECRET"
echo "$GENERATED_ENCRYPTION_SECRET" > "$ENCRYPTION_SECRET_FILE"
chmod 600 "$ENCRYPTION_SECRET_FILE"
echo " ENCRYPTION_SECRET generated and saved to $ENCRYPTION_SECRET_FILE"
fi
fi
# === DATABASE INITIALIZATION ===
DB_PATH=$(echo "$DATABASE_URL" | sed 's|^file:||')
if [ ! -f "$DB_PATH" ]; then
echo "Database not found. It will be created and initialized via Drizzle migrations on first app startup..."
touch "$DB_PATH"
else
echo "Database already exists, Drizzle will check for pending migrations on startup..."
fi
# === STARTUP SCRIPTS ===
# Initialize configuration from environment variables
echo "Checking for environment configuration..."
if [ -f "dist/scripts/startup-env-config.js" ]; then
echo "Loading configuration from environment variables..."
${pkgs.bun}/bin/bun dist/scripts/startup-env-config.js && \
echo " Environment configuration loaded successfully" || \
echo " Environment configuration loading completed with warnings"
fi
# Run startup recovery
echo "Running startup recovery..."
if [ -f "dist/scripts/startup-recovery.js" ]; then
${pkgs.bun}/bin/bun dist/scripts/startup-recovery.js --timeout=30000 && \
echo " Startup recovery completed successfully" || \
echo " Startup recovery completed with warnings"
fi
# Run repository status repair
echo "Running repository status repair..."
if [ -f "dist/scripts/repair-mirrored-repos.js" ]; then
${pkgs.bun}/bin/bun dist/scripts/repair-mirrored-repos.js --startup && \
echo " Repository status repair completed successfully" || \
echo " Repository status repair completed with warnings"
fi
# === SIGNAL HANDLING ===
shutdown_handler() {
echo "🛑 Received shutdown signal, forwarding to application..."
if [ ! -z "$APP_PID" ]; then
kill -TERM "$APP_PID" 2>/dev/null || true
wait "$APP_PID" 2>/dev/null || true
fi
exit 0
}
trap 'shutdown_handler' TERM INT HUP
# === START APPLICATION ===
echo "Starting Gitea Mirror..."
echo "Access the web interface at $BETTER_AUTH_URL"
${pkgs.bun}/bin/bun dist/server/entry.mjs &
APP_PID=$!
wait "$APP_PID"
EOF
chmod +x $out/bin/gitea-mirror
# Create database management helper
cat > $out/bin/gitea-mirror-db <<'EOF'
#!/usr/bin/env bash
export DATA_DIR=''${DATA_DIR:-"$HOME/.local/share/gitea-mirror"}
mkdir -p "$DATA_DIR"
cd $out/lib/gitea-mirror
exec ${pkgs.bun}/bin/bun scripts/manage-db.ts "$@"
EOF
chmod +x $out/bin/gitea-mirror-db
'';
meta = with pkgs.lib; {
description = "Self-hosted GitHub to Gitea mirroring service";
homepage = "https://github.com/RayLabsHQ/gitea-mirror";
license = licenses.mit;
maintainers = [ ];
platforms = platforms.linux ++ platforms.darwin;
};
};
in
{
packages = {
default = gitea-mirror;
gitea-mirror = gitea-mirror;
};
# Development shell
devShells.default = pkgs.mkShell {
buildInputs = with pkgs; [
bun
sqlite
openssl
];
shellHook = ''
echo "🚀 Gitea Mirror development environment"
echo ""
echo "Quick start:"
echo " bun install # Install dependencies"
echo " bun run dev # Start development server"
echo " bun run build # Build for production"
echo ""
echo "Database:"
echo " bun run manage-db init # Initialize database"
echo " bun run db:studio # Open Drizzle Studio"
'';
};
# NixOS module
nixosModules.default = { config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.gitea-mirror;
in {
options.services.gitea-mirror = {
enable = mkEnableOption "Gitea Mirror service";
package = mkOption {
type = types.package;
default = self.packages.${system}.default;
description = "The Gitea Mirror package to use";
};
dataDir = mkOption {
type = types.path;
default = "/var/lib/gitea-mirror";
description = "Directory to store data and database";
};
user = mkOption {
type = types.str;
default = "gitea-mirror";
description = "User account under which Gitea Mirror runs";
};
group = mkOption {
type = types.str;
default = "gitea-mirror";
description = "Group under which Gitea Mirror runs";
};
host = mkOption {
type = types.str;
default = "0.0.0.0";
description = "Host to bind to";
};
port = mkOption {
type = types.port;
default = 4321;
description = "Port to listen on";
};
betterAuthUrl = mkOption {
type = types.str;
default = "http://localhost:4321";
description = "Better Auth URL (external URL of the service)";
};
betterAuthTrustedOrigins = mkOption {
type = types.str;
default = "http://localhost:4321";
description = "Comma-separated list of trusted origins for Better Auth";
};
mirrorIssueConcurrency = mkOption {
type = types.int;
default = 3;
description = "Number of concurrent issue mirror operations (set to 1 for perfect ordering)";
};
mirrorPullRequestConcurrency = mkOption {
type = types.int;
default = 5;
description = "Number of concurrent PR mirror operations (set to 1 for perfect ordering)";
};
environmentFile = mkOption {
type = types.nullOr types.path;
default = null;
description = ''
Path to file containing environment variables.
Only needed if you want to set BETTER_AUTH_SECRET or ENCRYPTION_SECRET manually.
Otherwise, secrets will be auto-generated and stored in the data directory.
Example:
BETTER_AUTH_SECRET=your-32-character-secret-here
ENCRYPTION_SECRET=your-encryption-secret-here
'';
};
openFirewall = mkOption {
type = types.bool;
default = false;
description = "Open the firewall for the specified port";
};
};
config = mkIf cfg.enable {
users.users.${cfg.user} = {
isSystemUser = true;
group = cfg.group;
home = cfg.dataDir;
createHome = true;
};
users.groups.${cfg.group} = {};
systemd.services.gitea-mirror = {
description = "Gitea Mirror - GitHub to Gitea mirroring service";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
environment = {
DATA_DIR = cfg.dataDir;
DATABASE_URL = "file:${cfg.dataDir}/gitea-mirror.db";
HOST = cfg.host;
PORT = toString cfg.port;
NODE_ENV = "production";
BETTER_AUTH_URL = cfg.betterAuthUrl;
BETTER_AUTH_TRUSTED_ORIGINS = cfg.betterAuthTrustedOrigins;
PUBLIC_BETTER_AUTH_URL = cfg.betterAuthUrl;
MIRROR_ISSUE_CONCURRENCY = toString cfg.mirrorIssueConcurrency;
MIRROR_PULL_REQUEST_CONCURRENCY = toString cfg.mirrorPullRequestConcurrency;
};
serviceConfig = {
Type = "simple";
User = cfg.user;
Group = cfg.group;
ExecStart = "${cfg.package}/bin/gitea-mirror";
Restart = "always";
RestartSec = "10s";
# Security hardening
NoNewPrivileges = true;
PrivateTmp = true;
ProtectSystem = "strict";
ProtectHome = true;
ReadWritePaths = [ cfg.dataDir ];
# Load environment file if specified (optional)
EnvironmentFile = mkIf (cfg.environmentFile != null) cfg.environmentFile;
# Graceful shutdown
TimeoutStopSec = "30s";
KillMode = "mixed";
KillSignal = "SIGTERM";
};
};
# Health check timer (optional monitoring)
systemd.timers.gitea-mirror-healthcheck = mkIf cfg.enable {
description = "Gitea Mirror health check timer";
wantedBy = [ "timers.target" ];
timerConfig = {
OnBootSec = "5min";
OnUnitActiveSec = "5min";
};
};
systemd.services.gitea-mirror-healthcheck = mkIf cfg.enable {
description = "Gitea Mirror health check";
after = [ "gitea-mirror.service" ];
serviceConfig = {
Type = "oneshot";
ExecStart = "${pkgs.curl}/bin/curl -f http://${cfg.host}:${toString cfg.port}/api/health || true";
User = "nobody";
};
};
networking.firewall = mkIf cfg.openFirewall {
allowedTCPPorts = [ cfg.port ];
};
};
};
}
) // {
# Overlay for adding to nixpkgs
overlays.default = final: prev: {
gitea-mirror = self.packages.${final.system}.default;
};
};
}

View File

@@ -1,7 +1,7 @@
{
"name": "gitea-mirror",
"type": "module",
"version": "3.9.0",
"version": "3.8.11",
"engines": {
"bun": ">=1.2.9"
},

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 216 KiB

View File

@@ -2005,12 +2005,6 @@ export async function mirrorGitHubReleasesToGitea({
.slice(0, releaseLimit)
.sort((a, b) => getReleaseTimestamp(a) - getReleaseTimestamp(b));
console.log(`[Releases] Processing ${releasesToProcess.length} releases in chronological order (oldest to newest)`);
releasesToProcess.forEach((rel, idx) => {
const date = new Date(rel.published_at || rel.created_at);
console.log(`[Releases] ${idx + 1}. ${rel.tag_name} - Originally published: ${date.toISOString()}`);
});
for (const release of releasesToProcess) {
try {
// Check if release already exists
@@ -2021,14 +2015,8 @@ export async function mirrorGitHubReleasesToGitea({
}
).catch(() => null);
// Prepare release body with GitHub original date header
const githubPublishedDate = release.published_at || release.created_at;
const githubDateHeader = githubPublishedDate
? `> 📅 **Originally published on GitHub:** ${new Date(githubPublishedDate).toUTCString()}\n\n`
: '';
const originalReleaseNote = release.body || "";
const releaseNote = githubDateHeader + originalReleaseNote;
const releaseNote = release.body || "";
if (existingReleasesResponse) {
// Update existing release if the changelog/body differs
const existingRelease = existingReleasesResponse.data;
@@ -2051,11 +2039,9 @@ export async function mirrorGitHubReleasesToGitea({
Authorization: `token ${decryptedConfig.giteaConfig.token}`,
}
);
if (originalReleaseNote) {
console.log(`[Releases] Updated changelog for ${release.tag_name} (${originalReleaseNote.length} characters + GitHub date header)`);
} else {
console.log(`[Releases] Updated release ${release.tag_name} with GitHub date header`);
if (releaseNote) {
console.log(`[Releases] Updated changelog for ${release.tag_name} (${releaseNote.length} characters)`);
}
mirroredCount++;
} else {
@@ -2065,11 +2051,9 @@ export async function mirrorGitHubReleasesToGitea({
continue;
}
// Create new release with changelog/body content (includes GitHub date header)
if (originalReleaseNote) {
console.log(`[Releases] Including changelog for ${release.tag_name} (${originalReleaseNote.length} characters + GitHub date header)`);
} else {
console.log(`[Releases] Creating release ${release.tag_name} with GitHub date header (no changelog)`);
// Create new release with changelog/body content
if (releaseNote) {
console.log(`[Releases] Including changelog for ${release.tag_name} (${releaseNote.length} characters)`);
}
const createReleaseResponse = await httpPost(
@@ -2137,14 +2121,8 @@ export async function mirrorGitHubReleasesToGitea({
}
mirroredCount++;
const noteInfo = originalReleaseNote ? ` with ${originalReleaseNote.length} character changelog` : " without changelog";
const noteInfo = releaseNote ? ` with ${releaseNote.length} character changelog` : " without changelog";
console.log(`[Releases] Successfully mirrored release: ${release.tag_name}${noteInfo}`);
// Add delay to ensure proper timestamp ordering in Gitea
// Gitea sorts releases by created_unix DESC, and all releases created in quick succession
// will have nearly identical timestamps. The 1-second delay ensures proper chronological order.
console.log(`[Releases] Waiting 1 second to ensure proper timestamp ordering in Gitea...`);
await new Promise(resolve => setTimeout(resolve, 1000));
} catch (error) {
console.error(`[Releases] Failed to mirror release ${release.tag_name}: ${error instanceof Error ? error.message : String(error)}`);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 330 KiB

After

Width:  |  Height:  |  Size: 338 KiB

View File

@@ -22,7 +22,7 @@ export function Installation() {
steps: [
{
title: "Clone the repository",
command: "git clone https://github.com/RayLabsHQ/gitea-mirror.git && cd gitea-mirror",
command: "git clone https://github.com/RayLabsHQ/gitea-mirror.git\ncd gitea-mirror",
id: "docker-clone"
},
{
@@ -166,4 +166,4 @@ export function Installation() {
</div>
</section>
);
}
}