Add authenticated GitHub PAT for Forgejo mirror sync (#269)

## Summary

- **mirror-create**: Auto-includes GitHub PAT from 1Password for authenticated upstream fetches at mirror creation time
- **mirror-update-pats**: New mise task that SSHes into indri and rewrites the git remote URL in every GitHub mirror's bare repo config to embed the PAT. Idempotent, supports `--dry-run`
- **app.ini.j2**: Explicit `[mirror]` section with `DEFAULT_INTERVAL = 8h` and `MIN_INTERVAL = 10m` (bakes in the defaults for visibility)
- **manage-forgejo-mirrors**: New how-to doc covering mirror creation, PAT storage, the `mirror-update-pats` task, and the full 20-day PAT rotation procedure

## Context

GitHub tightened unauthenticated rate limits for git clone/fetch in May 2025. With 23 GitHub mirrors syncing every 8 hours, authenticated fetches avoid throttling. The PAT is stored in 1Password (`Forgejo Secrets` → `github-mirror-pat`) and has been applied to all existing mirrors.

## Deployment and Testing

- [x] `mirror-update-pats` dry-run verified (23 mirrors detected)
- [x] `mirror-update-pats` applied to all 23 GitHub mirrors on indri
- [x] Idempotency confirmed (re-run shows 0 updated, 23 skipped)
- [ ] Provision indri with `--tags forgejo` to apply `[mirror]` config
- [ ] Trigger a manual mirror sync and verify success in Forgejo UI

Reviewed-on: https://forge.ops.eblu.me/eblume/blumeops/pulls/269
This commit is contained in:
Erich Blume 2026-02-25 20:20:23 -08:00
commit 84338c32c2
7 changed files with 234 additions and 2 deletions

69
mise-tasks/mirror-update-pats Executable file
View file

@ -0,0 +1,69 @@
#!/usr/bin/env bash
#MISE description="Update GitHub PAT on all mirror repos on indri"
#USAGE flag "--dry-run" help="Show what would be done without changing anything"
set -euo pipefail
OP_GITHUB_PAT_REF="op://blumeops/w3663ffnvkewbftncqxtcpeavy/github-mirror-pat"
REPO_BASE="/opt/homebrew/var/forgejo/data/forgejo-repositories"
DB_PATH="/opt/homebrew/var/forgejo/data/forgejo.db"
DRY_RUN="${usage_dry_run:-false}"
echo "Reading GitHub PAT from 1Password..."
pat="$(op read "$OP_GITHUB_PAT_REF")"
if [[ -z "$pat" ]]; then
echo "Error: GitHub PAT is empty"
exit 1
fi
echo "Querying mirrors on indri..."
# Get all GitHub mirrors: org_name, repo_name, upstream_url
mirrors=$(ssh indri "sqlite3 '$DB_PATH' \"
SELECT u.name, r.name, m.remote_address
FROM mirror m
JOIN repository r ON m.repo_id = r.id
JOIN [user] u ON r.owner_id = u.id
WHERE m.remote_address LIKE '%github.com%'
\"" 2>/dev/null)
if [[ -z "$mirrors" ]]; then
echo "No GitHub mirrors found."
exit 0
fi
updated=0
skipped=0
while IFS='|' read -r org repo upstream_url; do
bare_repo="${REPO_BASE}/${org}/${repo}.git"
# Build authenticated URL: https://eblume:<pat>@github.com/...
auth_url="${upstream_url/https:\/\/github.com/https:\/\/eblume:${pat}@github.com}"
if [[ "$DRY_RUN" == "true" ]]; then
echo "[dry-run] ${org}/${repo}: would set origin to authenticated URL"
((updated++))
continue
fi
# Check current remote URL (< /dev/null prevents ssh from consuming loop stdin)
current_url=$(ssh indri "git -C '${bare_repo}' config remote.origin.url" < /dev/null 2>/dev/null || echo "")
if [[ "$current_url" == "$auth_url" ]]; then
echo " ${org}/${repo}: already up to date"
((skipped++))
continue
fi
ssh indri "git -C '${bare_repo}' remote set-url origin '${auth_url}'" < /dev/null 2>/dev/null
echo " ${org}/${repo}: updated"
((updated++))
done <<< "$mirrors"
echo
echo "Done. Updated: ${updated}, Skipped: ${skipped}"
if [[ "$DRY_RUN" == "true" ]]; then
echo "(dry-run mode — no changes were made)"
fi