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.
On your first sign-in the dashboard gates itself until your own GitHub and Google Drive are connected — it never shows stale or upstream data. A guided setup checklist tracks the four required steps and links straight to the relevant Settings sections. The dashboard automatically targets your fork (derived from the GitHub Pages URL), so every deployment shows its own live data.
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 includes an SLA badge and trend indicator. A colored top accent bar, the signed-in user's avatar in the nav, and system Segoe UI typography round out the v3.0.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, 30 daily backup sessions, workflow run history including a simulated failure) — all stats, graphs, delta composition and fan-out status are derived from one internally-consistent dataset. 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.
Every push and pull request to main runs the full CI pipeline: copyright header verification, ESLint, Jest tests, npm security audit, YAML lint, and secret scanning — all gates must pass before merge.
The dashboard opens with a System Health panel — a traffic-light summary of last backup, SLA, anomaly state, storage usage, and connection status. Below it, a Storage Growth area chart plots session size across the last 10 backups, and a 30-day Backup Timeline heatmap surfaces gaps and failures at a glance. Click any session to open a detail drawer showing a Restore Confidence Score and per-repo sizes.
Press ⌘/Ctrl+K anywhere to open a fuzzy command palette — jump to any page or fire an action (trigger backup, export CSV/JSON, toggle theme) without touching the mouse. Full arrow-key navigation and Enter to run.
The dashboard surfaces the latest session's delta composition (full / delta / unchanged) and 3-2-1 fan-out status per mirror target. The session drawer visualizes each repo's delta chain lineage and Restore Confidence. The Restore Wizard builds the exact gh workflow run command for any provider (GitHub / GitLab / Gitea / local) and session — copy and go.
| 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 |
| True delta uploads | INCREMENTAL_MODE=delta uploads only new git objects as a git bundle chain (full → delta → delta), skips unchanged repos entirely, and restores by replaying the chain |
| 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 |
| Age-based retention | Simple age-based cleanup (default 21 days, weekly Sunday schedule) in cleanup.yml; chain-aware so it never deletes a bundle a retained delta chain still needs |
| 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 |
| Multi-destination fan-out (3-2-1) | BACKUP_MIRROR_TARGETS=s3,b2 mirrors every archive, manifest & summary to secondary clouds with per-file size verification; Drive stays primary, mirror failures never fail the backup |
| Cross-provider restore | Restore a Drive session into GitHub, GitLab, or Gitea — pick the destination at dispatch time (RESTORE_TARGET_PROVIDER); git history, labels & milestones recreated on the target |
| Local restore | RESTORE_TARGET_PROVIDER=local reconstructs each repo to a bare mirror on disk — DR drills & offline inspection, no remote push |
| Chain-aware cleanup | Retention never deletes a base/intermediate bundle a retained delta chain still needs — no silent data loss |
| Chain compaction | INCREMENTAL_MAX_CHAIN caps delta depth, forcing a fresh full base so restores stay fast |
| Post-restore verification | After each restore, destination ref tips are compared against the source; result reported per repo |
| Repo config backup | INCLUDE=config captures settings, branch protection, collaborators, webhook shapes & Actions secret names (never values) |
| Issues/PR HTML archive | Every session gets a self-contained issues.html; optional best-effort issue re-creation on GitHub restore |
| WORM / object lock | S3_OBJECT_LOCK_DAYS / B2_OBJECT_LOCK_DAYS make mirrored copies immutable (ransomware-proof) |
| Integration smoke test | Secrets-gated CI: real delta backup + local restore round-trip verifying git history |
| Dependabot auto-merge | Grouped patch/minor updates auto-approved & merged once CI passes |
| Build provenance | CI generates an SPDX SBOM and signs it with actions/attest-build-provenance |
| JSON audit log | Structured JSON-lines audit entries (SIEM-ingestible) |
| Restore wizard | Dashboard modal builds the exact gh workflow run command for any provider/session |
| First-run onboarding | Setup checklist gates the dashboard until GitHub + Drive are configured; no stale/upstream data on first login |
| Fork-aware targeting | Dashboard auto-derives the target repo from the Pages URL, so each fork shows its own live data (override in Settings) |
| Dynamic demo data | Demo stats, graphs, composition & fan-out are all derived from one dataset — always internally consistent |
| Signed manifests | Optional Ed25519 signature over manifest.json (BACKUP_SIGNING_KEY); verified on restore to detect tampering/forgery, not just corruption |
| Recovery scorecard | Monthly restore drill publishes a "last verified restore + RTO" scorecard (docs/recovery-scorecard.json) surfaced as a README badge and dashboard tile |
| Tamper-evident audit log | Hash-chained JSON-lines audit entries — editing or deleting past entries is detectable |
| GitHub Action | Use as uses: OmarRao/github-gdrive-backup@v5 in any workflow — no fork required |
| Container image (GHCR) | Published to ghcr.io/omarrao/github-gdrive-backup on each release, with SBOM + signed provenance |
| 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 |
| System health panel | Traffic-light summary of last backup, SLA, anomaly, storage, and GitHub/Drive connection status |
| Storage growth chart | Inline SVG area chart of session size across the last 10 backups |
| Backup timeline heatmap | 30-day GitHub-style heatmap coloring each day by backup volume; failed days in red |
| Session detail drawer | Click a session for a Restore Confidence Score and per-repo size breakdown |
| Command palette | ⌘/Ctrl-K fuzzy launcher for pages and actions, full keyboard navigation |
| Notification center | Bell dropdown persists recent toasts with unread badge, stored in localStorage |
| Report JSON export | Export full run history as structured JSON alongside CSV and compliance PDF |
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) |
AWS_ACCESS_KEY_ID |
AWS access key (when STORAGE_TARGET=s3) |
AWS_SECRET_ACCESS_KEY |
AWS secret key (when STORAGE_TARGET=s3) |
AWS_BUCKET_NAME |
S3 bucket name (when STORAGE_TARGET=s3) |
AWS_REGION |
S3 bucket region, e.g. us-east-1 (when STORAGE_TARGET=s3, default: us-east-1) |
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 |
BACKUP_MIRROR_TARGETS |
Comma-separated secondary destinations for 3-2-1 fan-out, e.g. s3,b2 (each target's own secrets must also be set) |
GITLAB_TOKEN |
GitLab PAT (api, write_repository) — for cross-provider restore to GitLab |
GITLAB_HOST |
GitLab base URL (default https://gitlab.com) — for self-hosted GitLab |
GITEA_TOKEN |
Gitea access token with repo write scope — for cross-provider restore to Gitea |
GITEA_HOST |
Gitea base URL, e.g. https://gitea.example.com |
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
BACKUP_ZIP_LEVEL=6 # zip compression 0-9 (lower = faster, larger)
# Multi-destination fan-out (3-2-1) — Drive is always primary
BACKUP_MIRROR_TARGETS=s3,b2
# True delta uploads — only new git objects each session (git bundle chain)
INCREMENTAL_MODE=delta
PORT=3000The backup and restore paths are streaming and memory-bounded, so archive size — not available RAM — is the limit:
- Streaming hash & crypto. SHA-256 hashing and AES-256-CBC encrypt/decrypt
stream the archive through in fixed-size chunks (
src/lib/archive-crypto.js). Peak memory stays roughly constant regardless of repo size, avoiding the out-of-memory risk of loading multi-GB archives fully into aBuffer. - Configurable compression.
BACKUP_ZIP_LEVEL(default6) trades a little archive size for noticeably faster, less CPU-intensive zipping; set9for maximum compression or1for the fastest backups. - Batched git object checks. Delta backups validate prior refs with a single
git cat-file --batch-checkprocess instead of one per ref. - Parallel fan-out. Secondary-destination mirror uploads run concurrently.
The dashboard SPA is also tuned for fast loads: it ships no web fonts (system
UI stack), preconnect/dns-prefetch hints warm up the Firebase, Google
auth, GitHub, and Drive origins, inactive pages are display:none (skipped by
layout), and a cache-first service worker serves the app shell instantly on
repeat visits.
The 3-2-1 backup rule — 3 copies, on 2 different media, with 1 off-site — is the baseline for real resilience. This tool implements it by keeping Google Drive as the primary destination and mirroring every artifact to one or more secondary clouds.
Set BACKUP_MIRROR_TARGETS to any combination of s3, azure, and b2 (each target's own credentials must also be configured):
BACKUP_MIRROR_TARGETS=s3,b2On each run the orchestrator mirrors:
- every repository archive (
repo.zip/repo.zip.enc) - every wiki archive
- per-repo
metadata.json - the session
manifest.jsonandbackup-summary.json
Each mirrored file's byte size is verified against the source. Mirroring is best-effort: a failed or mismatched mirror is logged and recorded under summary.mirror.perTarget (with ok/failed counts per destination) but never fails the primary Drive backup. Encrypted archives (BACKUP_ENCRYPTION_KEY) are mirrored in their encrypted form.
GDRIVE_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 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— most git APIs do not support programmatic issue creation
A backup captured from GitHub isn't locked to GitHub. Because the archived git mirror carries the full history, it can be pushed to any git host — real disaster-recovery portability. Choose the destination when you dispatch the restore workflow (or set RESTORE_TARGET_PROVIDER):
| Provider | RESTORE_TARGET_PROVIDER |
Required secrets |
|---|---|---|
| GitHub (default) | github |
GH_BACKUP_TOKEN |
| GitLab (SaaS or self-hosted) | gitlab |
GITLAB_TOKEN, optional GITLAB_HOST (default https://gitlab.com) |
| Gitea (self-hosted) | gitea |
GITEA_TOKEN, GITEA_HOST |
| Local disk (no push) | local |
none — writes to RESTORE_LOCAL_DIR |
Each provider handles its own repo/project creation, authenticated remote URL, and label/milestone recreation (best-effort). The git push path is identical across providers, so branches and tags always transfer faithfully.
# Restore the latest session into a GitLab group
gh workflow run restore.yml \
-f target_provider=gitlab \
-f target_owner=my-gitlab-groupBy default each session uploads a full zip mirror of every repo. With INCREMENTAL_MODE=delta, the backup instead uploads a git bundle containing only the objects that are new since the last backup — typically a fraction of the size for repos that change little between runs.
How it works per repo:
| Situation | What's uploaded |
|---|---|
| First-ever backup | full bundle (all objects) — the chain base |
| Refs changed since last session | delta bundle (git bundle --all --not <prev tips>) |
| No refs changed | nothing — the repo is skipped, only a tiny state file is written |
| History rewritten / force-push loses the base | automatic fall-back to a fresh full bundle |
Each repo folder stores a backup-state.json recording the ref SHAs plus the chain (base session + ordered deltas). Restore is fully automatic: it reads the chain, downloads the base bundle and each delta in order, and replays them to reconstruct the complete history before pushing — so a delta session is exactly as restorable as a full one. Encryption and 3-2-1 fan-out both apply to bundles just like zips.
Note: the clone from GitHub is still a full mirror (needed to compute a correct delta); the savings are on storage and upload bandwidth — the cost that accrues over time on Drive/S3/B2.
gh workflow run backup.yml -f incremental_mode=deltaConfigure 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/
│ ├── CODEOWNERS — all paths require @OmarRao review
│ └── workflows/
│ ├── ci.yml — CI: lint, test, copyright, audit, secret scan
│ ├── backup.yml — daily cron + manual backup (SBOM, storage target)
│ ├── restore.yml — manual restore
│ ├── cleanup.yml — simple age-based retention cleanup (chain-aware)
│ ├── 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
├── tests/
│ ├── copyright.test.js — enforces copyright header on every src/*.js
│ ├── logger.test.js — logger export and call surface
│ └── storage.test.js — S3/Azure/B2 adapter interface
├── scripts/
│ └── check-headers.js — standalone copyright header audit
├── eslint.config.js — ESLint flat config (no-eval, eqeqeq, security rules)
├── 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.
- CODEOWNERS — every file and workflow requires
@OmarRaoreview before any PR can merge - Copyright headers —
// Copyright (c) Omar Rao. All rights reserved.is enforced on all 15src/**/*.jsfiles; CI fails if a new file is added without it credentials/and.envare in.gitignore— never committed- Dashboard tokens stored in
localStorageonly — never sent to any third party
| Gate | Tool | Must pass |
|---|---|---|
| Copyright headers | shell grep across src/ |
Yes |
| Linting | ESLint (no-eval, no-implied-eval, no-new-func, eqeqeq) |
Yes |
| Tests | Jest — 25 tests, 3 suites | Yes |
| Dependency audit | npm audit --audit-level=high |
Warn |
| Workflow YAML | yamllint |
Warn |
| Secret scanning | Gitleaks | Warn |
npm run lint # ESLint
npm test # Jest with coverage
npm run check-headers # Copyright header audit
npm run audit # npm audit --audit-level=high- 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.
A backup is only as good as its restore — so v5 makes recoverability and integrity provable.
- Signed manifests (Ed25519). Set
BACKUP_SIGNING_KEY(a PEM Ed25519 private key) and each session'smanifest.jsonis signed tomanifest.json.sig. On restore, setBACKUP_SIGNING_PUBLIC_KEYto verify it; SHA-256 catches corruption, the signature catches tampering/forgery. SetBACKUP_REQUIRE_SIGNATURE=trueto abort a restore on a missing/invalid signature. Generate a keypair:node -e "const s=require('./src/lib/manifest-signing');const k=s.generateKeyPair();require('fs').writeFileSync('signing.key',k.privateKey);require('fs').writeFileSync('signing.pub',k.publicKey);console.log('wrote signing.key + signing.pub')" - Recovery scorecard. The monthly restore drill (
monthly-restore-test.yml) publishesdocs/recovery-scorecard.json— last verified restore + RTO — shown as the Restore Verified badge above and a dashboard tile. "Backups exist" becomes "restores are verified." - Tamper-evident audit log. Audit entries (
src/audit/log.js) are hash-chained: each carries the previous entry's SHA-256, so editing or deleting history is detectable viaverifyChain().
No fork required — compose it into any workflow:
- uses: OmarRao/github-gdrive-backup@v5
with:
github-token: ${{ secrets.GH_BACKUP_TOKEN }}
gdrive-folder-id: ${{ secrets.GDRIVE_FOLDER_ID }}
google-client-secret: ${{ secrets.GOOGLE_CLIENT_SECRET }}
google-token: ${{ secrets.GOOGLE_TOKEN }}
incremental-mode: delta # optional
mirror-targets: s3,b2 # optional 3-2-1 fan-out
signing-key: ${{ secrets.BACKUP_SIGNING_KEY }} # optionalSee action.yml for all inputs. The action outputs a JSON summary (totals, delta composition, fan-out, signature).
Published to the GitHub Container Registry on each release with an SBOM and signed build provenance:
docker run --rm \
-e GITHUB_TOKEN=ghp_... -e GITHUB_USER=you \
-e GDRIVE_FOLDER_ID=... \
-v "$PWD/credentials:/app/credentials" \
ghcr.io/omarrao/github-gdrive-backup:v5 backupCopyright © 2026 Omar Rao.
This project uses a dual-license model:
-
Open source (AGPL-3.0). The public repository is licensed under the GNU Affero General Public License v3.0 — see LICENSE. You are free to use, study, modify, and share it, provided you comply with the AGPL-3.0 terms. In particular, AGPL-3.0 requires that if you run a modified version to provide a network service, you make the complete corresponding source code available to the users of that service.
-
Commercial license. Companies that want closed-source, proprietary, SaaS/hosted, reseller, distributor, or white-label rights — or otherwise do not wish to comply with AGPL-3.0 — must purchase a separate commercial license. See COMMERCIAL-LICENSE.md and contact omarsrao@gmail.com.
Trademarks. These licenses grant rights to the code, not to the brand. No product name, company name, logo, or trademark rights are granted — see TRADEMARKS.md.
Prior MIT releases. Versions of this software that were previously distributed under the MIT License remain governed by the license terms under which those versions were originally distributed. Adopting AGPL-3.0 and the commercial option applies to the current and future versions; it does not, and is not intended to, revoke or alter any rights already granted under MIT for previously released versions.
Omar Rao — Cybersecurity, Privacy and Resilience Expert
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