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

  1. 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.
  2. 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_repo scope for a public repository, repo for 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