Files
Gitea/scripts/github-migration/README.md
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

71 lines
3.5 KiB
Markdown

# 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:
```bash
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:
```bash
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.