On October 30 at 17:50 UTC, GitHub started investigating degraded performance for Actions and Webhooks. Ten minutes later Pages was on the list too. By 18:06 they were reporting delayed workflows, delayed Pages deployments, delayed webhook events, and errors in code search and live updates. The incident was marked resolved at 19:23.
That's about 90 minutes on the status page. In the Hacker News thread, one commenter pointed out that the same incident happened 11 days earlier, and another counted "several Actions outages in the past two weeks."
The same day, Tangled published The federated code forge, arguing that relying on someone else for your uptime "hasn't played out well." I don't think the timing was planned. It still works as an ad.
My take: don't move everything off GitHub because of one bad Friday afternoon. Spend an hour this week mirroring your repos to Codeberg, a self-hosted Forgejo or Tangled, so the next outage costs you 10 minutes instead of a lost evening. Below: a status check you can alias, four ways to mirror, the popular setup that fails during an outage, and a CI fallback.
Step 0: check if GitHub is down before you debug your own pipeline
I've seen people (fine, I've been one of them) rerun a stuck workflow four times, roll back a commit and blame a Dependabot bump before checking whether GitHub itself is broken. GitHub's status page has a public JSON API, so make the check a shell alias:
alias ghdown='curl -s https://www.githubstatus.com/api/v2/components.json \
| jq -r ".components[] | select(.status != \"operational\") | \"\(.name): \(.status)\""'
If that prints Actions: degraded_performance, close the laptop. To list active incidents, use https://www.githubstatus.com/api/v2/incidents/unresolved.json. The full endpoint list also covers summary, scheduled maintenance and incident history.
One caveat from the HN thread: people noticed the 30-day uptime bar staying green during acknowledged incidents, because partial degradation isn't counted as downtime. One commenter mentioned that a third-party status page had to be built to track Actions accurately. So a green bar on the status page tells you very little.
The trap: mirroring GitHub with a GitHub Action
The most common mirror setup I see is a workflow that runs git push --mirror to Codeberg on every push. It works fine until Actions goes down, and then the mirror stops updating at exactly the moment you need it.
Your mirror has to be updated by something that doesn't depend on GitHub's job queue. That leaves two options: your own machine, or a forge that pushes or pulls on its own schedule.
Option 1: one git push, two forges
This is the cheapest setup and needs no infrastructure. Git lets one remote have several push URLs:
# keep fetching from GitHub, but push everywhere
git remote set-url --add --push origin [email protected]:you/project.git
git remote set-url --add --push origin [email protected]:you/project.git
git remote set-url --add --push origin [email protected]:you.tngl.sh/project
git remote -v
# origin [email protected]:you/project.git (fetch)
# origin [email protected]:you/project.git (push)
# origin [email protected]:you/project.git (push)
# origin [email protected]:you.tngl.sh/project (push)
The first --add --push replaces the implicit push URL, which is why GitHub has to be listed explicitly. From then on, every git push goes to all three. You need to create the empty repos on Codeberg and Tangled first.
The weakness is that it only mirrors what you push from that clone. Merges done through the GitHub UI won't show up until someone pulls and pushes again. For a solo project that's fine. For a team, use option 3.
Option 2: Codeberg, which only takes pushed mirrors
Codeberg runs Forgejo, and Forgejo supports pull mirrors. Codeberg.org doesn't allow them, though. Their FAQ says that "mirrors that pull content from other code hosting services were problematic for Codeberg" and suggests manual mirrors with multiple remotes and git push --mirror instead (FAQ). That's option 1 again, and I think it's a reasonable rule for a nonprofit that pays for its own disks.
If the Codeberg copy is a backup and nothing more, turn off issues and PRs in the repo settings so nobody files bugs in the wrong place.
Option 3: self-hosted Forgejo pull mirror, then fan out
If you have a $5 VPS or a homelab box, this is the setup I'd pick, because it's the only one here that catches merges done in the GitHub UI. On your own Forgejo instance, create the repo with New Migration and check This repository will be a mirror. Forgejo then pulls from GitHub on a schedule, including changes made through the GitHub web UI.
You can then fan out from there: Settings > Repository > Mirror Settings > Add Push Mirror pushes to Codeberg, Tangled or GitLab. For GitLab, the Forgejo docs say to use oauth2 as the username and a token with the write_repository scope. SSH push mirrors generate an Ed25519 key that you add as a deploy key on the target.
Two warnings from those docs: push mirrors are forced and overwrite the remote, and LFS over SSH isn't implemented, so an SSH push mirror won't carry your LFS objects.
Option 4: Tangled, the federated forge on ATProto
Tangled is built on the AT Protocol, the same protocol Bluesky runs on. Repos live on "knots," which you can host yourself or leave on tangled.org. CI runs on "spindles." The October 30 release added knot2, a Rust rewrite of the knot server, and moved issues, PRs, comments, reactions and labels onto the knot as collaborative objects, each one "just a chain of Git objects in the repo itself." That matters for mirroring: if your knot holds the issues too, your backup carries the issue and PR discussion along with the commits. The blog's pitch is "Whatever you choose, you can still talk to everyone else."
Pushing to it is plain SSH. Add your key under Settings > Keys, then:
Host tangled.org
Hostname tangled.org
User git
IdentityFile ~/.ssh/id_ed25519
AddressFamily inet
For a backup, the gaps count for more than the features:
- Private repos aren't available yet. A co-founder said on HN they're rolling out "in the following couple weeks," and another team member said they have a working demo. Until they ship, Tangled only works for public code.
- HTTPS push and clone came up because people want knots behind Cloudflare. A team member replied that HTTPS push already works but isn't documented yet, so expect to figure it out yourself.
- On Windows, one commenter couldn't clone a repo with a colon in a filename. That's a Windows limit, not a Tangled bug, but it'll bite you the same way.
- Self-hosted knot operators have to upgrade to v1.20.0 for the new release.
Most of that thread is an argument about the protocol. One camp says ATProto "heavily lends itself to centralization." The other camp says relays are cheap and that ForgeFed, the ActivityPub approach Forgejo was working on, has been "in limbo forever." I lean toward Tangled here, but for a reason other than the protocol. A federated forge that ships stacked PRs today is more useful than a theoretically better protocol that hasn't shipped. Still, I wouldn't make a forge without private repos my only backup.
The CI fallback: Forgejo Actions, Woodpecker or Tangled spindles
A mirror with no CI is a read-only archive. You don't need to port your whole pipeline, just enough to run tests and cut a release by hand.
| Where it runs | Config | Notes | |
|---|---|---|---|
| Forgejo Actions | Your runner, or Codeberg's limited hosted Actions | .forgejo/workflows/*.yml |
Designed to feel familiar to GitHub Actions users, though not compatible; disabled by default per repo on Codeberg |
| Woodpecker CI | Codeberg's hosted instance (request access) or self-hosted | .woodpecker.yml |
What Codeberg's docs recommend for hosted CI |
| Tangled spindles | Self-hosted runner, or third-party CI via tack | .tangled/workflows/*.yml |
Nixery engine, deps from Nixpkgs |
A minimal spindle workflow looks like this. The pipeline docs list push, pull_request and manual as events, and nixery as the only engine for now:
when:
- event: ["push", "pull_request"]
branch: ["main"]
engine: "nixery"
dependencies:
nixpkgs:
- go
steps:
- name: test
command: "go test ./..."
The useful part of the Forgejo runner is that it connects outbound to the forge, so a box behind NAT works. Codeberg's docs say it "does not need a public IP address." An old laptop in a closet is enough to unblock a hotfix.
The one-hour checklist
- Add the
ghdownalias, or a bookmark tohttps://www.githubstatus.com/api/v2/summary.json. - Create empty repos on Codeberg and/or Tangled.
- Add them as extra push URLs on
origin, or set up a pull mirror on your own Forgejo. - Make sure the mirror doesn't depend on GitHub Actions.
- Copy your test job to one fallback CI and run it once, so you know it works before you need it.
- Write down which secrets the release job needs and where they'd live on the backup forge.
I'm not leaving GitHub. It's still where people find your project, and it will probably stay that way. I just want "GitHub is down" to mean I push somewhere else for 90 minutes and keep working.
So: do you keep a mirror of your repos anywhere besides GitHub, or are you all in? And if you've tried Tangled's ATProto setup, is it federation or a nicer-looking single point of failure?
Top comments (0)