GitHub pull requests
Review pull requests with the GitHub Action or ocra review --pr.
GitHub Action
ocra init --github writes the fork-safe workflow below, or with --same-repo-only this one, for your provider's key (ocra init). By hand, add this workflow to the repository:
name: ocra
on: pull_request
permissions:
contents: read
pull-requests: write
jobs:
review:
# A pull request from a fork gets no secrets on pull_request: skip it.
if: github.event.pull_request.head.repo.full_name == github.repository
# One review per pull request at a time: a new push cancels the older run.
concurrency:
group: ocra-${{ github.event.pull_request.number }}
cancel-in-progress: true
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- uses: jma49/Open-CR-Agent@6d4d4a21b6334c086e0f884c240d550b2e4c1f46 # v0.7.0
env:
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}- Pin every action by commit, with the version as a comment: a tag can move, a commit cannot. The recipes here and
ocra initdo,actions/checkoutincluded.git ls-remote --tags https://github.com/jma49/Open-CR-Agent v0.3.0prints a release's commit; Dependabot updates the pin and comment. - Keep
fetch-depth: 0: ocra needs the history between base and head; a shallow checkout stops with a message. - Keep
concurrency: otherwise two quick pushes post the same comments twice, and the slower run can overwrite the summary with an older head. A cancelled run publishes nothing (unless already publishing) and leaves its partial report in the job log. - Store the model key as a repository secret. Models come from the base branch's
.ocra/config.jsonorOCRA_MODEL_*variables inenv.
ocra retries GitHub's rate limits and short outages a few times, never a request that may already have posted a comment.
Since v0.1.1 the Action installs @open-cr-agent/cli from npm at its own version, dependencies pinned by its package-lock.json, install scripts off and, on the npm registry, signatures checked. It uses them only when each ocra package has provenance from this repository's release workflow at the version's tag; otherwise, or before the version reaches npm, it builds its own source with a warning (v0.1.0 always does). So @main runs main's Action code with the release main names, or builds main if its package dependencies changed since.
| Input | Default | Meaning |
|---|---|---|
github-token | ${{ github.token }} | Token for reading the pull request and posting the review |
args | Extra ocra review options, such as --reviewers correctness,security, split on spaces and newlines (no quoting) | |
fail-on-concerns | false | Fail the job when the verdict is significant_concerns (a critical finding the verifier confirmed). An incomplete review (exit code 3) always fails the job |
opencode | true | Install OpenCode, the default runtime. Set false when the configuration sets "runtime": "direct"; see The runtime. New in v0.3.0 |
sarif | false | Also write the review as SARIF 2.1.0 for code scanning, to the file the sarif output names. It takes the place of --format and --output in args |
Give the step an id to read its outputs. They are set whenever ocra ran, also when the step fails:
| Output | Meaning |
|---|---|
verdict | approved, approved_with_comments, minor_issues or significant_concerns |
exit-code | ocra's exit code, before fail-on-concerns turns 1 into a passing step |
run-id | The run id, also in the summary comment and the SARIF log |
report | Path to the JSON report, the shape of --format json, under $RUNNER_TEMP |
findings | The number of findings in the report |
sarif | Path to the SARIF log, when sarif is true |
verdict is empty unless the review completed (exit code 0 or 1): a run that did not review everything (2 or 3) may write a report, but its verdict says nothing about the unreviewed files. run-id, report and findings are empty when ocra wrote no report. To keep the report and put the verdict in the job summary:
- id: ocra
uses: jma49/Open-CR-Agent@6d4d4a21b6334c086e0f884c240d550b2e4c1f46 # v0.7.0
env:
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
- if: ${{ !cancelled() && steps.ocra.outputs.report != '' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ocra-report
path: ${{ steps.ocra.outputs.report }}
- if: ${{ !cancelled() }}
env:
VERDICT: ${{ steps.ocra.outputs.verdict }}
FINDINGS: ${{ steps.ocra.outputs.findings }}
RUN_ID: ${{ steps.ocra.outputs.run-id }}
run: echo "ocra: $VERDICT, $FINDINGS findings (run $RUN_ID)" >> "$GITHUB_STEP_SUMMARY"Pass outputs to a script through env, as here, never as ${{ }} inside run.
The verdict is advice (why): do not make the check a required status in place of human review, and with fail-on-concerns read a pass as "nothing confirmed", not "safe".
On pull_request, pull requests from forks get no secrets, so the if: skips them; to review them, see Pull requests from forks.
The runtime
OpenCode, the default runtime, with its native binary is 97 of the 108 packages the Action installs and about 165 MB of 175 MB. When every model in the base branch's configuration is on an endpoint you declare and it sets "runtime": "direct" (Choosing the runtime), set opencode: false (new in v0.3.0) to install 11 packages. If the configuration then selects opencode, the review stops before any model call with exit code 2, naming the missing package.
- uses: jma49/Open-CR-Agent@<commit> # v0.7.0
with:
opencode: false
env:
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}The install is not cached: each run uses a fresh npm cache, so every package, signature and provenance statement comes from the registry, never from a cache another workflow could have written.
What gets posted
- Inline comments for findings anchored in the diff, marked verified, unverified or not verified, in one
COMMENTreview. ocra never approves. Withgithub.requestChanges, asignificant_concernsverdict requests changes once, withdrawn (dismissed) when a later verdict no longer blocks or the commit is overridden. - One summary comment, updated in place: verdict, summary, findings outside the diff, what was fixed, coverage, cost and the run id. It counts unreviewed files, says whether the spend limit was reached, and lists partly reviewed files (a reviewer out of steps or stopped) with what to do.
Later pushes review only what changed: files changed since the previous review's commit, plus files it did not finish (failed task, reviewer out of steps, spend limit). Findings in unchanged files carry over, open, counted, not commented again. Everything is reviewed again when the previous commit left the branch (force-push, rebase), the push raises the risk tier, the summary was last edited by someone other than ocra (its remembered state is then ignored, though ocra still recognizes its own unedited inline comments), or you pass --full. Changing which reviewers run (reviewers, --reviewers, --ultra) does not; pass --full once. The summary says which kind of review it was and why.
Compared with the previous review:
- A finding already commented on is not commented again.
- A finding is fixed only when its code is gone from the file or the file was deleted; it is listed and its thread resolved.
- A finding not reported this time while its code is unchanged is not reported this time: model runs vary, so it stays open and counts at its severity until the code changes or it is dismissed. The same code gets the same verdict.
- A finding whose file was not reviewed this time is not re-checked and stays open.
Maintainers stay in charge. A maintainer, someone with write access (admin, maintain or write) other than the author, dismisses a finding by resolving its thread, replying /ocra dismiss, or replying with a message that opens with a clear decline ("won't fix", "will not fix", "by design", "false positive", "not a bug", "working as intended", "intended behavior") and is not a question. A dismissed finding no longer counts in this pull request unless a later run finds it at a higher severity. The words elsewhere in a reply ("this is not intended, good catch", "is this by design?") do not count, nor does "I disagree". Their other replies go to the judge when the finding returns; it may drop the finding for a specific reason, never a confirmed critical one. The author's replies are not shown and cannot dismiss, and a reply someone else edited does not count. A finding without an inline thread cannot be dismissed; ocra memory silences it instead: at any severity, in every pull request, once .ocra/memory.json is on the base branch.
Committable suggestions
A finding can carry a fix: code that replaces the lines it is about. Its inline comment then ends with a suggestion block that anyone with write access can commit (Commit suggestion, or in a batch). ocra offers one only when the comment sits on exactly the replaced lines, never in the summary, and posts the code exactly or not at all: a fix holding an HTML comment opener, /ocra, a carriage return or a control or bidirectional character, or longer than 20,000 characters, gets no block; the finding is still posted. A suggestion is a model's proposal: read it before committing.
Planned: no reviewer produces fixes yet; that waits for an evaluated prompt change (ADR-0029). Until then comments give suggestions as text.
Code scanning
To also see findings as code scanning alerts, write SARIF with the sarif input and upload it with upload-sarif:
permissions:
contents: read
pull-requests: write
security-events: write
jobs:
review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- id: ocra
uses: jma49/Open-CR-Agent@6d4d4a21b6334c086e0f884c240d550b2e4c1f46 # v0.7.0
with:
sarif: true
env:
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
- uses: github/codeql-action/upload-sarif@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
# Also after an incomplete review, which fails the step above.
if: ${{ !cancelled() && steps.ocra.outputs.sarif != '' }}
with:
sarif_file: ${{ steps.ocra.outputs.sarif }}
category: ocra- The review is still posted and
reportstill names the JSON report: SARIF only changes what ocra prints.args: --format sarif --output ocra.sarifdoes the same with your own path. - Code scanning shows only results with lines, so file-level findings are not shown, nor are earlier findings in files unchanged since (later pushes review only what changed). Add
--fulltoargsto show every finding on every push, at the cost of a full review each time. - Code scanning is available for public repositories; private ones need GitHub Code Security.
- File contents: SARIF output.
Importing an analyzer's results
Run Semgrep, CodeQL or another SARIF-writing analyzer first and pass its log with --import-sarif: its results on the change are verified and judged with ocra's own (SARIF input). ocra runs no tool itself.
- run: pipx run semgrep scan --config p/default --metrics=off --sarif --output semgrep.sarif
- uses: jma49/Open-CR-Agent@6d4d4a21b6334c086e0f884c240d550b2e4c1f46 # v0.7.0
with:
args: --import-sarif semgrep.sarif
env:
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}The import is tested on a Semgrep 1.178.0 log; this workflow has not run end to end in CI yet.
Overriding a blocking verdict
To merge a change ocra blocks, someone with write access other than the author comments:
/ocra override <full head commit id> <reason>For that commit the run exits 0 instead of 1, so fail-on-concerns passes and a requestChanges review is withdrawn. The verdict stays significant_concerns; the summary shows who overrode it and why, and prints the command with the commit id to copy. A new push needs a new override. The full id is required because a crafted commit can match a short prefix. The author cannot override their own pull request, a command someone else edited into a comment does not count, and an incomplete review (exit 3) is never overridden.
Someone with push access can push a workflow that gets the repository's secrets and posts as github-actions[bot]; with on: pull_request, even the review's workflow is their branch's copy. For a review they can neither change nor impersonate, use the setup below.
What the pull request can and cannot change
.ocra/config.json, .ocra/rules.json, .ocra/memory.json and AGENTS.md come from the base commit, and repository plugins never load. The diff, title and description reach reviewers as untrusted data.
Pull requests from forks
On pull_request, a fork's pull request runs without your secrets and with a read-only token. pull_request_target runs your default branch's workflow file with your secrets and the permissions you grant. This workflow replaces the one above and reviews your own branches too:
name: ocra
on:
pull_request_target:
types: [opened, synchronize, reopened, labeled]
permissions:
contents: read
pull-requests: write
jobs:
review:
# Members and collaborators: every push. Anyone else: one review each
# time a maintainer adds the ocra-review label.
if: >-
(github.event.action == 'labeled' && github.event.label.name == 'ocra-review') ||
(github.event.action != 'labeled' &&
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.pull_request.author_association))
# One review per pull request at a time: a new push cancels the older run.
# The key repeats the if:, so a run the if: skips gets a group of its
# own and cannot cancel the review in progress.
concurrency:
group: >-
ocra-${{ github.event.pull_request.number }}-${{
((github.event.action == 'labeled' && github.event.label.name == 'ocra-review') ||
(github.event.action != 'labeled' &&
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.pull_request.author_association)))
&& 'review' || github.run_id }}
cancel-in-progress: true
runs-on: ubuntu-latest
steps:
# The base branch. Nothing from the pull request is checked out.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- uses: jma49/Open-CR-Agent@6d4d4a21b6334c086e0f884c240d550b2e4c1f46 # v0.7.0
with:
args: --max-cost-usd 2
env:
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}GitHub warns against pull_request_target because a job that builds the pull request's code runs it with your secrets. ocra runs nothing from the pull request (Threat model); keep the workflow that way:
- Never check out, install, build or test the pull request here: no
ref: ${{ github.event.pull_request.head.sha }}, no dependency install, no test run. Do those in a separatepull_requestworkflow, which gets no secrets for forks. - Keep the
if:gate. Anyone can open a pull request, and every review costs money: an outsider's pull request is reviewed once each time a maintainer adds the label (remove and re-add it for the next review). An outside author's push during a labelled review is skipped and leaves that review running; re-add the label to review the new head.--max-cost-usdcaps each review. - Keep the condition in the concurrency group, and change both together. A run the
if:skips, such as one for an unrelated label, gets a group of its own, so it cannot cancel the review in progress. - Use a model key that serves only these reviews, with a spending limit at the provider.
- Pin the Action by commit.
author_association is MEMBER only for public organization members; private members need the label too. In a private repository, drop persist-credentials: false: ocra fetches the pull request's commits with the checkout's credentials.
A review your own developers cannot change
A workflow pushed by someone with push access gets the repository's secrets, and its GITHUB_TOKEN posts as github-actions[bot], the account ocra trusts by default. To keep the review out of their hands, put three things where only your default branch reaches them:
- An environment for the secrets. Create one, say
ocra-review(Settings → Environments), allowing only your default branch under Deployment branches and tags. Move the model key there and delete the repository secret. Apull_request_targetjob runs on the default branch and gets them; a workflow pushed on another branch, oron: pull_request, does not. - An account of ocra's own. Create a GitHub App with repository permissions Pull requests (read and write) and Contents (read), and install it. Store its private key as the environment secret
OCRA_APP_KEYand its client ID as the variableOCRA_APP_CLIENT_ID. On the default branch, setgithub.botLoginto the app's account, for example{ "github": { "botLogin": "my-ocra[bot]" } }, so a summary posted asgithub-actions[bot]counts for nothing. - Review for the workflow and the configuration. Protect the default branch and require a code owner's review for
.github/workflows/and.ocra/(CODEOWNERS).
name: ocra
on:
pull_request_target:
types: [opened, synchronize, reopened, labeled]
permissions:
contents: read
jobs:
review:
if: >-
(github.event.action == 'labeled' && github.event.label.name == 'ocra-review') ||
(github.event.action != 'labeled' &&
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.pull_request.author_association))
# One review per pull request at a time: a new push cancels the older run.
# The key repeats the if:, so a run the if: skips gets a group of its
# own and cannot cancel the review in progress.
concurrency:
group: >-
ocra-${{ github.event.pull_request.number }}-${{
((github.event.action == 'labeled' && github.event.label.name == 'ocra-review') ||
(github.event.action != 'labeled' &&
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.pull_request.author_association)))
&& 'review' || github.run_id }}
cancel-in-progress: true
runs-on: ubuntu-latest
# Its secrets reach only jobs that run on the default branch.
environment: ocra-review
steps:
- id: app
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ vars.OCRA_APP_CLIENT_ID }}
private-key: ${{ secrets.OCRA_APP_KEY }}
# The default branch. Nothing from the pull request is checked out.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- uses: jma49/Open-CR-Agent@6d4d4a21b6334c086e0f884c240d550b2e4c1f46 # v0.7.0
with:
github-token: ${{ steps.app.outputs.token }}
args: --max-cost-usd 2
env:
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}The rules for pull_request_target above apply. This exact setup has not been run end to end yet; if a step fails, open an issue.
Configuration
{ "github": { "requestChanges": false, "botLogin": "github-actions[bot]" } }botLogin is the account whose summary comment counts as ocra's earlier review; change it if you post with another token. Commands such as /ocra override never count from ocra's own summary or from model text, even when ocra posts with a person's token.
From the command line
export GITHUB_TOKEN=... # a token that can read the repository
ocra review --pr 42 # review and print, like a local review
ocra review --pr 42 --publish # also post the reviewThe token comes from GITHUB_TOKEN or GH_TOKEN; for GitHub Enterprise set GITHUB_API_URL (for example https://github.example.com/api/v3) and pass --repo. The repository comes from --repo owner/name, GITHUB_REPOSITORY, or an origin remote on github.com. ocra fetches missing pull request commits.