migrate to radicle

This commit is contained in:
Anish Lakhwara
2026-01-21 21:55:37 -08:00
parent 928a3f56ad
commit cd8bb0fe0f
9 changed files with 462 additions and 36 deletions
+2
View File
@@ -30,6 +30,7 @@
../profiles/gpodder ../profiles/gpodder
../profiles/transmission ../profiles/transmission
../profiles/raven ../profiles/raven
../profiles/radicle-node
# ../profiles/postgres_upgrade_script # one-time use # ../profiles/postgres_upgrade_script # one-time use
]; ];
@@ -58,6 +59,7 @@
"/tank/postgres" # postgres data "/tank/postgres" # postgres data
"/tank/media/photos" "/tank/media/photos"
"/tank/media/music" "/tank/media/music"
"/var/lib/radicle"
]; ];
}; };
+12 -4
View File
@@ -1,11 +1,18 @@
{ self, profiles, suites, pkgs, ... }: {
self,
profiles,
suites,
pkgs,
...
}:
{ {
imports = [ imports = [
./configuration.nix ./configuration.nix
../profiles/core ../profiles/core
../profiles/server ../profiles/server
# ../profiles/metrics # ../profiles/metrics
../profiles/gitea # ../profiles/gitea # Replaced by radicle
../profiles/radicle-seed
# ../profiles/woodpecker-server # ../profiles/woodpecker-server
../profiles/rss-bridge ../profiles/rss-bridge
# ../profiles/mount-mossnet # ../profiles/mount-mossnet
@@ -50,7 +57,7 @@
services.postgresqlBackup = { services.postgresqlBackup = {
# TODO needs working wireguard to box # TODO needs working wireguard to box
enable = false; enable = false;
databases = [ "gitea" "freshrss" ]; # "woodpecker" databases = [ "freshrss" ]; # gitea removed (now using radicle)
location = "/mnt/two/postgres"; location = "/mnt/two/postgres";
}; };
@@ -58,7 +65,8 @@
enable = true; enable = true;
name = "helix"; name = "helix";
paths = [ paths = [
"/var/lib/gitea" # "/var/lib/gitea" # Replaced by radicle
"/var/lib/radicle"
"/var/lib/freshrss" "/var/lib/freshrss"
# "/var/lib/woodpecker" # "/var/lib/woodpecker"
"/var/lib/microbin" "/var/lib/microbin"
+40 -20
View File
@@ -1,14 +1,22 @@
{ config, pkgs, lib, ... }: {
config,
pkgs,
lib,
...
}:
let let
adblockLocalZones = pkgs.stdenv.mkDerivation { adblockLocalZones = pkgs.stdenv.mkDerivation {
name = "unbound-zones-adblock"; name = "unbound-zones-adblock";
src = (pkgs.fetchFromGitHub { src = (
owner = "StevenBlack"; pkgs.fetchFromGitHub {
repo = "hosts"; owner = "StevenBlack";
rev = "3.12.21"; repo = "hosts";
sha256 = "Yzr6PY/zqQE+AHH0J6ioHTsgkikM+dz4aelbGpQJa1s="; rev = "3.12.21";
} + "/hosts"); sha256 = "Yzr6PY/zqQE+AHH0J6ioHTsgkikM+dz4aelbGpQJa1s=";
}
+ "/hosts"
);
phases = [ "installPhase" ]; phases = [ "installPhase" ];
@@ -40,9 +48,11 @@ let
"photos.mossnet.lan" "photos.mossnet.lan"
"pod.mossnet.lan" "pod.mossnet.lan"
"mast.mossnet.lan" "mast.mossnet.lan"
"rad.mossnet.lan"
]; ];
in { in
{
services.unbound = { services.unbound = {
enable = true; enable = true;
settings = { settings = {
@@ -53,9 +63,17 @@ in {
# private-address = "192.168.1.0/24"; # private-address = "192.168.1.0/24";
cache-min-ttl = 0; cache-min-ttl = 0;
serve-expired = "yes"; serve-expired = "yes";
interface = [ "0.0.0.0" "::" ]; interface = [
access-control = "0.0.0.0"
[ "127.0.0.0/8 allow" "192.168.1.0/24 allow" "10.0.69.0/24 allow" "::1 allow" "fd7d:587a:4300:1::/64 allow" ]; "::"
];
access-control = [
"127.0.0.0/8 allow"
"192.168.1.0/24 allow"
"10.0.69.0/24 allow"
"::1 allow"
"fd7d:587a:4300:1::/64 allow"
];
access-control-view = "10.0.69.0/24 wireguard"; access-control-view = "10.0.69.0/24 wireguard";
# so-reuseport = "yes"; # so-reuseport = "yes";
tls-upstream = "yes"; tls-upstream = "yes";
@@ -63,15 +81,17 @@ in {
local-zone = ''"mossnet.lan." redirect''; local-zone = ''"mossnet.lan." redirect'';
local-data = ''"mossnet.lan. IN A ${mossnet}"''; local-data = ''"mossnet.lan. IN A ${mossnet}"'';
}; };
forward-zone = [{ forward-zone = [
name = "."; {
forward-addr = [ name = ".";
"45.90.28.0#6939b9.dns.nextdns.io" forward-addr = [
"1.1.1.1@853#cloudflare-dns.com" "45.90.28.0#6939b9.dns.nextdns.io"
]; "1.1.1.1@853#cloudflare-dns.com"
# non-tls ];
# forward-addr = ["45.90.30.49" "45.90.28.49" "1.1.1.1" "8.8.8.8"] # non-tls
}]; # forward-addr = ["45.90.30.49" "45.90.28.49" "1.1.1.1" "8.8.8.8"]
}
];
view = { view = {
name = "wireguard"; name = "wireguard";
local-zone = ''"mossnet.lan." redirect''; local-zone = ''"mossnet.lan." redirect'';
+85
View File
@@ -0,0 +1,85 @@
{
self,
config,
pkgs,
...
}:
let
# Custom radicle-explorer configured for local box node
localExplorer = pkgs.radicle-explorer.withConfig {
preferredSeeds = [
{
hostname = "rad.mossnet.lan";
port = 80;
scheme = "http";
}
];
};
in
{
age.secrets.radicle-box-key.file = "${self}/secrets/radicle-box-key.age";
age.secrets.radicle-box-key.owner = "radicle";
services.radicle = {
enable = true;
privateKeyFile = config.age.secrets.radicle-box-key.path;
publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAII2QC5AbaTHCRVzGluWgXUlyBNFDxcLiIeViv81f3TYw mossnet.lan";
node = {
listenAddress = "0.0.0.0"; # Listen on all interfaces for local LAN access
listenPort = 8776;
openFirewall = true;
};
settings = {
node = {
alias = "mossnet.lan";
connect = [ "z6MkfPhJnbrHbB4FNcub7weT8CRcqFgfJinDfSYjPwK9tSXy@10.0.69.5:8776" ];
seedingPolicy.default = "block";
};
};
# HTTP API for local web access
httpd = {
enable = true;
listenAddress = "127.0.0.1";
listenPort = 8888;
};
};
# Nginx to serve radicle-explorer + proxy API
services.nginx = {
enable = true;
recommendedProxySettings = true;
virtualHosts."rad.mossnet.lan" = {
root = localExplorer;
locations."/" = {
tryFiles = "$uri $uri/ /index.html";
index = "index.html";
};
# Proxy API requests to radicle-httpd
locations."/api" = {
proxyPass = "http://127.0.0.1:8888";
};
# Proxy raw file access to radicle-httpd
locations."/raw" = {
proxyPass = "http://127.0.0.1:8888";
};
# Proxy git protocol requests (rad:xxx) to radicle-httpd
locations."~ ^/rad:" = {
proxyPass = "http://127.0.0.1:8888";
};
};
};
# Open firewall for nginx
networking.firewall.allowedTCPPorts = [ 80 ];
# rad CLI for interactive use
environment.systemPackages = [ pkgs.radicle-node ];
}
+85
View File
@@ -0,0 +1,85 @@
{
self,
config,
pkgs,
lib,
...
}:
let
# Custom radicle-explorer with our seed as preferred
customExplorer = pkgs.radicle-explorer.withConfig {
preferredSeeds = [
{
hostname = "git.sealight.xyz";
port = 443;
scheme = "https";
}
];
};
in
{
age.secrets.radicle-helix-key.file = "${self}/secrets/radicle-helix-key.age";
age.secrets.radicle-helix-key.owner = "radicle";
services.radicle = {
enable = true;
privateKeyFile = config.age.secrets.radicle-helix-key.path;
publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIA3x7XH24gEr8xHnt1qKQx38Se2AoXiUnb48/VwfL8/A git.sealight.xyz";
node = {
listenAddress = "0.0.0.0";
listenPort = 8776;
openFirewall = true;
};
settings = {
node = {
alias = "git.sealight.xyz";
externalAddresses = [ "git.sealight.xyz:8776" ];
connect = [ "z6MkoyrvcRdeGU5PyB2SbHj9mNj3nb5p34rZamkEz64GX1c3@10.0.69.4:8776" ];
seedingPolicy.default = "block";
};
};
httpd = {
enable = true;
listenAddress = "127.0.0.1";
listenPort = 8080;
# Don't use the module's nginx integration - we'll configure it manually
nginx = null;
};
};
# Configure nginx manually for radicle-explorer + httpd API
services.nginx.virtualHosts."git.sealight.xyz" = {
enableACME = true;
forceSSL = true;
# Serve radicle-explorer static files at root
root = customExplorer;
locations."/" = {
tryFiles = "$uri $uri/ /index.html";
index = "index.html";
};
# Proxy API requests to radicle-httpd
locations."/api" = {
proxyPass = "http://127.0.0.1:8080";
recommendedProxySettings = true;
};
# Proxy raw file access to radicle-httpd
locations."/raw" = {
proxyPass = "http://127.0.0.1:8080";
recommendedProxySettings = true;
};
# Proxy git protocol requests (rad:xxx) to radicle-httpd
# These are requests to /:rid/* where rid starts with "rad:"
locations."~ ^/rad:" = {
proxyPass = "http://127.0.0.1:8080";
recommendedProxySettings = true;
};
};
}
+168
View File
@@ -0,0 +1,168 @@
#!/usr/bin/env bash
set -euo pipefail
# Migration script: Forgejo bare repos -> Radicle
#
# Usage:
# 1. First rsync repos from helix:
# rsync -avz git.sealight.xyz:/var/lib/gitea/repositories/aynish/ /tmp/forgejo-migration/
#
# 2. Run this script:
# sudo -u radicle ./migrate-forgejo-to-radicle.sh /tmp/forgejo-migration
#
# Or run as root:
# ./migrate-forgejo-to-radicle.sh /tmp/forgejo-migration
SOURCE_DIR="${1:-/tmp/forgejo-migration}"
WORK_DIR="/var/lib/radicle/migration"
LOG_FILE="/var/lib/radicle/migration.log"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
log() { echo -e "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"; }
log_success() { log "${GREEN}$1${NC}"; }
log_warn() { log "${YELLOW}$1${NC}"; }
log_error() { log "${RED}$1${NC}"; }
# Use rad directly with proper environment (rad-system can't access files outside sandbox)
export RAD_HOME=/var/lib/radicle
export HOME=/var/lib/radicle
RAD_CMD="rad"
# Ensure keys are in place (agenix stores them in /run/agenix/)
if [[ -f /run/agenix/radicle-box-key ]] && [[ ! -s /var/lib/radicle/keys/radicle ]]; then
log "Setting up radicle keys..."
cp /run/agenix/radicle-box-key /var/lib/radicle/keys/radicle
chmod 600 /var/lib/radicle/keys/radicle
chown radicle:radicle /var/lib/radicle/keys/radicle
fi
# Get public key from the config if not present
if [[ ! -s /var/lib/radicle/keys/radicle.pub ]]; then
log "Generating public key..."
ssh-keygen -y -f /var/lib/radicle/keys/radicle > /var/lib/radicle/keys/radicle.pub 2>/dev/null || true
chown radicle:radicle /var/lib/radicle/keys/radicle.pub
fi
# Check source directory
if [[ ! -d "$SOURCE_DIR" ]]; then
log_error "Source directory not found: $SOURCE_DIR"
echo ""
echo "First rsync repos from helix:"
echo " rsync -avz git.sealight.xyz:/var/lib/gitea/repositories/aynish/ /tmp/forgejo-migration/"
exit 1
fi
# Create work directory
mkdir -p "$WORK_DIR"
cd "$WORK_DIR"
log "Starting Forgejo -> Radicle migration"
log "Source: $SOURCE_DIR"
log "Work dir: $WORK_DIR"
log "Using: $RAD_CMD"
# Count repos
REPOS=$(find "$SOURCE_DIR" -maxdepth 1 -name "*.git" -type d | sort)
TOTAL=$(echo "$REPOS" | grep -c . || echo 0)
if [[ "$TOTAL" -eq 0 ]]; then
log_error "No .git directories found in $SOURCE_DIR"
exit 1
fi
log "Found $TOTAL repositories to migrate"
echo ""
MIGRATED=0
SKIPPED=0
FAILED=0
for BARE_REPO in $REPOS; do
# Extract repo name (remove .git suffix)
REPO_NAME=$(basename "$BARE_REPO" .git)
log "Processing: $REPO_NAME"
CLONE_DIR="$WORK_DIR/$REPO_NAME"
# Clone bare repo to working directory
if [[ -d "$CLONE_DIR/.git" ]]; then
log " Directory exists, skipping clone"
else
log " Cloning from bare repo..."
if ! git clone --quiet "$BARE_REPO" "$CLONE_DIR" 2>>"$LOG_FILE"; then
log_error " Failed to clone $REPO_NAME"
((FAILED++)) || true
continue
fi
fi
cd "$CLONE_DIR"
# Check if already initialized in Radicle
if $RAD_CMD . >/dev/null 2>&1; then
log_warn " Already initialized in Radicle, skipping"
cd "$WORK_DIR"
((SKIPPED++)) || true
continue
fi
# Get current branch
CURRENT_BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || echo "main")
# Normalize to main if needed
if [[ "$CURRENT_BRANCH" != "main" ]]; then
log " Normalizing branch: $CURRENT_BRANCH -> main"
git branch -m "$CURRENT_BRANCH" main 2>>"$LOG_FILE" || true
fi
# Get description from git config if available
DESCRIPTION=$(git config --get gitweb.description 2>/dev/null || echo "Migrated from Forgejo")
# Truncate description if too long
if [[ ${#DESCRIPTION} -gt 255 ]]; then
DESCRIPTION="${DESCRIPTION:0:252}..."
fi
# Initialize in Radicle
log " Initializing in Radicle..."
if $RAD_CMD init \
--name "$REPO_NAME" \
--description "$DESCRIPTION" \
--default-branch main \
--public 2>>"$LOG_FILE"; then
# Push to radicle
log " Pushing to Radicle..."
if git push rad main 2>>"$LOG_FILE"; then
RID=$($RAD_CMD . 2>/dev/null || echo "unknown")
log_success " Migrated: $REPO_NAME -> $RID"
((MIGRATED++)) || true
else
log_error " Failed to push $REPO_NAME"
((FAILED++)) || true
fi
else
log_error " Failed to init $REPO_NAME in Radicle"
((FAILED++)) || true
fi
cd "$WORK_DIR"
done
echo ""
log "========================================"
log "Migration Summary:"
log " Total repos: $TOTAL"
log " Migrated: $MIGRATED"
log " Skipped: $SKIPPED"
log " Failed: $FAILED"
log "========================================"
echo ""
log "Verify with: $RAD_CMD ls"
log "Configure helix to follow: rad-system follow <box-nid> --alias mossnet"
Binary file not shown.
+9
View File
@@ -0,0 +1,9 @@
age-encryption.org/v1
-> ssh-ed25519 kgCrKA WzvIX43OcJw/qxPgTw3iUjorQ0ahFHYZEmnghnUz/xc
HtNx7XqWFZDNpjNiquxfRrnP/TcXxoPn6mMB0BMjlJw
-> ssh-ed25519 6DaCrw dZ9etYRaGuAO0gcA3Krdgvcg9vkwctAtJzKevgjE3wc
gbWQHzO//bGp3Jl513GIgzaM4FzJSiA5thOSWWsx7ik
--- QeO3vdOBbisSZaBECB1Wa603H6P7lXDJwxerJ8gH4+M
þlGêýX­É.ø­“®«$lp^^ ”0üzK88 QuQ•Þïq;Y­ðŽ85³ÈRM÷4†ÖçI:gŒßqÿ“žípܺÏåž,OãdPk­RèÚrq¤UëíM•‹!ž¥=EuÈ2 [_à!%CE—ê
íø"—åo² 3wû„l" :TŸ6àºÁTâÜ[[Pn”Éq¡Bì 7'àFü¨8ŸŒË:6E¦Í³àʽ\¾ ó#ê1ëÄžå¨K¾jŠ.·Ð¬êê)V³™ŸBN ê"Ë÷S,Àïµ^ïêDÅ&Æ©¨¾£h´%*ågé°ÂòvìN^¨-Òô2PïBñÃb¬'? ˆL¯jF,b«ç:¡,ˆx#ÑįÑ UY€_ÁQpÈÝø+ëjÂ!ÿU­Š ‡;úAÞúÑ˜Íærå‡5«Ó0ƒ¤Aá|<èÉ=j“ß²s»™q;bŠŸ!BW'Ý´¼8ÄAˆsË#ô= êêi½`|œNÙ¬ÇK‹o̰H–ö“)ŠŒÞ°
Î?Á"±„ 7–pç¯WÕÿä•áÒ
+60 -11
View File
@@ -7,9 +7,21 @@ let
helix = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAKrL6IDHNnHmxi0q9nzu87NOyidPm3HpE7klU368lEf root@helix"; helix = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAKrL6IDHNnHmxi0q9nzu87NOyidPm3HpE7klU368lEf root@helix";
helix2 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIK2G81z1E51ioJQGLHnTJEjgSdBqLM6mb72Z+0atE6Bf root@helix"; helix2 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIK2G81z1E51ioJQGLHnTJEjgSdBqLM6mb72Z+0atE6Bf root@helix";
work = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHOnfDvR2D2nGnC+DZYDUXiokzz+eLfZwkp+O8WjWutp anishlakhwara@Anishs-MacBook-Pro.local"; work = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHOnfDvR2D2nGnC+DZYDUXiokzz+eLfZwkp+O8WjWutp anishlakhwara@Anishs-MacBook-Pro.local";
curve = [ system user ]; curve = [
allUserKeys = [ system user mossnet ]; system
systemOnly = [ system mossnet lituus helix ]; user
];
allUserKeys = [
system
user
mossnet
];
systemOnly = [
system
mossnet
lituus
helix
];
in in
{ {
"taskwarrior-private.age".publicKeys = curve; "taskwarrior-private.age".publicKeys = curve;
@@ -20,10 +32,22 @@ in
"borg-password.age".publicKeys = systemOnly; "borg-password.age".publicKeys = systemOnly;
"borg-key.age".publicKeys = systemOnly; "borg-key.age".publicKeys = systemOnly;
"helix-wg.age".publicKeys = [ helix2 helix ]; "helix-wg.age".publicKeys = [
"freshrss-dbpass.age".publicKeys = [ helix2 helix ]; helix2
"woodpecker-server-secrets.age".publicKeys = [ helix2 helix ]; helix
"gitea-dbpass.age".publicKeys = [ helix helix2 ]; ];
"freshrss-dbpass.age".publicKeys = [
helix2
helix
];
"woodpecker-server-secrets.age".publicKeys = [
helix2
helix
];
"gitea-dbpass.age".publicKeys = [
helix
helix2
];
"synapse-config.age".publicKeys = [ lituus ]; "synapse-config.age".publicKeys = [ lituus ];
"synapse-database-password.age".publicKeys = [ lituus ]; "synapse-database-password.age".publicKeys = [ lituus ];
@@ -31,12 +55,37 @@ in
"telegram-matrix-env.age".publicKeys = [ lituus ]; "telegram-matrix-env.age".publicKeys = [ lituus ];
"wallabag.age".publicKeys = [ mossnet ]; "wallabag.age".publicKeys = [ mossnet ];
"woodpecker-agent-secret.age".publicKeys = [ mossnet helix helix2 ]; "woodpecker-agent-secret.age".publicKeys = [
mossnet
helix
helix2
];
"box-wg.age".publicKeys = [ mossnet ]; "box-wg.age".publicKeys = [ mossnet ];
"wallabag-password.age".publicKeys = [ mossnet ]; "wallabag-password.age".publicKeys = [ mossnet ];
"wallabag-secret.age".publicKeys = [ mossnet ]; "wallabag-secret.age".publicKeys = [ mossnet ];
"work-wg.age".publicKeys = [ work user system ]; "work-wg.age".publicKeys = [
"github-token.age".publicKeys = [ work user system mossnet ]; work
"anthropicToken.age".publicKeys = [ work user system mossnet ]; user
system
];
"github-token.age".publicKeys = [
work
user
system
mossnet
];
"anthropicToken.age".publicKeys = [
work
user
system
mossnet
];
# Radicle node keys
"radicle-box-key.age".publicKeys = [ mossnet ];
"radicle-helix-key.age".publicKeys = [
helix
helix2
];
} }