Radicle Sync Architecture
AC/DC repositories are automatically synchronized to the Radicle peer-to-peer network for decentralized, censorship-resistant code distribution.
Architecture
The sync architecture uses a two-server model to work around network topology constraints:
┌─────────────────────────────────────────────────────────────────────┐
│ Radicle Sync Flow │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 1. Push to Forgejo │
│ ──────────────── │
│ Developer pushes to Forgejo (source.ac-dc.network) │
│ │
│ 2. CI Workflow Triggered │
│ ───────────────────── │
│ Forgejo triggers CI workflow on CI Runner (ci.ac-dc.network) │
│ │
│ 3. Local Radicle Push │
│ ─────────────────── │
│ CI workflow pushes to CI Runner's local Radicle storage │
│ │
│ 4. Storage Rsync │
│ ────────────── │
│ CI Runner rsyncs storage to Source Server via private network │
│ (10.106.0.3 → 10.106.0.2) │
│ │
│ 5. Network Announce │
│ ──────────────── │
│ Source Server announces to Radicle seeds (network-connected) │
│ │
└─────────────────────────────────────────────────────────────────────┘
Why This Architecture?
The Problem
Both servers share the same Radicle Node ID but have separate storage:
- CI Runner: Executes CI jobs and pushes to local Radicle storage
- Source Server: Has working connectivity to Radicle seeds
The CI Runner cannot directly announce to Radicle seeds (connection timeouts), while the Source Server can successfully connect to 10+ seeds.
The Solution
Instead of direct announcement from CI Runner:
- CI Runner pushes to its local storage (fast, local operation)
- Storage is rsynced to Source Server over private network (fast, reliable)
- Source Server announces to Radicle network (working connectivity)
Components
Scripts on CI Runner (10.106.0.3)
/home/devops/radicle-full-sync.sh
Main sync script called by CI workflows:
#!/bin/bash
# Usage: radicle-full-sync.sh <rid>
# Example: radicle-full-sync.sh rad:z3XCPA2jQz5Fhh6LnYKxHMCAyoAMG
RID="${1:-}"
REPO_ID="${RID#rad:}"
SOURCE_SERVER="10.106.0.2"
RADICLE_STORAGE="/home/devops/.radicle/storage"
# Step 1: Rsync storage to source server
rsync -az --delete "${RADICLE_STORAGE}/${REPO_ID}/" \
"${SOURCE_SERVER}:${RADICLE_STORAGE}/${REPO_ID}/"
# Step 2: Trigger announce on source server
ssh ${SOURCE_SERVER} "rad sync ${RID} --announce"
/home/devops/push-radicle-to-source.sh
Single-repo sync with verbose output:
#!/bin/bash
# Usage: push-radicle-to-source.sh <rid>
Scripts on Source Server (10.106.0.2)
/home/devops/sync-radicle-all.sh
Cron job script that fetches from network:
#!/bin/bash
# Syncs all tracked repos from Radicle network
for rid in $(rad ls --format json | jq -r ".[].rid"); do
rad sync "$rid" --fetch
done
Cron Jobs
On Source Server:
# Sync all Radicle repos from network every 5 minutes
*/5 * * * * /home/devops/sync-radicle-all.sh
CI Workflow Integration
Each repository's CI workflow includes a Radicle sync step:
# .forgejo/workflows/sync.yml (or ci.yml)
jobs:
radicle-push:
runs-on: native
steps:
- name: Sync to Radicle
run: |
# ... push to local storage ...
- name: Sync to network via source server
run: |
RID="rad:z3XCPA2jQz5Fhh6LnYKxHMCAyoAMG"
$HOME/radicle-full-sync.sh "$RID"
Monitoring
Check Sync Status
# On source server - check all repo sync status
curl -s http://localhost:8081/api/repos | \
python3 -c "import sys,json; d=json.load(sys.stdin); \
synced=sum(1 for r in d['repos'] if r.get('sync_status')=='synced'); \
print(f'Synced: {synced}/{len(d[\"repos\"])} repos')"
Verify Specific Repo
# Check Forgejo HEAD
git ls-remote https://source.ac-dc.network/alpha-delta-network/REPO.git HEAD
# Check Radicle HEAD on source server
ssh -p 2584 devops@source.ac-dc.network "rad ls | grep REPO"
# Check Radicle HEAD on CI runner
ssh -p 2584 devops@ci.ac-dc.network "rad ls | grep REPO"
Manual Sync
# Force sync a specific repo
ssh -p 2584 devops@ci.ac-dc.network \
"/home/devops/push-radicle-to-source.sh rad:z3XCPA2jQz5Fhh6LnYKxHMCAyoAMG"
# Sync all repos
ssh -p 2584 devops@ci.ac-dc.network \
'for rid in $(rad ls 2>&1 | grep -oE "rad:[a-zA-Z0-9]+" | sort -u); do
/home/devops/push-radicle-to-source.sh "$rid"
done'
Troubleshooting
Repo Shows as "Unsynced"
- Check if CI workflow completed successfully
- Verify storage was rsynced: compare HEADs on both servers
- Check if announce succeeded: look for "Synced with N seed(s)" message
Announce Fails with "All seeds timed out"
This is expected on CI Runner. The source server handles network announces. If it happens on source server:
- Check Radicle node status:
rad node status - Verify seed connectivity:
nc -zv iris.radicle.xyz 8776 - Restart node if needed:
rad node stop && rad node start
Storage Out of Sync
# Force rsync from CI Runner to Source Server
ssh -p 2584 devops@ci.ac-dc.network \
"rsync -az --delete ~/.radicle/storage/REPO_ID/ \
10.106.0.2:~/.radicle/storage/REPO_ID/"
Network Information
Radicle Node ID
Both servers share: z6MkqVELL15xFNq4FfnJ87shQGXBnJekUgmrhgk491DQd2MP
Connected Seeds (Source Server)
The source server maintains connections to 10+ Radicle seeds including:
iris.radicle.xyz:8776rosa.radicle.xyz:8776seed.radicle.at:8776radicle.dpc.pw:8776