Files
gitea-mirror-sync/mirror-github-to-gitea.sh
T
2026-08-19 08:25:30 +03:00

118 lines
4.1 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
# --- Config ---
GITHUB_USER="andwati"
GITHUB_TOKEN="${GITHUB_TOKEN:?Set GITHUB_TOKEN env var (PAT with repo read access, incl. private repos)}"
GITEA_URL="${GITEA_URL:?Set GITEA_URL env var, e.g. https://gitea.andwati.com}"
GITEA_TOKEN="${GITEA_TOKEN:?Set GITEA_TOKEN env var (Gitea access token, repo scope)}"
GITEA_OWNER="${GITEA_OWNER:-andwati}"
GITEA_URL="${GITEA_URL%/}" # a trailing slash yields //api/v1/... which some proxies mangle
MIRROR_INTERVAL="${MIRROR_INTERVAL:-8h0m0s}"
# Optional: process a single repo instead of all of them, e.g. ONLY=blog ./mirror-github-to-gitea.sh
# Useful for smoke-testing one repo before turning the script loose on the whole account.
ONLY="${ONLY:-}"
# --- Fetch all repos for the GitHub user (paginated), including private ones ---
# Uses /user/repos (authenticated, sees private repos owned by the token's user)
# rather than /users/{user}/repos (public-only).
page=1
repos_json="[]"
while :; do
resp=$(curl -s -H "Authorization: token ${GITHUB_TOKEN}" \
"https://api.github.com/user/repos?per_page=100&page=${page}&affiliation=owner")
count=$(echo "$resp" | jq 'length')
[ "$count" -eq 0 ] && break
repos_json=$(jq -s '.[0] + .[1]' <(echo "$repos_json") <(echo "$resp"))
page=$((page + 1))
done
total=$(echo "$repos_json" | jq 'length')
echo "Found ${total} repos on GitHub for ${GITHUB_USER}"
if [ -n "$ONLY" ]; then
echo "ONLY=${ONLY} set - skipping every other repo."
fi
matched=0
while read -r entry; do
name=$(echo "$entry" | jq -r '.name')
private=$(echo "$entry" | jq -r '.private')
if [ -n "$ONLY" ] && [ "$name" != "$ONLY" ]; then
continue
fi
matched=$((matched + 1))
echo "Processing: ${name} (private=${private})"
# Check if it already exists in Gitea
existing_resp=$(curl -s -w "\n%{http_code}" \
-H "Authorization: token ${GITEA_TOKEN}" \
"${GITEA_URL}/api/v1/repos/${GITEA_OWNER}/${name}")
existing_status=$(echo "$existing_resp" | tail -n1)
existing_body=$(echo "$existing_resp" | sed '$d')
if [ "$existing_status" == "200" ]; then
gitea_private=$(echo "$existing_body" | jq -r '.private')
if [ "$gitea_private" != "$private" ]; then
echo " Already mirrored, but visibility differs (GitHub private=${private}, Gitea private=${gitea_private}). Syncing..."
patch_code=$(curl -s -o /tmp/gitea_patch_resp.json -w "%{http_code}" \
-X PATCH "${GITEA_URL}/api/v1/repos/${GITEA_OWNER}/${name}" \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"private\": ${private}}")
if [ "$patch_code" == "200" ]; then
echo " Visibility updated to private=${private}."
else
echo " Failed to update visibility (HTTP ${patch_code}):"
cat /tmp/gitea_patch_resp.json; echo
fi
else
echo " Already mirrored, visibility already matches (private=${gitea_private}). Skipping."
fi
continue
fi
payload=$(jq -n \
--arg clone_addr "https://github.com/${GITHUB_USER}/${name}.git" \
--arg repo_name "$name" \
--arg repo_owner "$GITEA_OWNER" \
--arg mirror_interval "$MIRROR_INTERVAL" \
--argjson private "$private" \
--arg auth_token "$GITHUB_TOKEN" \
'{
clone_addr: $clone_addr,
repo_name: $repo_name,
repo_owner: $repo_owner,
mirror: true,
mirror_interval: $mirror_interval,
private: $private,
service: "github",
auth_token: $auth_token
}')
http_code=$(curl -s -o /tmp/gitea_migrate_resp.json -w "%{http_code}" \
-X POST "${GITEA_URL}/api/v1/repos/migrate" \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
--data-raw "$payload")
if [ "$http_code" == "201" ]; then
echo " Created mirror (private=${private})."
else
echo " Failed (HTTP ${http_code}):"
cat /tmp/gitea_migrate_resp.json; echo
fi
sleep 1 # be gentle on both APIs
done < <(echo "$repos_json" | jq -c '.[] | {name, private}')
if [ -n "$ONLY" ] && [ "$matched" -eq 0 ]; then
echo "No repo named '${ONLY}' found among the ${total} repos on GitHub. Nothing done."
exit 1
fi
echo "Done."