Radicle CI — eine Pipeline, die auf Patches reagiert

Der Code des Labs liegt in einer P2P-Forge, die CI aber noch nicht: Builds laufen post-merge auf einer externen .build.yml. Wie ein Radicle-CI-Broker das schließt — er hört auf die Patch-Events der eigenen Seed-Node, baut und signiert in-cluster und schreibt das Ergebnis zurück auf den Patch. Plus das eigentlich harte Problem: Vertrauen in einer Forge ohne Owner.
Table of contents
Stand: Design/Roadmap. Issue 7f834f9 : „Wire Radicle CI once the lab repo is published (patches already wired)." Die Seed-Node läuft in-cluster, der Patch-Workflow steht — der CI-Broker ist das letzte fehlende Stück. Dieser Beitrag beschreibt, wie er andocken soll.

In Von SourceHut zu Radicle ist der Lab-Code in eine Peer-to-Peer-Forge umgezogen: keine zentrale Instanz mehr, Patches und Issues leben als signierte Collaborative Objects (COBs), Flux deployt vom kanonischen main. Ein Faden blieb dort offen — und genau um den geht es hier: Radicle ist eine Forge, kein CI-System. Der Code liegt P2P, der Build aber noch nicht.

Der offene Faden#

Heute läuft die CI als externes .build.yml, ausgelöst, nachdem ff0x nach main merged — nicht aus dem Cluster heraus. Der Signing-Flow ist dabei durchaus solide:

1buildx ──> skopeo copy :build-amd64 / :build-arm64   (unsignierte Staging-Tags)
2       ──> crane index append ──> :SHA und :latest
3       ──> cosign sign --recursive   (auf :SHA + :latest)
4       ──> cosign verify             (gleicher Index-Digest, fail-closed)
5       ──> oci.this-is-fine.io  (Zot)

Zwei Dinge stören daran. Erstens: Feedback gibt es erst nach dem Merge — ein kaputter Build fällt auf, wenn die Änderung schon kanonisch ist. Zweitens: Es ist die letzte harte Abhängigkeit von einer externen, zentralen Forge — ausgerechnet beim Schritt, der die Container-Images der eigenen Lieferkette baut und signiert.

Wie Radicle CI funktioniert#

Der Trick von Radicle CI ist nicht „noch ein Runner", sondern wo der Trigger herkommt. Eine Radicle-Node führt einen Event-Stream: Jede neue oder aktualisierte Patch-Ref, jedes COB-Update wird als Ereignis sichtbar. Ein CI-Broker abonniert diesen Stream, übersetzt ein Patch-Event in einen Pipeline-Lauf auf einem Backend und schreibt das Resultat als Check/Kommentar zurück auf den Patch — also vor dem Merge.

flowchart LR P["git push rad HEAD:refs/patches"] --> N["seed-node — COB-/Patch-Events (:8776)"] N --> B["CI-Broker (subscribed)"] B --> R["Runner: buildx → cosign → Zot"] R --> CK["Check-Resultat"] CK --> N

Das ist dieselbe Idee wie ein GitHub-Webhook auf einen Pull Request — nur dass der „Webhook" hier ein dezentraler, signierter Event-Stream ist, den jede replizierende Node mitliest.

Die Verdrahtung im Lab#

Das Schöne: Die Eventquelle steht schon. Die Seed-Node läuft in-cluster, hält die kanonischen Refs und spricht das Radicle-Protokoll auf :8776:

 1# radicle/seed-node/app/deployment.yaml (gekürzt)
 2containers:
 3  - name: seed-node
 4    image: "oci.${EXTERNAL_DOMAIN}/radicle/node:latest"
 5    env:
 6      - { name: MODE, value: seed-node }
 7    ports:
 8      - { containerPort: 8776 }    # Radicle-Node-Protokoll → Event-Quelle
 9    volumeMounts:
10      - { name: storage, mountPath: "/node/radicle" }

Was dazukommt, ist der Broker als eigenes Deployment, das an dieser Node lauscht und einen Runner startet. Der Runner führt denselben Flow wie heute aus — buildxskopeocrane indexcosign sign/verify → Push nach Zot — nur jetzt in-cluster und an den Patch gebunden statt an den Merge. Der Lauf rendert sich zurück in den Patch:

1Patch geöffnet  ──>  Broker startet Run  ──>  ✓/✗ als Check am Patch
2                                              (Build, Test, cosign-verify)

Damit verschiebt sich das Feedback vom „nach dem Merge" zum „am Vorschlag" — und der Build verlässt den Cluster nicht mehr.

Das eigentliche Problem: Vertrauen#

Hier liegt der interessante, P2P-spezifische Teil. In einer klassischen Forge gibt es einen Owner, der entscheidet, wessen Code die CI ausführen darf. In Radicle kann ein Patch von jeder NID kommen — die Forge hat keinen zentralen Türsteher. Und CI ist nichts anderes als die Ausführung beliebigen Codes mit Registry- und Cluster-nahen Secrets.

Im Lab ist das nicht theoretisch: Der ZeroClaw-Bot hat eine eigene NID und schlägt selbst Patches vor; der Hub seedet das Repo mit scope: all, damit auch die Nicht-Delegate-Namespaces (z. B. claw) repliziert werden. Der Broker muss also explizit entscheiden:

  • Wessen Patches laufen automatisch? Naheliegend: nur Patches von Delegates (den vertrauten Identitäten des Repos) lösen sofort einen Lauf aus.
  • Was passiert mit dem Rest? Patches fremder NIDs entweder gar nicht bauen, erst nach manueller Freigabe, oder in einer Sandbox ohne Zugriff auf Signing-Keys und Registry-Push.

Das ist die Frage, die GitHub einem abnimmt und die eine P2P-Forge zurückgibt. Sie muss vor dem Scharfschalten beantwortet sein — sonst ist der CI-Broker ein offenes Tor zu COSIGN_KEY und Registry-Credentials.

Reihenfolge zählt: Erst das Trust-Gating (Delegate-only-Auto-Run + Sandbox für den Rest), dann Secrets an den Runner geben. Ein Broker, der unsignierte Fremd-Patches mit Push-Rechten baut, ist schlimmer als gar keine CI.

Wo die Lieferkette zusammenläuft#

Zieht die CI in den Cluster, schließt sich der Kreis mit dem bestehenden Supply-Chain-Setup. Der Runner baut und signiert mit cosign , pusht in die eigene Zot-Registry , und Kyverno verifiziert die Signatur bei der Admission. Vom Patch bis zum laufenden Pod liegt dann jeder Schritt — Forge, Build, Signatur, Registry, Policy — auf eigener Infrastruktur:

1Patch  ->  CI-Broker  ->  build + cosign sign  ->  Zot  ->  Kyverno verify  ->  Pod
2(Radicle)  (in-cluster)        (eigener Key)   (eigene Reg)  (Admission)

Was noch fehlt#

„Patches already wired" heißt: Die Pipeline-Definition liegt bereits im Repo, das Repo ist inzwischen public, die Seed-Node läuft. Was bleibt: den Broker deployen, ihn an den Event-Stream der Node hängen und das Trust-Gating scharf schalten. Der Lohn ist der Wegfall der letzten externen Abhängigkeit im Dev-Loop: vom git push rad HEAD:refs/patches bis zum signierten Image in der eigenen Registry, ohne dass eine einzige zentrale Forge dazwischensteht.