feat: add reverse sync (Gitea → GitHub) for public backup
This commit is contained in:
parent
8f1add8346
commit
a0e1dec091
11
README.md
11
README.md
@ -79,6 +79,14 @@ The sync script and health check both send push notifications for significant ev
|
||||
- **Default** — sync completed successfully, new repos mirrored
|
||||
- **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
|
||||
|
||||
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_ROOT` | Where to store downloaded release assets | `/var/lib/breakglass/releases` |
|
||||
| `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
|
||||
|
||||
|
||||
@ -67,3 +67,22 @@ RELEASE_ROOT=/var/lib/breakglass/releases
|
||||
|
||||
# Force HTTP/1.1 (helps with Cloudflare Tunnel)
|
||||
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"
|
||||
|
||||
@ -45,6 +45,10 @@ RELEASE_KEEP="${RELEASE_KEEP:-3}"
|
||||
# Enable wiki and release syncing
|
||||
SYNC_WIKIS="${SYNC_WIKIS:-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"
|
||||
|
||||
@ -801,6 +805,163 @@ sync_repo_metadata() {
|
||||
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 ────────────────────────────────────────
|
||||
|
||||
notify() {
|
||||
@ -879,6 +1040,9 @@ for entry in "${OWNERS[@]}"; do
|
||||
done <<< "$repos"
|
||||
done
|
||||
|
||||
# ── Reverse sync (Gitea → GitHub) ────────────────────────
|
||||
run_reverse_sync
|
||||
|
||||
# ── 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) ))
|
||||
|
||||
Loading…
Reference in New Issue
Block a user