Mirrors git content, issues and patches/pull requests between Forgejo and Radicle
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
jeanbaptiste 568c110df1
Some checks failed
push-github / mirror (push) Has been cancelled
Merge fix/incident-20260915: post-incident hardening deployed on graft.cyberwild.org
State-loss recovery (peers.yaml), git sync fetch before ancestry checks,
per-host sync intervals, commit discussion threads, Bluesky replies to
account posts, wiki footer/trackback/author-link rendering fixes and
configurable wikirender.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 18:45:58 +02:00
.forgejo/workflows CI: fix push-github's anonymous origin clone rejecting the push-back to f1 2026-09-14 17:52:39 +02:00
cmd wikirender: link Mastodon trackbacks to the series' fediverse thread 2026-09-15 18:11:53 +02:00
docs Actualiser docs/federation.md 2026-09-15 09:57:43 +00:00
internal Record Bluesky replies to any post by the series account 2026-09-15 18:03:24 +02:00
systemd Initial commit: Forgejo <-> Radicle sync daemon 2026-09-12 17:03:32 +02:00
.gitignore Initial commit: Forgejo <-> Radicle sync daemon 2026-09-12 17:03:32 +02:00
config.example.yaml Harden sync after the 2026-09-15 state loss; fix wiki trackback rendering 2026-09-15 16:51:49 +02:00
go.mod Self-service federation onboarding + one-time secret sharing 2026-09-13 11:31:25 +02:00
go.sum Self-service federation onboarding + one-time secret sharing 2026-09-13 11:31:25 +02:00
LICENSE Initial commit: Forgejo <-> Radicle sync daemon 2026-09-12 17:03:32 +02:00
README.md Actualiser README.md 2026-09-15 08:45:31 +00:00
TUTORIAL.md Actualiser TUTORIAL.md 2026-09-13 22:10:58 +00:00

graft

A daemon and web-UI that mirrors repositories between Forgejo instances. Also connect to Radicle, Activity Pub and ATProto: git content, issues, comments and pull requests / patches.

It runs on a timer, keeps state in a local SQLite file, and never force-pushes. Real divergences between the two sides are reported and left for humans to resolve.

Nodes

What it syncs

  • Git content: the default branch, fast-forward only.
  • Issues: Forgejo issues become Radicle issues and vice versa. Create-only for now — edits made after the first mirror aren't propagated yet.
  • Patches / pull requests: a Forgejo PR opens a Radicle patch (and back), by pushing the PR's head commit to refs/patches on the Radicle side, or as a branch + PR on the Forgejo side.
  • Comments: on issues and patches, both directions, from four origins — native Forgejo ↔ Radicle, a Mastodon reply (ActivityPub), and a Bluesky reply (AT Proto). Dedup'd by content hash, prefixed with where it actually came from. See Comments.

Why

Forgejo and Radicle solve the same problem — hosting and reviewing changes to a git repository — with opposite architectures: one is a server you point a browser at, the other is a peer-to-peer protocol with no server at all. graft lets a project exist on both.

Dashboard

graft serves a status page (/) alongside the sync daemon: one row per federation, one cell per mirrored commit/issue/patch/comment, colored by kind. A mirror in sync with the rest of its federation shows the same cells as everyone else, even for content it didn't itself log; a mirror whose last sync pass actually failed shows only what it has confirmed, plus a visible error badge — the heatmap is never a silent source of "why does this one look behind."

Three buttons next to it (+ Add Forgejo peer, + Add Radicle peer, + New repo) let a peer join a federation or start a new one without shell access — instantly with the admin password, or as a request the admin approves from /admin/pending without one. Each is covered next to its manual equivalent in TUTORIAL.md; the shared mechanics are in § Self-service forms.

Fediverse / AT Proto

Each mirrored series can also speak ActivityPub: @series@your-host is followable from Mastodon, posting a note for every commit/issue/patch, with replies bridged back as real comments (see Comments above). A minimal AT Proto client can post the same to Bluesky. Both are off by default — set public_host in the config to turn ActivityPub on for a series.

Requirements

  • Go 1.23+ to build
  • git
  • the rad CLI, with git-remote-rad on PATH, and a Radicle node already running with its own identity (rad auth)
  • a Forgejo access token scoped to write:repository and write:issue on the repo you're syncing

Build

go build -o graft ./cmd/sync

Config

sync_interval: 5m
state_db: /var/lib/graft/state.db

repos:
  - name: my-project
    forgejo:
      base_url: https://git.example.org
      owner: alice
      repo: my-project
      token_file: /etc/graft/my-project.token
    radicle:
      rid: rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
      http_base_url: https://seed.example.org
      rad_home: /home/graft/.radicle
    sync:
      git: true
      issues: true
      patches: true

Tokens live in their own files, never inline in the config. graft reads them at startup and expects chmod 600.

Run

graft -config /etc/graft/config.yaml   # loop forever, per sync_interval
graft -config /etc/graft/config.yaml -once   # one pass, then exit

A systemd unit is in systemd/graft.service.

Full walkthrough, from a blank server to a running mirror: TUTORIAL.md.

License

GPLv3. See LICENSE.