#!/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