Playbook — Surviving GitHub Unavailability
Trigger: GitHub is unreachable, degraded, or access to radieu/p24-infra /
radieu/et-operational-platform is lost — for any reason:
- GitHub-wide outage (
git push/gh/Actions failing globally — check githubstatus.com) - Account suspension / lockout / 2FA-device loss on the
radieuorg owner - Accidental or malicious repo deletion / force-push / history rewrite
- Compromised PAT used to tamper with repos or Actions
- Billing lapse disabling private repos or Actions minutes
This is the prevention + response plan for that scenario. It is the GitHub
counterpart to ../disaster-recovery.md (which covers
VPS/Supabase loss) and complements its Scenario E — GitHub Actions Runner
Offline (a runner being down ≠ GitHub being down).
1. Why this matters — what depends on GitHub
GitHub is a single point of failure for far more than source hosting. If it disappears, the following stop working until restored or routed around:
| Dependency | What breaks when GitHub is down |
|---|---|
Git hosting (origin) | No push/pull/clone/PR for either repo |
| GitHub Actions (~45 workflows) | All CI/CD, deploys, backups-via-Actions, scheduled jobs (hourly-devops-triage, supabase-backup, credential-rotation, secrets-sync, …) stop firing |
Self-hosted runners (8 workflows use self-hosted) | Jobs queue forever; deploy-on-merge halts |
| GitHub Issues | Entire autonomous pipeline halts — triage, worker queue dispatch, gh issue create error-reporting, SLA watchdog |
| GitHub Secrets | CI-only secrets (VPS_SSH_PRIVATE_KEY, AGE_KEY_GHA, VPS IPs) become unreadable to any new workflow run |
gh CLI automation | Every script/skill that shells out to gh issue/gh pr/gh api errors |
| Error Notification Standard | The “create a GitHub Issue on error” half of the standard fails (Discord half still works) |
| Deploys | secrets-sync.yml, deploy-monitoring-config.yml, portal-deploy.yml, Vercel-via-Actions all stall |
Key insight on secrets: real infra secrets do not live only in GitHub —
they live in secrets/*.env.sops (age-encrypted, inside the git repo) and the
age private key is backed up offline (KeePass + the DR runbook). So as long as
we have an off-GitHub copy of the repo, we still have every secret. GitHub
Secrets only hold CI-only values, which are re-creatable. This is why the git
repo itself is the crown jewel to protect — protect the repo and you protect
the secrets.
2. Prevention — controls that must be in place before an outage
2.1 Off-GitHub git mirror to Wasabi (primary control)
A nightly job mirrors both repos (full history + all refs) into an age-encrypted bundle on Wasabi S3 — storage we already pay for and which is fully independent of GitHub.
- Script:
scripts/backup-git-mirror.sh - Destination:
s3://ecotrans-backups/git-mirror/{daily,weekly,monthly}/…/backup.tar.zst.age - Retention: 7 daily / 4 weekly / 3 monthly (same rotation as the other backups)
- Runs from VPS cron, NOT GitHub Actions — running it on Actions would make the resilience control depend on the very thing it protects against.
Install (run on bms-4, which has the repo, creds, and 32 GB headroom):
# 1. Ensure backup env is present (same vars as scripts/backup-ionos.sh):
# AGE_PUBKEY, WASABI_BACKUP_ACCESS_KEY, WASABI_BACKUP_SECRET_KEY,
# P24_DISCORD_INFRA_SCRIPTS_ERRORS_WEBHOOK_URL
# plus a clone token: GIT_MIRROR_GH_TOKEN (read-only PAT) or GH_TOKEN
grep -E 'AGE_PUBKEY|WASABI_BACKUP|GIT_MIRROR_GH_TOKEN' /root/.backup-env
# 2. Add cron (03:30 UTC daily, after the other nightly backups):
( crontab -l 2>/dev/null; \
echo '30 3 * * * /opt/p24-infra/scripts/backup-git-mirror.sh >> /var/log/p24-backup.log 2>&1' \
) | crontab -
# 3. Smoke-test once by hand:
/opt/p24-infra/scripts/backup-git-mirror.shThe script emits a Prometheus freshness metric
(git_mirror_last_success_timestamp) via the node_exporter textfile collector —
alert if it goes stale (see §2.4).
2.2 Live working clones on every node (already true — keep it that way)
Every VPS/bms node already has a full clone at /opt/p24-infra. A full git clone is a complete history backup. Do not shallow-clone the canonical
checkouts. During an outage these local clones are read-write and can serve as
the temporary source of truth (§3).
2.3 Optional warm standby remote (recommended, low effort)
Keep a second git remote that can become origin within minutes. Two
already-available options:
- GitLab — we already run an n8n workflow (
save-workflows-to-gitlab,lShfAU645lP0rcR6) that pushes to a GitLab repo, so a GitLab account/namespace exists. Createp24-infra+et-operational-platformmirror projects there. - Codeberg / self-hosted Gitea on a bms node — fully under our control.
Configure a push mirror so it stays current without manual effort:
cd /opt/p24-infra
git remote add mirror git@gitlab.com:<ns>/p24-infra.git
# Push all branches + tags on demand (or wire into backup-git-mirror.sh):
git push --mirror mirrorA warm standby turns “GitHub is down for hours” from a work-stoppage into a
one-command git remote set-url origin … failover.
2.4 Monitoring & detection
- Mirror freshness alert — Prometheus rule on
time() - git_mirror_last_success_timestamp > 129600(36 h) → warning. - GitHub reachability — a lightweight Blackbox/HTTP probe of
https://api.github.com/zen(or status page) so dashboards show GitHub up/down rather than guessing from failed jobs. - Status page — bookmark https://www.githubstatus.com/; subscribe
radieu@gmail.comto incident emails for instant external confirmation.
3. Response — what to do during an outage
Step 0 — Confirm scope (2 min)
curl -fsS https://api.github.com/zen # global GitHub up?
curl -fsS https://www.githubstatus.com/api/v2/status.json | jq .status.description
git ls-remote https://github.com/radieu/p24-infra HEAD # repo-level access?Classify:
- A — GitHub-wide outage (status page red): wait it out; switch to local workflow below; do not thrash.
- B — Account/repo access lost (status page green, we get 404/403): escalate to human immediately (§4) — likely auth/billing/suspension, not an outage.
- C — Repo tampered (force-push, deletion, bad history): freeze, then restore from mirror (§3.3).
3.1 Keep working locally (scenarios A & B)
GitHub down does not stop development — git is distributed.
# Work as normal on the local clone; commit freely.
cd /opt/p24-infra
git checkout -b patch/<issue>-<slug>
# … edit, commit …
# If a warm standby exists (§2.3), push there instead of GitHub:
git push mirror patch/<issue>-<slug>Queue the PRs mentally / in a local note; open them on GitHub once it returns.
Autonomous AI-Dev workers should pause new dispatch while GitHub Issues are
unreachable (the queue claim + gh issue view calls will fail fast) — this is
expected; do not retry-loop.
3.2 Deploys during an outage
Deploys are normally merge → Actions → server. With Actions down, deploy
manually from a node’s local clone (the same commands the workflows run):
# Monitoring stack (what deploy-monitoring-config.yml does):
ssh root@217.154.82.162 "cd /opt/p24-infra && git pull <local-or-mirror> && cd monitoring && docker compose up -d"
# Secrets (what secrets-sync.yml does): decrypt SOPS locally and write the .env, then restart the service.Because secrets live in SOPS in the repo (not only in GitHub Secrets), manual deploys work fully offline as long as you hold the age key.
3.3 Recover from repo tampering / loss (scenario C)
Restore the canonical repo from the Wasabi mirror:
# 1. Pull the latest encrypted mirror bundle:
AWS_ACCESS_KEY_ID=$WASABI_BACKUP_ACCESS_KEY AWS_SECRET_ACCESS_KEY=$WASABI_BACKUP_SECRET_KEY \
aws --endpoint-url https://s3.eu-central-1.wasabisys.com \
s3 cp s3://ecotrans-backups/git-mirror/daily/$(date -u +%F)/backup.tar.zst.age /tmp/gm.age
# 2. Decrypt + unpack (needs the age private key — /home/<user>/.age/p24-infra-keys.txt):
age -d -i ~/.age/p24-infra-keys.txt /tmp/gm.age | zstd -d | tar -C /tmp -xf -
# → /tmp/git-mirror/p24-infra.git (a --mirror clone: full history + all refs)
# 3. Re-create or repoint the GitHub repo, then push the recovered history:
cd /tmp/git-mirror/p24-infra.git
git push --mirror https://github.com/radieu/p24-infra.gitIf GitHub is permanently unavailable, push the mirror to the warm standby (§2.3)
and make it the new origin across all nodes:
git push --mirror git@gitlab.com:<ns>/p24-infra.git
# On each node: git remote set-url origin git@gitlab.com:<ns>/p24-infra.git4. Escalation
Follow the Error Notification Standard, but note its GitHub-Issue half may be unavailable — so Discord is the primary channel during a GitHub outage:
- Discord →
P24_DISCORD_INFRA_SCRIPTS_ERRORS_WEBHOOK_URL(red embed), title🔴 GitHub unavailable — <scope A/B/C>. - Email
radieu@gmail.comdirectly (do not rely on Issue-created email). - For scenario B/C (access lost or tampering) escalate to the human owner immediately — these need GitHub-account-level action (re-auth, billing, support ticket, repo restore) that automation cannot perform.
- File the GitHub Issue retroactively once GitHub returns, for the audit trail.
5. Prevention checklist (verify quarterly)
-
backup-git-mirror.shcron installed on bms-4 and last run succeeded (git_mirror_last_success_timestampfresh) - A restore drill performed: decrypt the latest mirror bundle and confirm
git fsckpasses on the unpacked--mirrorclone - Warm standby remote (GitLab/Gitea) exists and is < 7 days stale
- age private key backed up offline (KeePass) — without it the mirror is
useless (cross-ref
../disaster-recovery-runbook.md) - Mirror-freshness Prometheus alert active
-
radieu@gmail.comsubscribed to githubstatus.com incident emails
Prevention summary
| Control | Protects against | Status field to watch |
|---|---|---|
Wasabi git mirror (backup-git-mirror.sh) | Repo loss, tampering, GitHub-wide loss | git_mirror_last_success_timestamp |
| Live local clones on every node | Short outages, manual deploys | n/a (always present) |
| Warm standby remote | Prolonged outage / GitHub abandonment | mirror push timestamp |
| SOPS-in-repo secrets + offline age key | Loss of GitHub Secrets | DR runbook §SOPS key |
| Discord-first escalation | Issue-creation being unavailable | n/a |
Auto-generated by worker agent for issue #1236.