Files
JMR-devandClaude Opus 5.5 f672019c91 Add the GitHub to Gitea migration scripts
These moved every non-fork JMR-dev repository from GitHub into this Gitea
instance and made GitHub a push mirror. They are kept for re-runs and
for rotating the mirror PAT, which expires and fails silently.

- migrate.py    full one-time migration (code, issues, PRs, releases,
                wiki, LFS); skips anything already on Gitea
- verify.py     compares branch/tag shas, issue/PR/release counts, and
                the archived and visibility flags; read-only
- repoint.py    re-points local clones' JMR-dev remotes at Gitea,
                following GitHub renames; dry run unless --apply
- pushmirror.py sync-on-commit push mirrors to GitHub, created only when
                every GitHub branch and tag already matches Gitea, so
                the first force-push/prune cannot delete anything

Tokens come from the environment, sourced from Secret Manager. None
appear in these files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:16:18 +07:00
..

GitHub → Gitea migration

These scripts moved every non-fork repository owned by JMR-dev from GitHub into this Gitea instance and made Gitea the primary. GitHub is now a push mirror: Gitea pushes every commit to it. They are kept here for re-runs and for rotating the mirror token.

All of them are standard-library Python. They read tokens from the environment, never from arguments or files. verify.py, pushmirror.py and repoint.py also read GitHub through the local gh CLI.

Script What it does Writes to
migrate.py Full one-time migration: code, issues, PRs, releases, labels, milestones, wiki, LFS. Archives on Gitea whatever is archived on GitHub. Gitea only
verify.py Compares every branch and tag sha, the issue/PR/release counts, and the archived flag and visibility. nothing
repoint.py Re-points local clones' JMR-dev GitHub remotes at Gitea, following renames. Dry run unless --apply. local .git/config
pushmirror.py Creates a sync-on-commit push mirror to GitHub for each active repository. Gitea, then GitHub through the mirror

The input is a snapshot of the repository list:

gh repo list JMR-dev --limit 1000 \
  --json name,visibility,isFork,isArchived,diskUsage,description,defaultBranchRef > repos.json

Tokens

Every token lives in Secret Manager in the Gitea project and is passed in by environment:

export GITEA_TOKEN=$(gcloud secrets versions access latest --secret=gitea-migration-token --project=<project>)
export GITHUB_TOKEN=$(gcloud secrets versions access latest --secret=github-migration-pat --project=<project>)  # migrate.py
export MIRROR_PAT=$(gcloud secrets versions access latest --secret=github-mirror-pat --project=<project>)       # pushmirror.py
  • GITEA_TOKEN needs write:repository and read:issue.
  • GITHUB_TOKEN for migrating should be a read-only fine-grained PAT (Contents, Metadata, Issues, Pull requests). Read-only cannot see draft releases; copy those by hand.
  • MIRROR_PAT needs Contents and Workflows read & write. Without Workflows, GitHub rejects any push that touches .github/workflows/.

Why they are safe to re-run

  • migrate.py skips any repository that already exists on Gitea. It never deletes one in order to retry.
  • pushmirror.py skips repositories that already have a GitHub push mirror. It also refuses to create one unless every GitHub branch and tag already matches Gitea. A push mirror force-pushes and prunes, so this check is what guarantees the first sync cannot overwrite or delete anything on GitHub.
  • Archived repositories never get a push mirror, because GitHub rejects pushes to them.

Rotating the mirror PAT

Each mirror stores its own copy of the PAT, and an expired PAT fails silently: the only sign is the error on the repository's Settings → Mirror page. To rotate:

  1. Store the new token as a new version of github-mirror-pat.
  2. Delete each repository's GitHub mirror with DELETE /api/v1/repos/JMR-dev/<repo>/push_mirrors/<remote_name>.
  3. Re-run pushmirror.py.

The refs check runs again in step 3. Anything pushed to GitHub directly in the meantime shows up as blocked rather than being overwritten.

Behaviour worth knowing

  • New commits and branches reach GitHub within seconds.
  • A bare branch delete does not trigger a sync. It reaches GitHub at the next push that carries commits, or at the 8-hour interval.
  • Branches created on GitHub, such as Dependabot's, are pruned by the next sync.
  • GitHub Actions on: push workflows run for mirrored pushes.