From 491546a97c092b402835b5b6993657a4e98b1b74 Mon Sep 17 00:00:00 2001 From: Arunavo Ray Date: Fri, 31 Oct 2025 09:00:18 +0530 Subject: [PATCH] added basic nix pack --- .envrc | 1 + .gitignore | 5 + NIX.md | 153 +++++++++++++ README.md | 28 +++ docs/NIX_DEPLOYMENT.md | 475 +++++++++++++++++++++++++++++++++++++++++ flake.nix | 395 ++++++++++++++++++++++++++++++++++ 6 files changed, 1057 insertions(+) create mode 100644 .envrc create mode 100644 NIX.md create mode 100644 docs/NIX_DEPLOYMENT.md create mode 100644 flake.nix diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.gitignore b/.gitignore index 8fe8431..f0b1116 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,8 @@ certs/*.pem certs/*.cer !certs/README.md +# Nix build artifacts +result +result-* +.direnv/ + diff --git a/NIX.md b/NIX.md new file mode 100644 index 0000000..a7ef136 --- /dev/null +++ b/NIX.md @@ -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` diff --git a/README.md b/README.md index b4c970b..3026423 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/NIX_DEPLOYMENT.md b/docs/NIX_DEPLOYMENT.md new file mode 100644 index 0000000..5b68582 --- /dev/null +++ b/docs/NIX_DEPLOYMENT.md @@ -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 diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..fca131c --- /dev/null +++ b/flake.nix @@ -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; + }; + }; +}