AuthonAuthon Blog
tutorial9 min read

GitHub Actions Down Again? How to Mirror Your Repos to Codeberg Without Actions

A git push --mirror workflow stops the moment Actions does. Push to two forges at once, pull-mirror with Forgejo and keep a CI fallback ready.

AW
Alan West
Authon Team
GitHub Actions Down Again? How to Mirror Your Repos to Codeberg Without Actions

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:

bash
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:

bash
# keep fetching from GitHub, but push everywhere
git remote set-url --add --push origin git@github.com:you/project.git
git remote set-url --add --push origin git@codeberg.org:you/project.git
git remote set-url --add --push origin git@tangled.org:you.tngl.sh/project

git remote -v
# origin  git@github.com:you/project.git (fetch)
# origin  git@github.com:you/project.git (push)
# origin  git@codeberg.org:you/project.git (push)
# origin  git@tangled.org: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:

text
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:

yaml
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 ghdown alias, or a bookmark to https://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?

    Sources

    GitHub Actions Down Again? How to Mirror Your Repos to Codeberg Without Actions | Authon Blog