Forgejo Actions integration for OpenCodeReview (alibaba/open-code-review)
  • TypeScript 96.4%
  • Dockerfile 3.6%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
ocr-bot 5b0cf5c2a1
All checks were successful
Test / test (push) Successful in 4s
ci: pin DeepSeek to its own endpoint (same cost, unquantized)
2026-08-05 00:29:18 +02:00
.forgejo/workflows ci: pin DeepSeek to its own endpoint (same cost, unquantized) 2026-08-05 00:29:18 +02:00
workflow-template fix: pin DeepSeek to its own endpoint, not deepinfra/fp4 2026-08-05 00:28:48 +02:00
.gitignore feat: Forgejo Actions integration for OpenCodeReview 2026-08-04 21:52:47 +02:00
Dockerfile feat: Forgejo Actions integration for OpenCodeReview 2026-08-04 21:52:47 +02:00
package.json feat: Forgejo Actions integration for OpenCodeReview 2026-08-04 21:52:47 +02:00
post-review.test.ts feat: review every PR with two models in parallel 2026-08-05 00:03:38 +02:00
post-review.ts feat: review every PR with two models in parallel 2026-08-05 00:03:38 +02:00
README.md feat: group a review's calls into one OpenRouter session 2026-08-04 23:17:30 +02:00
run-review.test.ts feat: review every PR with two models in parallel 2026-08-05 00:03:38 +02:00
run-review.ts feat: review every PR with two models in parallel 2026-08-05 00:03:38 +02:00
tsconfig.json feat: Forgejo Actions integration for OpenCodeReview 2026-08-04 21:52:47 +02:00

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)
  1. run-review.ts resolves the PR from the Actions event payload.
  2. It runs ocr review --from origin/<base> --to <head> --format json.
  3. post-review.ts publishes 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_review
  • issue_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: PENDING requires 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 stock docker label): it ships git 2.39.5 and ocr requires >= 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.