- TypeScript 96.4%
- Dockerfile 3.6%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
|
||
| .forgejo/workflows | ||
| workflow-template | ||
| .gitignore | ||
| Dockerfile | ||
| package.json | ||
| post-review.test.ts | ||
| post-review.ts | ||
| README.md | ||
| run-review.test.ts | ||
| run-review.ts | ||
| tsconfig.json | ||
ocr-forgejo
Forgejo Actions integration for OpenCodeReview
(ocr) — Alibaba's AI code reviewer. Opens a PR, gets an inline review.
Upstream ships CI glue for GitHub, GitLab, Gerrit, GitFlic, Bitbucket and Codeup,
but not Forgejo. That is by design: upstream deliberately keeps
platform-specific publishing out of the ocr binary and puts it in the pipeline
(see examples/*/post_review.py). This repo is the Forgejo equivalent of that
glue, plus the job image to run it in.
How it works
PR opened -> Forgejo Actions -> ocr review --format json -> Forgejo review API
(ocr-forgejo image) (inline comments)
run-review.tsresolves the PR from the Actions event payload.- It runs
ocr review --from origin/<base> --to <head> --format json. post-review.tspublishes the findings as one Forgejo pull request review: inline comments on the diff, plus a summary note for anything that could not be placed inline.
Setup
Per repository
Copy workflow-template/ocr-review.yml to
.forgejo/workflows/ocr-review.yml. That is the whole per-repo cost — the image
carries the tool and the glue.
Org-level (already done for Evobug)
| Secret | What |
|---|---|
OCR_BOT_TOKEN |
PAT for the ocr-bot user (scopes: write:repository, read:user). Reviews are posted under this identity. |
OPENROUTER_API_KEY |
LLM key. ocr is bring-your-own-key; no reviews run without it. Swap for ANTHROPIC_API_KEY if you go direct — see Using OpenRouter. |
ocr-bot is a member of the bots team, which has includes_all_repositories,
so it gets write on every Evobug repo automatically — including new ones. Same
pattern as renovate-bot.
Triggers and cost
Reviews are billed per LLM call, so the default is deliberately not "every push":
pull_request—opened,reopened,ready_for_reviewissue_comment— a comment starting with/ocr(or/open-code-review,@ocr)
On a pull_request event the driver first checks whether ocr-bot has already
left a review carrying the <!-- open-code-review --> marker and, if so, exits
without calling the LLM. Comment triggers always re-review, which is how you ask
for a second pass after pushing fixes.
Configuration
Set these as env: on the review step.
| Variable | Default | Meaning |
|---|---|---|
OCR_LLM_MODEL |
(required) | Model id, e.g. claude-opus-5. ocr has no built-in default. |
OCR_LLM_TOKEN |
(required) | LLM API key. |
OCR_LLM_URL |
provider default | Override for an OpenAI-compatible endpoint. |
OCR_USE_ANTHROPIC |
true |
false selects the OpenAI protocol. |
OCR_LLM_PROTOCOL |
(unset) | Explicit protocol (anthropic / openai); wins over OCR_USE_ANTHROPIC. |
OCR_LLM_EXTRA_HEADERS |
(unset) | Extra request headers, K=V,K=V. Read by ocr directly. |
OCR_LLM_EXTRA_BODY |
(unset) | JSON merged into every request body (e.g. OpenRouter provider routing). |
FORGEJO_TOKEN |
(required) | Bot PAT used to post the review. |
FORGEJO_API_URL |
$GITHUB_API_URL |
Set to http://forgejo:3000/api/v1 to keep API traffic internal. |
OCR_BOT_LOGIN |
ocr-bot |
Login used for the already-reviewed check. |
OCR_SKIP_IF_REVIEWED |
true |
false re-reviews on every triggering event. |
OCR_ROUTE_SEVERITY_BELOW |
(unset) | Move findings at/below this severity to the summary instead of inline. |
OCR_ROUTE_CATEGORIES |
(unset) | Comma-separated categories to move to the summary. |
OCR_CONCURRENCY |
ocr default |
Concurrent LLM requests. |
OCR_EXTRA_ARGS |
(unset) | Extra flags passed to ocr review, e.g. --rule ./rules.json. |
Routing is fail-open: unknown severities and categories are ignored, and a routed finding is relocated to the summary, never dropped.
Using OpenRouter (or any OpenAI-compatible endpoint)
ocr has no OpenRouter preset, but it doesn't need one — the generic
OpenAI-compatible path covers it. Set the base URL without /chat/completions;
ocr appends it.
env:
OCR_LLM_URL: https://openrouter.ai/api/v1
OCR_LLM_TOKEN: ${{ secrets.OPENROUTER_API_KEY }}
OCR_LLM_MODEL: anthropic/claude-opus-5 # an OpenRouter model slug
OCR_USE_ANTHROPIC: "false" # OpenAI protocol
The model is an OpenRouter slug (vendor/model), not an Anthropic model id
— check https://openrouter.ai/models for the current one.
Sessions are automatic. One review is dozens of per-file completions, which
arrive on the OpenRouter dashboard as unattributable noise. When the endpoint is
OpenRouter, the driver adds session_id to every request so a whole review
groups into one session you can cost and debug as a unit:
evobug-spacejanitorce-pr-156
Scoped by repo, because PR numbers collide across them. Re-reviews of the same
PR deliberately share a session, so the dashboard shows that PR's full history
in one place. Override with OCR_SESSION_ID, or set it to the empty string to
opt out.
It is injected only for OpenRouter: extra_body keys are merged as
top-level request fields, and the Anthropic API rejects unknown ones — so
injecting unconditionally would 400 every Anthropic-direct run.
Two optional knobs worth knowing:
OCR_LLM_EXTRA_HEADERS: "HTTP-Referer=https://git.evobug.com,X-Title=ocr-forgejo"— OpenRouter's attribution headers.OCR_LLM_EXTRA_BODY: '{"provider":{"order":["Anthropic"]}}'— pin which upstream provider serves the request, so a review can't silently land on a quantized or differently-tuned host.
Why you might prefer it: one key and one balance across every model, easy A/B between vendors by changing a string, and no per-vendor account. The tradeoffs are an extra hop in the path and pricing slightly above going direct.
The one non-obvious thing
Forgejo does not validate comment anchors — this code has to.
Posting a review comment on a line outside the diff, or on a file the PR never
touched, returns HTTP 200. Forgejo then marks the comment invalidated and it
never appears in the diff view. Verified against Forgejo 16.0.2: new_position: 9999 and a nonexistent path both returned 200.
So a server error is not a usable signal. parseDiffPositions() builds the set
of commentable lines from git diff -U3 and anything outside it is demoted to
the summary before posting. Remove that check and findings disappear silently
— which is the exact failure fail-open exists to prevent.
Two smaller ones, both learned the same way:
- Creating a review with
event: PENDINGrequires a non-empty body (review event PENDING requires a body). A placeholder is sent at create and replaced by the real summary at submit. - The job image cannot be
node:22-bookworm(the stockdockerlabel): it ships git 2.39.5 andocrrequires >= 2.41. Alpine ships 2.49.
The image
git-ssh.evobug.com/evobug/ocr-forgejo:v1.8.8 — Bun 1.3 + git 2.49 + node 22 +
a checksum-verified ocr binary + this glue. ~230 MB.
node is present because Forgejo Actions runs JavaScript actions
(actions/checkout) with the job container's own node.
It is pushed to git-ssh.evobug.com rather than git.evobug.com because that
endpoint bypasses Cloudflare's request cap for registry traffic. Packages are
shared between both hostnames. The package is publicly readable, so jobs pull it
without credentials.
Bump OCR_VERSION in the Dockerfile and the tag together, so an image tag
always names an exact reviewer version.
Development
bun test # 67 tests, no network, no sleeping
docker build -t ocr-forgejo . # build the job image
publish() takes an injectable requester and makeRequester() takes an
injectable fetch and sleep, so routing, retry/backoff and the
inline -> summary demotion are all testable offline. Keep it that way.