PowerShell 7+ tool for two things:
- One-time migration of every repo from an old GitHub account to a new
one (
Migrate-ToNewAccount.ps1). - Ongoing backup of the new account's repos to GitLab (
backup.ps1).
Both create destination repos as private. The old GitHub account is never modified.
For each repo owned by your old account (the OldGitHubUser in config.psd1):
- If the new account doesn't already have a repo with that name, create one
(private; description copied if
PreserveDescription). git clone --mirrorfrom old (or fetch updates if cache exists).- If the repo uses LFS, fetch LFS blobs.
- Point the cache's
originat the new account. git push --mirror(andgit lfs push --allif applicable) to the new account.- Set the new repo's default branch to match the source (if
PreserveDefaultBranch).
Idempotent: safe to re-run after fixing partial failures.
For each repo owned by your new account (the GitHubUser in config.psd1):
git remote update --pruneagainst the cache (origin already points at the new account after migration).- If LFS is in use, fetch LFS blobs.
- Ensure a matching GitLab project exists under your personal namespace
(
gitlab.com/<your-handle>/<repo>); create private if not. git push --mirrorto GitLab (+git lfs push --allif applicable).
Forks are skipped by default; archived repos are included. Both are configurable.
- Windows 10/11
- PowerShell 7+ (
pwsh) - Git for Windows (with
git-lfsif you use LFS) - PATs:
- Old GitHub account (configured via
OldGitHubUser) — needed only during migration. Fine-grained PAT, read-only:Contents: Read+Metadata: Readon all repos. - New GitHub account (configured via
GitHubUser). For migration it needsAdministration: Write(to create repos) +Contents: Read/Write(to push) +Workflows: Read/Write(GitHub rejects pushes that touch.github/workflows/*without this) +Metadata: Read. For ongoing backup, onlyContents: Read+Metadata: Readare required — you can rotate down to a read-only PAT after migration if you want. - GitLab PAT with
apiscope.
- Old GitHub account (configured via
- No PowerShell module dependencies — the script talks to Windows Credential Manager directly via P/Invoke (the PSGallery
CredentialManagermodule doesn't work in PS7).
# 1. Copy the config template and edit it with your handles, cache path, etc.
# config.psd1 is gitignored so your personal values are never committed.
Copy-Item config.example.psd1 config.psd1
# ...then edit config.psd1
# 2. Store the NEW-account GitHub PAT + GitLab PAT, register Event Log
# source, create GitLab group.
pwsh -File .\Register-Setup.ps1
# 3. One-time: migrate old account -> new account. Will prompt for OLD PAT.
pwsh -File .\Migrate-ToNewAccount.ps1 -DryRun # preview
pwsh -File .\Migrate-ToNewAccount.ps1 # do it
# 4. Ongoing: back up new account -> GitLab.
pwsh -File .\backup.ps1
# 5. Optional: schedule the ongoing backup.
pwsh -File .\Install-Schedule.ps1- Self-elevates to admin (needed once to register the Event Log source)
- Prompts for the new-account GitHub PAT and the GitLab PAT
- Verifies the GitLab PAT works and reports the namespace mirrors will land
in (
gitlab.com/<your-handle>/<repo>) - Creates the cache + log directories
- Prompts for the old-account PAT if not already stored
- Lists every repo on the old account, applying the same
SkipForks/SkipArchivedrules as backup - For each repo: creates a private repo on the new account if needed, bare-mirrors it across, sets default branch, and repoints the cache's origin at the new account
- Pass
-DryRunto see what would be migrated without doing anything
Manually:
pwsh -File .\backup.ps1Output is written both to the console and to a timestamped log file under the
configured LogPath. Exit codes:
0— all repos succeeded1— one or more repos failed (others may have succeeded)2— fatal error before per-repo work started
Per-repo failures continue with the next repo. Failures also write an entry to
the Windows Application Event Log under source GitHubBackup, so Task
Scheduler will surface them.
pwsh -File .\Install-Schedule.ps1 # daily 03:00, default
pwsh -File .\Install-Schedule.ps1 -Time 02:30 -Frequency WeeklyThe task is registered as Interactive, which means it runs only while you
are logged on. If you need it to run when logged off, edit the task's
principal afterward (Task Scheduler → properties → security options),
which will require you to enter your Windows password.
config.psd1:
| Key | Default | Notes |
|---|---|---|
GitHubUser |
your-github-username |
New primary account. Determines backup source and migration target. |
OldGitHubUser |
your-old-github-username |
Migration source only. Not touched by backup.ps1. |
GitLabHost |
https://gitlab.com |
Change for self-managed GitLab. |
CachePath |
C:\github-backup-cache |
Bare clones, one per repo. Migration leaves these with origin pointing at the new account. Prefer a drive with room for a full mirror of every repo — point it at a data drive (e.g. D:\) if your system drive is small. |
LogPath |
C:\github-backup-cache\logs |
One log file per run, timestamped. |
EventLogSource |
GitHubBackup |
Windows Application Event Log source. |
GitHubCredentialName |
github-backup/github-pat |
New-account PAT target name in Credential Manager. |
OldGitHubCredentialName |
github-backup/old-github-pat |
Old-account PAT, only present during migration. |
GitLabCredentialName |
github-backup/gitlab-pat |
GitLab PAT target name. |
LfsHandling |
auto |
auto detects via .gitattributes per repo; skip never fetches LFS blobs. |
GitLabVisibility |
private |
Visibility for newly created GitLab projects. |
NewGitHubVisibility |
private |
Visibility for repos created on the new GitHub account during migration. Always private by design. |
PreserveDescription |
$true |
Copy repo description across during migration. |
PreserveDefaultBranch |
$true |
Set new repo's default branch to match the source. |
SkipForks |
$true |
Forks usually duplicate upstream; skip by default. |
SkipArchived |
$false |
Archived repos are included by default. |
backup.ps1 ongoing: new GitHub -> GitLab
Migrate-ToNewAccount.ps1 one-time: old GitHub -> new GitHub
Register-Setup.ps1 credential + group + event-log setup
Install-Schedule.ps1 register Task Scheduler entry
+ config.psd1
+ lib/
Credentials.psm1 Windows Credential Manager wrappers
GitHub.psm1 repo list/get/create + default-branch (REST)
GitLab.psm1 Get current user + Get/New project (REST v4)
Mirror.psm1 Sync-Mirror (backup) + Sync-MigrationMirror (one-time)
Logging.psm1 Write-Log + Write-FailureToEventLog
Tokens are injected per git invocation via
git -c http.<host>/.extraheader=Authorization: Basic <base64> — they are
never written into the bare cache's git config or remote URLs.
Mirror semantics:
push --mirroris a force-style operation that makes GitLab refs identical to the local cache. A branch deleted on GitHub disappears from GitLab on the next run.- A whole repo deleted, renamed, or made private on GitHub simply stops appearing in the listing. The corresponding GitLab project is left untouched — cold backup behavior.
- A renamed GitHub repo becomes a new GitLab project under the new name; the old one becomes a stale snapshot.
No stored credential named '...' — run Register-Setup.ps1. Credentials
are stored under your Windows user only; another user (or the same user after
profile reset) won't see them.
Could not write to Event Log — the source isn't registered. Run
Register-Setup.ps1 once as admin (it self-elevates).
Push to new GitHub fails with refusing to allow a Personal Access Token to create or update workflow ... without 'workflow' scope — the new-account PAT is missing the Workflows: Read and write permission. Edit the PAT at https://github.com/settings/personal-access-tokens, add the permission, save, then re-run the migration. The fix is idempotent.
GitLab returns 403 Forbidden on project creation — the new-account
identity-verification gate on gitlab.com. Visit
https://gitlab.com/-/identity_verification and complete it (typically by
adding a credit card — GitLab doesn't charge it). The gate only blocks
top-level group creation, not personal-namespace projects, so this is
unlikely to bite the current design.
git lfs: command not found — install Git LFS
(winget install GitHub.GitLFS) or set LfsHandling = 'skip' in
config.psd1.
Scheduled task doesn't run — the task is registered as Interactive, so
it only fires while you're logged on. To make it run while logged off, edit
the task's principal in Task Scheduler (you'll be prompted for your Windows
password).
Bug reports and pull requests are welcome. See CONTRIBUTING.md for how to set up the pre-commit secret scan and what to check before opening a PR.
This tool handles GitHub and GitLab access tokens. To report a vulnerability, see SECURITY.md — please do not open a public issue for security problems.
Licensed under the Apache License, Version 2.0. See NOTICE for attribution details.