GitHub Pull Request
用 GitHub Action 或 ocra review --pr 审查 Pull Request。
GitHub Action
在需要审查的仓库里添加一个 workflow:
name: ocra
on: pull_request
permissions:
contents: read
pull-requests: write
# 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
jobs:
review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: jma49/Open-CR-Agent@c2fda45c42880d4c8223b185704f7598c889f831 # v0.4.0
env:
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}这里把 Action 固定到 v0.4.0 发布版本的提交,并在注释里写明版本号。tag 可以被移动,固定到提交才能确保带着你的模型 key 运行的代码确定不变;git ls-remote --tags https://github.com/jma49/Open-CR-Agent v0.3.0 会打印某个版本对应的提交,Dependabot 也会更新这种固定方式及其注释。
从 v0.1.1 起,Action 从 npm 安装 ocra:安装与 Action 自身版本号相同的 @open-cr-agent/cli,每个依赖都固定在 Action 的 package-lock.json 锁定的版本,不运行安装脚本,使用 npm 官方源时还会校验源的签名。只有当 ocra 自己的每个包都带有 provenance,证明它由本仓库的发布 workflow 从该版本的标签构建时,才会使用这些包;否则改为从自身源码构建,并给出警告。npm 上已发布的版本不会再改变。某个版本还没发布到 npm 时(发布后的最初几分钟),Action 改为从自身源码构建;v0.1.0 始终从源码构建。所以 @main 运行的是 main 上的 Action 代码,加上 main 所写版本号对应的发布版,而不是尚未发布的包代码;只有 main 在那次发布之后改了包的依赖时,才会构建 main。
保留 fetch-depth: 0:ocra 需要 base 与 head 之间的历史,浅克隆会直接停下并给出提示。concurrency 分组也很重要:没有它,连续两次 push 会同时跑两次审查,重复发同样的行内评论,较慢的那次还可能用更旧的 head 覆盖摘要。被取消的运行会停下,把部分报告留在 job 日志里,不发布任何内容(Action 会把取消信号转给 ocra;如果信号到达时 ocra 已在发布,发布会完成)。遇到 GitHub 限流或短暂故障,ocra 会有限次地重试;可能已经发出评论的请求绝不会重复。
把模型 key 存为仓库 secret。模型配置来自 base 分支的 .ocra/config.json,或者 env 里的 OCRA_MODEL_* 变量。
| 输入 | 默认值 | 含义 |
|---|---|---|
github-token | ${{ github.token }} | 用来读取 PR 和发布评审的 token |
args | 额外的 ocra review 参数,例如 --reviewers correctness,security;按空格和换行拆分(不支持引号) | |
fail-on-concerns | false | 结论为 significant_concerns(存在经核查确认的 critical 问题)时让 job 失败。审查不完整(退出码 3)时 job 总会失败 |
opencode | true | 安装默认运行时 OpenCode。配置里设了 "runtime": "direct" 时可设为 false,见运行时。v0.3.0 新增 |
sarif | false | 同时把审查结果写成 SARIF 2.1.0,用于代码扫描,文件路径见输出 sarif。它会取代 args 里的 --format 和 --output |
给这一步设置 id,就能读取它的输出。只要 ocra 运行过,输出就会设置,步骤失败时也一样:
| 输出 | 含义 |
|---|---|
verdict | approved、approved_with_comments、minor_issues 或 significant_concerns |
exit-code | ocra 的退出码,是 fail-on-concerns 把 1 变成通过之前的值 |
run-id | 运行 id,摘要评论和 SARIF 日志里也有 |
report | JSON 报告的路径,格式同 --format json,位于 $RUNNER_TEMP 下 |
findings | 报告里的问题数 |
sarif | sarif 为 true 时 SARIF 日志的路径 |
只有评审完整完成时(退出码 0 或 1)才会设置 verdict:没能评审全部内容的运行(退出码 2 或 3)也可能写出报告,但其中的结论并不涵盖没评审的文件。ocra 没有写出报告时,run-id、report 和 findings 为空。例如,保存报告并在 job 摘要里写明结论:
- id: ocra
uses: jma49/Open-CR-Agent@c2fda45c42880d4c8223b185704f7598c889f831 # v0.4.0
env:
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
- if: ${{ !cancelled() && steps.ocra.outputs.report != '' }}
uses: actions/upload-artifact@v7
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"像这里一样通过 env 把输出交给脚本,不要把 ${{ }} 直接写进 run。
结论只是参考意见。模型会读到 PR 的内容,PR 作者可以在其中埋入文字,诱导审查员漏报问题,或说服 Judge 丢掉问题;未经核查的模型判断也可能是错的。不要用这项检查代替人工审查、把它设为必需的状态检查;如果打开 fail-on-concerns,通过只代表“没有被确认的问题”,不代表“安全”。
来自 fork 的 PR 在 pull_request 事件下拿不到 secret,审查会停下,并提示缺少哪个 key。如何审查这类 PR,见来自 fork 的 PR。
运行时
Action 默认安装 OpenCode 运行时:连同 runner 平台的原生二进制文件,它占 Action 所装 108 个包中的 97 个,175 MB 中的约 165 MB。如果 base 分支配置里的模型都在你声明的端点上,并且配置设了 "runtime": "direct"(见选择运行时),请设置 opencode: false:安装会略去 OpenCode,只装 11 个包。此时如果配置选的是 opencode,评审会在调用任何模型之前以退出码 2 停下,并说明缺了哪个包。这个输入是 v0.3.0 新增的。
- uses: jma49/Open-CR-Agent@<commit> # v0.4.0
with:
opencode: false
env:
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}安装不在运行之间缓存:每次都使用自己的 npm 缓存,所以每个包、签名和 provenance 声明都直接来自 npm 源,而不会来自仓库里其他 workflow 可能写入的缓存。
会发布什么
- 行内评论:落在 diff 内的问题(每条都标明已核实、未核实或未核查),放在同一个
COMMENT事件的评审里。ocra 从不批准 PR。设置github.requestChanges后,结论为significant_concerns时会请求修改:只请求一次,之后某次运行的结论不再阻塞、或者这个 commit 被放行时,ocra 会撤回(dismiss)自己的修改请求。 - 一条摘要评论:每次 push 都在原评论上更新,包括结论、总结、diff 之外的问题、上次评审后已修复的问题,以及覆盖范围、花费和运行 id。有文件没审到时,它会写明有多少个,以及是否达到了花费上限。
下一次 push 时,ocra 只审查有改动的部分:自上次评审所在的提交以来改动过的文件,加上上次没有审完的文件(比如任务失败,或者因为花费上限没能启动)。未改动文件里的问题原样沿用:保持打开,继续计入结论,不重复评论。以下情况会重新审查全部文件:上次的提交已不在分支上(force-push 或 rebase)、这次 push 让风险档位升高(在更高档位才启动的审查员还没看过未改动的文件)、摘要评论最后一次是被 ocra 以外的人编辑的(这时 ocra 也会忽略这条评论记录的全部状态,不过它仍能认出自己发过、没被别人编辑过的行内评论,不会再发一次),或者传了 --full(例如写在 Action 的 args 里)。只改变运行哪些审查员(base 分支的 reviewers、--reviewers、--ultra)不会让未改动的文件重新审查;这样改时请手动传一次 --full。摘要里会写明本次属于哪种情况以及原因。
随后 ocra 会和上一次评审对比:
- 已经评论过的问题不会重复评论。
- 只有当问题指向的代码已经不在文件里,或者文件已被删除,才算已修复。已修复的问题会列出来,并自动关闭对应的行内评论串。
- 如果这次没有任何审查员再报这个问题,而它指向的代码没有变,就列为本次未复现。模型每次运行的结果会有波动,所以这类问题保持打开:不关闭评论串,不重复评论,并按原来的严重程度继续计入结论,直到代码被修改或有审查者驳回它。同样的代码跑两次,结论不会变。
- 所在文件这次没有被审查的问题(比如任务失败)列为未重新检查,同样保持打开。
由审查者来做主。如果对仓库有写权限(admin、maintain 或 write 权限)且不是 PR 作者的人 resolve 了 ocra 的评论串,或者回复 /ocra dismiss,或者回复以明确的拒绝开头("won't fix"、"will not fix"、"by design"、"false positive"、"not a bug"、"working as intended"、"intended behavior")且不是问句,这条问题就算被驳回:之后不再报告,也不再影响结论,除非后续运行发现它的严重程度升高。同样的词出现在回复的其他位置(例如 "this is not intended, good catch"、"is this by design?")不算驳回,回复 "I disagree" 也不算。这类回复如果来自有资格驳回的人,会在这条问题每次再被报告时交给 Judge;回复给出了具体理由说明问题不成立时,Judge 可以丢弃它,但已确认的 critical 不行。PR 作者自己的回复不会交给 Judge。PR 作者也无法驳回自己 PR 上的问题;被他人编辑过的回复不算数(有写权限的人可以编辑任何评论),因此不能靠 resolve 评论串绕过检查。只有带行内评论串的问题能这样驳回,其他问题请用 ocra memory。
代码扫描
如果还想把问题显示为代码扫描(code scanning)告警,可以用输入 sarif 输出 SARIF,再用 GitHub 的 upload-sarif action 上传。在上面的 workflow 里加一个权限和几行配置:
permissions:
contents: read
pull-requests: write
security-events: write
jobs:
review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- id: ocra
uses: jma49/Open-CR-Agent@c2fda45c42880d4c8223b185704f7598c889f831 # v0.4.0
with:
sarif: true
env:
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
- uses: github/codeql-action/upload-sarif@v4
# 审查不完整时上一步会失败,这里也照样上传。
if: ${{ !cancelled() && steps.ocra.outputs.sarif != '' }}
with:
sarif_file: ${{ steps.ocra.outputs.sarif }}
category: ocra- 审查结果仍会发到 PR 上,输出
report也仍然指向 JSON 报告:SARIF 只改变 ocra 打印的内容。args: --format sarif --output ocra.sarif效果相同,路径由你指定。 - 代码扫描只显示带行号的结果。ocra 没能定位到行的问题在文件里,但不会在那里显示。之前的审查在此后没改动过的文件里报告的问题也是如此:PR 第一次审查之后,ocra 只审查有改动的部分。要让代码扫描在每次 push 后都显示全部问题,请在
args里加上--full,代价是每次都要完整审查一遍。 - 公开仓库可以使用代码扫描;私有仓库需要 GitHub Code Security。
- 文件里有什么,见 SARIF 输出。
导入分析工具的结果
同一个 job 也可以把 Semgrep、CodeQL 或其他能写 SARIF 的分析工具的结果交给 ocra:先运行工具,再用 --import-sarif 传入它的日志。只保留落在改动上的结果,它们会和 ocra 自己的问题一起核查和裁决(SARIF 输入)。ocra 自己不运行任何工具。
- run: pipx run semgrep scan --config p/default --metrics=off --sarif --output semgrep.sarif
- uses: jma49/Open-CR-Agent@c2fda45c42880d4c8223b185704f7598c889f831 # v0.4.0
with:
args: --import-sarif semgrep.sarif
env:
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}导入功能用 Semgrep 1.178.0 写出的日志测试过;这个 workflow 本身还没有在 CI 里端到端跑过。
强制放行阻塞的结论
如果 ocra 拦下了一个你们决定仍要合并的改动,由 PR 作者以外、有写权限的人评论:
/ocra override <完整的 head commit id> <理由>对这个 commit,运行的退出码从 1 变成 0,fail-on-concerns 就会通过,requestChanges 的修改请求也会撤回;结论本身仍是 significant_concerns,摘要里会写明谁、以什么理由放行(摘要里也会给出带 commit id、可以直接复制的命令)。新的 push 需要重新放行。之所以要完整的 commit id,是因为短前缀可能被专门构造的新 commit 撞上。写权限通过仓库权限接口确认;PR 作者不能放行自己的 PR;被他人编辑进别人评论的命令不算数;审查不完整(退出码 3)也不会被放行。
有推送权限的人可以推送自己的 workflow,它会拿到仓库的 secret,也能以 github-actions[bot] 的身份发评论;在 on: pull_request 下,连审查自己的 workflow 文件都是他们分支上的那份。要让审查既不能被他们修改、也不能被冒充,请用下面的配置。
PR 能改变什么、不能改变什么
PR 是不可信的输入。ocra 从 base 提交读取 .ocra/config.json、.ocra/rules.json、.ocra/memory.json 和 AGENTS.md,绝不从 PR 里读取,并且在这个模式下从不加载仓库插件。diff、标题和描述仍然会作为数据交给审查员,防护措施见安全。
来自 fork 的 PR
在 pull_request 事件下,来自 fork 的 PR 拿不到仓库的 secret,token 也是只读的。要审查这类 PR,请改在 pull_request_target 事件上运行 Action。这个事件运行的是默认分支上的 workflow 文件,带着你的 secret 和你授予的权限。下面的 workflow 用来替换上面那个,你自己分支上的 PR 也由它审查:
name: ocra
on:
pull_request_target:
types: [opened, synchronize, reopened, labeled]
permissions:
contents: read
pull-requests: write
concurrency:
group: ocra-${{ github.event.pull_request.number }}
cancel-in-progress: true
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))
runs-on: ubuntu-latest
steps:
# The base branch. Nothing from the pull request is checked out.
- uses: actions/checkout@v7
with:
fetch-depth: 0
persist-credentials: false
- uses: jma49/Open-CR-Agent@c2fda45c42880d4c8223b185704f7598c889f831 # v0.4.0
with:
args: --max-cost-usd 2
env:
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}GitHub 提醒慎用 pull_request_target,是因为它把你的 secret 交给了一个由别人控制 PR 内容的 job:如果 job 检出并构建 PR 的代码,这些代码就会带着你的 secret 运行。ocra 不运行 PR 里的任何东西。它获取 PR 的提交,只把它们当作数据来读;配置、规则和指南都从 base 提交读取;从不加载仓库插件(见威胁模型)。能否一直保持这一点,取决于它周围的 workflow:
- **不要在这个 workflow 里检出、安装、构建或测试 PR:**不要写
ref: ${{ github.event.pull_request.head.sha }},不要安装依赖,不要跑测试。这些放到单独的pull_requestworkflow 里,fork 的 PR 在那里拿不到 secret。 - **保留
if:这道门槛。**任何人都能开 PR,而每次审查都要花钱。有了这道门槛,外部贡献者的 PR 只在维护者每加一次标签时审查一次;下次审查前先移除标签再重新加上。--max-cost-usd限制每次审查的花费。 - 使用只用于这些审查的模型 key,并在供应商那里设置花费上限。
- 按提交固定 Action 的版本,如上所示。
只有组织的公开成员,author_association 才是 MEMBER;非公开成员也需要标签。私有仓库请去掉 persist-credentials: false:ocra 会用检出时的凭据获取 PR 的提交。
自己的开发者也改不了的审查
有推送权限的人推送的 workflow 会拿到仓库的 secret,它的 GITHUB_TOKEN 以 github-actions[bot] 的身份发言,而这正是 ocra 默认信任的账号。要让审查不在他们手里,把三样东西放到只有默认分支才能触及的地方:
- **放 secret 的环境。**创建一个环境(Settings → Environments),比如叫
ocra-review,在 Deployment branches and tags 里只允许默认分支。把模型密钥移到这里,并删掉仓库级的 secret。pull_request_target任务在默认分支上运行,能拿到这个环境的 secret;推送到其他分支的 workflow 拿不到,on: pull_request也拿不到。 - **ocra 自己的账号。**创建一个 GitHub App,仓库权限给 Pull requests(读写)和 Contents(只读),并安装到仓库上。把它的私钥存为环境 secret
OCRA_APP_KEY,client ID 存为环境变量OCRA_APP_CLIENT_ID。在默认分支的.ocra/config.json里把github.botLogin设为这个 App 的账号,比如{ "github": { "botLogin": "my-ocra[bot]" } }:之后以github-actions[bot]身份发的摘要一律不算数。 - **workflow 和配置需要审查。**保护默认分支,并要求
.github/workflows/和.ocra/的改动经过代码负责人审查(CODEOWNERS)。
name: ocra
on:
pull_request_target:
types: [opened, synchronize, reopened, labeled]
permissions:
contents: read
concurrency:
group: ocra-${{ github.event.pull_request.number }}
cancel-in-progress: true
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))
runs-on: ubuntu-latest
# 它的 secret 只给在默认分支上运行的任务。
environment: ocra-review
steps:
- id: app
uses: actions/create-github-app-token@v3
with:
client-id: ${{ vars.OCRA_APP_CLIENT_ID }}
private-key: ${{ secrets.OCRA_APP_KEY }}
# 默认分支。不检出 PR 里的任何东西。
- uses: actions/checkout@v7
with:
fetch-depth: 0
persist-credentials: false
- uses: jma49/Open-CR-Agent@c2fda45c42880d4c8223b185704f7598c889f831 # v0.4.0
with:
github-token: ${{ steps.app.outputs.token }}
args: --max-cost-usd 2
env:
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}上面关于 pull_request_target 的规则照样适用。项目还没有把这套配置完整跑过一遍;如果某一步失败,请提 issue。
配置
{ "github": { "requestChanges": false, "botLogin": "github-actions[bot]" } }botLogin 是哪个账号发的摘要评论才算 ocra 之前的评审;如果你用别的 token 发布,就改成对应的账号。即使 ocra 用某个人的 token 发布,它自己的摘要评论和模型写的文字里出现的 /ocra override 等命令也永远不算数。
在命令行里使用
export GITHUB_TOKEN=... # 能读取该仓库的 token
ocra review --pr 42 # 审查并打印,和本地审查一样
ocra review --pr 42 --publish # 同时发布评审token 来自 GITHUB_TOKEN 或 GH_TOKEN;使用 GitHub Enterprise 时设置 GITHUB_API_URL(例如 https://github.example.com/api/v3)。仓库来自 --repo owner/name、GITHUB_REPOSITORY 或指向 github.com 的 origin 远端(GitHub Enterprise 请传 --repo)。如果本地缺少 PR 的 commit,ocra 会自动拉取。