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 der ist genau der 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:

buildx ──► skopeo copy :build-amd64 / :build-arm64   (unsignierte Staging-Tags)
       ──► crane index append ──► :SHA und :latest
       ──► cosign sign --recursive   (auf :SHA + :latest)
       ──► cosign verify             (gleicher Index-Digest, fail-closed)
       ──► 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:

# radicle/seed-node/app/deployment.yaml (gekürzt)
containers:
  - name: seed-node
    image: "oci.${EXTERNAL_DOMAIN}/radicle/node:latest"
    env:
      - { name: MODE, value: seed-node }
    ports:
      - { containerPort: 8776 }    # Radicle-Node-Protokoll → Event-Quelle
    volumeMounts:
      - { 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:

Patch geöffnet  ──►  Broker startet Run  ──►  ✓/✗ als Check am Patch
                                              (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 willkürliche Code-Ausführung 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 beim Admission. Vom Patch bis zum laufenden Pod liegt dann jeder Schritt — Forge, Build, Signatur, Registry, Policy — auf eigener Infrastruktur:

Patch  →  CI-Broker  →  build + cosign sign  →  Zot  →  Kyverno verify  →  Pod
(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, ist den Broker zu deployen, ihn an den Event-Stream der Node zu hängen und das Trust-Gating zu setzen. 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.