Back up every GitHub repository to Google Drive — code, issues, PRs, releases, wiki, labels, milestones — and restore with one click.
Runs automatically every day at 02:00 UTC via GitHub Actions. A light-mode dashboard hosted on GitHub Pages lets you trigger backups, browse sessions, generate reports, and manage settings — no server required.
Live Dashboard · Releases · Actions
Full technical user guide: USERGUIDE.md
Try it instantly at the Live Dashboard — click "Explore with sample data →" to enter Demo Mode. No sign-in required: the dashboard loads realistic sample repositories, backup sessions, workflow runs, and reports so you can explore every screen before connecting your own accounts. Sign in with Google (Firebase Authentication) to connect your real GitHub and Google Drive.
A polished Firebase-authenticated login screen gates the dashboard, with Google Sign-In and a one-click Demo Mode. The main overview shows repository count, backup session count, time since last run, and active workflow status — each stat card now includes a trend indicator. A colored top accent bar, the signed-in user's avatar in the nav, and refreshed Inter typography round out the v2.1.0 visual overhaul.
Select individual repos or back up everything in one shot. Toggle exactly what to include — source code, issues, PRs, releases, wiki, labels, and milestones — then trigger the GitHub Actions workflow instantly from the dashboard.
Browse timestamped backup sessions loaded live from Google Drive. Select a session, optionally filter repos and specify a target owner, then trigger the restore workflow directly without leaving the dashboard.
Track backup success rates, consecutive-day streaks, failure counts, and full run history. Every backup and restore run is listed with its status, trigger type, duration, and a direct link. Export the full history as a CSV with one click.
The Session Diff view compares any two backup sessions side by side — showing added, removed, changed, and unchanged repositories with exact byte deltas so you can spot unexpected growth or missing repos instantly.
The dashboard automatically detects when a backup session size deviates significantly from the 7-day rolling average. An amber banner surfaces the alert immediately so you can investigate before a silent data loss becomes a problem.
Configure your GitHub Personal Access Token, connect Google Drive via OAuth, verify your backup folder ID, and review all required Actions secrets. The five Settings tabs — Google Drive, Storage, SLA, Access Control, and Security — cover quota monitoring, S3/Azure targets, SLA tracking, restore allow-lists, and encryption. Tokens are stored in localStorage only — never sent to any third party.
Click "Explore with sample data →" on the login screen to enter Demo Mode — no authentication required. The dashboard is populated with realistic sample data (5 repositories, 47 backup sessions, workflow run history including a simulated failure) and a persistent banner reminds you that live actions are disabled until you sign in.
Install the dashboard directly on your desktop or mobile home screen. The included service worker caches the app shell for instant offline loading — no app store needed.
| Feature | Details |
|---|---|
| Full backup | Git mirror (all branches + tags), issues, PRs, releases, wiki, labels, milestones |
| Full restore | Recreates repos on GitHub, pushes all branches and tags, rebuilds labels and milestones |
| Reports dashboard | Success rate, streak counter, per-run breakdown, CSV export |
| Light-mode dashboard | Static HTML on GitHub Pages — no server, no build step |
| Daily automation | GitHub Actions cron at 02:00 UTC, plus manual trigger at any time |
| Selective backup | Pick specific repos and choose exactly which data types to include |
| Private repo support | Backs up both public and private repositories via PAT with repo scope |
| Drive session browser | Connect Google Drive in the dashboard to browse sessions live |
| Concurrent processing | Configurable parallel operations — default 3 repos at once |
| Rotating logs | Winston log files uploaded as GitHub Actions artifacts, retained 30 days |
| Dark mode | Toggle between light and dark themes — persists in browser |
| Keyboard shortcuts | D/B/R/W/P/S navigate pages, ? shows shortcut help |
| Toast notifications | Non-blocking success/error feedback on all workflow actions |
| Restore preview | Confirm modal shows session, target owner, and impact before restore |
| Multi-account | Add multiple GitHub accounts/orgs and switch between them |
| Retention policy | UI to configure auto-deletion of old Drive sessions (30–365 days) |
| Failure alerts | Slack and email notifications when backup fails (notify.yml) |
| Incremental backup | Optional mode that only backs up repos changed since last session |
| Auto-cleanup | Weekly workflow removes Drive sessions beyond retention threshold |
| Health status | docs/status.json updated on each run; live README badge reflects current status via shields.io dynamic badge |
| Email digest | Daily/weekly HTML summary via SendGrid — set SENDGRID_API_KEY to activate (notify.yml) |
| MS Teams webhook | Adaptive Card notifications for backup success/failure — set TEAMS_WEBHOOK_URL (notify.yml) |
| PAT rotation reminder | Weekly pat-check.yml cron warns via Teams + email when PAT is ≤7 days from expiry |
| Repo search | Filter the backup repo list by name directly in the Backup tab |
| Session diff | Compare any two backup sessions side by side — added, removed, changed repos with byte deltas |
| GFS retention | Grandfather-Father-Son policy (daily × 7, weekly × 4, monthly × 12) in cleanup.yml |
| SLA breach alerts | Hourly sla-check.yml posts Teams/email alert if backup age exceeds SLA_HOURS |
| Compliance CSV export | One-click CSV export of full run history for audit evidence packages |
| Anomaly detection | Auto-detects session size deviation >20% from 7-day average; dismissible dashboard banner |
| Azure Blob storage | STORAGE_TARGET=azure — uses @azure/storage-blob with connection string + container name |
| Backblaze B2 | STORAGE_TARGET=b2 — S3-compatible, reuses @aws-sdk/client-s3 with custom B2 endpoint |
| SBOM generation | Optional include_sbom=true input generates SPDX SBOM via anchore/sbom-action@v0 |
| Auto-restore test | Monthly monthly-restore-test.yml dry-run verifies restore integrity; result appended to audit log |
| PWA / offline | manifest.json + cache-first service worker — install dashboard to home screen, works offline |
Browser (GitHub Pages — static, no server)
│ GitHub API + Google Drive API called directly from browser
▼
GitHub Actions Workflows
backup.yml — daily cron 02:00 UTC + manual dispatch
restore.yml — manual dispatch only
│
▼
Node.js 22 (ubuntu-latest runner)
├── Clone repos via git mirror (all branches + tags)
├── Fetch issues, PRs, releases, wiki, labels, milestones
├── Zip per-repo archive + metadata.json
└── Upload to Google Drive → timestamped session folder
Google Drive
└── backup-2026-06-15T02-00-00-000Z/
├── backup-summary.json
├── api-service/
│ ├── api-service.zip
│ └── metadata.json
└── frontend-app/
├── frontend-app.zip
└── metadata.json
git clone https://github.com/OmarRao/github-gdrive-backup.git
cd github-gdrive-backup
npm install- Go to Google Cloud Console → APIs & Services → Credentials
- Enable the Google Drive API for your project
- Create an OAuth 2.0 Client ID — type: Web application
- Add
https://YOUR_GITHUB_USERNAME.github.ioas an Authorised JavaScript origin - Add
http://localhost:8080as an Authorised redirect URI - Download the JSON and save as
credentials/google-client-secret.json
python get_token.pyFollow the browser prompt. The token is auto-captured and saved to credentials/google-token.json.
Create a folder in Drive. Copy the folder ID from its URL:
https://drive.google.com/drive/folders/YOUR_FOLDER_ID
Go to Settings → Secrets and variables → Actions and add these 5 secrets:
| Secret | Value |
|---|---|
GH_BACKUP_TOKEN |
GitHub PAT with repo, workflow, read:org, read:user scopes |
GH_USER |
Your GitHub username or organisation name |
GDRIVE_FOLDER_ID |
The folder ID from step 4 |
GOOGLE_CLIENT_SECRET |
Full JSON contents of credentials/google-client-secret.json |
GOOGLE_TOKEN |
Full JSON contents of credentials/google-token.json |
Optional notification & feature secrets:
| Secret | Value |
|---|---|
SLACK_WEBHOOK_URL |
Incoming webhook URL for Slack failure alerts |
TEAMS_WEBHOOK_URL |
MS Teams incoming webhook URL for Adaptive Card notifications |
SENDGRID_API_KEY |
SendGrid API key for email digest (notify.yml, sla-check.yml, pat-check.yml) |
PAT_EXPIRY_DATE |
Your PAT expiry date in YYYY-MM-DD format — triggers rotation reminder workflow |
SLA_HOURS |
Max hours since last backup before SLA breach alert fires (default: 26) |
AZURE_STORAGE_CONNECTION_STRING |
Azure Blob connection string (when STORAGE_TARGET=azure) |
AZURE_CONTAINER_NAME |
Azure container name (default: gh-backups) |
B2_ENDPOINT |
Backblaze B2 S3-compatible endpoint URL (when STORAGE_TARGET=b2) |
B2_KEY_ID |
Backblaze B2 application key ID |
B2_APP_KEY |
Backblaze B2 application key |
B2_BUCKET |
Backblaze B2 bucket name |
Go to Actions → Scheduled GitHub → Google Drive Backup → Run workflow, or open the live dashboard and click Backup → Trigger Backup Workflow.
The dashboard at https://omarrao.github.io/github-gdrive-backup/ has six pages:
| Page | What it does |
|---|---|
| Dashboard | Stats overview and recent workflow run history |
| Backup | Select repos, choose data types, toggle incremental mode, trigger backup |
| Restore | Browse Drive sessions, preview impact, trigger restore |
| Workflow Runs | Full list of all backup and restore runs with live status |
| Reports | Success rate, streak, run breakdown table, CSV export |
| Settings — GitHub | GitHub token, username, multi-account management |
| Settings — Google Drive | OAuth connect, folder ID |
| Settings — Retention | Configure auto-deletion period and schedule |
| Settings — Actions Secrets | Reference for all required secrets |
| Settings — Setup Guide | Step-by-step onboarding |
Security: GitHub token and Drive token are stored in
localStorageonly. They are sent toapi.github.comandwww.googleapis.comrespectively — no third party ever receives them.
Press ? anywhere in the dashboard to open the shortcuts panel.
| Key | Action |
|---|---|
D |
Dashboard |
B |
Backup |
R |
Restore |
W |
Workflow Runs |
P |
Reports |
S |
Settings |
? |
Show shortcut help |
Esc |
Close modals |
GITHUB_TOKEN=ghp_...
GITHUB_USER=your-username
GOOGLE_CLIENT_SECRET_PATH=./credentials/google-client-secret.json
GOOGLE_TOKEN_PATH=./credentials/google-token.json
GDRIVE_FOLDER_ID=1abc...xyz
BACKUP_INCLUDE=code,issues,pull_requests,releases,wiki,labels,milestones
BACKUP_CONCURRENCY=3
BACKUP_TMP_DIR=./tmp
PORT=3000GDRIVE_FOLDER_ID/
└── backup-2026-06-15T02-00-00-000Z/
├── backup-summary.json
├── repo-name/
│ ├── repo-name.zip — full git mirror (all branches + tags)
│ ├── repo-name-wiki.zip — wiki mirror (if repo has one)
│ └── metadata.json — issues, PRs, releases, labels, milestones
└── ...
- Creates the target GitHub repo if it does not exist (private by default)
- Pushes all branches and tags with
--force— safe to re-run - Recreates labels and milestones exactly as backed up
- Issues and PRs are preserved in
metadata.json— the GitHub API does not support programmatic issue creation
Configure automatic deletion of old sessions in Settings → Retention. Available periods: 30, 60, 90, 180 days, or 1 year.
The cleanup.yml workflow runs every Sunday at 03:00 UTC (or on demand). It paginates through all session folders in your Drive backup folder and deletes any created before the retention cutoff.
To trigger manually: Settings → Retention → Run Cleanup Workflow, or via Actions → Cleanup Old Backup Sessions → Run workflow.
npm run backup # Back up all repos immediately
npm run restore # Restore from the latest Drive session
npm start # Self-hosted Express dashboard on http://localhost:3000
npm run dev # Dev mode with nodemon auto-reload
python get_token.py # One-time Google OAuth → credentials/google-token.jsongithub-gdrive-backup/
├── .github/workflows/
│ ├── backup.yml — daily cron + manual backup (SBOM, storage target)
│ ├── restore.yml — manual restore
│ ├── cleanup.yml — GFS / simple retention cleanup
│ ├── notify.yml — Teams + SendGrid email digest
│ ├── pat-check.yml — weekly PAT expiry reminder
│ ├── sla-check.yml — hourly SLA breach alert
│ └── monthly-restore-test.yml — monthly dry-run restore integrity check
├── docs/
│ ├── index.html — GitHub Pages dashboard SPA
│ ├── manifest.json — PWA manifest
│ ├── sw.js — cache-first service worker
│ └── screenshots/ — SVG mockups for README and User Guide
├── src/
│ ├── auth/google-auth.js
│ ├── backup/
│ │ ├── github.js — GitHub API client
│ │ ├── gdrive.js — Google Drive client
│ │ ├── storage/
│ │ │ ├── s3.js — Amazon S3 adapter
│ │ │ ├── azure.js — Azure Blob adapter
│ │ │ └── b2.js — Backblaze B2 adapter
│ │ └── index.js — backup orchestrator
│ ├── restore/index.js — restore orchestrator
│ ├── server/ — optional self-hosted Express server
│ └── logger.js
├── credentials/ — git-ignored, OAuth files go here
├── get_token.py — one-time Google OAuth script
├── .env.example
└── README.md
notify.yml fires after every backup run and delivers rich, multi-channel notifications:
- MS Teams — color-coded Adaptive Card (green = success, red = failure). Set
TEAMS_WEBHOOK_URL. - SendGrid email digest — HTML table summarizing run status, repo count, and session link. Set
SENDGRID_API_KEY. - Slack — plain text message (legacy). Set
SLACK_WEBHOOK_URL. docs/status.json— updated on every run; live README badge reflects current status.
Additional proactive alerts:
- PAT rotation reminder (
pat-check.yml, weekly Monday 08:00 UTC) — warns via Teams + email whenPAT_EXPIRY_DATEis ≤7 days away. - SLA breach alert (
sla-check.yml, hourly) — fires Teams + email if backup age exceedsSLA_HOURS(default 26). - Monthly restore test (
monthly-restore-test.yml, 1st of month 03:00 UTC) — dry-run restore integrity check; result appended todocs/audit.log.
credentials/and.envare in.gitignore— never committed- Dashboard tokens stored in
localStorageonly — never sent to any third party - GitHub PAT scopes:
repo,workflow,read:org,read:user— read-only for backup, no destructive permissions - Google Drive token scoped to
drive.filein Actions,drive.readonlyin the dashboard - Self-hosted Express server has no built-in auth — run locally or behind a reverse proxy
See CONTRIBUTING.md for development setup, project structure, and guidelines.
MIT — see LICENSE
Omar Rao — Engineer, Data Resilience, Cybersecurity & Privacy
Writing about data resilience, backup engineering, and practical cybersecurity at omarrao.substack.com
Run the backup tool in a container:
docker run -e GITHUB_TOKEN=ghp_... \
-e GITHUB_USER=your-username \
-e GDRIVE_FOLDER_ID=your-folder-id \
-e GOOGLE_CLIENT_SECRET_PATH=/app/credentials/google-client-secret.json \
-e GOOGLE_TOKEN_PATH=/app/credentials/google-token.json \
-v /path/to/credentials:/app/credentials \
ghcr.io/omarrao/github-gdrive-backup:latest backupThe Docker image is published to GitHub Container Registry on each release. Pull the latest:
docker pull ghcr.io/omarrao/github-gdrive-backup:latestAll three workflows (backup, restore, cleanup) support self-hosted runners. When dispatching manually, set the Runner label input to your runner's label (e.g. self-hosted or my-mac-mini).
Self-hosted runners are useful when you need:
- Faster network access to your GitHub Enterprise instance
- Pre-installed tools (e.g.
syftfor SBOM generation) - Custom credentials storage
Back up GitLab projects alongside GitHub repos. Set these GitHub Actions secrets:
| Secret | Value |
|---|---|
GITLAB_TOKEN |
GitLab personal access token with read_api and read_repository scopes |
Optionally set GITLAB_HOST if you use a self-hosted GitLab instance (default: https://gitlab.com).
When dispatching the backup workflow manually, enter your token in the GitLab Token input. GitLab repos are uploaded with a gitlab- filename prefix to the same Drive session folder.
Set the BACKUP_ENCRYPTION_KEY GitHub Actions secret to a 32-byte hex string (64 hex characters) to enable AES-256-CBC encryption of all backup zips.
Generate a key:
openssl rand -hex 32When encryption is enabled, backup files are stored as .zip.enc instead of .zip. The first 16 bytes of each encrypted file are the IV; the remainder is the ciphertext. The restore workflow automatically decrypts files when BACKUP_ENCRYPTION_KEY is set.
See the community/ directory for ready-to-post submissions:
community/show-hn.md— Show HN submissioncommunity/awesome-selfhosted.md— awesome-selfhosted entrycommunity/product-hunt.md— Product Hunt launch copy