Mirroring
This repository lives on a self-hosted Forgejo. That is where the branches, pull requests and CI are, and none of it moves. What GitHub gets is a push mirror: a copy that Forgejo writes to after every push, one way, so a URL people already know how to read points at the same commits.
The target is github:jamesmstone/duckweb. Setting it up is a human job — it needs a GitHub token, and a token is not something a build can hold.
Before anything on the Forgejo side
- Create the GitHub repository
jamesmstone/duckweb, empty: no README, no licence, no.gitignore. A push mirror force-pushes every ref it carries, so an initial commit on the GitHub side is not merged, it is erased — and until the first sync runs the repository looks like it lost its history. - Mint a token GitHub will accept for
git push:- Fine-grained (preferred): Settings → Developer settings → Personal access tokens → Fine-grained tokens. Scope it to that one repository and grant Contents: Read and write. Metadata: Read is added for you and is required. Nothing else — a mirror pushes refs and does not touch issues, actions or settings.
- Classic: the
public_reposcope for a public repository,repofor a private one. Classic tokens are account-wide, which is the reason to prefer the fine-grained one. - Give it an expiry you will notice. When it lapses the mirror fails quietly in Forgejo’s log and the GitHub copy simply stops moving.
In the Forgejo UI
Repository → Settings → Repository → Mirror Settings → Add Push Mirror:
| field | value |
|---|---|
| Git Remote Repository URL | https://github.com/jamesmstone/duckweb.git |
| Authorization — username | your GitHub username |
| Authorization — password | the token, not the account password |
| Sync when new commits are pushed | on |
| Sync interval | 8h — the fallback for a push the hook missed |
Save, then press Synchronize Now and watch the mirror row for an error. The first sync is the one that fails, and it fails for one of two reasons: the token cannot write, or the GitHub repository was not empty.
Or over the API
Same thing, if the UI is not to hand. Forgejo 15’s POST /api/v1/repos/{owner}/{repo}/push_mirrors takes:
curl -X POST \
-u "$FORGEJO_USER:$FORGEJO_TOKEN" \
-H 'Content-Type: application/json' \
http://hermanii:3000/api/v1/repos/james/duckweb/push_mirrors \
-d '{
"remote_address": "https://github.com/jamesmstone/duckweb.git",
"remote_username": "jamesmstone",
"remote_password": "<github token>",
"interval": "8h0m0s",
"sync_on_commit": true
}'GET the same path to list mirrors, and POST /api/v1/repos/james/duckweb/push_mirrors-sync to force one. The Forgejo token needs write:repository.
What the mirror is and is not
- It is one way. A commit pushed to GitHub is overwritten at the next sync; the mirror force-pushes.
- Pull requests, issues and releases are not mirrored — only git refs. A contributor arriving through GitHub has to be sent here, so the GitHub repository’s description should say so.
- Every branch is carried, including working branches. Delete a branch here and the mirror deletes it there.
branch_filter(a field in the API, a box in the UI) narrows that to a glob if the working branches are noise. - Nothing in this repository’s builds depends on the mirror existing. It is reach, not infrastructure.