- .gitea/workflows/ai-review.yml 原样拷贝引擎模板(含 pr_number 经 env 传参的 注入防护),引擎钉 v1 移动标签,通过 REVIEW_ENGINE_TOKEN 只读 PAT 拉取 - .gitea/ai-review.md 按本仓库技术栈写审查侧重:金额精度(Decimal/NUMERIC)、 缺失值语义与对账容差、文件路径符号链接安全、endpoint 角色与 worker 撤权核验、 事务并发与 revision、Excel 公式注入与长度上限、前端异步状态、测试真实性 - 仓库级 Actions 配置(Gitea 设置,不在本提交内):REVIEW_ENGINE_TOKEN secret、 REVIEW_BASE_URL/MODEL/PUBLIC_URL/PRICES、FIX_MODEL/FIX_MODEL2/FIX_PLAN_MODEL/ FIX_PLAN_THINKING,以及新仓库推荐起点 REVIEW_FIX_SUGGEST_ONLY=1(只给改法)
203 lines
12 KiB
YAML
203 lines
12 KiB
YAML
name: AI Review
|
||
|
||
# 对抗式 PR 审查:先一遍找出候选缺陷,再对每一条派一个反驳者去证伪它,只有
|
||
# 活下来的才会发到 PR 上。这个不对称是刻意的——AI 审查员死于误报远早于死于漏
|
||
# 报,所以反驳阶段默认站在"这是误报"一边,"什么都没发现"是可接受的结果。
|
||
#
|
||
# 引擎不在本仓库,在 vertai-tech/ai-review-engine。本文件只负责把三样东西摆到
|
||
# 位再调它:被审查的代码、本仓库的审查侧重、引擎本身。
|
||
#
|
||
# 接入一个仓库需要(各一次):
|
||
# 1. 引擎仓库的 templates/ai-review.yml -> 本仓库 .gitea/workflows/ai-review.yml
|
||
# 2. 引擎仓库的 templates/ai-review.md -> 本仓库 .gitea/ai-review.md,按本仓库
|
||
# 的技术栈改写
|
||
# 3. 下面这些组织级机密 / 变量(组织设置 → Actions)配好之后,新仓库就不用再
|
||
# 单独配任何东西:
|
||
# secret REVIEW_API_KEY 模型网关的 key
|
||
# secret REVIEW_ENGINE_TOKEN 只读 PAT,用于拉私有的引擎仓库
|
||
# variable REVIEW_BASE_URL 任意 OpenAI 格式的根地址
|
||
# variable REVIEW_MODEL 支持 function calling 的模型
|
||
#
|
||
# 自动修复默认是关的。要开,在**本仓库**的 设置 → Actions → 变量 里加一个(这两条
|
||
# 都不要配到组织级,那样等于给所有仓库都开了),并在组织级配一次
|
||
# REVIEW_FIX_MODEL:
|
||
# REVIEW_AUTOFIX=1 改代码、提交、推回 PR 分支
|
||
# REVIEW_FIX_SUGGEST_ONLY=1 只把改法写进评论,一个字节都不落盘
|
||
# 两个都配的时候,不动仓库的那个赢。fork 来的 PR 推不回去,apply 会自动降级成
|
||
# 只给改法。
|
||
#
|
||
# 默认只修一轮:修完推一个 commit,那次推送起的下一轮只审不改。要"一直修到没
|
||
# 意见",把仓库变量 REVIEW_FIX_ROUNDS 调大——它数的是这个分支已经被自动修过几
|
||
# 轮,轮次写在提交消息里。
|
||
#
|
||
# 改法本身是两步定出来的:一个模型决定这条该不该由机器改、以及改完长什么样,另一
|
||
# 个模型把它落成逐字命中的替换。想让这两步用不同的模型,就再配一个组织级的
|
||
# REVIEW_FIX_PLAN_MODEL——判断那步值得用贵的,抄写那步不值得。不配也照样分两步,
|
||
# 只是两步都用 REVIEW_FIX_MODEL。
|
||
#
|
||
# 成本由模型决定而不是由这个设计决定:同一个 PR 在便宜模型上是几分钱,在旗舰
|
||
# 模型上是几美元。换模型前先看一眼那家的每 token 价格。
|
||
# 换了模型记得同步组织变量 REVIEW_PRICES,缺单价的那轮评论不报金额。
|
||
#
|
||
# 只发 COMMENT 类型的 review,从不 approve,也从不 request changes。所有发现集中
|
||
# 在一条评论里按 1、2、3 编号,不再逐条钉成行内评论。
|
||
|
||
on:
|
||
pull_request:
|
||
# 没有 ready_for_review:Gitea Actions 不实现这个活动类型(它支持的是
|
||
# opened / edited / closed / reopened / assigned / unassigned /
|
||
# synchronize / labeled / unlabeled / milestoned / demilestoned /
|
||
# review_requested / review_request_removed)。写上去只会让人以为有这个
|
||
# 覆盖,实际没有。
|
||
#
|
||
# 留下的缺口:草稿 PR 会被下面的 draft == false 挡掉,所以"取消草稿但不再
|
||
# 推代码"不会触发任何东西。再推一次,或者用下面的 workflow_dispatch 手动
|
||
# 指定 PR 号——那条路就是为这个场景留的。
|
||
#
|
||
# 用 edited 能覆盖取消草稿,但它同时会在每次改标题、改描述时触发,而每一
|
||
# 次都是一整轮 find + refute 的钱。为一个"再推一次就解决"的场景不值。
|
||
types: [opened, synchronize, reopened]
|
||
workflow_dispatch:
|
||
inputs:
|
||
pr_number:
|
||
description: "PR number to review"
|
||
required: true
|
||
type: string
|
||
|
||
# 连着推两次的 PR 只该被审一次——取消在飞的那轮,而不是花钱买一份关于旧代码
|
||
# 的报告。
|
||
concurrency:
|
||
group: ai-review-${{ github.event.pull_request.number || inputs.pr_number }}
|
||
cancel-in-progress: true
|
||
|
||
jobs:
|
||
review:
|
||
# 草稿按定义就是没写完的东西;手动 dispatch 是明确的请求,所以绕过这条。
|
||
# vars 那两个条件意味着本文件在网关配好之前合入也是安全的——job 直接跳过。
|
||
if: >-
|
||
(github.event_name == 'workflow_dispatch' || github.event.pull_request.draft == false)
|
||
&& vars.REVIEW_BASE_URL != ''
|
||
&& vars.REVIEW_MODEL != ''
|
||
runs-on: ubuntu-latest
|
||
# 不设的话是 Gitea 默认的 6 小时,而下面的 continue-on-error 帮不上忙——它
|
||
# 兜的是失败,不是挂起——于是一次卡在不返回的 API 调用上的审查会一直占着
|
||
# runner,PR 的 check 一下午都是 pending。fantap-server 真踩过这个。
|
||
#
|
||
# 60 分钟是照实测定的:一次阶段一实测 710 秒、29k 输出 token(其中 28.7k
|
||
# 是推理),阶段二还要在这之上再加最多八条反驳,开了自动修复的话阶段三再加
|
||
# 最多八条的规划加执行。阶段三从每条一次调用变成每条两次(执行没对上文件还
|
||
# 会重试)之后,原来的 45 分钟只在八条全中时才不够,但不够的表现是卡在超时
|
||
# 上而不是干净报错,所以往上留一档。上面 concurrency 的 cancel-in-progress
|
||
# 负责拦住堆积。
|
||
timeout-minutes: 60
|
||
permissions:
|
||
# write 只有 REVIEW_AUTOFIX 那个模式才用得上——它要把改动推回 PR 分支。
|
||
# 没开自动修复、或者只开了 REVIEW_FIX_SUGGEST_ONLY 的仓库,把这一行改回
|
||
# read 就是了:只给改法的模式一个字节都不落盘,不需要写权限。留着 write
|
||
# 也没事,引擎只在 apply 模式下才提交任何东西。
|
||
contents: write
|
||
pull-requests: write
|
||
steps:
|
||
# 一、被审查的代码。反驳阶段要读 PR head 上的整份文件,所以检出 head,
|
||
# 不是 merge commit。
|
||
- uses: actions/checkout@v4
|
||
with:
|
||
ref: ${{ github.event.pull_request.head.sha || format('refs/pull/{0}/head', inputs.pr_number) }}
|
||
|
||
# 二、本仓库的审查侧重(.gitea/ai-review.md),取默认分支的那一份,不取
|
||
# PR 分支的——否则一个 PR 可以先改掉审查重点,再让改过的重点来审自己。
|
||
# || 'main' 是给 Gitea 兜底:它的 workflow_dispatch 载荷不总是带
|
||
# github.event.repository。
|
||
- uses: actions/checkout@v4
|
||
with:
|
||
ref: ${{ github.event.repository.default_branch || 'main' }}
|
||
sparse-checkout: .gitea
|
||
path: .review-profile
|
||
|
||
# 三、引擎。跟着 v1 这个移动标签走:引擎修了 bug 移一次标签,所有接入的
|
||
# 仓库同时生效。某个仓库被新版引擎坑到时,把这里单独钉到具体 commit 就能
|
||
# 脱身,不用回滚全局。
|
||
#
|
||
# 引擎仓库是私有的,Gitea 的 GITHUB_TOKEN 只对本仓库有效,拉不动它,所以
|
||
# 要一个组织级的只读 PAT。continue-on-error 在这里是必要的:PAT 没配或者
|
||
# 过期时,这一步失败不该把别人的 PR 弄红——下面那步会说明情况然后干净退出。
|
||
- uses: actions/checkout@v4
|
||
continue-on-error: true
|
||
with:
|
||
repository: vertai-tech/ai-review-engine
|
||
ref: v1
|
||
path: .review-engine
|
||
token: ${{ secrets.REVIEW_ENGINE_TOKEN }}
|
||
|
||
- name: Adversarial review
|
||
# 设计上就是建议性的:引擎自己的错误已经会干净退出,这里兜的是剩下的
|
||
# (进程被杀、脚本没看见的 curl 失败)。审查机器人不该让人的 PR 变红。
|
||
continue-on-error: true
|
||
# 比 job 的预算小一档,这样超时是记在这一步上,而不是整个 job 被砍——
|
||
# 后者在界面上看不出是谁超的时。引擎内部 curl 的 max-time 比这再小一档
|
||
# (1200s),所以正常顺序是那一层带着响应体和日志先报错,而不是这一层
|
||
# 无声地杀掉进程。
|
||
timeout-minutes: 55
|
||
env:
|
||
REVIEW_API_KEY: ${{ secrets.REVIEW_API_KEY }}
|
||
REVIEW_BASE_URL: ${{ vars.REVIEW_BASE_URL }}
|
||
REVIEW_MODEL: ${{ vars.REVIEW_MODEL }}
|
||
# 可选,组织级变量。网关不在响应里报金额时(sub2api 就不报),评论按这张
|
||
# 单价表算金额;不配就只报 token。格式见引擎 README「每轮花了多少」。
|
||
REVIEW_PRICES: ${{ vars.REVIEW_PRICES }}
|
||
# 上游报过载 / 限流时每次重试前等几秒。不配就是 10,30,60,写 0 关掉。
|
||
REVIEW_BUSY_RETRY_WAITS: ${{ vars.REVIEW_BUSY_RETRY_WAITS }}
|
||
# 评论里行号链接用的站点地址,如 https://git.sciark.cn。runner 在容器里访问
|
||
# Gitea 时,API 返回的 html_url 和 GITHUB_SERVER_URL 都可能是容器内地址,
|
||
# 不配这个链接就打不开。组织级配一次。
|
||
REVIEW_PUBLIC_URL: ${{ vars.REVIEW_PUBLIC_URL }}
|
||
# PR 号和仓库名经 env 进脚本,不直接拼进 run:手动 dispatch 的 pr_number
|
||
# 是任意字符串,表达式在 shell 解析之前就被展开,带引号就能注入命令。
|
||
PR_NUMBER: ${{ github.event.pull_request.number || inputs.pr_number }}
|
||
REVIEW_REPO: ${{ github.repository }}
|
||
# 引擎的 Gitea REST helper 读这个。GITHUB_API_URL 由 act_runner 导出,
|
||
# 已经指向 <instance>/api/v1。
|
||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||
# 自动修复。REVIEW_AUTOFIX 没配或不是 1 的话,下面几个都没有意义。
|
||
REVIEW_AUTOFIX: ${{ vars.REVIEW_AUTOFIX }}
|
||
REVIEW_FIX_SUGGEST_ONLY: ${{ vars.REVIEW_FIX_SUGGEST_ONLY }}
|
||
REVIEW_FIX_MODEL: ${{ vars.REVIEW_FIX_MODEL }}
|
||
# 一个分支最多自动修复几轮。不配就是 1:修一次,下一轮只审,跟从前
|
||
# 一样。配大了才是"一直修到没意见",代价是往作者分支上推更多 commit、
|
||
# 一个 PR 烧更多轮的钱。硬上限在引擎里。
|
||
REVIEW_FIX_ROUNDS: ${{ vars.REVIEW_FIX_ROUNDS }}
|
||
# 规划用哪个模型。这不是开关——不配也照样先规划再执行,只是两步同一个
|
||
# 模型。配了才是"贵的判断、便宜的抄写"那种分工。
|
||
REVIEW_FIX_PLAN_MODEL: ${{ vars.REVIEW_FIX_PLAN_MODEL }}
|
||
# 只有当写修复的模型不在审查那个网关上时,才需要这两个。
|
||
REVIEW_FIX_BASE_URL: ${{ vars.REVIEW_FIX_BASE_URL }}
|
||
REVIEW_FIX_API_KEY: ${{ secrets.REVIEW_FIX_API_KEY }}
|
||
# 同上,只有当规划的模型连修复那个网关都不在时才需要。
|
||
REVIEW_FIX_PLAN_BASE_URL: ${{ vars.REVIEW_FIX_PLAN_BASE_URL }}
|
||
REVIEW_FIX_PLAN_API_KEY: ${{ secrets.REVIEW_FIX_PLAN_API_KEY }}
|
||
# 规划的思考强度。不配就是模型默认;写 high 之类的词按 reasoning_effort
|
||
# 发,写 JSON 对象就原样并进请求。各家写法见引擎 README。
|
||
REVIEW_FIX_PLAN_THINKING: ${{ vars.REVIEW_FIX_PLAN_THINKING }}
|
||
# 对照用的第二个执行模型。只在直接改的模式下跑,改动不落盘,只和
|
||
# REVIEW_FIX_MODEL 的改动一起贴进评论,方便比较哪个模型抄得好。
|
||
REVIEW_FIX_MODEL2: ${{ vars.REVIEW_FIX_MODEL2 }}
|
||
REVIEW_FIX_MODEL2_BASE_URL: ${{ vars.REVIEW_FIX_MODEL2_BASE_URL }}
|
||
REVIEW_FIX_MODEL2_API_KEY: ${{ secrets.REVIEW_FIX_MODEL2_API_KEY }}
|
||
# 只有当上面那个自动 token 推不动分支时,才需要这个写权限 PAT。
|
||
REVIEW_FIX_TOKEN: ${{ secrets.REVIEW_FIX_TOKEN }}
|
||
run: |
|
||
ENGINE=.review-engine/scripts/ai_review.py
|
||
if [ ! -f "$ENGINE" ]; then
|
||
# 拉引擎那步失败了。说清楚原因,否则这里的沉默会被读成"没发现问题"。
|
||
echo "::warning::引擎没拉下来(多半是组织级 secret REVIEW_ENGINE_TOKEN 没配或已过期),本轮未审查。"
|
||
exit 0
|
||
fi
|
||
|
||
case "$PR_NUMBER" in
|
||
''|*[!0-9]*)
|
||
echo "::warning::PR 号必须是数字,收到的是:$PR_NUMBER。本轮未审查。"
|
||
exit 0 ;;
|
||
esac
|
||
python3 "$ENGINE" --repo "$REVIEW_REPO" --pr "$PR_NUMBER"
|
||
|