Mirror all repos you own on GitHub (incl. archived; forks optional) to GitLab, enforcing private visibility, with parallel jobs, retries, and Discord notifications.
- Includes public + private + archived repositories
- Includes forks by default (see Skip forks to exclude them)
- Forces GitLab visibility = private for every mirrored project
- Parallel mirroring (up to 25 concurrent jobs) with 2x retry on failure
- Discord webhook notifications with per-repo status breakdown
- Zero per-repo config — run it from a single backup repo
.github/workflows/mirror.yml # Workflow with 3 jobs: discover → backup → notify
scripts/
discover.js # Lists all repos you OWN (includes forks + archived) → JSON array of names
ensure.js # Ensures GitLab project exists under your namespace; forces visibility=private
mirror.js # Mirrors a single repo (git clone --mirror → git push --mirror)
notify.js # Sends a Discord notification with run summary (updated/unchanged/failed counts)
- Node.js 20 (set up automatically by the workflow)
- Secrets (in the backup repo → Settings → Secrets and variables → Actions):
| Secret | Purpose |
|---|---|
GH_PAT |
GitHub Personal Access Token — fine-grained with Contents: Read and Metadata: Read scopes (must include private repos you own) |
GITLAB_TOKEN |
GitLab Personal Access Token with api scope |
GITLAB_HOST |
GitLab hostname, usually gitlab.com (or your self-hosted domain) |
GITLAB_NAMESPACE |
Your GitLab username or group path where projects should live (e.g. decoded-cipher) |
DISCORD_WEBHOOK_URL |
Discord webhook URL for run notifications (optional — the notify job exits gracefully if missing) |
GH_USERis taken automatically from${{ github.repository_owner }}in the workflow. No need to set it manually.
- Push the files to your backup repo
- Go to Actions → GH → GL Backup (staged, parallel) → Run workflow
The workflow is pre-configured to run every Monday at 00:00 UTC via cron (0 0 * * 1). To change the schedule, edit the cron expression in .github/workflows/mirror.yml:
on:
schedule:
- cron: '0 0 * * 1' # every Monday at 00:00 UTC
workflow_dispatch:- Runs
scripts/discover.jsusing your GitHub token - Collects all repos you own (public, private, archived — and forks by default)
- Emits a JSON array of repo names to the next job
For each repo name (matrix, max-parallel: 25, fail-fast: false):
- Ensure —
scripts/ensure.js- Resolves your GitLab namespace (user or group)
- Creates the project if it doesn't exist and forces visibility to
private
- Mirror —
scripts/mirror.jsgit clone --mirrorfrom GitHub →git push --mirrorto GitLab- Writes a per-repo result file (
updated/unchanged/failed) for the notify step - Step has 2x retry with backoff to handle transient failures
- Upload — per-repo result files are uploaded as artifacts for the notify job
- Downloads all mirror result artifacts
- Runs
scripts/notify.jsto send a Discord embed with:- Overall status (success / failure)
- Updated, unchanged, and failed repo counts
- Links to updated and failed repos
- Run duration and next scheduled run time
By default discover.js includes forks. To exclude them, change one line in scripts/discover.js:
// Before (includes forks):
if (r.owner?.login === GH_USER) names.push(r.name);
// After (skips forks):
if (r.owner?.login === GH_USER && !r.fork) names.push(r.name);Currently ensure.js forces every project to private. To mirror visibility from GitHub (public → public, private → private), pass visibility from discover.js through the matrix and update ensure.js to set it accordingly.
In the backup job strategy:
strategy:
max-parallel: 25Lower this if you hit rate limits; raise it for speed if your runner and network allow.
This flow uses git clone --mirror and git push --mirror, which mirror refs and LFS pointers only.
If you need to back up LFS objects as well, augment mirror.js to install git-lfs and run:
git lfs install
git lfs fetch --all
git lfs push --all "<gitlab-remote-url>"Consider enabling this only for repos that actually use LFS.