Files
Alex Mikhalev 1c8ab5939e refactor: Replace GitHub Actions mirroring with Gitea native pull mirror
- 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>
2026-02-18 15:24:37 +01:00

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 push
  • mirror-check.yml -- checked sync status every 6 hours

Problems:

  1. GitHub Actions billing issues caused silent failures
  2. Required storing a GITEA_TOKEN secret in GitHub
  3. Consumed GitHub Actions minutes
  4. 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)