Forgejo Actions Fallback
.forgejo/workflows/ holds a standby copy of every pipeline in
.github/workflows/, so the homelab can keep building, packaging and deploying
when github.com is unavailable. Forgejo reads .forgejo/workflows/ and ignores
.github/workflows/ entirely; GitHub does the reverse. The two directories never
see each other’s files, which is why the workflows are duplicated rather than
shared.
When something is broken, go to TROUBLESHOOTING.md — it lists every failure hit while bringing this up, by symptom.
Everything runs on forgejo.toolsera.lan, against the
forgejo-runner
stack on the ubuntu-vm host.
What each workflow does differently
Section titled “What each workflow does differently”| Workflow | Same as GitHub | Changed |
|---|---|---|
publish-package.yml | tag pkg/<name>-<version>, build.sh, --expected-version | publishes to the Forgejo Debian registry instead of Nexus; release created through the Forgejo API |
publish-docker-image.yml | tag image/<name>-<version>, Harbor target, BWS credentials | runner label only |
deploy-docs.yml | push to main, npm test, npm run build, Cloudflare Pages | no setup-node (the job image ships Node 22), no wrangler-action (npx wrangler instead) |
Three substitutions recur, all of them there to remove a dependency on github.com at run time:
- Actions are fully qualified as
https://data.forgejo.org/actions/.... Forgejo would resolve a bareactions/checkout@v4against its ownDEFAULT_ACTIONS_URLanyway, but spelling it out means the workflow does not change meaning if that default does. - Marketplace actions with no Forgejo mirror are replaced by the command they
wrap.
cloudflare/wrangler-actionbecomesnpx wrangler pages deploy;softprops/action-gh-releasebecomescurlagainst the Forgejo release API. runs-on: deb-builder— a single label the runner maps to a job image, in place of GitHub’s[self-hosted, homelab, deb-builder]list.
Every workflow also sets NODE_EXTRA_CA_CERTS at the top. Job containers get the
host CA bundle bind-mounted in, which covers curl and apt, but Node ships its
own trust store and would otherwise fail to clone from forgejo.toolsera.lan over
Step CA TLS — in the checkout step, before anything else has run.
Repository setup
Section titled “Repository setup”Getting the code there
Section titled “Getting the code there”Forgejo needs a copy of the repo that is current when GitHub is not.
It must be a normal repository, not a pull mirror. A mirror is read-only, so a
pkg/* or image/* tag can only reach it by syncing from GitHub — during a GitHub
outage nothing syncs, nothing can be pushed, and the standby can never be triggered.
Forgejo also disables the Actions unit on mirrors by design, which is what a missing
Actions tab means. Neither is fixable by ticking a setting; the repository has to
stop being a mirror.
Note the owner is unknown224 on Forgejo and markkpamy on GitHub — the paths
differ.
Git SSH does not go through forgejo.toolsera.lan. The toolsera.lan zone
wildcards to Traefik (192.168.1.14), which does not listen on 222, so the clone URL
Forgejo displays does not work from the LAN. Forgejo’s SSH is on the host running the
docker-apps-cifs stack — 192.168.1.180:222. Give it an alias so the remote URL
survives the address changing:
Host forgejo HostName 192.168.1.180 Port 222 User gitThen add Forgejo as its own remote and leave origin alone:
git remote add forgejo forgejo:unknown224/infrastructure-toolkit.gitgit push forgejo maingit push forgejo --tagsTags matter as much as main — they are what trigger the package and image
pipelines, so a release has to be tagged on both forges to be built on both.
A second push URL on origin would give one-command dual push, but it misbehaves in
the two ways that matter here. Git only falls back to a remote’s fetch URL for
pushing while that remote has no pushurl at all, so adding the Forgejo URL alone
silently stops pushes reaching GitHub — the GitHub URL has to be re-added
explicitly, which reads like a no-op and is not. And with both URLs on one remote, a
git push during a GitHub outage fails on the GitHub URL, leaving a partial result
that depends on URL order. A separate remote keeps each forge independently
reachable, which is the entire point. For one-command convenience without the
coupling:
git config alias.pushall '!git push origin && git push forgejo'Secrets
Section titled “Secrets”Repository → Settings → Actions → Secrets:
| Secret | Used by | Notes |
|---|---|---|
BWS_ACCESS_TOKEN | image | Same machine-account token as GitHub |
BWS_PROJECT_ID | image | Optional; scopes the BWS injection |
PACKAGE_TOKEN | package | Forgejo token with write:package |
CLOUDFLARE_API_TOKEN | docs | |
CLOUDFLARE_ACCOUNT_ID | docs | |
RELEASE_TOKEN | package | Only if the automatic run token cannot create releases |
Names here cannot begin with FORGEJO_ — Forgejo reserves that prefix, the same
way it reserves GITHUB_, and rejects the secret or variable on save. That is why
these are PACKAGE_TOKEN and DEB_COMPONENT rather than the FORGEJO_* names the
scripts read internally; the workflow maps one to the other.
Variables
Section titled “Variables”Repository → Settings → Actions → Variables:
| Variable | Default when unset | Meaning |
|---|---|---|
PACKAGE_OWNER | the repository owner | User or org whose package registry receives the .deb |
DEB_DISTRIBUTION | bookworm | APT suite |
DEB_COMPONENT | main | APT component |
DEB_OVERWRITE | false | Replace an existing package of the same version instead of failing |
CLOUDFLARE_PAGES_PROJECT | infrastructure-toolkit-docs | Point the standby docs build at a different Pages project |
Each forge owns one APT repo
Section titled “Each forge owns one APT repo”Nexus is GitHub Actions’ target and Forgejo never writes to it. The Forgejo pipeline publishes to the Forgejo Debian registry and stops there.
That keeps the two from racing: on an ordinary release both forges see the same
pkg/* tag and build the same .deb, and if both then uploaded it to Nexus the
second one would either be rejected as a duplicate or overwrite the first, with
two rebuild-index calls landing on top of each other. With separate targets each
run is unambiguous, and the standby pipeline stays green without doing anything
contended.
The trade-off is on the consuming side: a package released while GitHub is down
reaches Nexus only when someone re-pushes the tag to GitHub afterwards. Hosts
that need to install it in the meantime have to already have the Forgejo APT source
configured — see the next section. Add it as a second source now, while nothing is
broken; an unused source costs one extra index fetch per apt-get update.
The docs and image workflows share their targets happily and need no such split: pushing an identical image tag to Harbor overwrites, and a duplicate Pages deployment is just an extra deployment record.
Installing from the Forgejo package registry
Section titled “Installing from the Forgejo package registry”The Forgejo registry signs and rebuilds its APT metadata on upload, so unlike
Nexus there is no rebuild-index call. Add it as a second source alongside the
Nexus one that
custom_apt_repo
configures — APT merges the two and installs whichever has the higher version:
owner=unknown224sudo curl -fsSL \ "https://forgejo.toolsera.lan/api/packages/${owner}/debian/repository.key" \ -o "/etc/apt/keyrings/forgejo-${owner}.asc"
echo "deb [signed-by=/etc/apt/keyrings/forgejo-${owner}.asc] \https://forgejo.toolsera.lan/api/packages/${owner}/debian bookworm main" \ | sudo tee /etc/apt/sources.list.d/forgejo-${owner}.list
sudo apt-get updateThe host must already trust the Step CA for that HTTPS fetch to work — that is
what the step_ca_client role does.
Failover drill
Section titled “Failover drill”Worth running once, and after any change to either forge:
- Bump a package version, commit, push to both remotes.
- Push the
pkg/<name>-<version>tag to Forgejo only — this is the outage being simulated, so GitHub gets nothing. - Confirm the Forgejo run went green, the
.debis listed under the owner’s Packages tab, and a release exists with the.debattached. - On a host with the Forgejo APT source configured,
apt-get updatefollowed byapt-cache policy <package>shows the new version coming fromforgejo.toolsera.lan, and it installs. - Push the tag to GitHub to let the normal pipeline put the same version into Nexus and cut the GitHub release.