PR Lens

Reference

The PR Lens GitHub Action: bring your own key

Run PR Lens from your own CI on your own model key: one workflow file, one repository secret, and the same diagrams the hosted App posts.

The data-flow lens. The Action renders both lenses by default.

Yes, you can run PR Lens in your own CI on your own key. The GitHub Action is one workflow file and one repository secret. It reads the diff between the pull request's base and head commits, asks the model provider you name, renders the light and dark SVGs, publishes them, and posts a comment on the pull request. The key reaches the model through the environment, never through a command line. Nothing is sent to any PR Lens service on this path.

This is the whole setup. Commit it as .github/workflows/pr-lens.yml.

name: PR Lens
 
on:
  pull_request:
 
permissions:
  contents: write        # to publish the rendered SVGs
  pull-requests: write   # to post the comment
 
concurrency:             # one run per pull request; a push supersedes the last
  group: pr-lens-${{ github.event.pull_request.number }}
  cancel-in-progress: true
 
jobs:
  lens:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0   # the diff is between two commits, so both must be here
      - uses: coldteadotai/pr-lens/packages/action@v0
        with:
          api-key: ${{ secrets.GEMINI_API_KEY }}

The secret is called GEMINI_API_KEY because provider defaults to Gemini. Set provider to something else and the secret is whatever that endpoint wants; the name is yours.

Two lines in that file are load bearing and easy to lose. fetch-depth: 0 is there because PR Lens diffs two commits, and a shallow checkout has neither of them. The Action checks for both before it does anything and fails with a message telling you to fix the checkout, rather than drawing half a change. The concurrency block I will come back to.

What the Action does, in order

It analyzes the diff between the pull request's base and head commits, and validates the model's answer against the PR Lens contract before anything else happens. A model that writes an edge pointing at a node it never declared gets the error back once, with the path, and gets one more attempt. A model that cannot fix a named path in one round does not fix it in three.

It renders the document as self-contained light and dark SVGs, applying the repository's corrections, and writes the document those pictures actually show. That second document is the one the comment is composed from, which matters more than it sounds: excluding a node can empty a whole drill-down section, and a comment built from the document that went in would announce a section nobody drew.

It publishes the SVGs to an orphan branch called pr-lens, under pr/<number>/<head-sha>/. That branch holds no code and is never merged. Each render gets its own path because GitHub proxies comment images through a cache that never revalidates, so a URL you reuse is a picture your reviewers stop seeing updates to.

Then it comments: one comment per pull request, updated in place on every push. Finding that comment takes more than the hidden marker, because anyone can paste a marker into a comment of their own. A comment is only ever edited when the account the Action comments as wrote it, which is the comment-author input.

Which model it asks

Three request shapes are implemented rather than a list of vendors.

providerEndpointWhat else you need
gemini (default)Google's own APInothing
openaiOpenAI's own APImodel
openai-compatibleanything speaking /chat/completionsmodel and base-url

OpenAI is listed separately from the servers that copied it because the two have drifted. OpenAI renamed the output limit to max_completion_tokens and its newer models reject max_tokens, which is the only spelling the compatible servers know. There is no field both accept, so you say which endpoint you are talking to rather than having the CLI guess from a hostname.

Pointing at a self-hosted or third-party endpoint looks like this:

      - uses: coldteadotai/pr-lens/packages/action@v0
        with:
          api-key: ${{ secrets.OPENROUTER_API_KEY }}
          provider: openai-compatible
          base-url: https://openrouter.ai/api/v1
          model: qwen3-coder

Whichever endpoint you name, the traffic is one call out of your runner to that address. The diff goes to the provider you named and nowhere else.

Every input the Action takes

InputDefaultWhat it is
api-keynonerequired; the model provider key
providergeminior openai, or openai-compatible
modelnonerequired for openai and openai-compatible
base-urlnonerequired for openai-compatible
lensbothcomma separated: architecture, data-flow
brandingtruethe "Rendered by PR Lens" footer
commenttrueset false to render and publish without commenting
data-branchpr-lensthe orphan branch the SVGs are committed to
cli-version0.4.0version of @coldtea/pr-lens-cli to run
comment-authorgithub-actions[bot]the login that owns the comment
github-token${{ github.token }}used to publish and to comment

It produces two outputs: graph, the path of the document that was made, and assets-url, where the SVGs were published. Set comment: false and use those two if you would rather build something else out of the diagrams.

Nothing else exists. If you want a flag you do not see in that table, it is not there yet, and the honest answer is to open an issue rather than guess at an input name.

Why the concurrency block is not decoration

Every run of every pull request writes to one shared branch and one shared comment. Two runs of the same pull request racing each other is ordinary, not exotic: someone pushes twice in thirty seconds. A concurrency group per pull request means the newer push supersedes the render it replaces instead of the two fighting over the comment.

Cancellation is not a lock, and the Action does not pretend it is one. Cancellation arrives when it arrives, and a request already on its way to GitHub still lands. So publishing replays onto the branch tip rather than failing, and the comment step asks GitHub for the pull request's head immediately before it writes. A run that was overtaken while it drew says so and posts nothing. An older diagram never replaces a newer one.

The comment the Action posts is static

The picture, the numbers, the <details> tree. There is no checkbox to tick and nothing to click into.

An Action cannot hold state between runs, and a comment with a checkbox that does nothing when you tick it is a comment that lies to its reader. The hosted App keeps a baseline and a stored graph between pushes, which is what makes its checkboxes real: toggling one re-renders from the stored document without re-running analysis. If you want that, install the GitHub App instead. The two ways of running it are compared in what is PR Lens.

Corrections apply here too

Commit .github/pr-lens.yml and the Action picks it up. Renames, exclusions, lane pins and groupings are applied when the diagrams are drawn, over whatever the latest analysis inferred.

schemaVersion: 0.1.1
map:
  rename:
    - match: functions/src/broadcast/sendBroadcastBulk.ts
      to: Broadcast sender
  exclude:
    - "**/*.test.ts"

A match beginning with id: addresses one node exactly. Anything else is a path glob matched against a node's file paths, which is the one you usually want, because a glob keeps holding after the model names the node differently on the next run. The full set of options is in configuring PR Lens.

Forks

A pull request from a fork gets no secrets, so there is no key for the job to use and the job cannot run. That is GitHub's rule and the right one, because a fork can change the workflow file that would read your key.

For a repository that takes drive-by contributions, this is the deciding factor. Run the Action on same-repository pull requests, or run it from a workflow a maintainer triggers, or use the hosted App, which does not depend on a secret living in your repository at all.

If you want the steps one at a time

Analyzing, rendering and composing the comment are the CLI with arguments; publishing to the data branch is plain git. You can run the same steps on your laptop, or in Jenkins, or in a shell script, and the commands are the ones the Action shells out to. They are written up in the PR Lens CLI.

Questions people ask

Can I run PR Lens in CI with my own model key?

Yes. The GitHub Action at coldteadotai/pr-lens/packages/action@v0 takes an api-key input and calls the provider directly from your runner. The key is passed to the CLI through the environment, so it never appears in a command line or a log, and the diff goes to that provider and nowhere else.

Which models does the PR Lens Action support?

The provider input takes gemini (the default), openai, or openai-compatible. With openai-compatible you also give base-url and model, which covers OpenRouter, DeepSeek, Ollama, llama.cpp and any other endpoint that answers /chat/completions.

Where does the Action put the rendered SVGs?

It commits them to an orphan branch called pr-lens, under pr/<number>/<head-sha>/. That branch holds no code and is never merged. Each render lives at its own path because GitHub proxies comment images through a cache that never revalidates, so overwriting one path would show readers a stale picture.

Does the PR Lens Action work on pull requests from forks?

No. A pull request from a fork gets no repository secrets, so there is no key for the job to use and it cannot run. That is GitHub's rule and it is the right one, because a fork can change the workflow file. Run the Action on same-repository pull requests, or use the hosted GitHub App.

Should I use the GitHub Action or the GitHub App?

Use the App unless you need the analysis to happen inside your own infrastructure or on a model you choose. The App holds state between pushes, so its comment carries working checkboxes and a link to the canvas. The Action cannot hold state between runs, so its comment is a static picture.

Sources

Keep reading

Turn it on

Install the App and every pull request in the repositories you pick gets drawn. Or hand the prompt to the agent you already have open.

Add to GitHub