Self-contained PR review as a GitHub Action: complexity analysis, agent bug
review, and a summary, posted back to the PR as inline comments and
workflow annotations, with the workflow job as the single status check. No
server, no database, no recurring bill. The action clones the PR
by SHA, reviews it, and writes the results straight to GitHub using the
workflow's built-in GITHUB_TOKEN.
Add a workflow at .github/workflows/lien-review.yml:
name: Lien Review
on:
pull_request:
permissions:
contents: read
pull-requests: write
jobs:
review:
runs-on: ubuntu-latest
steps:
- uses: getlien/lien-review@v1
with:
openrouter-api-key: ${{ secrets.OPENROUTER_API_KEY }}That single uses: line is the whole integration: no actions/checkout
step is needed. Lien self-clones the PR head (and base, for deltas) by SHA
using the same token, so adding actions/checkout is unnecessary and, on fork
PRs, unsafe (see Fork PRs).
A copy-paste workflow (including the fork variant) lives in
examples/lien-review.yml.
The consumer workflow MUST grant these permissions or the comment writes will 403:
permissions:
contents: read # clone the PR head/base by SHA
pull-requests: write # post inline review commentsPut the permissions: block at the workflow top level (as above) or on the
individual job. If your repository's default GITHUB_TOKEN permissions are set
to "read-only" in Settings → Actions → General → Workflow permissions, the
explicit block is what re-grants the write scopes this action needs.
The agent (bug) review needs an LLM key, provided as a workflow secret:
-
Get an OpenRouter API key (preferred: runs OpenRouter's calibrated default model, cheaper than Anthropic) or an Anthropic API key.
-
Add it to your repo under Settings → Secrets and variables → Actions → New repository secret as
OPENROUTER_API_KEY(orANTHROPIC_API_KEY). -
Pass it through the action's
with:block:with: openrouter-api-key: ${{ secrets.OPENROUTER_API_KEY }} # or: # anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
If both keys are omitted the review still runs, but complexity-only: the agent bug/summary/architectural passes are skipped. When both are present OpenRouter wins.
Cost: a typical PR review costs roughly $0.02-$0.15 in OpenRouter
tokens on the default model (measured across the 2026-07 cross-repo study;
~$0.03/vote median, with complex multi-pass reviews at the high end;
OpenRouter's own billing can run ~1.5-2× the harness-reported figure). The
exact cost of every run is printed in the job's step summary (the
**Tokens:** ... · **Cost:** $... line).
Never hard-code an API key in the workflow YAML. Always reference it from
secrets.
| Input | Required | Default | Description |
|---|---|---|---|
github-token |
no | ${{ github.token }} |
Token used to clone the PR and post inline comments. Needs contents:read and pull-requests:write. |
openrouter-api-key |
no | '' |
OpenRouter API key for agent review (preferred provider: runs OpenRouter's calibrated default model, currently moonshotai/kimi-k2.7-code; see packages/review/src/defaults.ts for the source of truth). If omitted, falls back to anthropic-api-key, then complexity-only. |
anthropic-api-key |
no | '' |
Anthropic API key for agent review (fallback when openrouter-api-key is not set). If both are omitted, review is complexity-only. |
threshold |
no | 15 |
Complexity threshold above which violations are reported. |
review-types |
no | complexity,bugs,summary |
Comma-separated review types to enable. complexity toggles the complexity check. bugs, architectural, and summary all come from the single agent reviewer, so they switch it on/off as a group (and only when an API key is set); they can't be toggled independently. |
block-on-new-errors |
no | false |
Post REQUEST_CHANGES (instead of COMMENT) when the PR introduces new error-level complexity violations. |
fail-on |
no | never |
Whether the review fails the check, so a Required check can block the PR. Default never (advisory). See Blocking a PR on the review for the gating options, and Fail-loudly guarantee for the one case that always fails regardless of this setting. |
The action posts no check run of its own: the workflow job is the single status check. Findings surface as workflow annotations (inline on the diff), inline PR comments, and a step summary. Use
fail-onto decide whether the job check gates the PR.
| Output | Description |
|---|---|
conclusion |
The review conclusion: success, failure, or neutral. |
findings-count |
Total number of findings produced. |
error-count |
Number of error-severity findings. |
Reference them from a later step via the step id:
- uses: getlien/lien-review@v1
id: lien
with:
openrouter-api-key: ${{ secrets.OPENROUTER_API_KEY }}
- run: echo "Lien found ${{ steps.lien.outputs.error-count }} errors"Beyond the inputs above, a few behaviors are tunable only via an environment variable on the action step, not a formal input:
- uses: getlien/lien-review@v1
env:
LIEN_REVIEW_DOC_PASS: '0' # disable the doc-truth second pass
LIEN_DOC_TRUTH_V2: 'off' # opt back into the pass's older open-findings-list behavior
LIEN_REVIEW_TOKEN_BUDGET: '400000' # raise the main pass's token budget for large diffs
with:
openrouter-api-key: ${{ secrets.OPENROUTER_API_KEY }}LIEN_REVIEW_DOC_PASS=0(orfalse): disables the dedicated doc-truth second pass, a claims-only re-review that runs only on PRs touching documentation/guidance surfaces and checks their prose against the code. On by default.LIEN_DOC_TRUTH_V2=off(or0/false): reverts the doc-truth pass to its earlier open-findings-list behavior. On (v2) by default; see Agent-Review Pass Architecture for what v2 changes. Only takes effect when the doc-truth pass itself is enabled (i.e., not overridden byLIEN_REVIEW_DOC_PASSabove).LIEN_REVIEW_TOKEN_BUDGET=<int>: an absolute override for the main pass's final token budget, for repos whose PRs reproducibly starve the diff-scaled/blast-radius-scaled formula on large diffs (see Agent-Review Pass Architecture's budget section for exactly where this applies). Unset by default — every consumer gets the same diff-scaled budget as before. A non-numeric, non-integer, zero, or negative value is ignored (fails open to the computed budget, never crashes the run); a valid value is clamped to [60,000, 1,250,000] (5× the existing 250,000 ceiling). Raising this raises the run's worst-case per-PR OpenRouter cost — set it deliberately, not speculatively.
There is currently no model input. The OpenRouter path pins the calibrated
default deliberately, since the calibration evidence backing this review
(see the test harness)
only covers that one model.
By default the review is advisory (fail-on: never) for its own findings:
it never fails CI on those, so adding the action can't break anyone's
pipeline. To gate merges on findings, opt in by setting fail-on and marking
the workflow's job as a Required status check in your branch protection
rules. With fail-on: error the action exits non-zero only when the review's
overall conclusion is a failure (driven by block-on-new-errors); fail-on: any is stricter (any error- or warning-level finding fails the check). A
total LLM-provider failure is a separate case that always fails the check,
even under fail-on: never. See Fail-loudly guarantee
below.
If the agent review's main pass never runs at all (every request to the LLM
provider failed terminally: insufficient credits, an invalid/expired key, a
provider outage, etc.), Lien marks the result with an error-severity
finding and a failure conclusion naming the cause, instead of a
clean-looking review.
This is treated as an operational failure, not an advisory finding: a
review that never ran isn't something fail-on gates on, because there's
nothing to be advisory about (no code was analyzed). The check fails
regardless of fail-on, including the advisory default never. An
incomplete main pass (it bailed on a budget/turn limit, or hit an
unrecoverable corrupted stop-turn) gets the identical treatment, since the
agent couldn't vouch for full coverage of the PR either way. Only an
incomplete extra pass (doc-truth, or one of the candidate-loop passes)
is a genuine advisory finding and still obeys fail-on as before, since
the main pass's own coverage is intact in that case. Either way, the step
summary, PR description, and conclusion output make the failure
impossible to mistake for "no issues found."
On a pull_request event triggered from a fork, GitHub forces the built-in
GITHUB_TOKEN to read-only, so Lien can clone and review the code, and its
findings still appear as workflow annotations and in the step summary,
but it cannot post inline PR comments (those writes 403). Lien emits a clear
::warning:: about this. The check still reflects the findings per fail-on
(the review ran and its results are delivered via annotations); set
fail-on: never if you'd rather fork reviews never block CI.
To get inline comments on fork PRs too, opt in via the
pull_request_target event, which runs in the base repo's context and
therefore gets a writable token:
on:
pull_request_target:
permissions:
contents: read
pull-requests: write
jobs:
review:
runs-on: ubuntu-latest
steps:
- uses: getlien/lien-review@v1
with:
openrouter-api-key: ${{ secrets.OPENROUTER_API_KEY }}pull_request_target is normally dangerous because the writable token plus a
naive actions/checkout of the PR head would let an attacker's fork run its
own code with your secrets. Lien is safe here for one specific reason: it
never executes the checked-out code. It self-clones the head by SHA with
git init/fetch/checkout (object fsck + symlink-escape guards on), then only
reads and parses the source with tree-sitter. There is no npm install, no
build, no test run, no script execution.
To keep that guarantee, with the pull_request_target variant you MUST:
- NOT add an
actions/checkoutstep that checks out the PR head ref (ref: ${{ github.event.pull_request.head.sha }}orhead.ref). Lien does its own read-only clone; an explicit head checkout would place untrusted code on disk for other steps to potentially execute. - Keep this workflow minimal: ideally the single
uses: getlien/lien-review@v1step and nothing that runs PR-authored code.
If you add other steps to this workflow, treat the PR contents as untrusted and do not execute them.
- Reads the
pull_requestevent payload ($GITHUB_EVENT_PATH) to get the PR number and the head SHA (event.pull_request.head.sha, notGITHUB_SHA, which onpull_requestis the ephemeral merge commit). - Clones the head (and base, for complexity deltas) by SHA over HTTPS using the
github-token. A base-clone failure degrades gracefully to a no-delta review. - Runs the enabled review passes (
@liendev/review): complexity analysis and the agent bug/summary/architectural review. - Posts inline PR comments for each finding and emits the findings as workflow
annotations (inline on the diff), writes a run summary to
$GITHUB_STEP_SUMMARY, sets the action outputs, and exits perfail-on. It creates no check run of its own; the workflow job is the status check.
The action ships as a Docker container action pulling a prebuilt image from
ghcr.io/getlien/lien-review (tree-sitter's native bindings rule out a
JavaScript/composite action), so each run pulls the image rather than building
it.
Lien Review is licensed AGPL-3.0. Running the unmodified, published
getlien/lien-review action/image in your own CI against your own repos
(including private ones) does not trigger AGPL §13's network-copyleft
obligations toward your codebase: the license governs Lien's own source,
not the code Lien reviews. §13 obligations attach to modifications of Lien
itself that you convey or offer as a network service to others. This is a
factual summary, not legal advice; consult counsel for your specific
situation.
This section is for Lien maintainers cutting a release, not action consumers.
The getlien/lien-review dist repo exists and syncs automatically on
release, so uses: getlien/lien-review@v1 resolves to a real, published
release (tags v1, 0.62.0-0.64.0, backed by
docker://ghcr.io/getlien/lien-review:v1). The one-time setup steps below are
only needed again for re-provisioning after a token rotation, or when setting
up a fork of this repo. If GH_DIST_TOKEN is ever unset or revoked,
.github/workflows/publish-action.yml still publishes the GHCR image, but the
dist-repo sync step no-ops with a ::notice:: log line instead of failing the
build.
- Create the
getlien/lien-reviewrepo in thegetlienGitHub org (public, empty: the workflow pushesaction.yml+README.mdto it; the image itself lives in GHCR, not this repo). - Create a
GH_DIST_TOKENsecret on this repo (Settings → Secrets and variables → Actions), a PAT or fine-grained token withcontents:writeongetlien/lien-review. - First publish: once both exist, either
- land the next changeset release as usual (see below), or
- run
publish-action.ymlmanually via Actions → Publish Action → Run workflow (workflow_dispatch); note this path only pushes the immutablesha-<commit>image tag, not:v1/:latest/a version tag, so follow it with a tagged release to move those.
publish-action.yml triggers on push of an @liendev/lien@* tag. That's the
tag changesets/action creates on this repo's normal release flow
(.github/workflows/release.yml, npm run release on merge to main).
@liendev/parser, @liendev/core, and @liendev/lien are version-linked
(.changeset/config.json), so every monorepo release bumps @liendev/lien
and creates that tag exactly once. packages/action and packages/review are
private/unpublished, so changesets versions them but (by design:
privatePackages.tag defaults to false) never tags them independently;
piggybacking on the CLI's tag avoids adding a dedicated tag scheme.
Caveat: an action-only change that doesn't also bump @liendev/lien's
version won't auto-trigger a publish. Include a changeset that touches
@liendev/lien (even a patch-level one) when you want a release to carry an
action-only fix. workflow_dispatch is not a substitute for that: it only
publishes the immutable sha-<commit> image tag (see "One-time human setup"
above and "What gets published where" below); the dist-repo sync and the
:v1/:latest tag move are gated to tag pushes only, so dispatch never runs
them.
| Artifact | Tag/ref | Where |
|---|---|---|
| Docker image | <version> (e.g. 0.51.0), v1, latest, sha-<commit> |
ghcr.io/getlien/lien-review |
action.yml + README.md |
v1 (floating major) |
getlien/lien-review (dist repo) |
v1 is the Action's own public-interface major version (its inputs:/
outputs: contract): it's a fixed literal in the workflow, not derived from
the CLI's semver, and is bumped manually (to v2, ...) only on a breaking
action.yml change, the same convention as actions/checkout@v4 and similar.