| name | mirror-sync |
| description | Operate the safe Forgejo <-> GitHub mirror reconcile - auto-merges GitHub's merged-PR commits into canonical Forgejo and republishes both sides. Covers when to run it, the per-repo reconcile model, dry-run vs live dispatch, conflict handling, and the staged cutover. Aliases - mirror sync, mirror reconcile, forgejo github mirror, reconcile mirrors, github mirror, forgejo mirror, mirror drift, mirror-reconcile. |
Forgejo <-> GitHub mirror-sync
Status: 🛠 Runbook | Last updated: 2026-07-03
Overview
A safe replacement for the old push --mirror timer pair. Forgejo is canonical,
GitHub is a real PR surface, so both sides advance. The old scripts force-overwrote
one side and lost commits under divergence. This reconcile auto-merges GitHub's
merged-PR commits into Forgejo and republishes to both, never force-pushing. See
infra#365 (supersedes the dead mirror timer, infra#329).
Why a skill, not just a script: the reconcile
(scripts/coilysiren-mirror-reconcile.sh) stays the source of truth for what each
per-repo case does. The judgment lives here - which dispatch mode to pick, how far
the cutover has gone, what a conflict alert means.
What runs where
- Script (the "what"):
scripts/coilysiren-mirror-reconcile.sh, per repo,
default branch only. Its header comment is the env/flag contract
(FORGEJO_TOKEN, GITHUB_TOKEN, DRY_RUN, ONLY_REPO, SINCE_HOURS).
- Entrypoint: Forgejo Actions workflow
.forgejo/workflows/mirror-sync.yml,
runs-on: docker, workflow_dispatch-only and dry-run by default. The live
schedule: is omitted until the cutover below completes.
- Being retired: the legacy
push --mirror timers
(scripts/coilysiren-{github,forgejo}-mirror.sh + systemd/ units) - ExecStart
plumbing, left alone until cutover step 5 removes them.
- Separate:
scripts/coilysiren-tangled-knot-mirror.sh (one-way GitHub -> Tangled
knot) is not part of this reconcile.
Reconcile model (per repo, default branch only)
- in sync - nothing.
- Forgejo ahead - fast-forward FJ -> GH.
- GitHub ahead (merged PR, no FJ work) - FF Forgejo to GH, push to FJ.
- diverged - merge
gh/<branch> into Forgejo (merge commit), push to both.
- merge conflict - abort that repo, leave both sides untouched, alert.
Never force-pushes, never deletes refs. A conflict is a human merge - the script
exits non-zero on any conflict/failure, turning the Actions run red.
Staged cutover (do in order, verify each)
Figure out which step the rollout is on before dispatching anything.
- Dry-run, all repos - dispatch
live=no. Every repo should read in-sync / ff /
merged, conflicts called out. Nothing is pushed.
- Pilot, one repo live -
live=yes, only_repo=<low-risk repo>. Confirm both
remotes land and history is a clean merge, not a reset.
- Full live -
live=yes, no only_repo. Confirm the digest.
- Enable the schedule - add a
schedule: cron (e.g. hourly). Failures surface as
a red Actions run.
- Delete the kai-server timers - remove
systemd/coilysiren-{github,forgejo}-mirror.*
and the two scripts, disable them on kai-server. Closes infra#329.
Prerequisites (two Forgejo Actions secrets)
FORGEJO_PUSH_TOKEN - the broad Forgejo token, from ward exec forgejo-admin-token or another operator-only source. Never SSM.
Never echo it elsewhere.
GH_PUSH_TOKEN - a new repo-scoped GitHub PAT (the /github/pat is
read:packages only). Mint it, stash in SSM /github/push-pat, record in
docs/k3s-deploy-notes-ssm-inventory.md, set as the Actions secret.
Operating it
- Dry-run all: dispatch
mirror-sync with live=no.
- One repo live:
live=yes, only_repo=coilyco-flight-deck/<name>.
- A conflict alert means a GitHub PR and Forgejo work touched the same lines - merge
that repo by hand (pull
gh/<branch>, resolve, push to Forgejo), then re-run.