Files
prajwal a181625c89 Initial commit: backup + mirror automation for self-hosted Gitea
Includes:
- gitea-backups/bin/backup.sh (per-push bundle + DB snapshot to local + S3)
- gitea-backups/bin/install-hooks.sh (idempotent post-receive shim installer)
- gitea-backups/bin/retention.sh (count-based retention: keep newest 7 dates)
- gitea-mirror/bin/auto-mirror.sh (Gitea -> GitHub push mirror automation,
  hardened against Gitea outages)
- crontab.txt (reference for the 3 cron entries)
- README.md (architecture, layout, bootstrap)
2026-05-09 06:02:09 +00:00

177 lines
6.2 KiB
Bash
Executable File

#!/usr/bin/env bash
# Auto-configure GitHub push mirrors for every Gitea repo.
# Idempotent: safe to run anytime, including via cron.
# - For each Gitea repo, ensures a same-named private repo exists on GitHub.
# - Ensures a push mirror is configured in Gitea pointing to that GitHub repo.
# - On first configuration, triggers an initial sync so existing history uploads.
# Failures log to ${LOG} but never abort the script (other repos still get processed).
set -uo pipefail
CONFIG_DIR="/home/ubuntu/gitea-mirror"
GITEA_TOKEN_FILE="${CONFIG_DIR}/gitea.token"
GITHUB_TOKEN_FILE="${CONFIG_DIR}/github.token"
LOG="${CONFIG_DIR}/logs/auto-mirror.log"
GITEA_BASE="https://127.0.0.1:3030"
# --insecure because Gitea uses a self-signed cert; this is a loopback call so
# MITM risk is non-existent. Remove -k once a real cert is in place.
CURL_OPTS="--insecure"
GITEA_OWNER="prajwal" # mirror only repos owned by this Gitea user
GITHUB_USER="prajwalpatil-toqqer"
GITHUB_API="https://api.github.com"
QUIET=0
[[ "${1:-}" == "--quiet" ]] && QUIET=1
log() { printf '%s %s\n' "$(date -u +%FT%TZ)" "$*" >> "$LOG"; }
say() { [[ $QUIET -eq 0 ]] && echo "$*"; log "$*"; }
# --- preflight ---------------------------------------------------------------
[[ -r "$GITEA_TOKEN_FILE" ]] || { log "FAIL: missing $GITEA_TOKEN_FILE"; exit 0; }
[[ -r "$GITHUB_TOKEN_FILE" ]] || { log "FAIL: missing $GITHUB_TOKEN_FILE"; exit 0; }
GITEA_TOKEN="$(<"$GITEA_TOKEN_FILE")"
GITHUB_TOKEN="$(<"$GITHUB_TOKEN_FILE")"
# --- helpers -----------------------------------------------------------------
gitea_repos() {
# echoes one repo name per line for $GITEA_OWNER
# On any failure (Gitea restart, non-200, malformed JSON), prints nothing and logs a warning,
# so the cron run becomes a clean no-op instead of dumping a stack trace.
local body code tmp
tmp=$(mktemp)
code=$(curl -sS ${CURL_OPTS} -o "$tmp" -w "%{http_code}" \
-H "Authorization: token ${GITEA_TOKEN}" \
"${GITEA_BASE}/api/v1/users/${GITEA_OWNER}/repos?limit=50" 2>/dev/null || echo "000")
if [[ "$code" != "200" ]]; then
log "WARN gitea_repos: HTTP ${code} from Gitea — skipping this run"
rm -f "$tmp"
return 0
fi
python3 -c "
import sys, json
try:
data = json.load(open('${tmp}'))
except Exception as e:
sys.exit(0) # silent: logged separately
for r in data:
if r.get('owner', {}).get('login') == '${GITEA_OWNER}':
print(r['name'])
" 2>/dev/null
rm -f "$tmp"
}
github_repo_exists() {
local repo="$1"
local code
code=$(curl -sS -o /dev/null -w "%{http_code}" \
-H "Authorization: token ${GITHUB_TOKEN}" \
"${GITHUB_API}/repos/${GITHUB_USER}/${repo}")
[[ "$code" == "200" ]]
}
github_create_repo() {
local repo="$1"
local code
code=$(curl -sS ${CURL_OPTS} -o /dev/null -w "%{http_code}" -X POST \
-H "Authorization: token ${GITHUB_TOKEN}" \
-H "Content-Type: application/json" \
"${GITHUB_API}/user/repos" \
-d "{\"name\":\"${repo}\",\"private\":true,\"auto_init\":false,\"description\":\"Mirror of Gitea ${GITEA_OWNER}/${repo}\"}")
[[ "$code" == "201" ]]
}
mirror_already_configured() {
local repo="$1"
# Returns: 0 = mirror exists / 1 = does NOT exist / 2 = unknown (API failure → caller skips)
local body code tmp
tmp=$(mktemp)
code=$(curl -sS ${CURL_OPTS} -o "$tmp" -w "%{http_code}" \
-H "Authorization: token ${GITEA_TOKEN}" \
"${GITEA_BASE}/api/v1/repos/${GITEA_OWNER}/${repo}/push_mirrors" 2>/dev/null || echo "000")
if [[ "$code" != "200" ]]; then
log "WARN mirror_already_configured(${repo}): HTTP ${code} — treating as unknown"
rm -f "$tmp"
return 2
fi
python3 -c "
import sys, json
try:
mirrors = json.load(open('${tmp}'))
except Exception:
sys.exit(2)
target = 'github.com/${GITHUB_USER}/${repo}'
for m in mirrors:
if target in m.get('remote_address',''):
sys.exit(0)
sys.exit(1)" 2>/dev/null
local rc=$?
rm -f "$tmp"
return $rc
}
mirror_configure() {
local repo="$1"
local body
body=$(python3 -c "
import json
print(json.dumps({
'remote_address': f'https://github.com/${GITHUB_USER}/${repo}.git',
'remote_username': '${GITHUB_USER}',
'remote_password': '${GITHUB_TOKEN}',
'interval': '0h0m0s',
'sync_on_commit': True,
}))")
local code
code=$(curl -sS ${CURL_OPTS} -o /dev/null -w "%{http_code}" -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
"${GITEA_BASE}/api/v1/repos/${GITEA_OWNER}/${repo}/push_mirrors" \
-d "$body")
[[ "$code" == "200" || "$code" == "201" ]]
}
mirror_sync_now() {
local repo="$1"
curl -sS ${CURL_OPTS} -o /dev/null -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
"${GITEA_BASE}/api/v1/repos/${GITEA_OWNER}/${repo}/push_mirrors-sync" || true
}
# --- main loop ---------------------------------------------------------------
configured=0
already=0
errors=0
skipped=0
while IFS= read -r repo; do
[[ -z "$repo" ]] && continue
mirror_already_configured "$repo"
case $? in
0) already=$((already + 1)); continue ;; # exists
2) skipped=$((skipped + 1)); continue ;; # API failure → skip safely, retry next minute
# 1 → does not exist, fall through to configure
esac
# New repo (no mirror yet) — ensure GitHub side exists, configure mirror, kick sync
if ! github_repo_exists "$repo"; then
if github_create_repo "$repo"; then
say " created GitHub repo: ${GITHUB_USER}/${repo}"
else
say " FAIL: could not create GitHub repo ${GITHUB_USER}/${repo}"
errors=$((errors + 1))
continue
fi
fi
if mirror_configure "$repo"; then
mirror_sync_now "$repo"
configured=$((configured + 1))
say " configured mirror: ${GITEA_OWNER}/${repo} -> ${GITHUB_USER}/${repo} (sync triggered)"
else
say " FAIL: could not configure mirror on ${GITEA_OWNER}/${repo}"
errors=$((errors + 1))
fi
done < <(gitea_repos)
say "done — newly-configured: ${configured}, already-current: ${already}, skipped: ${skipped}, errors: ${errors}"
exit 0