The previous implementation called os.Lstat(absPath) which only avoids following the *final* path component. A PR committing .review-bot/ as a directory symlink pointing outside the repo would pass the filepath.Rel confinement check because the textual path was inside the root while the resolved destination was not. Fix: call filepath.EvalSymlinks after filepath.Abs to resolve ALL symlink components before the confinement check. If EvalSymlinks fails (dangling symlink, nonexistent target) the path is rejected. The filepath.Rel check then operates on the fully-resolved path. Semantic change: file-level in-repo symlinks (target also within root) are now allowed — the invariant is about where the content lives, not whether the entry is a symlink. The test TestValidateDocmapPath_Symlink is updated to test an out-of-repo symlink target, which must still be rejected. Tests: - TestValidateDocmapPath_DirSymlinkBypass: reproduces the attack vector (dir symlink bypassing textual confinement check) and verifies it is now rejected - TestValidateDocmapPath_Symlink: updated to test out-of-repo symlink Coverage: 54.0%
review-bot
AI-powered code review bot for Gitea pull requests. Fetches diff + context, sends to an LLM, and posts a structured review (APPROVE / REQUEST_CHANGES) back to the PR.
Features
- Multi-provider: OpenAI-compatible, Anthropic Messages API, and SAP AI Core
- Context-aware: Fetches full file content, conventions, language patterns, CI status
- Path-scoped docs:
doc-mapconfig injects only the governing design docs for changed paths - Smart budget: Automatically trims context to fit model token limits
- Idempotent reviews: Posts new review, then cleans up stale ones (one review per bot)
- Custom prompts: Load additional instructions from a file (e.g. security-focused review)
- Minimal dependencies: Go stdlib +
github.com/goccy/go-yamlonly
Quick Start: Composite Action
The easiest way to use review-bot in your Gitea CI:
# .gitea/workflows/review.yml
name: Review
on:
pull_request:
types: [opened, synchronize]
jobs:
review:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: https://gitea.weiker.me/rodin/review-bot/.gitea/actions/review@v0.1.0
with:
reviewer-token: ${{ secrets.REVIEW_TOKEN }}
reviewer-name: code-review
llm-base-url: ${{ secrets.LLM_BASE_URL }}
llm-api-key: ${{ secrets.LLM_API_KEY }}
llm-model: gpt-4.1
That's it. Every PR gets an automated review.
Examples
Single reviewer with conventions
jobs:
review:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: https://gitea.weiker.me/rodin/review-bot/.gitea/actions/review@v0.1.0
with:
reviewer-token: ${{ secrets.REVIEW_TOKEN }}
reviewer-name: reviewer
llm-base-url: ${{ secrets.LLM_BASE_URL }}
llm-api-key: ${{ secrets.LLM_API_KEY }}
llm-model: gpt-4.1
conventions-file: CONVENTIONS.md
timeout: '600'
Two reviewers with different models (diversity of opinion)
jobs:
review:
runs-on: ubuntu-24.04
strategy:
matrix:
include:
- name: gpt
model: gpt-4.1
token_secret: GPT_REVIEW_TOKEN
- name: claude
model: claude-sonnet-4-20250514
token_secret: CLAUDE_REVIEW_TOKEN
provider: anthropic
steps:
- uses: actions/checkout@v4
- uses: https://gitea.weiker.me/rodin/review-bot/.gitea/actions/review@v0.1.0
with:
reviewer-token: ${{ secrets[matrix.token_secret] }}
reviewer-name: ${{ matrix.name }}
llm-base-url: ${{ secrets.LLM_BASE_URL }}
llm-api-key: ${{ secrets.LLM_API_KEY }}
llm-model: ${{ matrix.model }}
llm-provider: ${{ matrix.provider }}
conventions-file: CONVENTIONS.md
Each reviewer posts independently and only cleans up its own stale reviews.
Multiple review types from a single bot account
Use the same Gitea token but different reviewer-name values to run specialized reviews without needing multiple bot accounts:
jobs:
review:
runs-on: ubuntu-24.04
strategy:
matrix:
include:
- name: code-quality
model: gpt-4.1
- name: security
model: gpt-4.1
system_prompt_file: .review/SECURITY.md
- name: performance
model: gpt-4.1
system_prompt_file: .review/PERFORMANCE.md
steps:
- uses: actions/checkout@v4
- uses: https://gitea.weiker.me/rodin/review-bot/.gitea/actions/review@v0.1.0
with:
reviewer-token: ${{ secrets.REVIEW_TOKEN }}
reviewer-name: ${{ matrix.name }}
llm-base-url: ${{ secrets.LLM_BASE_URL }}
llm-api-key: ${{ secrets.LLM_API_KEY }}
llm-model: ${{ matrix.model }}
system-prompt-file: ${{ matrix.system_prompt_file }}
The sentinel <!-- review-bot:security --> ensures the security review only replaces previous security reviews, never the code-quality or performance reviews.
With language patterns from another repo
- uses: https://gitea.weiker.me/rodin/review-bot/.gitea/actions/review@v0.1.0
with:
reviewer-token: ${{ secrets.REVIEW_TOKEN }}
reviewer-name: reviewer
llm-base-url: ${{ secrets.LLM_BASE_URL }}
llm-api-key: ${{ secrets.LLM_API_KEY }}
llm-model: gpt-4.1
conventions-file: CLAUDE.md
patterns-repo: rodin/go-patterns,rodin/kubernetes-conventions
patterns-files: "README.md,patterns/"
Pattern repos are fetched at review time. The reviewer uses them as criteria for idiomatic code.
Dry run (test without posting)
- uses: https://gitea.weiker.me/rodin/review-bot/.gitea/actions/review@v0.1.0
with:
reviewer-token: ${{ secrets.REVIEW_TOKEN }}
reviewer-name: test
llm-base-url: ${{ secrets.LLM_BASE_URL }}
llm-api-key: ${{ secrets.LLM_API_KEY }}
llm-model: gpt-4.1
dry-run: 'true'
Prints the review to CI logs without posting to the PR. Useful for testing prompt changes.
Using Anthropic directly
- uses: https://gitea.weiker.me/rodin/review-bot/.gitea/actions/review@v0.1.0
with:
reviewer-token: ${{ secrets.REVIEW_TOKEN }}
reviewer-name: claude
llm-base-url: https://api.anthropic.com
llm-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
llm-model: claude-sonnet-4-20250514
llm-provider: anthropic
Using SAP AI Core
For SAP environments with AI Core deployments, use the aicore provider for native authentication:
- uses: https://gitea.weiker.me/rodin/review-bot/.gitea/actions/review@v0.1.0
with:
reviewer-token: ${{ secrets.REVIEW_TOKEN }}
reviewer-name: aicore-review
llm-model: anthropic--claude-4.6-sonnet # or gpt-5
llm-provider: aicore
aicore-client-id: ${{ secrets.AICORE_CLIENT_ID }}
aicore-client-secret: ${{ secrets.AICORE_CLIENT_SECRET }}
aicore-auth-url: ${{ secrets.AICORE_AUTH_URL }}
aicore-api-url: ${{ secrets.AICORE_API_URL }}
aicore-resource-group: default
AI Core handles OAuth token management and deployment discovery automatically. Model names must match the deployment name in AI Core (e.g. anthropic--claude-4.6-sonnet, gpt-5).
Action Inputs
| Input | Required | Default | Description |
|---|---|---|---|
reviewer-token |
Yes | — | Gitea token for posting reviews (needs write:issue, write:repository) |
reviewer-name |
No | "" |
Logical identity for this reviewer. Used as sentinel for idempotent cleanup. Set this when running multiple review bots on the same PR. |
llm-base-url |
No* | "" |
LLM API base URL (required unless using aicore provider) |
llm-api-key |
No* | "" |
LLM API key (required unless using aicore provider) |
llm-model |
Yes | — | Model name |
llm-provider |
No | openai |
API provider: openai, anthropic, or aicore |
aicore-client-id |
No** | "" |
SAP AI Core client ID |
aicore-client-secret |
No** | "" |
SAP AI Core client secret |
aicore-auth-url |
No** | "" |
SAP AI Core authentication URL |
aicore-api-url |
No** | "" |
SAP AI Core API URL |
aicore-resource-group |
No | default |
SAP AI Core resource group |
conventions-file |
No | "" |
Path to coding conventions file in the repo |
patterns-repo |
No | "" |
Comma-separated repos with language patterns (e.g. rodin/go-patterns) |
patterns-files |
No | README.md |
Files/directories to fetch from pattern repos |
system-prompt-file |
No | "" |
Local file with additional system prompt instructions |
doc-map |
No | "" |
Path to a YAML file mapping source path globs to governing design docs |
doc-map-max-bytes |
No | 102400 |
Maximum bytes of injected doc content from doc-map (default 100KB) |
persona |
No | "" |
Built-in persona name (security, architect, docs) |
persona-file |
No | "" |
Path to persona file (YAML or JSON) with custom review focus |
temperature |
No | 0 |
LLM temperature (0 = server default) |
timeout |
No | 300 |
LLM request timeout in seconds |
dry-run |
No | false |
Print review to stdout instead of posting |
update-existing |
No | true |
Delete previous review from same bot before posting. Accepts: true/1/yes or false/0/no |
version |
No | latest |
review-bot version to install |
*Required for openai and anthropic providers, not for aicore.
**Required only for aicore provider.
Runner Requirements
The composite action requires these tools on the runner:
| Tool | Used For |
|---|---|
python3 |
JSON parsing during version detection |
sha256sum |
Checksum verification of downloaded binary |
curl |
Downloading releases and querying the API |
All three are pre-installed on ubuntu-* runners (e.g. ubuntu-24.04). If you use a custom runner image, ensure these are available.
How Review Cleanup Works
When reviewer-name is set, the bot embeds a hidden sentinel in each review:
<!-- review-bot:code-review -->
On the next run, it finds and deletes any review containing its own sentinel (except the one it just posted). This means:
- One review per bot per PR — no clutter from repeated pushes
- Multiple bots coexist — each only cleans up its own reviews
- Same token, different roles — a single bot account can post "code-review" and "security" reviews without conflict
- No extra permissions — identity comes from the sentinel, not the API
If reviewer-name is empty, cleanup is skipped (reviews stack like before).
Shared Token: Worst-Wins Behavior
When multiple review types share the same Gitea bot account (e.g. code-quality and security), Gitea determines the user's approval state from their most recent review. This creates a race condition: if security finds issues (REQUEST_CHANGES) but code-quality finishes last (APPROVE), the PR appears approved.
review-bot handles this automatically with worst-wins reconciliation: before posting, each job checks whether any sibling review from the same user already has REQUEST_CHANGES. If so and this job would post APPROVE, it posts as REQUEST_CHANGES instead — maintaining the block. This ensures the PR stays blocked until all checks pass, regardless of execution order.
If you need independent approval/block per review type, use separate Gitea bot accounts with their own tokens.
Custom Review Prompts
Use system-prompt-file to specialize the review focus. The file contents are appended to the base system prompt as "Additional Review Instructions."
Example SECURITY_REVIEW.md:
You are performing a security-focused code review.
Focus areas:
- Injection attacks (SQL, command, path traversal, template)
- Authentication/Authorization (missing checks, privilege escalation)
- Secrets exposure (hardcoded credentials, tokens in logs)
- Input validation (unsanitized input, unsafe deserialization)
- Race conditions (TOCTOU, unsynchronized shared state)
Rules:
- Only report findings with security implications
- Ignore style, naming, and general code quality
- MAJOR = exploitable vulnerability, MINOR = hardening opportunity, NIT = theoretical risk
- If no security-relevant changes exist, APPROVE with empty findings
CLI Usage
review-bot \
--vcs-url https://gitea.example.com \
--repo owner/name \
--pr 42 \
--reviewer-token "$GITEA_TOKEN" \
--reviewer-name "code-review" \
--llm-base-url https://api.openai.com/v1 \
--llm-api-key "$OPENAI_API_KEY" \
--llm-model gpt-4.1 \
--conventions-file CONVENTIONS.md
Subcommands
validate-docmap
Verifies that a doc-map.yml is consistent before running a review. Two checks:
- Coverage: every changed file is matched by at least one
paths:glob. - Stale docs: every
docs:entry exists on disk under--repo-root.
# Typical CI usage — pipe git diff into the command
git diff --name-only origin/main HEAD | \
review-bot validate-docmap \
--docmap .review-bot/doc-map.yml \
--repo-root .
| Flag | Required | Default | Description |
|---|---|---|---|
--docmap |
Yes | — | Path to doc-map YAML file |
--repo-root |
No | . (cwd) |
Root for resolving docs: paths |
Exit codes: 0=clean, 1=failures found, 2=usage/parse error.
validate-url
Resolves a URL and verifies all IPs are publicly routable (used in CI to prevent SSRF).
review-bot validate-url https://gitea.example.com
Exit codes: 0=safe, 1=blocked/private IP, 2=error.
Environment Variables
All flags have environment variable equivalents:
| Flag | Env Var |
|---|---|
--vcs-url |
VCS_URL (fallback: GITEA_URL) |
--repo |
GITEA_REPO |
--pr |
PR_NUMBER |
--reviewer-token |
REVIEWER_TOKEN |
--reviewer-name |
REVIEWER_NAME |
--llm-base-url |
LLM_BASE_URL |
--llm-api-key |
LLM_API_KEY |
--llm-model |
LLM_MODEL |
--llm-provider |
LLM_PROVIDER |
--conventions-file |
CONVENTIONS_FILE |
--patterns-repo |
PATTERNS_REPO |
--patterns-files |
PATTERNS_FILES |
--system-prompt-file |
SYSTEM_PROMPT_FILE |
--llm-temperature |
LLM_TEMPERATURE |
--llm-timeout |
LLM_TIMEOUT |
--update-existing |
UPDATE_EXISTING |
Setup
- Create a Gitea bot account (e.g.
review-bot) - Generate a token with scopes:
write:issue,write:repository - Add secrets to your Gitea repo (Settings → Actions → Secrets):
REVIEW_TOKEN— the bot's Gitea tokenLLM_BASE_URL— your LLM endpointLLM_API_KEY— your LLM key
- Add the workflow (see Quick Start above)
Token Scopes Required
| Scope | Purpose |
|---|---|
write:issue |
Post and delete reviews |
write:repository |
Read PR diffs, file content, commit statuses |
read:user |
Self-request as reviewer (optional but recommended) |
Without read:user, the bot still works but cannot add itself to the PR's reviewer list.
Development
go test ./... # Unit tests
go vet ./... # Static analysis
go build -o review-bot ./cmd/review-bot
# Integration tests (requires env vars set)
go test -tags=integration ./...
Architecture
cmd/review-bot/ CLI entrypoint + orchestration
gitea/ Gitea API client (reviews, PRs, files)
llm/ Multi-provider LLM client (OpenAI + Anthropic)
review/ Prompt building, response parsing, formatting
budget/ Token estimation + context trimming
License
MIT
Review Personas
Personas provide role-based review specialization. Instead of generic code review, each persona focuses on a specific domain (security, architecture, documentation) with tailored prompts and severity calibration.
Built-in Personas
| Persona | Focus |
|---|---|
security |
Vulnerabilities, auth bypass, secrets exposure, injection attacks |
architect |
Design patterns, code organization, API contracts, testability |
docs |
Documentation quality, API clarity, error messages |
Using Built-in Personas
- uses: rodin/review-bot/.gitea/actions/review@v1
with:
reviewer-name: security
persona: security
llm-model: claude-opus-4-20250514 # Security benefits from strong reasoning
...
Multiple Personas in Parallel
jobs:
review:
strategy:
matrix:
include:
- name: security
persona: security
- name: architect
persona: architect
steps:
- uses: rodin/review-bot/.gitea/actions/review@v1
with:
reviewer-name: ${{ matrix.name }}
persona: ${{ matrix.persona }}
...
Each persona posts independently with its own sentinel, so reviews don't interfere.
Custom Personas
Create a YAML file with your domain-specific review focus:
# .review/personas/trading.yaml
name: trading
display_name: Trading Domain Expert
identity: |
You are a trading systems expert reviewing code for correctness.
Your expertise:
- Order lifecycle and state machines
- Fill handling and partial fills
- Position tracking and P&L calculations
- Event sourcing invariants
focus:
- Order state machine correctness
- Fill handling edge cases (partial, overfill)
- Position and P&L calculation accuracy
- Event replay determinism
- Decimal precision for money
ignore:
- Code style
- General performance
- Documentation formatting
severity:
major: "Bugs that cause incorrect positions, fills, or money calculations"
minor: "Edge cases that could cause issues under unusual conditions"
nit: "Clarity improvements for domain logic"
Use it in CI:
- uses: rodin/review-bot/.gitea/actions/review@v1
with:
reviewer-name: trading
persona-file: .review/personas/trading.yaml
...
YAML is the recommended format for personas because it supports:
- Multi-line strings with
|blocks (cleaner identity definitions) - Comments for documentation
- More readable arrays and nested structures
JSON is also supported for backwards compatibility—just use .json extension.
Persona vs system-prompt-file
| Feature | persona / persona-file |
system-prompt-file |
|---|---|---|
| Replaces base prompt | Yes | No (appends) |
| Structured format | Yes (YAML/JSON) | No (freeform) |
| Focus/ignore lists | Yes | Manual |
| Severity calibration | Yes | Manual |
| Header display name | Yes | No |
| Built-in options | Yes | No |
Use personas for domain-specialized reviews. Use system-prompt-file for minor tweaks to the generic review.