feat: add reverse sync (Gitea → GitHub) for public backup

This commit is contained in:
mineracks 2026-04-06 18:03:44 +10:00
parent 8f1add8346
commit a0e1dec091
3 changed files with 194 additions and 0 deletions

View File

@ -79,6 +79,14 @@ The sync script and health check both send push notifications for significant ev
- **Default** — sync completed successfully, new repos mirrored - **Default** — sync completed successfully, new repos mirrored
- **Low** — routine status updates - **Low** — routine status updates
### Reverse sync (Gitea to GitHub)
In addition to pulling from GitHub into Gitea, you can push your own Gitea repos back to GitHub as public backups. This turns GitHub into a public mirror of your self-hosted work — your Gitea instance stays the source of truth, and GitHub is a redundant, publicly-accessible copy.
Configure which repos to push in `mirror.env` using `REVERSE_SYNC_REPOS`. The script will create the GitHub repo automatically if it doesn't exist, then push all branches and tags. Backup refs (the append-only safety net) are kept private and not pushed to GitHub.
This runs at the end of each daily sync cycle alongside the GitHub-to-Gitea mirror pulls.
### Disk space monitoring ### Disk space monitoring
The health check includes disk usage monitoring. It warns at 80% usage and sends a critical alert at 90%, giving you time to expand storage or prune release assets before the mirror runs out of space. The health check includes disk usage monitoring. It warns at 80% usage and sends a critical alert at 90%, giving you time to expand storage or prune release assets before the mirror runs out of space.
@ -165,6 +173,9 @@ owners:
| `RELEASE_KEEP` | How many releases to keep per repo | `3` | | `RELEASE_KEEP` | How many releases to keep per repo | `3` |
| `RELEASE_ROOT` | Where to store downloaded release assets | `/var/lib/breakglass/releases` | | `RELEASE_ROOT` | Where to store downloaded release assets | `/var/lib/breakglass/releases` |
| `FORCE_HTTP11` | Force HTTP/1.1 (helps with Cloudflare Tunnel) | `true` | | `FORCE_HTTP11` | Force HTTP/1.1 (helps with Cloudflare Tunnel) | `true` |
| `REVERSE_SYNC_REPOS` | Gitea repos to push to GitHub (space-separated) | — |
| `GITHUB_PUSH_TOKEN` | GitHub PAT for reverse sync (repo + admin scope) | — |
| `GITHUB_PUSH_OWNER` | GitHub owner/org for reverse sync | — |
## Day-to-day commands ## Day-to-day commands

View File

@ -67,3 +67,22 @@ RELEASE_ROOT=/var/lib/breakglass/releases
# Force HTTP/1.1 (helps with Cloudflare Tunnel) # Force HTTP/1.1 (helps with Cloudflare Tunnel)
FORCE_HTTP11="true" FORCE_HTTP11="true"
# ── Reverse Sync (Gitea → GitHub) ────────────────────────
# Push your own Gitea repos to GitHub as public backups.
# Requires a GitHub PAT with repo + admin:repo scope.
# Format: space-separated list of "gitea_org/repo:github_owner/repo"
# If the GitHub side is the same, you can use short form: "gitea_org/repo"
#
# Examples:
# REVERSE_SYNC_REPOS="mineracks/foss_breakglass_mirror_v2"
# REVERSE_SYNC_REPOS="mineracks/foss_breakglass_mirror_v2 mineracks/my-other-project"
# REVERSE_SYNC_REPOS="mineracks/foss_breakglass_mirror_v2:mineracks/breakglass-mirror"
REVERSE_SYNC_REPOS=""
# GitHub PAT for pushing (needs repo + admin:repo scopes)
# This is separate from GITHUB_TOKEN which is read-only for mirroring
GITHUB_PUSH_TOKEN=""
# GitHub owner/org to push to (default: same as Gitea org)
GITHUB_PUSH_OWNER="mineracks"

View File

@ -45,6 +45,10 @@ RELEASE_KEEP="${RELEASE_KEEP:-3}"
# Enable wiki and release syncing # Enable wiki and release syncing
SYNC_WIKIS="${SYNC_WIKIS:-true}" SYNC_WIKIS="${SYNC_WIKIS:-true}"
SYNC_RELEASES="${SYNC_RELEASES:-true}" SYNC_RELEASES="${SYNC_RELEASES:-true}"
# Reverse sync (Gitea → GitHub)
REVERSE_SYNC_REPOS="${REVERSE_SYNC_REPOS:-}"
GITHUB_PUSH_TOKEN="${GITHUB_PUSH_TOKEN:-}"
GITHUB_PUSH_OWNER="${GITHUB_PUSH_OWNER:-}"
mkdir -p "$MIRROR_ROOT" "$RELEASE_ROOT" "$LOG_DIR" "$AUDIT_DIR" mkdir -p "$MIRROR_ROOT" "$RELEASE_ROOT" "$LOG_DIR" "$AUDIT_DIR"
@ -801,6 +805,163 @@ sync_repo_metadata() {
touch "$marker" touch "$marker"
} }
# ═══════════════════════════════════════════════════════════
# REVERSE SYNC: Push Gitea repos → GitHub (public backup)
# ═══════════════════════════════════════════════════════════
reverse_sync_repo() {
local gitea_org="$1" repo="$2" gh_owner="$3" gh_repo="$4"
log " reverse-sync ${gitea_org}/${repo} → github:${gh_owner}/${gh_repo}"
audit "REVERSE_SYNC_START repo=${gitea_org}/${repo} target=${gh_owner}/${gh_repo}"
# ── Ensure GitHub repo exists (create if not) ──────────
local gh_result
gh_result=$(curl -sfS --connect-timeout 15 --max-time 30 \
-H "Authorization: Bearer ${GITHUB_PUSH_TOKEN}" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/${gh_owner}/${gh_repo}" 2>/dev/null) || true
if ! echo "$gh_result" | grep -q '"id"'; then
log " creating GitHub repo: ${gh_owner}/${gh_repo}"
local create_payload="{\"name\":\"${gh_repo}\",\"private\":false,\"description\":\"Public backup from git.mineracks.com/${gitea_org}/${repo}\"}"
# Try org endpoint first, fall back to user endpoint
local create_result
create_result=$(curl -sfS --connect-timeout 15 --max-time 30 \
-X POST \
-H "Authorization: Bearer ${GITHUB_PUSH_TOKEN}" \
-H "Accept: application/vnd.github+json" \
-H "Content-Type: application/json" \
-d "$create_payload" \
"https://api.github.com/orgs/${gh_owner}/repos" 2>/dev/null) || true
if ! echo "$create_result" | grep -q '"id"'; then
# Try as user repo
create_result=$(curl -sfS --connect-timeout 15 --max-time 30 \
-X POST \
-H "Authorization: Bearer ${GITHUB_PUSH_TOKEN}" \
-H "Accept: application/vnd.github+json" \
-H "Content-Type: application/json" \
-d "$create_payload" \
"https://api.github.com/user/repos" 2>/dev/null) || true
if ! echo "$create_result" | grep -q '"id"'; then
warn "could not create GitHub repo ${gh_owner}/${gh_repo}"
audit "REVERSE_SYNC_FAILED repo=${gitea_org}/${repo} reason=cannot_create_github_repo"
return 1
fi
fi
log " GitHub repo created: ${gh_owner}/${gh_repo}"
fi
# ── Clone from Gitea (or use existing) ─────────────────
local work_dir="${MIRROR_ROOT}/.reverse-sync/${gitea_org}/${repo}.git"
# ── Build authed Gitea URL ──────────────────────────────
local authed_gitea
if [[ "${GITEA_URL}" == https://* ]]; then
authed_gitea="${GITEA_URL/https:\/\//https:\/\/${GITEA_USER}:${GITEA_TOKEN}@}/${gitea_org}/${repo}.git"
else
authed_gitea="${GITEA_URL/http:\/\//http:\/\/${GITEA_USER}:${GITEA_TOKEN}@}/${gitea_org}/${repo}.git"
fi
if [[ ! -d "$work_dir" ]]; then
log " cloning from Gitea …"
mkdir -p "$(dirname "$work_dir")"
if ! git clone --bare "$authed_gitea" "$work_dir" 2>>"$LOG_FILE"; then
warn "reverse-sync clone failed for ${gitea_org}/${repo}"
return 1
fi
fi
cd "$work_dir" || return 1
# ── Configure remotes ──────────────────────────────────
# Gitea as origin (source)
git remote set-url origin "$authed_gitea" 2>/dev/null || git remote add origin "$authed_gitea"
# GitHub as push target
local authed_github="https://${GITHUB_PUSH_TOKEN}@github.com/${gh_owner}/${gh_repo}.git"
if git remote get-url github &>/dev/null; then
git remote set-url github "$authed_github"
else
git remote add github "$authed_github"
fi
# ── Fetch latest from Gitea ────────────────────────────
log " fetching from Gitea …"
if ! git fetch origin '+refs/heads/*:refs/heads/*' '+refs/tags/*:refs/tags/*' 2>>"$LOG_FILE"; then
warn "reverse-sync fetch from Gitea failed for ${gitea_org}/${repo}"
return 1
fi
# ── Push to GitHub ─────────────────────────────────────
# Only push heads and tags (not backup refs — those stay private)
log " pushing to GitHub …"
if ! retry 3 git push github \
'+refs/heads/*:refs/heads/*' \
'+refs/tags/*:refs/tags/*' 2>>"$LOG_FILE"; then
warn "reverse-sync push to GitHub failed for ${gh_owner}/${gh_repo}"
audit "REVERSE_SYNC_FAILED repo=${gitea_org}/${repo} reason=push_failed"
return 1
fi
log " ✓ reverse-synced to github.com/${gh_owner}/${gh_repo}"
audit "REVERSE_SYNC_OK repo=${gitea_org}/${repo} target=${gh_owner}/${gh_repo}"
return 0
}
run_reverse_sync() {
local repos="${REVERSE_SYNC_REPOS:-}"
[[ -z "$repos" ]] && return 0
local gh_push_owner="${GITHUB_PUSH_OWNER:-}"
if [[ -z "${GITHUB_PUSH_TOKEN:-}" ]]; then
warn "REVERSE_SYNC_REPOS set but GITHUB_PUSH_TOKEN is empty — skipping reverse sync"
return 0
fi
log ""
log "═══ Reverse sync (Gitea → GitHub) ═══"
local reverse_ok=0 reverse_fail=0
for entry in $repos; do
local gitea_side gh_side
if [[ "$entry" == *:* ]]; then
# Explicit mapping: gitea_org/repo:github_owner/repo
gitea_side="${entry%%:*}"
gh_side="${entry##*:}"
else
# Short form: gitea_org/repo → same on GitHub
gitea_side="$entry"
gh_side="${gh_push_owner:+${gh_push_owner}/}${entry##*/}"
# If no GITHUB_PUSH_OWNER, use the gitea org
[[ "$gh_side" == /* ]] && gh_side="${entry}"
fi
local g_org="${gitea_side%%/*}"
local g_repo="${gitea_side##*/}"
local gh_owner="${gh_side%%/*}"
local gh_repo="${gh_side##*/}"
if reverse_sync_repo "$g_org" "$g_repo" "$gh_owner" "$gh_repo"; then
(( reverse_ok++ )) || true
else
(( reverse_fail++ )) || true
fi
done
log "═══ Reverse sync done: ${reverse_ok} ok, ${reverse_fail} failed ═══"
if (( reverse_fail > 0 )); then
notify "Reverse sync: ${reverse_ok} ok, ${reverse_fail} failed — check $LOG_FILE" "high" "warning"
elif (( reverse_ok > 0 )); then
notify "Reverse sync: ${reverse_ok} repos pushed to GitHub" "low" "arrow_right"
fi
}
# ── Notifications ──────────────────────────────────────── # ── Notifications ────────────────────────────────────────
notify() { notify() {
@ -879,6 +1040,9 @@ for entry in "${OWNERS[@]}"; do
done <<< "$repos" done <<< "$repos"
done done
# ── Reverse sync (Gitea → GitHub) ────────────────────────
run_reverse_sync
# ── Summary ────────────────────────────────────────────── # ── Summary ──────────────────────────────────────────────
DURATION=$(( $(date +%s) - $(date -d "$TIMESTAMP" +%s 2>/dev/null || date -u -d "${TIMESTAMP:0:8} ${TIMESTAMP:9:2}:${TIMESTAMP:11:2}:${TIMESTAMP:13:2}" +%s 2>/dev/null || echo 0) )) DURATION=$(( $(date +%s) - $(date -d "$TIMESTAMP" +%s 2>/dev/null || date -u -d "${TIMESTAMP:0:8} ${TIMESTAMP:9:2}:${TIMESTAMP:11:2}:${TIMESTAMP:13:2}" +%s 2>/dev/null || echo 0) ))