- Remove sync-to-gitea.yml and mirror-check.yml GitHub Actions workflows - Set up Gitea as pull mirror from GitHub (1h interval, via POST /repos/migrate) - Rewrite MIRRORING-GUIDE.md with pull mirror setup and gotchas - Update HANDOVER.md and lessons-learned.md with new approach Gitea now automatically pulls from GitHub. No Actions, no dual-push, no billing dependency. Push to GitHub only; Gitea syncs within 1 hour. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
5.6 KiB
Repository Mirroring: GitHub -> Gitea Pull Mirror
Overview
GitHub is the primary repository. Gitea automatically pulls from GitHub every hour using its native pull mirror feature. No GitHub Actions, no manual dual-push, no cron jobs.
Developer -> git push -> GitHub (primary)
|
v (automatic, every 1h)
Gitea (read-only mirror)
Current Configuration
| Setting | Value |
|---|---|
| Primary | https://github.com/terraphim/gitea-infrastructure (private) |
| Mirror | https://git.terraphim.cloud/terraphim/gitea-infrastructure |
| Direction | GitHub -> Gitea (one-way pull) |
| Interval | 1 hour |
| Service | github (uses GitHub API for private repo access) |
| Auth | GitHub token stored in Gitea migration config |
Daily Workflow
Push to GitHub only. Gitea syncs automatically.
git push origin main
That is all. Gitea pulls within 1 hour.
Manual Sync
Trigger an immediate sync without waiting for the interval:
source ~/op_zesticai_non_prod.sh
GITEA_TOKEN=$(op read "op://TerraphimPlatform/git.terraphim.cloud-admin-token/credential")
curl -s -X POST \
"https://git.terraphim.cloud/api/v1/repos/terraphim/gitea-infrastructure/mirror-sync" \
-H "Authorization: token $GITEA_TOKEN"
Check Mirror Status
source ~/op_zesticai_non_prod.sh
GITEA_TOKEN=$(op read "op://TerraphimPlatform/git.terraphim.cloud-admin-token/credential")
curl -s "https://git.terraphim.cloud/api/v1/repos/terraphim/gitea-infrastructure" \
-H "Authorization: token $GITEA_TOKEN" | \
jq '{mirror, mirror_interval, mirror_updated, empty, size}'
Change Sync Interval
curl -s -X PATCH \
"https://git.terraphim.cloud/api/v1/repos/terraphim/gitea-infrastructure" \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"mirror_interval": "30m"}'
Minimum interval is 10 minutes (enforced by Gitea server config).
Recreating the Mirror
If the mirror breaks (e.g., GitHub token expires, repo gets corrupted):
source ~/op_zesticai_non_prod.sh
GITEA_TOKEN=$(op read "op://TerraphimPlatform/git.terraphim.cloud-admin-token/credential")
GH_TOKEN=$(gh auth token)
# Step 1: Delete existing mirror
curl -s -X DELETE \
"https://git.terraphim.cloud/api/v1/repos/terraphim/gitea-infrastructure" \
-H "Authorization: token $GITEA_TOKEN"
# Step 2: Wait for deletion to complete
sleep 10
# Step 3: Verify deleted
curl -s "https://git.terraphim.cloud/api/v1/repos/terraphim/gitea-infrastructure" \
-H "Authorization: token $GITEA_TOKEN" -w "\nHTTP %{http_code}\n"
# Should return HTTP 404
# Step 4: Recreate as pull mirror
curl -s -X POST "https://git.terraphim.cloud/api/v1/repos/migrate" \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"clone_addr\": \"https://github.com/terraphim/gitea-infrastructure\",
\"repo_name\": \"gitea-infrastructure\",
\"repo_owner\": \"terraphim\",
\"service\": \"github\",
\"auth_token\": \"$GH_TOKEN\",
\"mirror\": true,
\"mirror_interval\": \"1h\"
}"
# Step 5: Verify (wait a few seconds for async clone)
sleep 15
curl -s "https://git.terraphim.cloud/api/v1/repos/terraphim/gitea-infrastructure" \
-H "Authorization: token $GITEA_TOKEN" | \
jq '{id, mirror, mirror_interval, mirror_updated, empty, size}'
# Should show: mirror: true, empty: false
Critical Gotchas
1. Existing repos cannot be converted to mirrors
Gitea only creates mirrors via the POST /repos/migrate endpoint. There is no way to convert an existing repo to a mirror. You must delete and recreate.
2. The service field matters for private repos
| Service | Behavior |
|---|---|
github |
Uses GitHub API. Requires auth_token for private repos. |
git |
Uses git clone. Fails with "terminal prompts disabled" for private repos. |
Always use service: "github" with a valid GitHub token for private repositories.
3. Failed migrations leave empty repos
If the async clone fails (bad token, network error), Gitea still creates the repo entry with mirror: true, empty: true. The mirror-sync endpoint returns "Repository is not a mirror" on these broken repos. Delete and recreate.
4. The github.personal.token in 1Password is expired
As of 2026-02-18, the op://TerraphimPlatform/github.personal.token/token returns a token that GitHub rejects with HTTP 401. Use gh auth token instead for a working token.
5. Wait between delete and create
Gitea needs time to fully delete a repo. Without a sleep 10 between delete and create, you get 409 Conflict: The repository with the same name already exists.
Why Not GitHub Actions?
The previous approach used two GitHub Actions workflows:
sync-to-gitea.yml-- pushed to Gitea on every GitHub pushmirror-check.yml-- checked sync status every 6 hours
Problems:
- GitHub Actions billing issues caused silent failures
- Required storing a
GITEA_TOKENsecret in GitHub - Consumed GitHub Actions minutes
- Added unnecessary complexity
The Gitea pull mirror replaces both workflows with zero configuration on the GitHub side.
API Reference
| Method | Endpoint | Purpose |
|---|---|---|
POST |
/api/v1/repos/migrate |
Create repo as pull mirror |
POST |
/api/v1/repos/{owner}/{repo}/mirror-sync |
Trigger immediate sync |
PATCH |
/api/v1/repos/{owner}/{repo} |
Change mirror_interval |
GET |
/api/v1/repos/{owner}/{repo} |
Check mirror status |
DELETE |
/api/v1/repos/{owner}/{repo} |
Delete (before recreating) |