- Shell 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
Was vague about 'create a token'; explicit now: which scope, where to find the menu (Forgejo + Gitea), where the repos and gists land, and what to do when a run dies halfway (re-run; mirror-push is idempotent). |
||
| github-backup.sh | ||
| push-to-forgejo.sh | ||
| README.md | ||
github-backup
A small bash script that snapshots everything in your GitHub account to a local directory: repos (mirror clones), wikis, gists, issues/PRs/releases/labels/milestones as JSON, your profile, your stars, your followers, your SSH and GPG keys.
Useful for:
- Periodic offline archival
- Migrating to a self-hosted forge (Forgejo, Gitea, GitLab — push the mirror clones)
- Knowing that whatever GitHub decides tomorrow, you have a byte-exact copy today
What it produces
~/github-backup/
├── account/
│ ├── profile.json
│ ├── starred.json ← repos you've starred (outbound)
│ ├── followers.json
│ ├── following.json
│ ├── watching.json
│ ├── gpg_keys.json
│ ├── ssh_keys.json
│ └── repos.json ← the list this script iterates over
├── repos/
│ └── <name>.git/ ← bare mirror clone, fetch-on-rerun via `remote update`
├── wikis/
│ └── <name>.wiki.git/
├── gists/
│ └── <gist_id>/
├── meta/
│ └── <name>/
│ ├── issues.json
│ ├── pulls.json
│ ├── releases.json
│ ├── labels.json
│ └── milestones.json
└── backup.log
Requirements
gh(GitHub CLI), logged in viagh auth loginwith therepoandread:userscopesgitjq- bash, coreutils
That's it. The script reads your username from gh api user --jq .login and uses gh's stored token for both API calls and git clone (via gh's credential helper) — there is no token to paste into this script.
Usage
./github-backup.sh
Re-runs are incremental: existing mirror clones get git remote update --prune, metadata JSONs get overwritten with current state. Safe to put on a cron.
Bandwidth on a re-run is roughly proportional to what's changed since last time, plus a constant ~few MB of JSON.
What it does NOT capture
- Private notes (only stored client-side in GitHub's UI)
- Discussions API content (could be added — PRs welcome)
- Codespaces / Pages build artifacts (out of scope; the source is in the repo mirror)
- Two-factor recovery codes (regenerate those separately)
- Outbound social graph at the org level (only your personal account)
Publishing the backup to a Forgejo / Gitea instance
The companion push-to-forgejo.sh script mirror-pushes every repos/*.git/ and gists/*.git/ from the backup to a self-hosted Forgejo (or Gitea) instance.
Three steps
-
Sign up on the target instance and create a personal access token with the
write:repositoryscope (pluswrite:organizationif you want the gist namespace auto-created). For Forgejo: Settings → Applications → Generate New Token. For Gitea: same path. -
Get this repo:
git clone https://git.example.com/somebody/github-backup.git cd github-backup -
Point the script at your backup + your forge and run it:
export FORGEJO_HOST=https://git.example.com export FORGEJO_USER=yourname # the namespace repos land under export FORGEJO_TOKEN=<the token from step 1> ./push-to-forgejo.sh
That's it. Every repo lands under git.example.com/yourname/<repo> and gists land under git.example.com/gists/<id> (override the gist org with GIST_ORG=somethingelse).
Idempotency
- Repos that already exist on the target return 409 from the create call — the script just skips that step and still mirror-pushes, so any commits added since the previous run land.
- Forgejo's
git push --mirroroverwrites refs cleanly, so this is safe to cron right aftergithub-backup.sh.
What if it dies halfway
Just re-run. ~/github-backup/push.log records what was attempted; the next run picks up exactly where this one stopped, modulo the few seconds of overlap.
Sister scripts (not yet published)
scrub-and-deploy.sh— strip secrets frommeta/JSONs before re-publishing them anywhereupload-and-adopt.sh— register the mirrors with a content-tracking tool
Open an issue if you want either of those.
License
Public domain / CC0. Do whatever you want with it.