Herald

Herald is a Gitea bot that performs automated AI-powered code reviews on pull requests using OpenRouter.

Features

  • Listens for Gitea webhook events and triggers code reviews on pull request comments
  • Streams reviews back to Gitea as PR comments
  • Concurrent review processing with configurable parallelism
  • Graceful shutdown — in-progress reviews finish before the process exits
  • Error tracking via Sentry
  • Prometheus metrics endpoint for monitoring
  • Tiny memory footprint (~4MB) thanks to Rust

Installation

Requirements: Rust toolchain (rustup.rs)

cargo build --release
./target/release/herald

Herald reads its configuration from environment variables (a .env file is supported):

Variable Description
HTTP_PORT Port to listen on
WEBHOOK_SIG_HEADER_SECRET Gitea webhook secret for signature verification
OPEN_ROUTER_API_KEY OpenRouter API key
OPEN_ROUTER_MODEL Model to use (e.g. deepseek/deepseek-v4-flash)
OPEN_ROUTER_TIMEOUT OpenRouter request timeout in seconds
BOT_MAX_CONCURRENT Maximum number of concurrent reviews
GITEA_URL Base URL of your Gitea instance
GITEA_TOKEN Gitea API token
GITEA_TIMEOUT Gitea API request timeout in seconds
METRICS_BIND_ADDR (optional) Bind address for the Prometheus metrics endpoint (e.g. 0.0.0.0:9100). If unset, the metrics exporter is disabled.
SENTRY_DSN (optional) Sentry DSN for error tracking
RUST_LOG (optional) Log level, defaults to info
SANDBOX_MAX_ITERATIONS (optional) Maximum number of tool-calling iterations per sandboxed review. Defaults to 8
DOCKER_HOST (optional) Container daemon socket Herald drives, e.g. unix:///run/user/1000/podman/podman.sock for a rootless podman. Defaults to unix:///var/run/docker.sock

Sandboxed reviews

Herald reviews pull requests inside an ephemeral Dev Container. For each review it:

  1. clones the pull request head into a temporary directory,
  2. builds and starts the repository's devcontainer (devcontainer-rs),
  3. reads the pull request diff and file list from the Gitea API with GITEA_TOKEN (so private repositories work), tells the model which files and lines changed — additions and deletions, with the line numbers of the new and old versions of the file respectively — then lets it explore the repository with read-only tools (ls, read_file, grep, find) run inside the container: the code itself is not sent, so the model reads it at those lines,
  4. posts the review, anchoring each comment on the added or removed line it refers to, and removes the container and the temporary clone.

Herald drives the container daemon through its socket: DOCKER_HOST (default unix:///var/run/docker.sock), which covers both docker and podman's Docker-compatible socket. The repository must contain a .devcontainer/devcontainer.json.

Herald can therefore run inside a container with only that socket mounted (no shared workspace directory is required): the clone is streamed to the daemon over the socket, like the build context, instead of being bind-mounted from a host path the daemon would have to see. This is the setup the Containerfile produces, e.g.:

podman run --env-file=.env -p 3001:3001 \
    -v /run/user/$(id -u)/podman/podman.sock:/var/run/docker.sock \
    herald:latest

The runArgs of that file are read but deliberately not passed to the daemon: they come from an untrusted pull request, and one of them (--network host) would attach the container to another network and quietly defeat the network cut described below. A devcontainer that relies on them (--gpus all, --cap-add, --shm-size…) will not get them.

Each sandbox is isolated: it gets its own image tag, container and network. The container starts with network access so the postCreateCommand / postStartCommand hooks can install dependencies (e.g. npm install); once the hooks have run, the container is disconnected from the network for the rest of the review. Every container command is bounded by a timeout, and the container, network and image are removed when the review ends (including on failure).

Development

The easiest way to get started is with the provided Dev Container (VS Code or Zed with the dev container extension).

Open the project and reopen it in the container — the Rust toolchain is pre-installed, along with rootless podman, so sandboxed reviews can be exercised locally: start its API socket with podman system service --time=0 & and set DOCKER_HOST=unix:///run/user/$(id -u)/podman/podman.sock.

Without Dev Container, you just need a Rust toolchain:

rustup toolchain install stable
cargo run

Copy .env.example to .env and fill in your values before running.

Metrics

Herald optionally exposes a Prometheus metrics endpoint, useful for scraping with an OpenTelemetry Collector or any Prometheus-compatible scraper.

Set METRICS_BIND_ADDR (e.g. 0.0.0.0:9100) to enable it. The metrics are then available at http://<host>:9100/metrics.

Exposed metrics

Metric Type Description
herald_webhooks_received_total counter Total webhooks received (label: event_type)
herald_webhooks_duplicate_total counter Webhooks rejected as duplicates (label: event_type)
herald_webhooks_channel_full_total counter Webhooks dropped because the bot channel was full (label: event_type)
herald_bot_tasks_active gauge Bot tasks currently in progress
herald_bot_tasks_completed_total counter Bot tasks completed successfully (label: event_type)
herald_bot_tasks_failed_total counter Bot tasks that failed (label: event_type)
herald_openrouter_cost_cents_total counter Total OpenRouter cost in cents (divide by 100 for USD)

OTel collector example

receivers:
  prometheus:
    config:
      scrape_configs:
        - job_name: herald
          scrape_interval: 15s
          static_configs:
            - targets: ["herald:9100"]

service:
  pipelines:
    metrics:
      receivers: [prometheus]
      exporters: [otlp]
S
Description
A self-hosted Gitea bot powered by OpenRouter that reviews pull requests, answers questions, and implements features directly from issue comments.
Readme
296 KiB
2026-09-20 17:07:14 +02:00
Languages
Rust 99%
Dockerfile 0.8%
Shell 0.2%