diff --git a/hosts/fw/modules/dnsmasq.nix b/hosts/fw/modules/dnsmasq.nix index 12ce8e1..7461ae6 100644 --- a/hosts/fw/modules/dnsmasq.nix +++ b/hosts/fw/modules/dnsmasq.nix @@ -102,6 +102,7 @@ "/snapcast.cloonar.com/${config.networkPrefix}.97.21" "/lms.cloonar.com/${config.networkPrefix}.97.21" "/git.cloonar.com/${config.networkPrefix}.97.50" + "/forgejo.cloonar.com/${config.networkPrefix}.97.55" "/feeds.cloonar.com/188.34.191.144" "/nukibridge1a753f72.cloonar.smart/${config.networkPrefix}.100.112" "/allywatch.cloonar.com/${config.networkPrefix}.97.5" diff --git a/hosts/fw/modules/forgejo.nix b/hosts/fw/modules/forgejo.nix index ec6a237..c6fead7 100644 --- a/hosts/fw/modules/forgejo.nix +++ b/hosts/fw/modules/forgejo.nix @@ -20,6 +20,9 @@ in users.groups.forgejo = group; # Reuse the existing git.cloonar.com ACME cert from gitea.nix + security.acme.certs."forgejo.cloonar.com" = { + group = "nginx"; + }; containers.forgejo = { autoStart = false; # Don't start until migration is complete @@ -27,14 +30,15 @@ in privateNetwork = true; hostBridge = "server"; hostAddress = "${networkPrefix}.97.1"; - localAddress = "${networkPrefix}.97.51/24"; # Different from gitea's .50 + localAddress = "${networkPrefix}.97.55/24"; # Different from gitea's .50 bindMounts = { "/var/lib/forgejo" = { hostPath = "/var/lib/forgejo/"; isReadOnly = false; }; "/var/lib/acme/forgejo/" = { - hostPath = config.security.acme.certs.${domain}.directory; + # hostPath = config.security.acme.certs.${domain}.directory; + hostPath = config.security.acme.certs."forgejo.cloonar.com".directory; isReadOnly = true; }; "/run/secrets/forgejo-mailer-password" = { diff --git a/hosts/fw/modules/web/proxies.nix b/hosts/fw/modules/web/proxies.nix index 5b33e43..5cc42d5 100644 --- a/hosts/fw/modules/web/proxies.nix +++ b/hosts/fw/modules/web/proxies.nix @@ -7,6 +7,15 @@ proxyPass = "https://git.cloonar.com/"; }; }; + services.nginx.virtualHosts."forgejo.cloonar.com" = { + forceSSL = true; + enableACME = true; + acmeRoot = null; + locations."/" = { + proxyPass = "http://${config.networkPrefix}.97.55:3001/"; + proxyWebsockets = true; + }; + }; services.nginx.virtualHosts."foundry-vtt.cloonar.com" = { forceSSL = true; enableACME = true; diff --git a/scripts/migrate-gitea-to-forgejo.env.example b/scripts/migrate-gitea-to-forgejo.env.example new file mode 100644 index 0000000..7695ad0 --- /dev/null +++ b/scripts/migrate-gitea-to-forgejo.env.example @@ -0,0 +1,19 @@ +# Gitea to Forgejo Migration - Environment Configuration +# +# Copy this file to migrate-gitea-to-forgejo.env and adjust values. +# Then run: ./scripts/migrate-gitea-to-forgejo.sh +# +# IMPORTANT: Ensure Gitea is stopped before running migration. + +# Source (Gitea) - READ ONLY, never modified +# This is the original Gitea data directory +SOURCE_DATA=/var/lib/gitea + +# Target (Forgejo) - where data will be copied +# Must be on a filesystem with enough space (1.2x source size) +TARGET_DATA=/var/lib/forgejo + +# User/group for target files +# These should match your Forgejo service user +TARGET_USER=forgejo +TARGET_GROUP=forgejo diff --git a/scripts/migrate-gitea-to-forgejo.sh b/scripts/migrate-gitea-to-forgejo.sh new file mode 100755 index 0000000..aef9354 --- /dev/null +++ b/scripts/migrate-gitea-to-forgejo.sh @@ -0,0 +1,497 @@ +#!/usr/bin/env bash +# +# Gitea 1.25.4 to Forgejo Migration Script +# +# This script copies data from Gitea to Forgejo and rolls back the database +# schema from version 322/323 to 304, allowing Forgejo to run its own migrations. +# +# IMPORTANT: This script NEVER modifies source data. All operations work on copies, +# so the original Gitea instance can be restarted as a rollback. +# +# Usage: +# 1. Copy migrate-gitea-to-forgejo.env.example to migrate-gitea-to-forgejo.env +# 2. Edit the .env file with your paths +# 3. Stop Gitea +# 4. Run: ./scripts/migrate-gitea-to-forgejo.sh +# 5. Update NixOS config and deploy +# +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ENV_FILE="${SCRIPT_DIR}/migrate-gitea-to-forgejo.env" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +log_info() { echo -e "${BLUE}[INFO]${NC} $*"; } +log_success() { echo -e "${GREEN}[OK]${NC} $*"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } + +# Load environment file +if [[ ! -f "$ENV_FILE" ]]; then + log_error "Environment file not found: $ENV_FILE" + log_info "Copy migrate-gitea-to-forgejo.env.example to migrate-gitea-to-forgejo.env and configure it." + exit 1 +fi + +# shellcheck source=/dev/null +source "$ENV_FILE" + +# Verify required variables +: "${SOURCE_DATA:?SOURCE_DATA must be set in $ENV_FILE}" +: "${TARGET_DATA:?TARGET_DATA must be set in $ENV_FILE}" +: "${TARGET_USER:?TARGET_USER must be set in $ENV_FILE}" +: "${TARGET_GROUP:?TARGET_GROUP must be set in $ENV_FILE}" + +echo "========================================" +echo "Gitea to Forgejo Migration Script" +echo "========================================" +echo "" +echo "Source: $SOURCE_DATA (read-only)" +echo "Target: $TARGET_DATA" +echo "User: $TARGET_USER:$TARGET_GROUP" +echo "" + +# ============================================ +# PHASE 1: Pre-flight Checks +# ============================================ +log_info "Phase 1: Pre-flight checks..." + +# Check if running as root (needed for chown) +if [[ $EUID -ne 0 ]]; then + log_error "This script must be run as root (for chown operations)" + exit 1 +fi + +# Verify SQLite version >= 3.35 (required for DROP COLUMN) +if ! command -v sqlite3 &> /dev/null; then + log_error "sqlite3 command not found. Please install SQLite." + exit 1 +fi + +sqlite_version=$(sqlite3 --version | cut -d' ' -f1) +sqlite_major=$(echo "$sqlite_version" | cut -d'.' -f1) +sqlite_minor=$(echo "$sqlite_version" | cut -d'.' -f2) +if [[ "$sqlite_major" -lt 3 ]] || { [[ "$sqlite_major" -eq 3 ]] && [[ "$sqlite_minor" -lt 35 ]]; }; then + log_error "SQLite $sqlite_version is too old. Need 3.35+ for DROP COLUMN support." + exit 1 +fi +log_success "SQLite version: $sqlite_version" + +# Verify rsync is available (needed for incremental copying) +if ! command -v rsync &> /dev/null; then + log_error "rsync command not found. Please install rsync." + exit 1 +fi +log_success "rsync available" + +# Verify source exists +if [[ ! -d "$SOURCE_DATA" ]]; then + log_error "Source directory not found: $SOURCE_DATA" + exit 1 +fi +log_success "Source directory exists" + +# Find source database (could be gitea.db or forgejo.db depending on setup) +SOURCE_DB="" +if [[ -f "$SOURCE_DATA/data/gitea.db" ]]; then + SOURCE_DB="$SOURCE_DATA/data/gitea.db" +elif [[ -f "$SOURCE_DATA/gitea.db" ]]; then + SOURCE_DB="$SOURCE_DATA/gitea.db" +else + log_error "Source database not found in $SOURCE_DATA/data/ or $SOURCE_DATA/" + exit 1 +fi +log_success "Source database found: $SOURCE_DB" + +# Verify source app.ini exists +SOURCE_INI="" +if [[ -f "$SOURCE_DATA/custom/conf/app.ini" ]]; then + SOURCE_INI="$SOURCE_DATA/custom/conf/app.ini" +elif [[ -f "$SOURCE_DATA/conf/app.ini" ]]; then + SOURCE_INI="$SOURCE_DATA/conf/app.ini" +else + log_error "Source app.ini not found in $SOURCE_DATA/custom/conf/ or $SOURCE_DATA/conf/" + exit 1 +fi +log_success "Source app.ini found: $SOURCE_INI" + +# Check disk space (need 1.2x source size) +source_size=$(du -sb "$SOURCE_DATA" | cut -f1) +required=$((source_size * 12 / 10)) +target_parent=$(dirname "$TARGET_DATA") +mkdir -p "$target_parent" +available=$(df --output=avail -B1 "$target_parent" | tail -1) +if [[ "$available" -lt "$required" ]]; then + log_error "Not enough disk space. Need $(numfmt --to=iec $required), have $(numfmt --to=iec $available)" + exit 1 +fi +log_success "Disk space OK: need $(numfmt --to=iec $required), have $(numfmt --to=iec $available)" + +# Warn if target exists (rsync will sync incrementally) +if [[ -d "$TARGET_DATA" ]]; then + log_warn "Target directory exists: $TARGET_DATA" + log_info "rsync will perform incremental sync (only copying changed files)" + read -p "Continue with incremental sync? (y/N) " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + log_error "Aborted by user" + exit 1 + fi +fi + +# ============================================ +# PHASE 2: Copy All Data +# ============================================ +log_info "Phase 2: Copying data..." + +mkdir -p "$TARGET_DATA/data" +mkdir -p "$TARGET_DATA/custom/conf" + +# Copy database +log_info "Copying database..." +rsync -a --info=progress2 "$SOURCE_DB" "$TARGET_DATA/data/forgejo.db" +log_success "Database copied" + +# Copy all data directories (preserve attributes, sync incrementally) +for dir in repositories avatars attachments packages lfs custom queues indexers; do + if [[ -d "$SOURCE_DATA/$dir" ]]; then + log_info "Syncing $dir..." + mkdir -p "$TARGET_DATA/$dir" + rsync -a --delete --info=progress2 "$SOURCE_DATA/$dir/" "$TARGET_DATA/$dir/" + log_success "Synced $dir" + fi +done + +# Also check data/ subdirectory structure +for dir in repositories avatars attachments packages lfs; do + if [[ -d "$SOURCE_DATA/data/$dir" ]]; then + log_info "Syncing data/$dir..." + mkdir -p "$TARGET_DATA/data/$dir" + rsync -a --delete --info=progress2 "$SOURCE_DATA/data/$dir/" "$TARGET_DATA/data/$dir/" + log_success "Synced data/$dir" + fi +done + +# ============================================ +# PHASE 3: Database Schema Rollback +# ============================================ +log_info "Phase 3: Rolling back database schema..." + +TARGET_DB="$TARGET_DATA/data/forgejo.db" + +# Show current schema version +current_version=$(sqlite3 "$TARGET_DB" "SELECT version FROM version WHERE id=1;") +log_info "Current Gitea schema version: $current_version" +log_info "Target version: 304" + +# Create rollback SQL script +ROLLBACK_SQL=$(mktemp) +cat > "$ROLLBACK_SQL" << 'ROLLBACK_EOF' +-- ================================================================ +-- Gitea 1.25.4 to Forgejo Rollback Script +-- Rolls back migrations 305-322 to allow Forgejo to migrate cleanly +-- ================================================================ + +-- Enable foreign keys check after we're done +PRAGMA foreign_keys = OFF; + +-- ============================================ +-- MIGRATION 305: Drop repo_license table +-- ============================================ +DROP TABLE IF EXISTS repo_license; + +-- ============================================ +-- MIGRATION 308 & 317: Drop action table indices +-- (These are the main conflict source) +-- ============================================ +DROP INDEX IF EXISTS IDX_action_r_u_d; +DROP INDEX IF EXISTS IDX_action_au_r_c_u_d; +DROP INDEX IF EXISTS IDX_action_c_u_d; +DROP INDEX IF EXISTS IDX_action_c_u; +DROP INDEX IF EXISTS IDX_action_au_c_u; +-- Alternative naming conventions +DROP INDEX IF EXISTS UQE_action_r_u_d; +DROP INDEX IF EXISTS UQE_action_au_r_c_u_d; +DROP INDEX IF EXISTS UQE_action_c_u_d; +DROP INDEX IF EXISTS UQE_action_c_u; +DROP INDEX IF EXISTS UQE_action_au_c_u; + +-- ============================================ +-- MIGRATION 309: Drop notification table indices +-- ============================================ +DROP INDEX IF EXISTS IDX_notification_u_s_uu; +DROP INDEX IF EXISTS IDX_notification_user_id; +DROP INDEX IF EXISTS IDX_notification_repo_id; +DROP INDEX IF EXISTS IDX_notification_status; +DROP INDEX IF EXISTS IDX_notification_source; +DROP INDEX IF EXISTS IDX_notification_issue_id; +DROP INDEX IF EXISTS IDX_notification_commit_id; +DROP INDEX IF EXISTS IDX_notification_updated_by; +DROP INDEX IF EXISTS UQE_notification_u_s_uu; + +-- ============================================ +-- MIGRATION 313: Drop issue_pin table +-- (pin_order restoration handled separately) +-- ============================================ +DROP TABLE IF EXISTS issue_pin; + +-- ============================================ +-- MIGRATION 306: Drop protected_branch column +-- ============================================ +ALTER TABLE protected_branch DROP COLUMN IF EXISTS block_admin_merge_override; + +-- ============================================ +-- MIGRATION 310: Drop protected_branch column +-- ============================================ +ALTER TABLE protected_branch DROP COLUMN IF EXISTS priority; + +-- ============================================ +-- MIGRATION 311: Drop issue column +-- ============================================ +ALTER TABLE issue DROP COLUMN IF EXISTS time_estimate; + +-- ============================================ +-- MIGRATION 312: Drop pull_auto_merge column +-- ============================================ +ALTER TABLE pull_auto_merge DROP COLUMN IF EXISTS delete_branch_after_merge; + +-- ============================================ +-- MIGRATION 315: Drop action_runner column +-- ============================================ +ALTER TABLE action_runner DROP COLUMN IF EXISTS ephemeral; + +-- ============================================ +-- MIGRATION 316: Drop description columns +-- ============================================ +ALTER TABLE secret DROP COLUMN IF EXISTS description; +ALTER TABLE action_variable DROP COLUMN IF EXISTS description; + +-- ============================================ +-- MIGRATION 318: Drop repo_unit column +-- ============================================ +ALTER TABLE repo_unit DROP COLUMN IF EXISTS anonymous_access_mode; + +-- ============================================ +-- MIGRATION 319: Drop label column +-- ============================================ +ALTER TABLE label DROP COLUMN IF EXISTS exclusive_order; + +-- ============================================ +-- MIGRATION 320: Drop login_source column +-- ============================================ +ALTER TABLE login_source DROP COLUMN IF EXISTS two_factor_policy; + +-- ============================================ +-- SET VERSION TO 304 +-- ============================================ +UPDATE version SET version = 304 WHERE id = 1; + +PRAGMA foreign_keys = ON; +ROLLBACK_EOF + +log_info "Executing schema rollback..." + +# SQLite doesn't support DROP COLUMN IF EXISTS, so we need to handle errors gracefully +# Execute each ALTER TABLE separately to handle missing columns +sqlite3 "$TARGET_DB" << 'SQL_PART1' +PRAGMA foreign_keys = OFF; + +-- Drop tables +DROP TABLE IF EXISTS repo_license; +DROP TABLE IF EXISTS issue_pin; + +-- Drop indices (these always work, even if index doesn't exist) +DROP INDEX IF EXISTS IDX_action_r_u_d; +DROP INDEX IF EXISTS IDX_action_au_r_c_u_d; +DROP INDEX IF EXISTS IDX_action_c_u_d; +DROP INDEX IF EXISTS IDX_action_c_u; +DROP INDEX IF EXISTS IDX_action_au_c_u; +DROP INDEX IF EXISTS UQE_action_r_u_d; +DROP INDEX IF EXISTS UQE_action_au_r_c_u_d; +DROP INDEX IF EXISTS UQE_action_c_u_d; +DROP INDEX IF EXISTS UQE_action_c_u; +DROP INDEX IF EXISTS UQE_action_au_c_u; +DROP INDEX IF EXISTS IDX_notification_u_s_uu; +DROP INDEX IF EXISTS IDX_notification_user_id; +DROP INDEX IF EXISTS IDX_notification_repo_id; +DROP INDEX IF EXISTS IDX_notification_status; +DROP INDEX IF EXISTS IDX_notification_source; +DROP INDEX IF EXISTS IDX_notification_issue_id; +DROP INDEX IF EXISTS IDX_notification_commit_id; +DROP INDEX IF EXISTS IDX_notification_updated_by; +DROP INDEX IF EXISTS UQE_notification_u_s_uu; +SQL_PART1 + +# Function to drop column if it exists +drop_column_if_exists() { + local table="$1" + local column="$2" + local exists + exists=$(sqlite3 "$TARGET_DB" "SELECT COUNT(*) FROM pragma_table_info('$table') WHERE name='$column';") + if [[ "$exists" -gt 0 ]]; then + log_info "Dropping column $table.$column..." + sqlite3 "$TARGET_DB" "ALTER TABLE $table DROP COLUMN $column;" + log_success "Dropped $table.$column" + else + log_info "Column $table.$column does not exist, skipping" + fi +} + +# Drop columns added in migrations 306-320 +drop_column_if_exists "protected_branch" "block_admin_merge_override" +drop_column_if_exists "protected_branch" "priority" +drop_column_if_exists "issue" "time_estimate" +drop_column_if_exists "pull_auto_merge" "delete_branch_after_merge" +drop_column_if_exists "action_runner" "ephemeral" +drop_column_if_exists "secret" "description" +drop_column_if_exists "action_variable" "description" +drop_column_if_exists "repo_unit" "anonymous_access_mode" +drop_column_if_exists "label" "exclusive_order" +drop_column_if_exists "login_source" "two_factor_policy" + +# Check if pin_order column needs to be added back to issue table (migration 313 removed it) +log_info "Checking if pin_order column needs to be restored to issue table..." +has_pin_order=$(sqlite3 "$TARGET_DB" "SELECT COUNT(*) FROM pragma_table_info('issue') WHERE name='pin_order';") +if [[ "$has_pin_order" -eq 0 ]]; then + log_info "Adding pin_order column back to issue table..." + sqlite3 "$TARGET_DB" "ALTER TABLE issue ADD COLUMN pin_order INTEGER DEFAULT 0;" + log_success "Added pin_order column to issue table" +else + log_info "pin_order column already exists in issue table" +fi + +# Set version to 304 (allows Forgejo to run migration 305 which converts two_factor.secret from TEXT to BLOB) +sqlite3 "$TARGET_DB" "UPDATE version SET version = 304 WHERE id = 1;" +log_success "Database version set to 304" + +rm -f "$ROLLBACK_SQL" + +# ============================================ +# PHASE 4: Clear Regeneratable Data +# ============================================ +log_info "Phase 4: Clearing regeneratable data..." + +# Remove indexers (will be rebuilt on first start) +if [[ -d "$TARGET_DATA/indexers" ]]; then + rm -rf "$TARGET_DATA/indexers" + log_success "Removed indexers (will be rebuilt)" +fi + +# Remove queues (will be recreated) +if [[ -d "$TARGET_DATA/queues" ]]; then + rm -rf "$TARGET_DATA/queues" + log_success "Removed queues (will be recreated)" +fi + +# ============================================ +# PHASE 5: Update Configuration +# ============================================ +log_info "Phase 5: Updating configuration..." + +# Copy app.ini +rsync -a --info=progress2 "$SOURCE_INI" "$TARGET_DATA/custom/conf/app.ini" +log_success "Copied app.ini" + +# Update paths from gitea to forgejo +sed -i 's|/var/lib/gitea|/var/lib/forgejo|g' "$TARGET_DATA/custom/conf/app.ini" +log_success "Updated paths in app.ini" + +# Check if WAL mode is already configured +if ! grep -q "SQLITE_JOURNAL_MODE" "$TARGET_DATA/custom/conf/app.ini"; then + # Add WAL mode after [database] section + sed -i '/^\[database\]/a SQLITE_JOURNAL_MODE = WAL' "$TARGET_DATA/custom/conf/app.ini" + log_success "Enabled SQLite WAL mode" +else + log_info "SQLite journal mode already configured" +fi + +# ============================================ +# PHASE 6: Set Permissions +# ============================================ +log_info "Phase 6: Setting permissions..." + +chown -R "$TARGET_USER:$TARGET_GROUP" "$TARGET_DATA" +chmod 750 "$TARGET_DATA" +chmod 640 "$TARGET_DATA/data/forgejo.db" +log_success "Permissions set for $TARGET_USER:$TARGET_GROUP" + +# ============================================ +# PHASE 7: Verify Database Integrity +# ============================================ +log_info "Phase 7: Verifying database integrity..." + +sqlite3 "$TARGET_DB" << 'VERIFY_SQL' +.headers off +.mode list + +-- Verify version was set correctly +SELECT 'Version: ' || CASE WHEN version = 304 THEN 'PASS (304)' ELSE 'FAIL (version=' || version || ')' END +FROM version WHERE id = 1; + +-- Check critical tables exist +SELECT 'Users: ' || CASE WHEN COUNT(*) > 0 THEN 'PASS (' || COUNT(*) || ' users)' ELSE 'WARN (empty)' END FROM user; +SELECT 'Repositories: ' || CASE WHEN COUNT(*) > 0 THEN 'PASS (' || COUNT(*) || ' repos)' ELSE 'WARN (empty)' END FROM repository; +SELECT 'Secrets: PASS (' || COUNT(*) || ' secrets)' FROM secret; +SELECT 'Runners: PASS (' || COUNT(*) || ' runners)' FROM action_runner; +SELECT 'Variables: PASS (' || COUNT(*) || ' variables)' FROM action_variable; +VERIFY_SQL + +# Verify dropped tables are gone +repo_license_exists=$(sqlite3 "$TARGET_DB" "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='repo_license';") +issue_pin_exists=$(sqlite3 "$TARGET_DB" "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='issue_pin';") + +if [[ "$repo_license_exists" -eq 0 ]]; then + log_success "repo_license table: DROPPED" +else + log_warn "repo_license table: STILL EXISTS" +fi + +if [[ "$issue_pin_exists" -eq 0 ]]; then + log_success "issue_pin table: DROPPED" +else + log_warn "issue_pin table: STILL EXISTS" +fi + +# ============================================ +# PHASE 8: Print Next Steps +# ============================================ +echo "" +echo "========================================" +echo -e "${GREEN}Migration complete!${NC}" +echo "========================================" +echo "" +echo "Data copied to: $TARGET_DATA" +echo "Database schema rolled back to version 304" +echo "" +echo "Next steps:" +echo "" +echo "1. Update NixOS configuration:" +echo " - Create hosts/fw/modules/forgejo.nix based on gitea.nix" +echo " - Change services.gitea to services.forgejo" +echo " - Update bind mount paths in container config" +echo " - Update runner configuration for Forgejo" +echo "" +echo "2. Deploy:" +echo " nixos-rebuild switch" +echo "" +echo "3. Monitor first startup:" +echo " journalctl -u container@git -f" +echo "" +echo "4. Verify functionality:" +echo " [ ] Forgejo starts without errors" +echo " [ ] Login via OpenID (auth.cloonar.com)" +echo " [ ] All repositories visible" +echo " [ ] Can push/pull to repositories" +echo " [ ] CI/CD runners connect" +echo " [ ] Workflow with secrets runs" +echo " [ ] Packages registry accessible" +echo "" +echo -e "${YELLOW}ROLLBACK:${NC} If anything fails, original Gitea data is untouched." +echo "Just revert NixOS config and restart Gitea container." +echo "========================================"