Remove the openrouter-rs dependency in favor of a minimal in-tree OpenRouter chat-completions client, and drop the BOT_NAME and SANDBOX_ENABLED config options. Reviews now always run inside the sandbox, and the review prompt asks the model to read files with the available tools instead of embedding the diff.
119 lines
5.1 KiB
Markdown
119 lines
5.1 KiB
Markdown
# Herald
|
|
|
|
Herald is a Gitea bot that performs automated AI-powered code reviews on pull requests using [OpenRouter](https://openrouter.ai/).
|
|
|
|
## 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](https://rustup.rs))
|
|
|
|
```sh
|
|
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` |
|
|
| `CONTAINER_RUNTIME` | *(optional)* Container runtime binary used for the sandbox (`docker` or `podman`). Defaults to `docker` |
|
|
| `SANDBOX_MAX_ITERATIONS` | *(optional)* Maximum number of tool-calling iterations per sandboxed review. Defaults to `8` |
|
|
|
|
## Sandboxed reviews
|
|
|
|
Herald reviews pull requests inside an ephemeral
|
|
[Dev Container](https://containers.dev/). 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.
|
|
|
|
The container runtime is selected with `CONTAINER_RUNTIME` (`docker` or
|
|
`podman`). The repository must contain a `.devcontainer/devcontainer.json`.
|
|
|
|
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](https://containers.dev/) (VS Code or Zed with the dev container extension).
|
|
|
|
Open the project and reopen it in the container — the Rust toolchain is pre-installed.
|
|
|
|
**Without Dev Container**, you just need a Rust toolchain:
|
|
|
|
```sh
|
|
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](https://opentelemetry.io/docs/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
|
|
|
|
```yaml
|
|
receivers:
|
|
prometheus:
|
|
config:
|
|
scrape_configs:
|
|
- job_name: herald
|
|
scrape_interval: 15s
|
|
static_configs:
|
|
- targets: ["herald:9100"]
|
|
|
|
service:
|
|
pipelines:
|
|
metrics:
|
|
receivers: [prometheus]
|
|
exporters: [otlp]
|
|
``` |