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>
This commit was merged in pull request #3.
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user