Skip to content

Architecture ​

bupha is a port/adapter monorepo: every external concern sits behind a trait (a port), and each integration is a crate that implements it (an impl). bupha-core is the pure domain — config shape, queue semantics, shared primitives — and depends on nothing outside itself; the binary is a thin composition root that wires impls to ports.

Crate map ​

cratelayerpurpose
bupha-coredomainbupha domain core: config types, job queues, shared primitives — no ports, no integrations
bupha-accessportAccess port: who may trigger a repo
bupha-attestportAttest port: signed run provenance (SLSA-style predicate)
bupha-cacheportCache port: artifact-cache backend for runners
bupha-configportConfig port: where bupha.toml-shaped config comes from
bupha-queueportQueueStore port: queue persistence
bupha-queue-leaseportQueue-lease port: multi-worker fencing (try_claim + Fence)
bupha-runnerportRunner port: how a job becomes a spawned process
bupha-secretportSecret port: resolve named secrets from storage
bupha-serviceportService port: OS service lifecycle
bupha-statusportStatusSink port: where run outcomes are reported
bupha-telemetryportTelemetry port: fire-and-forget pipeline events
bupha-triggerportTrigger port: where pushes come from
bupha-tunnelportTunnel port: expose the webhook listener publicly
bupha-vcsportVcs port: VCS operations for repos and worktrees
bupha-access-fileadapterAccess adapter: allowlist of forge logins from config
bupha-access-forgeadapterAccess adapter: forge collaborator-permission check
bupha-attest-fileadapterAttest adapter: SLSA-shape predicate JSON per run (unsigned v1)
bupha-attest-sigstoreadapterAttest adapter: cosign-signed run predicates (needs cosign + key)
bupha-cache-sccacheadapterCache impl: sccache-compatible conf (file/S3/R2/GCS via conf)
bupha-cloudflaredadapterTunnel impl: cloudflared (named tunnel preferred, quick tunnel fallback)
bupha-config-tomladapterConfig impl: bupha.toml file (explicit path, env, ./, ~/.config)
bupha-containeradapterRunner adapter: unprivileged container (podman/docker), network-off
bupha-daggeradapterRunner impl: dagger call <fn> per job
bupha-dotenvadapterSecret impl: KEY=VAL env file (chmod 600)
bupha-gitadapterVcs impl: git via the git CLI
bupha-githubadapterGitHub impl: push-webhook trigger + commit-status sink
bupha-historyadapterRun-history adapter: JSONL records per finished run
bupha-keyringadapterSecret impl: OS keyring via secret-tool (libsecret)
bupha-queue-redbadapterQueueStore impl: redb embedded store (single file, ACID)
bupha-queue-redisadapterQueueStore adapter: redis (multi-worker; SET NX claim + INCR fence)
bupha-shelladapterRunner impl: bash -c <cmd> per job — no containers
bupha-sshadapterRunner impl: remote execution over ssh (rsync worktree + run)
bupha-systemdadapterService impl: systemd user units
bupha-telemetry-oteladapterTelemetry adapter: OpenMetrics textfile for Prometheus
buphabinarybupha — the oxpecker on your repo's back: headless CI CLI

modules/dagger/ (Python SDK) is the shared pipeline module bupha init installs into consumer repos — pinned to a sha.

Request flow ​

POST /hook
  → 1. path check (404 otherwise)
  → 2. read body (≤ 2 MiB)
  → 3. signature verify (Trigger port — github impl: HMAC
       sha256, constant-time)
  → 4. dedup delivery id (bounded 10k ring; github impl uses
       X-GitHub-Delivery)
  → 5. parse — skip deletes/zero-sha; revision must be path-safe
  → 6. route — repository.full_name → watched [[repo]] entry;
       git_ref must equal repo.branch
  → 7. enqueue — per-repo queue; persist via QueueStore (redb,
       ACID)
  → 8. one global worker — Vcs fetch, detached worktree
       at <base>-<repo>-<sha12>
  → 9. spawn run handle (token via env:// name, never argv)
  → 10. status — pending → success/error; superseded and
        cancelled shas get honest error statuses

Queue semantics ​

policypendingon new pushuse for
latest (default)1 shasupersedes pending; cancel_running may kill in-flightCI gates
allbounded FIFO (32)appends; oldest evicted past cap; cancel_running rejected at load — it would drop a promised commitreleases

Persistence: accepted work writes through the QueueStore port (redb, single embedded file) inside worktree.base; on restart the interrupted running sha is re-queued at the front.

One global worker serializes runs across all repos — a second push queues behind the in-flight job. Deliberate (one box, bounded resource use), not a hang: journalctl --user -u bupha-watch shows the queue moving.

Security model ​

  • mandatory HMAC (constant-time compare) — unauthenticated payloads never reach the queue
  • body cap, delivery-id replay dedup, hex-sha validation before the value touches paths or status URLs
  • webhook takeover is exact-URL only; hook API calls are timed out
  • config names secrets (secret_env/token_env) — values resolve env → OS keyring → ~/.config/bupha/env (0600); the runner gets the token through its own mechanism (env:// for dagger)

Extending ​

to add…write…core changes?
forge (gitea/gitlab)Trigger + StatusSink adapter crateno
pipeline backendRunner adapter crateno
ingress (fly/tailscale)Tunnel adapter crateno