Compare commits
7 Commits
nix
...
security-a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fcc92b8f27 | ||
|
|
749ad4a694 | ||
|
|
0f752acae5 | ||
|
|
652bd220c2 | ||
|
|
9f2eaaf04e | ||
|
|
63d3f0e86c | ||
|
|
25e7d234ba |
43
.github/workflows/nix-build.yml
vendored
@@ -1,43 +0,0 @@
|
|||||||
name: Nix Build and Cache
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
tags:
|
|
||||||
- 'v*'
|
|
||||||
pull_request:
|
|
||||||
branches: [main]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
os: [ubuntu-latest, macos-latest]
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
|
|
||||||
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 # Your cache name
|
|
||||||
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
|
||||||
|
|
||||||
- name: Build package
|
|
||||||
run: nix build --print-build-logs
|
|
||||||
|
|
||||||
- name: Check flake
|
|
||||||
run: nix flake check
|
|
||||||
|
|
||||||
- name: Test run (dry run)
|
|
||||||
run: |
|
|
||||||
# Just verify the binary exists and is executable
|
|
||||||
test -x ./result/bin/gitea-mirror
|
|
||||||
./result/bin/gitea-mirror --version || echo "Version check skipped"
|
|
||||||
5
.gitignore
vendored
@@ -32,8 +32,3 @@ certs/*.pem
|
|||||||
certs/*.cer
|
certs/*.cer
|
||||||
!certs/README.md
|
!certs/README.md
|
||||||
|
|
||||||
# Nix build artifacts
|
|
||||||
result
|
|
||||||
result-*
|
|
||||||
.direnv/
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,193 +0,0 @@
|
|||||||
# Nix Distribution - Ready to Use! 🎉
|
|
||||||
|
|
||||||
## Current Status: ✅ WORKS NOW
|
|
||||||
|
|
||||||
Your Nix package is **already distributable**! Users can run it directly from GitHub without any additional setup on your end.
|
|
||||||
|
|
||||||
## How Users Will Use It
|
|
||||||
|
|
||||||
### Simple: Just Run From GitHub
|
|
||||||
|
|
||||||
```bash
|
|
||||||
nix run --extra-experimental-features 'nix-command flakes' github:RayLabsHQ/gitea-mirror
|
|
||||||
```
|
|
||||||
|
|
||||||
That's it! No releases, no CI, no infrastructure needed. It works right now.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## What Happens When They Run This?
|
|
||||||
|
|
||||||
1. **Nix fetches** your repo from GitHub
|
|
||||||
2. **Nix reads** `flake.nix` and `flake.lock`
|
|
||||||
3. **Nix builds** the package on their machine
|
|
||||||
4. **Nix runs** the application
|
|
||||||
5. **Result cached** in `/nix/store` for reuse
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Do You Need CI or Releases?
|
|
||||||
|
|
||||||
### For Basic Usage: **NO**
|
|
||||||
Users can already use it from GitHub. No CI or releases required.
|
|
||||||
|
|
||||||
### For Better UX: **Recommended**
|
|
||||||
Set up binary caching so users don't compile from source.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Next Steps (Optional but Recommended)
|
|
||||||
|
|
||||||
### Option 1: Add Binary Cache (5 minutes)
|
|
||||||
|
|
||||||
**Why:** Users download pre-built binaries instead of compiling (much faster!)
|
|
||||||
|
|
||||||
**How:**
|
|
||||||
1. Create free account at https://cachix.org/
|
|
||||||
2. Create cache named `gitea-mirror`
|
|
||||||
3. Add GitHub secret: `CACHIX_AUTH_TOKEN`
|
|
||||||
4. GitHub Actions workflow already created at `.github/workflows/nix-build.yml`
|
|
||||||
5. Add to your docs:
|
|
||||||
```bash
|
|
||||||
# Users run once
|
|
||||||
cachix use gitea-mirror
|
|
||||||
|
|
||||||
# Then they get fast binary downloads
|
|
||||||
nix run github:RayLabsHQ/gitea-mirror
|
|
||||||
```
|
|
||||||
|
|
||||||
### Option 2: Release Versioning (2 minutes)
|
|
||||||
|
|
||||||
**Why:** Users can pin to specific versions
|
|
||||||
|
|
||||||
**How:**
|
|
||||||
```bash
|
|
||||||
# When ready to release
|
|
||||||
git tag v3.8.11
|
|
||||||
git push origin v3.8.11
|
|
||||||
|
|
||||||
# Users can then pin to this version
|
|
||||||
nix run github:RayLabsHQ/gitea-mirror/v3.8.11
|
|
||||||
```
|
|
||||||
|
|
||||||
No additional CI needed - tags work automatically with flakes!
|
|
||||||
|
|
||||||
### Option 3: Submit to nixpkgs (Long Term)
|
|
||||||
|
|
||||||
**Why:** Maximum discoverability and trust
|
|
||||||
|
|
||||||
**When:** After package is stable and well-tested
|
|
||||||
|
|
||||||
**How:** Submit PR to https://github.com/NixOS/nixpkgs
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Files Created
|
|
||||||
|
|
||||||
### Essential (Already Working)
|
|
||||||
- ✅ `flake.nix` - Package definition
|
|
||||||
- ✅ `flake.lock` - Dependency lock file
|
|
||||||
- ✅ `.envrc` - direnv integration
|
|
||||||
|
|
||||||
### Documentation
|
|
||||||
- ✅ `NIX.md` - Quick reference for users
|
|
||||||
- ✅ `docs/NIX_DEPLOYMENT.md` - Complete deployment guide
|
|
||||||
- ✅ `docs/NIX_DISTRIBUTION.md` - Distribution guide for you (maintainer)
|
|
||||||
- ✅ `README.md` - Updated with Nix instructions
|
|
||||||
|
|
||||||
### CI (Optional, Already Set Up)
|
|
||||||
- ✅ `.github/workflows/nix-build.yml` - Builds + caches to Cachix
|
|
||||||
|
|
||||||
### Updated
|
|
||||||
- ✅ `.gitignore` - Added Nix artifacts
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Comparison: Your Distribution Options
|
|
||||||
|
|
||||||
| Setup | Time | User Experience | What You Need |
|
|
||||||
|-------|------|----------------|---------------|
|
|
||||||
| **Direct GitHub** | 0 min ✅ | Slow (build from source) | Nothing! Works now |
|
|
||||||
| **+ Cachix** | 5 min | Fast (binary download) | Cachix account + token |
|
|
||||||
| **+ Git Tags** | 2 min | Versionable | Just push tags |
|
|
||||||
| **+ nixpkgs** | Hours | Official/Trusted | PR review process |
|
|
||||||
|
|
||||||
**Recommendation:** Start with Direct GitHub (already works!), add Cachix this week for better UX.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Testing Your Distribution
|
|
||||||
|
|
||||||
You can test it right now:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Test direct GitHub usage
|
|
||||||
nix run --extra-experimental-features 'nix-command flakes' github:RayLabsHQ/gitea-mirror
|
|
||||||
|
|
||||||
# Test with specific commit
|
|
||||||
nix run github:RayLabsHQ/gitea-mirror/$(git rev-parse HEAD)
|
|
||||||
|
|
||||||
# Validate flake
|
|
||||||
nix flake check
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## User Documentation Locations
|
|
||||||
|
|
||||||
Users will find instructions in:
|
|
||||||
1. **README.md** - Installation section (already updated)
|
|
||||||
2. **NIX.md** - Quick reference
|
|
||||||
3. **docs/NIX_DEPLOYMENT.md** - Detailed guide
|
|
||||||
|
|
||||||
All docs include the correct commands with experimental features flags.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## When to Release New Versions
|
|
||||||
|
|
||||||
### For Git Tag Releases:
|
|
||||||
```bash
|
|
||||||
# 1. Update version in package.json
|
|
||||||
vim package.json
|
|
||||||
|
|
||||||
# 2. Update version in flake.nix (line 17)
|
|
||||||
vim flake.nix # version = "3.8.12";
|
|
||||||
|
|
||||||
# 3. Commit and tag
|
|
||||||
git add package.json flake.nix
|
|
||||||
git commit -m "chore: bump version to v3.8.12"
|
|
||||||
git tag v3.8.12
|
|
||||||
git push origin main
|
|
||||||
git push origin v3.8.12
|
|
||||||
```
|
|
||||||
|
|
||||||
Users can then use: `nix run github:RayLabsHQ/gitea-mirror/v3.8.12`
|
|
||||||
|
|
||||||
### No Release Needed For:
|
|
||||||
- Bug fixes
|
|
||||||
- Small changes
|
|
||||||
- Continuous updates
|
|
||||||
|
|
||||||
Users can always use latest from main: `nix run github:RayLabsHQ/gitea-mirror`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
**✅ Ready to distribute RIGHT NOW**
|
|
||||||
- Just commit and push your `flake.nix`
|
|
||||||
- Users can run directly from GitHub
|
|
||||||
- No CI, releases, or infrastructure required
|
|
||||||
|
|
||||||
**🚀 Recommended next: Add Cachix (5 minutes)**
|
|
||||||
- Much better user experience
|
|
||||||
- Workflow already created
|
|
||||||
- Free for public projects
|
|
||||||
|
|
||||||
**📦 Optional later: Submit to nixpkgs**
|
|
||||||
- Maximum discoverability
|
|
||||||
- Official Nix repository
|
|
||||||
- Do this once package is stable
|
|
||||||
|
|
||||||
See `docs/NIX_DISTRIBUTION.md` for complete details!
|
|
||||||
189
NIX.md
@@ -1,189 +0,0 @@
|
|||||||
# Nix Deployment Quick Reference
|
|
||||||
|
|
||||||
## TL;DR
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# From GitHub (no clone needed!)
|
|
||||||
nix run --extra-experimental-features 'nix-command flakes' github:RayLabsHQ/gitea-mirror
|
|
||||||
|
|
||||||
# Or from local clone
|
|
||||||
nix run --extra-experimental-features 'nix-command flakes' .#gitea-mirror
|
|
||||||
```
|
|
||||||
|
|
||||||
Secrets auto-generate, database auto-initializes, and the web UI starts at http://localhost:4321.
|
|
||||||
|
|
||||||
**Note:** If you have flakes enabled in your nix config, you can omit `--extra-experimental-features 'nix-command flakes'`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Installation Options
|
|
||||||
|
|
||||||
### 1. Run Without Installing (from GitHub)
|
|
||||||
```bash
|
|
||||||
# Latest version from main branch
|
|
||||||
nix run --extra-experimental-features 'nix-command flakes' github:RayLabsHQ/gitea-mirror
|
|
||||||
|
|
||||||
# Pin to specific version
|
|
||||||
nix run github:RayLabsHQ/gitea-mirror/v3.8.11
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Install to Profile
|
|
||||||
```bash
|
|
||||||
# Install from GitHub
|
|
||||||
nix profile install --extra-experimental-features 'nix-command flakes' github:RayLabsHQ/gitea-mirror
|
|
||||||
|
|
||||||
# Run the installed binary
|
|
||||||
gitea-mirror
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Use Local Clone
|
|
||||||
```bash
|
|
||||||
# Clone and run
|
|
||||||
git clone https://github.com/RayLabsHQ/gitea-mirror.git
|
|
||||||
cd gitea-mirror
|
|
||||||
nix run --extra-experimental-features 'nix-command flakes' .#gitea-mirror
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. 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;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. Development (Local Clone)
|
|
||||||
```bash
|
|
||||||
nix develop --extra-experimental-features 'nix-command flakes'
|
|
||||||
# or
|
|
||||||
direnv allow # Handles experimental features automatically
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Enable Flakes Permanently (Recommended)
|
|
||||||
|
|
||||||
To avoid typing `--extra-experimental-features` every time, add to `~/.config/nix/nix.conf`:
|
|
||||||
```
|
|
||||||
experimental-features = nix-command flakes
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 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 (add --extra-experimental-features 'nix-command flakes' if needed)
|
|
||||||
nix develop # Enter dev shell
|
|
||||||
nix build # Build package
|
|
||||||
nix flake check # Validate flake
|
|
||||||
nix flake update # Update dependencies
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 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
|
|
||||||
|
|
||||||
- **[docs/NIX_DEPLOYMENT.md](docs/NIX_DEPLOYMENT.md)** - Complete deployment guide
|
|
||||||
- NixOS module configuration
|
|
||||||
- Home Manager integration
|
|
||||||
- Production deployment examples
|
|
||||||
- Migration from Docker
|
|
||||||
- Troubleshooting guide
|
|
||||||
|
|
||||||
- **[docs/NIX_DISTRIBUTION.md](docs/NIX_DISTRIBUTION.md)** - Distribution guide for maintainers
|
|
||||||
- How users consume the package
|
|
||||||
- Setting up binary cache (Cachix)
|
|
||||||
- Releasing new versions
|
|
||||||
- Submitting to nixpkgs
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 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`
|
|
||||||
32
README.md
@@ -150,38 +150,6 @@ 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.
|
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 --extra-experimental-features 'nix-command flakes' github:RayLabsHQ/gitea-mirror
|
|
||||||
|
|
||||||
# Or build and run locally
|
|
||||||
nix build --extra-experimental-features 'nix-command flakes'
|
|
||||||
./result/bin/gitea-mirror
|
|
||||||
|
|
||||||
# Or install to profile
|
|
||||||
nix profile install --extra-experimental-features 'nix-command flakes' 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
|
### Manual Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
496
Security-Audit.md
Normal file
@@ -0,0 +1,496 @@
|
|||||||
|
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.
|
||||||
@@ -1,483 +0,0 @@
|
|||||||
# 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 2.4+ installed
|
|
||||||
- For NixOS module: NixOS 23.05+
|
|
||||||
|
|
||||||
### Enable Flakes (Recommended)
|
|
||||||
|
|
||||||
To enable flakes permanently and avoid typing flags, add to `/etc/nix/nix.conf` or `~/.config/nix/nix.conf`:
|
|
||||||
```
|
|
||||||
experimental-features = nix-command flakes
|
|
||||||
```
|
|
||||||
|
|
||||||
**Note:** If you don't enable flakes globally, add `--extra-experimental-features 'nix-command flakes'` to all nix commands shown below.
|
|
||||||
|
|
||||||
## Quick Start (Zero Configuration!)
|
|
||||||
|
|
||||||
### Run Immediately - No Setup Required
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Run directly from the flake (local)
|
|
||||||
nix run --extra-experimental-features 'nix-command flakes' .#gitea-mirror
|
|
||||||
|
|
||||||
# Or from GitHub (once published)
|
|
||||||
nix run --extra-experimental-features 'nix-command flakes' github:RayLabsHQ/gitea-mirror
|
|
||||||
|
|
||||||
# If you have flakes enabled globally, simply:
|
|
||||||
nix run .#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 --extra-experimental-features 'nix-command flakes'
|
|
||||||
|
|
||||||
# Or use direnv for automatic environment loading (handles flags automatically)
|
|
||||||
echo "use flake" > .envrc
|
|
||||||
direnv allow
|
|
||||||
```
|
|
||||||
|
|
||||||
### Build and Install
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Build the package
|
|
||||||
nix build --extra-experimental-features 'nix-command flakes'
|
|
||||||
|
|
||||||
# Run the built package
|
|
||||||
./result/bin/gitea-mirror
|
|
||||||
|
|
||||||
# Install to your profile
|
|
||||||
nix profile install --extra-experimental-features 'nix-command flakes' .#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 --extra-experimental-features 'nix-command flakes' .#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 --extra-experimental-features 'nix-command flakes'
|
|
||||||
|
|
||||||
# Rebuild
|
|
||||||
nix build --extra-experimental-features 'nix-command flakes'
|
|
||||||
|
|
||||||
# Or update profile
|
|
||||||
nix profile upgrade --extra-experimental-features 'nix-command flakes' gitea-mirror
|
|
||||||
```
|
|
||||||
|
|
||||||
### NixOS
|
|
||||||
```bash
|
|
||||||
# Update input
|
|
||||||
sudo nix flake lock --update-input gitea-mirror --extra-experimental-features 'nix-command flakes'
|
|
||||||
|
|
||||||
# Rebuild system
|
|
||||||
sudo nixos-rebuild switch --flake .#your-hostname
|
|
||||||
```
|
|
||||||
|
|
||||||
## 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
|
|
||||||
# Note: GitHub Actions runner usually has flakes enabled by install-nix-action
|
|
||||||
```
|
|
||||||
|
|
||||||
## 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
|
|
||||||
@@ -1,352 +0,0 @@
|
|||||||
# Nix Package Distribution Guide
|
|
||||||
|
|
||||||
This guide explains how Gitea Mirror is distributed via Nix and how users can consume it.
|
|
||||||
|
|
||||||
## Distribution Methods
|
|
||||||
|
|
||||||
### Method 1: Direct GitHub Usage (Zero Infrastructure)
|
|
||||||
|
|
||||||
**No CI, releases, or setup needed!** Users can consume directly from GitHub:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Latest from main branch
|
|
||||||
nix run --extra-experimental-features 'nix-command flakes' github:RayLabsHQ/gitea-mirror
|
|
||||||
|
|
||||||
# Pin to specific commit
|
|
||||||
nix run github:RayLabsHQ/gitea-mirror/abc123def
|
|
||||||
|
|
||||||
# Pin to git tag
|
|
||||||
nix run github:RayLabsHQ/gitea-mirror/v3.8.11
|
|
||||||
```
|
|
||||||
|
|
||||||
**How it works:**
|
|
||||||
1. Nix fetches the repository from GitHub
|
|
||||||
2. Nix reads `flake.nix` and `flake.lock`
|
|
||||||
3. Nix builds the package locally on the user's machine
|
|
||||||
4. Package is cached in `/nix/store` for reuse
|
|
||||||
|
|
||||||
**Pros:**
|
|
||||||
- Zero infrastructure needed
|
|
||||||
- Works immediately after pushing code
|
|
||||||
- Users always get reproducible builds
|
|
||||||
|
|
||||||
**Cons:**
|
|
||||||
- Users must build from source (slower first time)
|
|
||||||
- Requires build dependencies (Bun, etc.)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Method 2: Binary Cache (Recommended)
|
|
||||||
|
|
||||||
Pre-build packages and cache them so users download binaries instead of building:
|
|
||||||
|
|
||||||
#### Setup: Cachix (Free for Public Projects)
|
|
||||||
|
|
||||||
1. **Create account:** https://cachix.org/
|
|
||||||
2. **Create cache:** `gitea-mirror` (public)
|
|
||||||
3. **Add secret to GitHub:** `Settings → Secrets → CACHIX_AUTH_TOKEN`
|
|
||||||
4. **GitHub Actions builds automatically** (see `.github/workflows/nix-build.yml`)
|
|
||||||
|
|
||||||
#### User Experience:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# First time: Configure cache
|
|
||||||
cachix use gitea-mirror
|
|
||||||
|
|
||||||
# Or add to nix.conf:
|
|
||||||
# substituters = https://cache.nixos.org https://gitea-mirror.cachix.org
|
|
||||||
# trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= gitea-mirror.cachix.org-1:YOUR_KEY_HERE
|
|
||||||
|
|
||||||
# Then use normally - downloads pre-built binaries!
|
|
||||||
nix run github:RayLabsHQ/gitea-mirror
|
|
||||||
```
|
|
||||||
|
|
||||||
**Pros:**
|
|
||||||
- Fast installation (no compilation)
|
|
||||||
- Reduced bandwidth/CPU for users
|
|
||||||
- Professional experience
|
|
||||||
|
|
||||||
**Cons:**
|
|
||||||
- Requires Cachix account (free for public)
|
|
||||||
- Requires CI setup
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Method 3: nixpkgs Submission (Official Distribution)
|
|
||||||
|
|
||||||
Submit to the official Nix package repository for maximum visibility.
|
|
||||||
|
|
||||||
#### Process:
|
|
||||||
|
|
||||||
1. **Prepare package** (already done with `flake.nix`)
|
|
||||||
2. **Test thoroughly**
|
|
||||||
3. **Submit PR to nixpkgs:** https://github.com/NixOS/nixpkgs
|
|
||||||
|
|
||||||
#### User Experience:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# After acceptance into nixpkgs
|
|
||||||
nix run nixpkgs#gitea-mirror
|
|
||||||
|
|
||||||
# NixOS configuration
|
|
||||||
environment.systemPackages = [ pkgs.gitea-mirror ];
|
|
||||||
```
|
|
||||||
|
|
||||||
**Pros:**
|
|
||||||
- Maximum discoverability (official repo)
|
|
||||||
- Trusted by Nix community
|
|
||||||
- Included in NixOS search
|
|
||||||
- Binary caching by cache.nixos.org
|
|
||||||
|
|
||||||
**Cons:**
|
|
||||||
- Submission/review process
|
|
||||||
- Must follow nixpkgs guidelines
|
|
||||||
- Updates require PRs
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Current Distribution Strategy
|
|
||||||
|
|
||||||
### Phase 1: Direct GitHub (Immediate) ✅
|
|
||||||
|
|
||||||
Already working! Users can:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
nix run github:RayLabsHQ/gitea-mirror
|
|
||||||
```
|
|
||||||
|
|
||||||
### Phase 2: Binary Cache (Recommended Next)
|
|
||||||
|
|
||||||
Set up Cachix for faster installs:
|
|
||||||
|
|
||||||
1. Create Cachix cache
|
|
||||||
2. Add `CACHIX_AUTH_TOKEN` secret to GitHub
|
|
||||||
3. Workflow already created in `.github/workflows/nix-build.yml`
|
|
||||||
4. Add instructions to docs
|
|
||||||
|
|
||||||
### Phase 3: Version Releases (Optional)
|
|
||||||
|
|
||||||
Tag releases for version pinning:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git tag v3.8.11
|
|
||||||
git push origin v3.8.11
|
|
||||||
|
|
||||||
# Users can then pin:
|
|
||||||
nix run github:RayLabsHQ/gitea-mirror/v3.8.11
|
|
||||||
```
|
|
||||||
|
|
||||||
### Phase 4: nixpkgs Submission (Long Term)
|
|
||||||
|
|
||||||
Once package is stable and well-tested, submit to nixpkgs.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## User Documentation
|
|
||||||
|
|
||||||
### For Users: How to Install
|
|
||||||
|
|
||||||
Add this to your `docs/NIX_DEPLOYMENT.md`:
|
|
||||||
|
|
||||||
#### Option 1: Direct Install (No Configuration)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Run immediately
|
|
||||||
nix run --extra-experimental-features 'nix-command flakes' github:RayLabsHQ/gitea-mirror
|
|
||||||
|
|
||||||
# Install to profile
|
|
||||||
nix profile install --extra-experimental-features 'nix-command flakes' github:RayLabsHQ/gitea-mirror
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Option 2: With Binary Cache (Faster)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# One-time setup
|
|
||||||
cachix use gitea-mirror
|
|
||||||
|
|
||||||
# Then install (downloads pre-built binary)
|
|
||||||
nix profile install github:RayLabsHQ/gitea-mirror
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Option 3: Pin to Specific Version
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Pin to git tag
|
|
||||||
nix run github:RayLabsHQ/gitea-mirror/v3.8.11
|
|
||||||
|
|
||||||
# Pin to commit
|
|
||||||
nix run github:RayLabsHQ/gitea-mirror/abc123def
|
|
||||||
|
|
||||||
# Lock in flake.nix
|
|
||||||
inputs.gitea-mirror.url = "github:RayLabsHQ/gitea-mirror/v3.8.11";
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Option 4: NixOS Configuration
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{
|
|
||||||
inputs = {
|
|
||||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
|
||||||
gitea-mirror.url = "github:RayLabsHQ/gitea-mirror";
|
|
||||||
# Or pin to version:
|
|
||||||
# gitea-mirror.url = "github:RayLabsHQ/gitea-mirror/v3.8.11";
|
|
||||||
};
|
|
||||||
|
|
||||||
outputs = { nixpkgs, gitea-mirror, ... }: {
|
|
||||||
nixosConfigurations.your-host = nixpkgs.lib.nixosSystem {
|
|
||||||
modules = [
|
|
||||||
gitea-mirror.nixosModules.default
|
|
||||||
{
|
|
||||||
services.gitea-mirror = {
|
|
||||||
enable = true;
|
|
||||||
betterAuthUrl = "https://mirror.example.com";
|
|
||||||
openFirewall = true;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Maintaining the Distribution
|
|
||||||
|
|
||||||
### Releasing New Versions
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Update version in package.json
|
|
||||||
vim package.json # Update version field
|
|
||||||
|
|
||||||
# 2. Update flake.nix version (line 17)
|
|
||||||
vim flake.nix # Update version = "X.Y.Z";
|
|
||||||
|
|
||||||
# 3. Commit changes
|
|
||||||
git add package.json flake.nix
|
|
||||||
git commit -m "chore: bump version to vX.Y.Z"
|
|
||||||
|
|
||||||
# 4. Create git tag
|
|
||||||
git tag vX.Y.Z
|
|
||||||
git push origin main
|
|
||||||
git push origin vX.Y.Z
|
|
||||||
|
|
||||||
# 5. GitHub Actions builds and caches automatically
|
|
||||||
```
|
|
||||||
|
|
||||||
Users can then pin to the new version:
|
|
||||||
```bash
|
|
||||||
nix run github:RayLabsHQ/gitea-mirror/vX.Y.Z
|
|
||||||
```
|
|
||||||
|
|
||||||
### Updating Flake Lock
|
|
||||||
|
|
||||||
The `flake.lock` file pins all dependencies. Update it periodically:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Update all inputs
|
|
||||||
nix flake update
|
|
||||||
|
|
||||||
# Update specific input
|
|
||||||
nix flake lock --update-input nixpkgs
|
|
||||||
|
|
||||||
# Test after update
|
|
||||||
nix build
|
|
||||||
nix flake check
|
|
||||||
|
|
||||||
# Commit the updated lock file
|
|
||||||
git add flake.lock
|
|
||||||
git commit -m "chore: update flake dependencies"
|
|
||||||
git push
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Troubleshooting Distribution Issues
|
|
||||||
|
|
||||||
### Users Report Build Failures
|
|
||||||
|
|
||||||
1. **Check GitHub Actions:** Ensure CI is passing
|
|
||||||
2. **Test locally:** `nix flake check`
|
|
||||||
3. **Check flake.lock:** May need update if dependencies changed
|
|
||||||
|
|
||||||
### Cachix Not Working
|
|
||||||
|
|
||||||
1. **Verify cache exists:** https://gitea-mirror.cachix.org
|
|
||||||
2. **Check GitHub secret:** `CACHIX_AUTH_TOKEN` is set
|
|
||||||
3. **Review workflow logs:** Ensure build + push succeeded
|
|
||||||
|
|
||||||
### Version Pinning Not Working
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Verify tag exists
|
|
||||||
git tag -l
|
|
||||||
|
|
||||||
# Ensure tag is pushed
|
|
||||||
git ls-remote --tags origin
|
|
||||||
|
|
||||||
# Test specific tag
|
|
||||||
nix run github:RayLabsHQ/gitea-mirror/v3.8.11
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Advanced: Custom Binary Cache
|
|
||||||
|
|
||||||
If you prefer self-hosting instead of Cachix:
|
|
||||||
|
|
||||||
### Option 1: S3-Compatible Storage
|
|
||||||
|
|
||||||
```nix
|
|
||||||
# Generate signing key
|
|
||||||
nix-store --generate-binary-cache-key cache.example.com cache-priv-key.pem cache-pub-key.pem
|
|
||||||
|
|
||||||
# Push to S3
|
|
||||||
nix copy --to s3://my-nix-cache?region=us-east-1 $(nix-build)
|
|
||||||
```
|
|
||||||
|
|
||||||
Users configure:
|
|
||||||
```nix
|
|
||||||
substituters = https://my-bucket.s3.amazonaws.com/nix-cache
|
|
||||||
trusted-public-keys = cache.example.com:BASE64_PUBLIC_KEY
|
|
||||||
```
|
|
||||||
|
|
||||||
### Option 2: Self-Hosted Nix Store
|
|
||||||
|
|
||||||
Run `nix-serve` on your server:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# On server
|
|
||||||
nix-serve -p 8080
|
|
||||||
|
|
||||||
# Behind nginx/caddy
|
|
||||||
proxy_pass http://localhost:8080;
|
|
||||||
```
|
|
||||||
|
|
||||||
Users configure:
|
|
||||||
```nix
|
|
||||||
substituters = https://cache.example.com
|
|
||||||
trusted-public-keys = YOUR_KEY
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Comparison: Distribution Methods
|
|
||||||
|
|
||||||
| Method | Setup Time | User Speed | Cost | Discoverability |
|
|
||||||
|--------|-----------|------------|------|-----------------|
|
|
||||||
| Direct GitHub | 0 min | Slow (build) | Free | Low |
|
|
||||||
| Cachix | 5 min | Fast (binary) | Free (public) | Medium |
|
|
||||||
| nixpkgs | Hours/days | Fast (binary) | Free | High |
|
|
||||||
| Self-hosted | 30+ min | Fast (binary) | Server cost | Low |
|
|
||||||
|
|
||||||
**Recommendation:** Start with **Direct GitHub** (works now), add **Cachix** for better UX (5 min), consider **nixpkgs** later for maximum reach.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Resources
|
|
||||||
|
|
||||||
- [Nix Flakes Documentation](https://nixos.wiki/wiki/Flakes)
|
|
||||||
- [Cachix Documentation](https://docs.cachix.org/)
|
|
||||||
- [nixpkgs Contributing Guide](https://github.com/NixOS/nixpkgs/blob/master/CONTRIBUTING.md)
|
|
||||||
- [Nix Binary Cache Setup](https://nixos.org/manual/nix/stable/package-management/binary-cache-substituter.html)
|
|
||||||
61
flake.lock
generated
@@ -1,61 +0,0 @@
|
|||||||
{
|
|
||||||
"nodes": {
|
|
||||||
"flake-utils": {
|
|
||||||
"inputs": {
|
|
||||||
"systems": "systems"
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1731533236,
|
|
||||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nixpkgs": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1761672384,
|
|
||||||
"narHash": "sha256-o9KF3DJL7g7iYMZq9SWgfS1BFlNbsm6xplRjVlOCkXI=",
|
|
||||||
"owner": "NixOS",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"rev": "08dacfca559e1d7da38f3cf05f1f45ee9bfd213c",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "NixOS",
|
|
||||||
"ref": "nixos-unstable",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": {
|
|
||||||
"inputs": {
|
|
||||||
"flake-utils": "flake-utils",
|
|
||||||
"nixpkgs": "nixpkgs"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"systems": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1681028828,
|
|
||||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": "root",
|
|
||||||
"version": 7
|
|
||||||
}
|
|
||||||
395
flake.nix
@@ -1,395 +0,0 @@
|
|||||||
{
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "gitea-mirror",
|
"name": "gitea-mirror",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "3.8.11",
|
"version": "3.9.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"bun": ">=1.2.9"
|
"bun": ">=1.2.9"
|
||||||
},
|
},
|
||||||
|
|||||||
BIN
public/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
3
public/favicon.svg
Normal file
|
After Width: | Height: | Size: 216 KiB |
@@ -2005,6 +2005,12 @@ export async function mirrorGitHubReleasesToGitea({
|
|||||||
.slice(0, releaseLimit)
|
.slice(0, releaseLimit)
|
||||||
.sort((a, b) => getReleaseTimestamp(a) - getReleaseTimestamp(b));
|
.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) {
|
for (const release of releasesToProcess) {
|
||||||
try {
|
try {
|
||||||
// Check if release already exists
|
// Check if release already exists
|
||||||
@@ -2015,8 +2021,14 @@ export async function mirrorGitHubReleasesToGitea({
|
|||||||
}
|
}
|
||||||
).catch(() => null);
|
).catch(() => null);
|
||||||
|
|
||||||
const releaseNote = release.body || "";
|
// 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;
|
||||||
|
|
||||||
if (existingReleasesResponse) {
|
if (existingReleasesResponse) {
|
||||||
// Update existing release if the changelog/body differs
|
// Update existing release if the changelog/body differs
|
||||||
const existingRelease = existingReleasesResponse.data;
|
const existingRelease = existingReleasesResponse.data;
|
||||||
@@ -2039,9 +2051,11 @@ export async function mirrorGitHubReleasesToGitea({
|
|||||||
Authorization: `token ${decryptedConfig.giteaConfig.token}`,
|
Authorization: `token ${decryptedConfig.giteaConfig.token}`,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (releaseNote) {
|
if (originalReleaseNote) {
|
||||||
console.log(`[Releases] Updated changelog for ${release.tag_name} (${releaseNote.length} characters)`);
|
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`);
|
||||||
}
|
}
|
||||||
mirroredCount++;
|
mirroredCount++;
|
||||||
} else {
|
} else {
|
||||||
@@ -2051,9 +2065,11 @@ export async function mirrorGitHubReleasesToGitea({
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new release with changelog/body content
|
// Create new release with changelog/body content (includes GitHub date header)
|
||||||
if (releaseNote) {
|
if (originalReleaseNote) {
|
||||||
console.log(`[Releases] Including changelog for ${release.tag_name} (${releaseNote.length} characters)`);
|
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)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const createReleaseResponse = await httpPost(
|
const createReleaseResponse = await httpPost(
|
||||||
@@ -2121,8 +2137,14 @@ export async function mirrorGitHubReleasesToGitea({
|
|||||||
}
|
}
|
||||||
|
|
||||||
mirroredCount++;
|
mirroredCount++;
|
||||||
const noteInfo = releaseNote ? ` with ${releaseNote.length} character changelog` : " without changelog";
|
const noteInfo = originalReleaseNote ? ` with ${originalReleaseNote.length} character changelog` : " without changelog";
|
||||||
console.log(`[Releases] Successfully mirrored release: ${release.tag_name}${noteInfo}`);
|
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) {
|
} catch (error) {
|
||||||
console.error(`[Releases] Failed to mirror release ${release.tag_name}: ${error instanceof Error ? error.message : String(error)}`);
|
console.error(`[Releases] Failed to mirror release ${release.tag_name}: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
www/public/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
3
www/public/favicon.svg
Normal file
|
After Width: | Height: | Size: 216 KiB |
|
Before Width: | Height: | Size: 338 KiB After Width: | Height: | Size: 330 KiB |
@@ -22,7 +22,7 @@ export function Installation() {
|
|||||||
steps: [
|
steps: [
|
||||||
{
|
{
|
||||||
title: "Clone the repository",
|
title: "Clone the repository",
|
||||||
command: "git clone https://github.com/RayLabsHQ/gitea-mirror.git\ncd gitea-mirror",
|
command: "git clone https://github.com/RayLabsHQ/gitea-mirror.git && cd gitea-mirror",
|
||||||
id: "docker-clone"
|
id: "docker-clone"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -166,4 +166,4 @@ export function Installation() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||