Merge pull request 'feat(skills): add gitlink-gatekeeper — Policy-as-Code PR merge gate' (#90) from recorder/gitlink-cli:feat/gatekeeper-skill into master
This commit is contained in:
commit
f3e5d4aba0
|
|
@ -153,6 +153,7 @@ skills/
|
|||
| **gitlink-pm** | 项目管理 | 通过 Raw API 访问 |
|
||||
| **gitlink-workflow** | AI 工作流 | Issue 分类、PR Review、Release Notes |
|
||||
| **gitlink-health** | 开源项目健康度 | 详情见SKILL.md |
|
||||
| **gitlink-gatekeeper** | Policy-as-Code PR 合并门禁:按 `gatekeeper.yaml` 策略算 0-100 评分卡 + 三态裁决(PASS/REQUEST_CHANGES/COMMENT),默认 dry-run、绝不自动合并 | `pr +view/+files/+diff`, `pr +comment`, `label +create` |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,244 @@
|
|||
# gitlink-gatekeeper REFERENCE
|
||||
|
||||
> 查字段 / 查算法的参考手册。工作流与裁决步骤见 [`SKILL.md`](./SKILL.md),权威定义见 [`./REFERENCE.md`](./REFERENCE.md)(SSOT)。
|
||||
> 本文与 SSOT 不一致时,**以 SSOT 为准**。所有数值、字段名、公式均逐条对齐 SSOT 第 2–7 节。
|
||||
|
||||
**CRITICAL — 认证、全局参数、GitLink 真实 API 坑见 [`gitlink-shared/SKILL.md`](../gitlink-shared/SKILL.md)。**
|
||||
**CRITICAL — GitLink 平台只用 `gitlink-cli`,禁止 `gh`。**
|
||||
|
||||
---
|
||||
|
||||
## 1. `gatekeeper.yaml` 全字段参考
|
||||
|
||||
策略默认从仓库根目录 `gatekeeper.yaml` 读取,可用 `--policy <path>` 指定;找不到时回退到内置默认策略(即下表「默认值」列)。
|
||||
|
||||
### 1.1 顶层
|
||||
|
||||
| 字段 | 类型 | 默认值 | 含义 | 约束 |
|
||||
|------|------|--------|------|------|
|
||||
| `version` | int | `1` | 策略 schema 版本 | 当前固定为 `1` |
|
||||
| `weights` | map | 见 §1.2 | 五维加权评分权重 | 五项之和 **必须 = 100** |
|
||||
| `hard_gates` | map | 见 §1.3 | 硬门禁开关 | — |
|
||||
| `severity_penalty` | map | 见 §1.4 | 严重度 → 扣分 | 仅用于 `review_findings` 维度 |
|
||||
| `thresholds` | map | 见 §1.5 | 总分 → 裁决阈值 | `pass ≥ request_changes`,均 ∈ 0..100 |
|
||||
| `labels` | map | 见 §1.6 | 三态裁决回写的标签名 | — |
|
||||
| `source_globs` | list[str] | 见 §1.7 | 源码文件判定 glob | 用于 `test_coverage` / 测试硬门禁 |
|
||||
| `test_globs` | list[str] | 见 §1.7 | 测试文件判定 glob | 同上 |
|
||||
| `behavior` | map | 见 §1.8 | 行为开关 | — |
|
||||
|
||||
### 1.2 `weights`(五项之和必须 = 100)
|
||||
|
||||
| 字段 | 类型 | 默认值 | 含义 |
|
||||
|------|------|--------|------|
|
||||
| `review_findings` | int | `40` | AI 代码审查发现(按严重度扣分) |
|
||||
| `test_coverage` | int | `20` | 改动的源码是否伴随测试 |
|
||||
| `pr_hygiene` | int | `15` | PR 描述 / 关联 Issue / 体量 |
|
||||
| `commit_quality` | int | `15` | commit 是否符合 Conventional Commits |
|
||||
| `ci_status` | int | `10` | CI 是否通过 |
|
||||
|
||||
> 校验:`Σweights == 100`。不等于 100 应报错并拒绝运行(避免总分不再是 0–100)。
|
||||
|
||||
### 1.3 `hard_gates`(任一命中 → 直接 `REQUEST_CHANGES`,无视总分)
|
||||
|
||||
| 字段 | 类型 | 默认值 | 含义 |
|
||||
|------|------|--------|------|
|
||||
| `forbid_blocker_findings` | bool | `true` | 出现 blocker 级发现即拦截 |
|
||||
| `require_ci_pass` | bool | `true` | CI 未通过即拦截 |
|
||||
| `require_tests_for_src_changes` | bool | `true` | 改了源码却没加测试即拦截 |
|
||||
| `require_linked_issue` | bool | `false` | PR 是否必须关联 Issue |
|
||||
| `max_changed_files` | int | `80` | 超过该改动文件数即拦截;**`0` 表示不限** |
|
||||
|
||||
### 1.4 `severity_penalty`(用于 `review_findings`)
|
||||
|
||||
| 字段 | 类型 | 默认值 | 含义 |
|
||||
|------|------|--------|------|
|
||||
| `blocker` | int | `100` | 单条 blocker 即可把本维度清零(≥ 权重 40) |
|
||||
| `major` | int | `25` | |
|
||||
| `minor` | int | `5` | |
|
||||
| `nit` | int | `1` | |
|
||||
|
||||
> `finding.severity` 取值域:`{blocker, major, minor, nit}`。未知严重度不应计入(视为 0 或报错),由 SKILL 侧约束 AI 只产出这四类。
|
||||
|
||||
### 1.5 `thresholds`
|
||||
|
||||
| 字段 | 类型 | 默认值 | 含义 | 约束 |
|
||||
|------|------|--------|------|------|
|
||||
| `pass` | int | `85` | 总分 ≥ `pass` 且无硬门禁失败 → `PASS` | 0..100 |
|
||||
| `request_changes` | int | `60` | 总分 < `request_changes` → `REQUEST_CHANGES` | 0..100,且 `≤ pass` |
|
||||
|
||||
> 介于 `[request_changes, pass)` 之间 → `COMMENT`。
|
||||
|
||||
### 1.6 `labels`(依赖 `gitlink-cli label`)
|
||||
|
||||
| 字段 | 类型 | 默认值 | 对应裁决 |
|
||||
|------|------|--------|----------|
|
||||
| `pass` | str | `"gatekeeper:pass"` | `PASS` |
|
||||
| `request_changes` | str | `"gatekeeper:needs-changes"` | `REQUEST_CHANGES` |
|
||||
| `comment` | str | `"gatekeeper:review"` | `COMMENT` |
|
||||
|
||||
### 1.7 `source_globs` / `test_globs`
|
||||
|
||||
| 字段 | 类型 | 默认值 |
|
||||
|------|------|--------|
|
||||
| `source_globs` | list[str] | `["**/*.go", "**/*.py", "**/*.js", "**/*.ts", "**/*.rs", "**/*.java"]` |
|
||||
| `test_globs` | list[str] | `["**/*_test.go", "**/test_*.py", "**/*.test.*", "**/*.spec.*", "tests/**"]` |
|
||||
|
||||
> 一个文件先按 `test_globs` 命中算测试;否则按 `source_globs` 命中算源码。`test_globs` 优先,避免 `foo_test.go` 同时被 `**/*.go` 计为源码。
|
||||
|
||||
### 1.8 `behavior`
|
||||
|
||||
| 字段 | 类型 | 默认值 | 含义 |
|
||||
|------|------|--------|------|
|
||||
| `dry_run_default` | bool | `true` | 默认只预览,不写任何东西(不传 `--apply` 即生效) |
|
||||
| `post_comment` | bool | `true` | 把评分卡作为评论回写 PR |
|
||||
| `apply_label` | bool | `true` | 按裁决打标签 |
|
||||
| `auto_merge` | bool | `false` | **仅当 `true` 且裁决=PASS 且显式 `--apply` 时才合并** |
|
||||
| `merge_method` | enum | `squash` | `merge` \| `rebase` \| `squash` |
|
||||
|
||||
---
|
||||
|
||||
## 2. 评分算法规格(确定性,可复现)
|
||||
|
||||
输入:一个 PR 的上下文(变更文件、diff、commits、CI 状态、PR 元信息)+ AI 审查产出的**发现列表**(每条带 `severity ∈ {blocker, major, minor, nit}`、`file`、`line`、`message`)。
|
||||
|
||||
每个维度产出 `0..weights[dim]` 的得分,五维相加得 `total ∈ 0..100`。同策略 + 同输入 → 同 total → 同裁决。
|
||||
|
||||
记号:`W.x` = `weights.x`,`SP[s]` = `severity_penalty[s]`。
|
||||
|
||||
### 2.1 `review_findings`(默认权重 40)
|
||||
|
||||
```
|
||||
penalty = Σ over all findings SP[finding.severity]
|
||||
score = W.review_findings * max(0, 1 - penalty / W.review_findings)
|
||||
```
|
||||
|
||||
- 累计扣分达到该维度权重即扣到 0(不为负)。
|
||||
- **边界(无发现)**:`penalty = 0 → score = W.review_findings`(满分)。
|
||||
- **边界(含 blocker)**:单条 blocker 默认 `SP=100 ≥ 40`,本维度即清零;通常同时触发 `forbid_blocker_findings` 硬门禁。
|
||||
|
||||
### 2.2 `test_coverage`(默认权重 20)
|
||||
|
||||
```
|
||||
changed_src = 变更文件中匹配 source_globs 的数量
|
||||
changed_tests = 变更文件中匹配 test_globs 的数量
|
||||
|
||||
if changed_src == 0: score = W.test_coverage # 无源码改动,不扣,满分
|
||||
elif changed_tests == 0: score = 0 # 改了源码但零测试
|
||||
else: ratio = min(1, changed_tests / changed_src)
|
||||
score = round(W.test_coverage * (0.5 + 0.5*ratio))
|
||||
```
|
||||
|
||||
- **边界(无源码改动)**:纯文档 / 配置 PR → 满分(不应因没测试被罚)。
|
||||
- 有测试即至少拿一半分;测试文件数 ≥ 源码文件数 → `ratio=1` → 满分。
|
||||
- `round` 为四舍五入到整数。
|
||||
|
||||
### 2.3 `pr_hygiene`(默认权重 15,三项各占 1/3)
|
||||
|
||||
逐项累加命中比例 `hit ∈ {0, 1/6, 1/3, ...}`:
|
||||
|
||||
| 子项 | 命中条件 | 贡献 |
|
||||
|------|----------|------|
|
||||
| 描述 | PR 描述非空且长度 ≥ 30 字符 | +1/3 |
|
||||
| 关联 Issue | PR body 含 `#<n>` 或 API 标记关联了 Issue | +1/3 |
|
||||
| 体量 | `changed_files ≤ max_changed_files / 2` → +1/3;`max_changed_files/2 < changed_files ≤ max_changed_files` → +1/6;超上限 → +0 | +1/3 或 +1/6 或 0 |
|
||||
|
||||
```
|
||||
score = round(W.pr_hygiene * hit_ratio) # hit_ratio = 三项贡献之和
|
||||
```
|
||||
|
||||
- **边界(`max_changed_files == 0` 表示不限)**:体量子项视为满足,记 +1/3。
|
||||
- 体量子项与 `max_changed_files` 硬门禁独立计算;硬门禁只看是否 `> max_changed_files`。
|
||||
|
||||
### 2.4 `commit_quality`(默认权重 15)
|
||||
|
||||
```
|
||||
conforming = 符合 Conventional Commits(type(scope): subject)的 commit 数
|
||||
total = commit 总数
|
||||
|
||||
if total == 0: score = W.commit_quality # 满分
|
||||
else: score = round(W.commit_quality * conforming / total)
|
||||
```
|
||||
|
||||
- **边界(`total == 0`)**:取不到 commit(API 空 / 异常)时给满分,不因数据缺失惩罚。
|
||||
- Conventional Commits 判定:`type(optional-scope): subject`,常见 `type` ∈ {feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert}(`scope` 可省略)。
|
||||
|
||||
### 2.5 `ci_status`(默认权重 10)
|
||||
|
||||
```
|
||||
CI 通过 → score = W.ci_status
|
||||
CI 失败 → score = 0
|
||||
CI 未知 / 无 → score = round(W.ci_status * 0.5)
|
||||
```
|
||||
|
||||
- **边界(CI 未知/无)**:拿一半分,避免对没有 CI 的仓库一刀切清零。
|
||||
- 注意与硬门禁的区别:`require_ci_pass` 只在 CI **明确失败**时拦截;CI 未知不触发硬门禁,但本维度仅得半分。
|
||||
|
||||
---
|
||||
|
||||
## 3. 硬门禁判定表(SSOT §4)
|
||||
|
||||
按 `hard_gates` 逐项判定,命中任一 → `hard_gate_failed = true`:
|
||||
|
||||
| 门禁字段 | 触发条件 | 失败说明(写入评分卡) |
|
||||
|----------|----------|------------------------|
|
||||
| `forbid_blocker_findings` | `true` 且存在 ≥1 条 blocker 发现 | 存在 blocker 级问题 |
|
||||
| `require_ci_pass` | `true` 且 CI **未通过**(失败;未知不触发) | CI 未通过 |
|
||||
| `require_tests_for_src_changes` | `true` 且 `changed_src > 0` 且 `changed_tests == 0` | 改了源码但无测试 |
|
||||
| `require_linked_issue` | `true` 且 PR 未关联 Issue | 未关联 Issue |
|
||||
| `max_changed_files` | `> 0` 且 `changed_files > max_changed_files` | 改动文件数超上限 |
|
||||
|
||||
> `max_changed_files == 0` → 该门禁直接跳过(不限)。多项命中时全部记入评分卡的 Hard gate failures 区块。
|
||||
|
||||
---
|
||||
|
||||
## 4. 裁决伪代码(SSOT §5)
|
||||
|
||||
```python
|
||||
def decide(total, hard_gate_failed, thresholds):
|
||||
if hard_gate_failed:
|
||||
return "REQUEST_CHANGES" # 硬门禁优先,无视总分
|
||||
if total >= thresholds.pass:
|
||||
return "PASS"
|
||||
if total < thresholds.request_changes:
|
||||
return "REQUEST_CHANGES"
|
||||
return "COMMENT" # total ∈ [request_changes, pass)
|
||||
```
|
||||
|
||||
裁决映射(用于评分卡标题与回写):
|
||||
|
||||
| verdict | emoji | 回写标签(默认) | 是否允许合并 |
|
||||
|---------|:-----:|------------------|--------------|
|
||||
| `PASS` | ✅ | `gatekeeper:pass` | 仅当 `behavior.auto_merge==true` 且显式 `--apply` |
|
||||
| `REQUEST_CHANGES` | ❌ | `gatekeeper:needs-changes` | 否 |
|
||||
| `COMMENT` | 💬 | `gatekeeper:review` | 否 |
|
||||
|
||||
> 评分卡 Markdown 模板见 SSOT §6 / SKILL.md,本文不重复。
|
||||
|
||||
---
|
||||
|
||||
## 5. CLI 命令映射速查(SSOT §7,已对 `shortcuts/` 逐条核验)
|
||||
|
||||
| 步骤 | 数据 | 命令 | 备注 |
|
||||
|------|------|------|------|
|
||||
| PR 元信息 | 标题/描述/作者/关联 | `gitlink-cli pr +view -i <id> --format json` | `+view`,`-i`/`--id` |
|
||||
| 变更文件 | 文件路径列表 | `gitlink-cli pr +files -i <id> --format json` | `+files` |
|
||||
| Diff | 变更内容供 AI 审查 | `gitlink-cli pr +diff -i <id> --format json` | `+diff`;当前实现与 `+files` 命中同一 `/pulls/:id/files` 端点 |
|
||||
| commits | commit 列表 | `gitlink-cli api GET /:owner/:repo/pulls/:id/commits --format json` | Raw API(无对应 shortcut) |
|
||||
| CI 状态 | 构建结果 | `gitlink-cli ci +builds --format json` | `+builds`(`-p`/`-l` 分页) |
|
||||
| 回写评论 | 评分卡 | `gitlink-cli pr +comment -i <id> -b "<scorecard>"` | `+comment` 底层走 issue journals(评论流);评审记录形式用 `pr +review -i <id> -s common -c "..."`(走 reviews 端点,payload 字段是 `content`/`status`,status 取 `common`/`approved`/`rejected`)。评分卡作为建议性回写,二者均用 `common` |
|
||||
| 创建标签 | 裁决标签 | `gitlink-cli label +create -n "<name>" -c "#RRGGBB"` | `+create`(本作品子题一新增;`label +list/+update/+delete` 同组) |
|
||||
| 挂标签 | 把标签挂到 PR 对应 Issue | `ISSUE_ID=$(gitlink-cli pr +view -i <pr> --format json \| jq -r '.data.issue.id')` 后 `gitlink-cli api POST /:owner/:repo/issues/$ISSUE_ID --body '{"issue_tag_ids":[<id>],"done_ratio":0,"subject":"...","description":"..."}'` | `:id` 是 PR 关联的 **issue.id(非 PR 号)**;更新须带回 `subject`/`description`(见 gitlink-shared)。亦可用 `gitlink-cli issue +update`(自动保留 subject/description) |
|
||||
| 合并(受限) | 仅 PASS+`auto_merge`+`--apply` | `gitlink-cli pr +merge -i <id> --method <merge_method>` | **CLI 标志为 `--method`/`-m`**(值 `merge`/`rebase`/`squash`);底层 API payload 字段才叫 `do`(不要当 CLI flag 用) |
|
||||
|
||||
### 真实 API 注意(GitLink 平台特性,gatekeeper 直接受影响)
|
||||
|
||||
| 坑 | 对 gatekeeper 的影响 |
|
||||
|----|----------------------|
|
||||
| PR review `status` 为 `common`/`approved`/`rejected`(非 GitHub 的 event=COMMENT/APPROVE) | gatekeeper 刻意把自动裁决统一以建议性 `common` 回写(或用 `pr +comment`),强语义 `approved`/`rejected` 留给人工;三态裁决靠评分卡标题 + `gatekeeper:*` 标签表达状态 |
|
||||
| PR list `--state` 过滤不准 | 列 PR 时用返回体 `pull_request_status` 字段客户端过滤(0=open, 1=merged, 2=closed) |
|
||||
| Issue 更新需带 `subject`/`description`/`done_ratio` | 经 `issue_tag_ids` 挂标签时,Raw API 须先 GET 再回传这些字段,否则可能清空描述 |
|
||||
| `create_file` 的 `content` 须 base64 | 子题三创建 tracking issue 若需落文件时遵守 |
|
||||
| 分支删除 API 是平台 bug | gatekeeper 不依赖删分支;勿用 `DELETE .../branches/:name` |
|
||||
| GitLink 主分支为 `master` | 合并 base、`pr +create --base` 默认 `master` |
|
||||
|
||||
> 写操作前必须向用户复述将要做什么;不传 `--apply` 时一律 dry-run(`behavior.dry_run_default`)。Token 不回显。
|
||||
|
|
@ -0,0 +1,370 @@
|
|||
---
|
||||
name: gitlink-gatekeeper
|
||||
version: 1.0.0
|
||||
description: "Policy-as-Code 的 PR 合并门禁:按版本化的 gatekeeper.yaml 策略对一个 Pull Request 聚合多路信号、算出 0-100 评分卡、给出三态裁决(PASS / REQUEST_CHANGES / COMMENT),并把结论作为结构化评论回写、打标签。当用户需要判断 PR 能否合并、做合并门禁/质量闸门、生成可复现的 PR 裁决,或问『这个 PR 达标了吗』时触发。默认 dry-run,绝不自动合并。"
|
||||
metadata:
|
||||
requires:
|
||||
bins: ["gitlink-cli"]
|
||||
cliHelp: "gitlink-cli pr --help"
|
||||
---
|
||||
|
||||
# gitlink-gatekeeper(Policy-as-Code PR 合并门禁)
|
||||
|
||||
**CRITICAL — 开始前必须先阅读 [`gitlink-shared/SKILL.md`](../gitlink-shared/SKILL.md),其中包含认证、权限处理和 GitLink API 注意事项(PR list state 过滤不准、主分支是 master、issue 更新需带回 subject/description 等)。**
|
||||
**CRITICAL — 任何写操作(回写评论、打标签、合并)前,必须向用户复述将要做什么并得到确认;默认 dry-run 不写任何东西。绝不默认自动合并。**
|
||||
**CRITICAL — GitLink 操作只能用 `gitlink-cli`。禁止用 `gh`(GitHub CLI)操作 GitLink 资源。`gh` 仅适用于 GitHub 平台。**
|
||||
|
||||
> **前置条件:** 先阅读 [`gitlink-shared/SKILL.md`](../gitlink-shared/SKILL.md) 了解认证和全局参数;本 Skill 的全字段策略与算法细节见同目录 [`REFERENCE.md`](./REFERENCE.md),故障排查见 [`TROUBLESHOOTING.md`](./TROUBLESHOOTING.md)。
|
||||
|
||||
---
|
||||
|
||||
## 定位与产物
|
||||
|
||||
gitlink-gatekeeper 是一个**可复现的 PR 合并门禁**:团队把合并标准写进版本化的 `gatekeeper.yaml`,gatekeeper 据此对一个 PR 算出**透明的 0-100 评分卡**,给出**三态裁决**,并把结论回写。与 `gitlink-code-review`(只产出主观评论、无阈值、无结论)相比,本 Skill 的核心是**可裁决、可审计、可复现**:同一策略 + 同一 PR → 同一裁决。
|
||||
|
||||
| 裁决 | emoji | 触发条件 | 回写标签 |
|
||||
|------|:----:|----------|----------|
|
||||
| PASS | ✅ | 无硬门禁失败 且 `total ≥ thresholds.pass` | `gatekeeper:pass` |
|
||||
| REQUEST_CHANGES | ❌ | 命中任一硬门禁,或 `total < thresholds.request_changes` | `gatekeeper:needs-changes` |
|
||||
| COMMENT | 💬 | 介于两阈值之间且无硬门禁失败 | `gatekeeper:review` |
|
||||
|
||||
> GitLink 原生 PR review 的 `status` 为 `common`/`approved`/`rejected`(`rejected` 即「请求修改」的等价)。gitlink-gatekeeper **刻意把所有自动裁决以建议性的 `common` 评论回写**(评分卡标题明确标注三态裁决),把强语义的 `approved`/`rejected` 留给人工授意;裁决状态通过标签(`gatekeeper:needs-changes` 等)承载——这正是本作品复用 `label` 命令组的原因。
|
||||
|
||||
---
|
||||
|
||||
## 工作流概览
|
||||
|
||||
| 阶段 | 操作 | AI Agent 角色 |
|
||||
|------|------|--------------|
|
||||
| ① 加载策略 | 读 `gatekeeper.yaml`,找不到则回退内置默认策略 | 解析 / 校验 / 回退 |
|
||||
| ② 采集上下文 | 拉 PR 元信息、变更文件、diff、commits、CI 状态 | 执行 CLI 命令采集数据 |
|
||||
| ③ 产出发现 | 逐文件审查,按 severity 分级标记问题 | AI 分析,输出发现列表 |
|
||||
| ④ 逐维评分 | 五维各算 `0..weight` 得分,相加得 `total` | 确定性计算(非主观) |
|
||||
| ⑤ 硬门禁 | 逐项判定 `hard_gates`,命中即拦截 | 布尔判定 |
|
||||
| ⑥ 裁决 | 由硬门禁 + total + 阈值得出三态 | 确定性计算 |
|
||||
| ⑦ 渲染评分卡 | 按模板渲染 Markdown 评分卡 | 模板填充 |
|
||||
| ⑧ 回写(受 `--apply`) | dry-run 仅打印;`--apply` 才回写评论 + 打标签 | 写操作,需确认 |
|
||||
| ⑨ 安全规则 | 默认 dry-run,绝不自动合并 | 守门 |
|
||||
|
||||
---
|
||||
|
||||
## 详细工作流
|
||||
|
||||
### Step ① 加载策略 gatekeeper.yaml
|
||||
|
||||
策略默认从**仓库根目录** `gatekeeper.yaml` 读取,可用 `--policy <path>` 指定。找不到时**回退到下文内置默认策略**(务必在评分卡 footer 注明用的是哪个策略来源)。
|
||||
|
||||
加载后做两项校验(不通过则报错并停止):
|
||||
- `weights` 五项之和必须 **= 100**(`review_findings + test_coverage + pr_hygiene + commit_quality + ci_status`)。
|
||||
- `thresholds.pass`、`thresholds.request_changes` 为 0-100 整数,且 `pass ≥ request_changes`。
|
||||
|
||||
**内置默认策略**(与 `examples/gatekeeper.yaml` 一致,回退时使用,policy 标记为 `<built-in default>@v1`):
|
||||
|
||||
```yaml
|
||||
version: 1
|
||||
weights:
|
||||
review_findings: 40
|
||||
test_coverage: 20
|
||||
pr_hygiene: 15
|
||||
commit_quality: 15
|
||||
ci_status: 10
|
||||
hard_gates:
|
||||
forbid_blocker_findings: true
|
||||
require_ci_pass: true
|
||||
require_tests_for_src_changes: true
|
||||
require_linked_issue: false
|
||||
max_changed_files: 80
|
||||
severity_penalty: { blocker: 100, major: 25, minor: 5, nit: 1 }
|
||||
thresholds: { pass: 85, request_changes: 60 }
|
||||
labels:
|
||||
pass: "gatekeeper:pass"
|
||||
request_changes: "gatekeeper:needs-changes"
|
||||
comment: "gatekeeper:review"
|
||||
source_globs: ["**/*.go", "**/*.py", "**/*.js", "**/*.ts", "**/*.rs", "**/*.java"]
|
||||
test_globs: ["**/*_test.go", "**/test_*.py", "**/*.test.*", "**/*.spec.*", "tests/**"]
|
||||
behavior:
|
||||
dry_run_default: true
|
||||
post_comment: true
|
||||
apply_label: true
|
||||
auto_merge: false
|
||||
merge_method: squash
|
||||
```
|
||||
|
||||
### Step ② 采集 PR 上下文(CLI 命令映射)
|
||||
|
||||
用以下命令采集一个 PR 的全部输入。**始终带 `--format json` 便于解析**;`--owner`/`--repo` 在 git 仓库目录下可自动从 remote 解析,否则显式传入。
|
||||
|
||||
| 步骤 | 数据 | 命令 |
|
||||
|------|------|------|
|
||||
| PR 元信息 | 标题/描述/作者/关联 issue | `gitlink-cli pr +view -i <id> --format json` |
|
||||
| 变更文件 | 文件路径列表 | `gitlink-cli pr +files -i <id> --format json` |
|
||||
| Diff | 变更内容供 AI 审查 | `gitlink-cli pr +diff -i <id> --format json` |
|
||||
| commits | commit 列表(消息供 commit_quality) | `gitlink-cli api GET /:owner/:repo/pulls/:id/commits --format json` |
|
||||
| CI 状态 | 构建结果 | `gitlink-cli ci +builds --format json` |
|
||||
|
||||
> 实测注意:`pr +files` 与 `pr +diff` 底层都打 `/pulls/:id/files`——`+files` 取路径列表,`+diff` 取含 patch 的同一份数据,按需取用即可。无 `pr +commits` 快捷命令,commit 列表只能走 Raw API。CI 通过/失败需从 builds 返回的 `status` 字段判断;**无 build 记录时按「CI 未知」处理**(见 §3.5)。
|
||||
|
||||
```bash
|
||||
PR=42
|
||||
gitlink-cli pr +view -i "$PR" --format json # title / body / 关联 issue
|
||||
gitlink-cli pr +files -i "$PR" --format json # changed files
|
||||
gitlink-cli pr +diff -i "$PR" --format json # diff(供 AI 审查)
|
||||
gitlink-cli api GET /:owner/:repo/pulls/$PR/commits --format json
|
||||
gitlink-cli ci +builds --format json
|
||||
```
|
||||
|
||||
### Step ③ AI 按 severity 分级产出发现
|
||||
|
||||
对 diff 逐文件审查(检查项参考 `gitlink-code-review`:安全红线、错误处理、并发、资源管理、魔法数字、测试覆盖等),产出**结构化发现列表**,每条带:
|
||||
|
||||
```
|
||||
{ severity: blocker|major|minor|nit, file: "<path>", line: <n>, message: "<问题描述>" }
|
||||
```
|
||||
|
||||
severity 判定准则(与 §3.1 扣分表对应):
|
||||
|
||||
| severity | 含义 | 典型例子 |
|
||||
|----------|------|----------|
|
||||
| `blocker` | 必须拦截,单条即清零本维度并触发硬门禁 | 硬编码密钥/Token、SQL/命令注入、路径遍历、不安全反序列化、XSS |
|
||||
| `major` | 严重缺陷,强烈建议修 | 未处理错误/异常吞掉、资源泄漏、并发竞态、明显逻辑错误 |
|
||||
| `minor` | 一般问题 | 风格偏离、轻微复杂度、缺注释、魔法数字 |
|
||||
| `nit` | 吹毛求疵 | 命名小瑕疵、格式建议 |
|
||||
|
||||
> 控制信噪比:宁可少而准。同时记录 `Strengths`(做得好的点),用于评分卡 `✅ Strengths` 区。
|
||||
|
||||
### Step ④ 按 SSOT 算法逐维评分(确定性)
|
||||
|
||||
每维产出 `0..weights[dim]`,五维相加得 `total ∈ 0..100`。**这是确定性计算,不是再次主观打分**——同输入必同分。
|
||||
|
||||
**4.1 review_findings(默认 40)**
|
||||
```
|
||||
penalty = Σ severity_penalty[finding.severity] # 对所有发现求和
|
||||
score = weights.review_findings * max(0, 1 - penalty / weights.review_findings)
|
||||
```
|
||||
扣分累计达本维度权重即扣到 0;blocker 单条(penalty=100)即清零本维度。
|
||||
|
||||
**4.2 test_coverage(默认 20)**
|
||||
```
|
||||
changed_src = 变更文件中匹配 source_globs 的数量
|
||||
changed_tests = 变更文件中匹配 test_globs 的数量
|
||||
if changed_src == 0: score = weights.test_coverage # 无源码改动,不扣
|
||||
elif changed_tests == 0: score = 0
|
||||
else: ratio = min(1, changed_tests / changed_src)
|
||||
score = round(weights.test_coverage * (0.5 + 0.5*ratio)) # 有测试至少拿一半
|
||||
```
|
||||
|
||||
**4.3 pr_hygiene(默认 15,三项各占 1/3)**
|
||||
- 描述非空且长度 ≥ 30 字符 → +1/3
|
||||
- 关联了 Issue(PR body 含 `#<n>` 或 API 标记)→ +1/3
|
||||
- 体量适中(`changed_files ≤ max_changed_files/2`)→ +1/3;超过一半但未超上限 → +1/6
|
||||
```
|
||||
score = round(weights.pr_hygiene * 命中比例)
|
||||
```
|
||||
|
||||
**4.4 commit_quality(默认 15)**
|
||||
```
|
||||
conforming = 符合 Conventional Commits (type(scope): subject) 的 commit 数
|
||||
total_c = commit 总数
|
||||
score = round(weights.commit_quality * conforming / total_c) # total_c=0 给满分
|
||||
```
|
||||
|
||||
**4.5 ci_status(默认 10)**
|
||||
```
|
||||
CI 通过 → weights.ci_status
|
||||
CI 失败 → 0
|
||||
CI 未知/无 → round(weights.ci_status * 0.5)
|
||||
```
|
||||
|
||||
### Step ⑤ 硬门禁评估
|
||||
|
||||
按 `hard_gates` 逐项判定,命中任一 → `hard_gate_failed = true`(记下命中的门禁名供评分卡 `⛔ Hard gate failures` 区):
|
||||
|
||||
| 门禁 | 命中条件 |
|
||||
|------|----------|
|
||||
| `forbid_blocker_findings` | 为 true 且存在 blocker 发现 |
|
||||
| `require_ci_pass` | 为 true 且 CI **明确失败**(`failing`);`unknown`/无 build 记录**不触发**本门禁 |
|
||||
| `require_tests_for_src_changes` | 为 true 且 `changed_src > 0` 且 `changed_tests == 0` |
|
||||
| `require_linked_issue` | 为 true 且未关联 Issue |
|
||||
| `max_changed_files` | `> 0` 且 `changed_files > max_changed_files` |
|
||||
|
||||
### Step ⑥ 裁决计算
|
||||
|
||||
```
|
||||
if hard_gate_failed: verdict = REQUEST_CHANGES
|
||||
elif total >= thresholds.pass: verdict = PASS
|
||||
elif total < thresholds.request_changes: verdict = REQUEST_CHANGES
|
||||
else: verdict = COMMENT
|
||||
```
|
||||
|
||||
### Step ⑦ 渲染评分卡(回写到 PR 的 Markdown)
|
||||
|
||||
按下列模板填充(emoji:PASS=✅,REQUEST_CHANGES=❌,COMMENT=💬)。无硬门禁失败时省略 `⛔` 区;各发现区无内容时省略。
|
||||
|
||||
```markdown
|
||||
## 🛡️ Gatekeeper Report — PR #<id> <title>
|
||||
|
||||
**Verdict: <emoji> <PASS|REQUEST_CHANGES|COMMENT>** · Score: <total>/100 · policy: <policy_path>@v<version>
|
||||
|
||||
| Dimension | Weight | Score | Notes |
|
||||
|-----------|:------:|:-----:|-------|
|
||||
| Review findings | 40 | <s>/40 | <n blocker / n major / n minor / n nit> |
|
||||
| Test coverage | 20 | <s>/20 | <changed_src> src / <changed_tests> test files |
|
||||
| PR hygiene | 15 | <s>/15 | <desc / linked issue / size 命中情况> |
|
||||
| Commit quality | 15 | <s>/15 | <conforming>/<total> conventional |
|
||||
| CI status | 10 | <s>/10 | <passing/failing/unknown> |
|
||||
|
||||
### ⛔ Hard gate failures (<n>)
|
||||
- `<gate>`: <说明>
|
||||
|
||||
### 🔴 Must fix (<n>)
|
||||
- [<severity>] <message> — <file>:<line>
|
||||
|
||||
### 🟡 Should fix (<n>)
|
||||
- ...
|
||||
|
||||
### 🔵 Nits (<n>)
|
||||
|
||||
### ✅ Strengths
|
||||
- <做得好的点>
|
||||
|
||||
### Next steps
|
||||
1. <按裁决给出的最高优先级行动>
|
||||
---
|
||||
*Generated by gitlink-gatekeeper · policy-as-code PR gate · re-run after changes*
|
||||
```
|
||||
|
||||
### Step ⑧ 回写:dry-run(默认)vs --apply
|
||||
|
||||
**默认 dry-run**:不传 `--apply` 时,只把评分卡**打印给用户**,不回写、不打标签、不合并。
|
||||
**`--apply` 才写**:写操作前**先向用户复述**「将向 PR #<id> 回写评分卡评论 + 打标签 `<label>`」,确认后执行。
|
||||
|
||||
回写评论(二选一,优先用 `pr +review` 因其自带 `--dry-run` 预览):
|
||||
|
||||
```bash
|
||||
# 方式 A:作为 PR review 回写(status 用 common,即 GitLink 的 COMMENT 等价)
|
||||
gitlink-cli pr +review -i "$PR" --status common --content "$(cat scorecard.md)"
|
||||
|
||||
# 方式 B:作为普通 PR 评论回写
|
||||
gitlink-cli pr +comment -i "$PR" --body "$(cat scorecard.md)"
|
||||
```
|
||||
|
||||
> ⚠️ `pr +review --status` 只接受 `common` / `approved` / `rejected`。评分卡评论一律用 `common`(不要因为裁决=PASS 就 `approved`,APPROVE 是更强的批准语义,需用户显式授意)。裁决信息已写在评分卡标题里,状态由标签承载。
|
||||
|
||||
打标签(GitLink 无「直接给 PR 挂标签」的命令,标签挂在 PR 背后的 Issue 上):
|
||||
|
||||
```bash
|
||||
# 1) 确保裁决对应的标签存在(首次需创建;已存在则跳过)
|
||||
gitlink-cli label +list --format json # 查现有标签拿 id
|
||||
gitlink-cli label +create --name "gatekeeper:needs-changes" --color "#D73A4A" \
|
||||
--description "PR gate: changes requested" # 不存在才创建
|
||||
|
||||
# 2) 取 PR 背后的 issue id(pr +view 返回里有 issue 对象)
|
||||
ISSUE_ID=$(gitlink-cli pr +view -i "$PR" --format json | jq -r '.data.issue.id')
|
||||
|
||||
# 3) 通过 Issue 更新挂标签(必须带回当前 subject/description,否则会被清空——见 gitlink-shared)
|
||||
gitlink-cli api POST /:owner/:repo/issues/$ISSUE_ID --body '{
|
||||
"issue_tag_ids": [<tag_id>],
|
||||
"done_ratio": 0,
|
||||
"subject": "<原始标题>",
|
||||
"description": "<原始描述>"
|
||||
}'
|
||||
```
|
||||
|
||||
### Step ⑨ 安全规则(硬性,不可绕过)
|
||||
|
||||
- **默认 dry-run**:不传 `--apply` 一律只打印,不产生任何写副作用。
|
||||
- **绝不默认自动合并**:`behavior.auto_merge` 默认 `false`;即便策略里设为 `true`,也必须**同时**满足 `verdict == PASS` **且**命令显式带 `--apply` 才允许合并,且合并前再次向用户复述确认。
|
||||
- 合并命令(仅在上述全部条件满足时):
|
||||
```bash
|
||||
# merge_method 来自策略 behavior.merge_method(merge|rebase|squash)
|
||||
gitlink-cli pr +merge -i "$PR" --method squash
|
||||
```
|
||||
> 注意 CLI 标志是 `--method`(底层 API 字段才是 `do`)。
|
||||
- 不回显 Token;遵循 `gitlink-shared/SKILL.md` 的认证与 API 注意事项(401 引导重登、403 查权限)。
|
||||
- REQUEST_CHANGES 永不触发合并;COMMENT/PASS 默认也不合并,除非满足自动合并三条件。
|
||||
|
||||
---
|
||||
|
||||
## 完整示例
|
||||
|
||||
### 示例 1:dry-run(默认,安全,不写任何东西)
|
||||
|
||||
```bash
|
||||
# 在目标仓库目录下,对 PR #42 跑门禁,仅预览评分卡
|
||||
PR=42
|
||||
gitlink-cli pr +view -i "$PR" --format json
|
||||
gitlink-cli pr +files -i "$PR" --format json
|
||||
gitlink-cli pr +diff -i "$PR" --format json
|
||||
gitlink-cli api GET /:owner/:repo/pulls/$PR/commits --format json
|
||||
gitlink-cli ci +builds --format json
|
||||
# → AI 产出发现 → 按 §4 评分 → §5 硬门禁 → §6 裁决 → §7 渲染评分卡
|
||||
# → dry-run:仅把评分卡打印给用户,结尾提示「如需回写到 PR,请加 --apply」
|
||||
```
|
||||
|
||||
预期产出(节选):
|
||||
```
|
||||
**Verdict: ❌ REQUEST_CHANGES** · Score: 58/100 · policy: gatekeeper.yaml@v1
|
||||
⛔ Hard gate failures (1)
|
||||
- require_tests_for_src_changes: 改了 3 个源码文件但没有新增测试
|
||||
(dry-run:未回写、未打标签、未合并)
|
||||
```
|
||||
|
||||
### 示例 2:--apply(用户确认后回写评论 + 打标签)
|
||||
|
||||
```bash
|
||||
PR=42
|
||||
# …(同上采集 + 评分,得到裁决=REQUEST_CHANGES,渲染出 scorecard.md)…
|
||||
|
||||
# 写操作前向用户复述:将向 PR #42 回写评分卡评论 + 打标签 gatekeeper:needs-changes
|
||||
# 用户确认后:
|
||||
|
||||
# 1) 回写评分卡(review 形式,自带 dry-run 可先预演)
|
||||
gitlink-cli pr +review -i "$PR" --status common --dry-run --content "$(cat scorecard.md)" # 预演
|
||||
gitlink-cli pr +review -i "$PR" --status common --content "$(cat scorecard.md)" # 实际回写
|
||||
|
||||
# 2) 打标签 gatekeeper:needs-changes(按 Step ⑧ 取/建 tag_id 与 issue_id 后)
|
||||
gitlink-cli api POST /:owner/:repo/issues/$ISSUE_ID --body '{
|
||||
"issue_tag_ids": [101], "done_ratio": 0,
|
||||
"subject": "feat: add rate limiter", "description": "<原始描述>"
|
||||
}'
|
||||
|
||||
# 注意:裁决=REQUEST_CHANGES → 绝不合并。
|
||||
# 即便裁决=PASS,也只有在 policy.auto_merge=true 且本次显式 --apply 时才允许:
|
||||
# gitlink-cli pr +merge -i "$PR" --method squash
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 策略预设
|
||||
|
||||
`examples/` 提供三套可直接 `--policy` 引用的预设:
|
||||
|
||||
| 预设 | 特点 | 适用 |
|
||||
|------|------|------|
|
||||
| `gatekeeper.yaml` | 默认平衡策略(pass 85 / rc 60) | 大多数仓库 |
|
||||
| `gatekeeper.strict.yaml` | 高阈值、`require_linked_issue: true`、更小 `max_changed_files` | 核心库 / 发布分支 |
|
||||
| `gatekeeper.lenient.yaml` | 低阈值、关掉部分硬门禁 | 早期项目 / 文档仓库 |
|
||||
|
||||
```bash
|
||||
gitlink-cli pr +view -i 42 --format json # 采集后用严格策略评分(评分逻辑同上,仅阈值/门禁不同)
|
||||
# 评分时加载:--policy examples/gatekeeper.strict.yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **先 dry-run 再 `--apply`**:永远先看评分卡内容,确认无误再回写。
|
||||
2. **策略进版本库**:把 `gatekeeper.yaml` 提交到仓库,让裁决标准对所有贡献者透明、可审计。
|
||||
3. **可复现优先**:评分是确定性的——若两次裁决不同,先查是不是 PR 内容或策略变了,而非「AI 心情」。
|
||||
4. **控制发现数量**:最严重的 3-5 条比 20 条琐碎问题更有价值;nit 折叠展示。
|
||||
5. **大 PR 分段处理**:`pr +diff` 输出可能很大,Agent 应分段读取 diff 再汇总发现。
|
||||
6. **标签复用**:同一仓库的三个 `gatekeeper:*` 标签建一次即可,后续只更新挂载关系。
|
||||
|
||||
## 注意事项
|
||||
|
||||
- PR review/评论提交后会通知关注该 PR 的参与者,评分卡内容保持专业、可操作。
|
||||
- `--state` 过滤 PR 列表不精确,需用 `pull_request_status` 字段客户端判断(0=open,1=merged,2=closed)。
|
||||
- GitLink 主分支是 `master`(非 `main`);合并方式由策略 `merge_method` 决定。
|
||||
- 对 draft PR 应提示用户先标记为 Ready for Review 再门禁。
|
||||
- 端到端「PR 看门人闭环」(路由建议 reviewer → 裁决 → 回写 + 为 REQUEST_CHANGES 自动建 tracking issue)的完整工作流形态见配套独立仓库 recorder/gitlink-gatekeeper;策略字段与算法见 `REFERENCE.md`。
|
||||
|
|
@ -0,0 +1,319 @@
|
|||
# gitlink-gatekeeper — 故障排查(TROUBLESHOOTING)
|
||||
|
||||
**CRITICAL — 开始前请先阅读 [`gitlink-shared/SKILL.md`](../gitlink-shared/SKILL.md)(认证、全局参数、真实 API 坑)与 [`./SKILL.md`](./SKILL.md)(工作流)、[`./REFERENCE.md`](./REFERENCE.md)(策略字段与评分算法)。**
|
||||
**CRITICAL — gatekeeper 默认 dry-run,绝不自动合并。本文档中任何「修复」都不应放宽该安全默认,除非用户明确要求。**
|
||||
**CRITICAL — GitLink 资源只能用 `gitlink-cli` 操作,禁止用 `gh`/`glab`。**
|
||||
|
||||
本文档列出运行 gatekeeper 时的常见问题,按「症状 / 原因 / 解决」三段式给出。先看下表速查,再到对应小节读细节。
|
||||
|
||||
## 速查表
|
||||
|
||||
| # | 症状 | 根因 | 一句话解决 |
|
||||
|---|------|------|-----------|
|
||||
| 1 | 提示「未找到策略文件」 | 仓库根无 `gatekeeper.yaml` 且未传 `--policy` | 回退内置默认策略,或 `--policy <path>` 指定 |
|
||||
| 2 | 报「weights 之和 ≠ 100」 | 五维权重总和不是 100 | 调整权重让 `review+test+hygiene+commit+ci = 100` |
|
||||
| 3 | `401`(请登录)/ `403`(无权限) | Token 过期 / owner-repo 错或无权限 | `gitlink-cli auth login` / 核对 owner、repo、权限 |
|
||||
| 4 | `pr +diff` 输出巨大、超出处理窗口 | 大型 PR 的 diff 一次性返回太大 | 改用 `pr +files` 选关键文件 + `version-diff -f` 按文件分段 |
|
||||
| 5 | 评分卡里 CI 显示 `unknown` | `ci +builds` 取不到与该 PR 对应的构建 | 按 unknown 计 0.5×权重,不要当失败 |
|
||||
| 6 | 如何表达 REQUEST_CHANGES 裁决 | 自动门禁刻意不替人按 `approved`/`rejected` | 用建议性 `common` 评论 + 标题标注裁决 + 打 `gatekeeper:needs-changes` 标签表达 |
|
||||
| 7 | 打标签报「标签不存在」 | 该 label 尚未在仓库创建 | 先 `label +list` 查,缺则 `label +create` 再挂到 issue |
|
||||
| 8 | `auto_merge` 开了却没合并 | 三条件未同时满足 | 需 `verdict==PASS` + 命令带 `--apply` + 策略 `auto_merge: true` |
|
||||
| 9 | 评分卡对 draft PR 给出裁决 | 草稿 PR 不应被门禁裁决 | 检测 draft 标志,提示先转 Ready for Review |
|
||||
| 10 | 大仓库只看到前 20 条 PR/构建/标签 | 列表接口默认分页 limit=20 | 用 `--page` / `--limit` 翻页,按 `meta.total_count` 判完整 |
|
||||
| 11 | `--state open` 仍返回已合并/关闭的 PR | GitLink `--state` 仅影响计数 | 用返回里的 `pull_request_status` 客户端过滤 |
|
||||
| 12 | `pr +merge` 报 `--do` 不是已知 flag | flag 名记错(`do` 是底层 API 字段) | CLI flag 是 `--method`/`-m`,不是 `--do` |
|
||||
|
||||
---
|
||||
|
||||
## 1. 找不到 `gatekeeper.yaml`(回退默认)
|
||||
|
||||
**症状**:运行时提示「未在仓库根目录找到 `gatekeeper.yaml`」,或评分卡页脚 `policy:` 显示为内置默认而非你期望的文件。
|
||||
|
||||
**原因**:策略文件默认从**仓库根目录**的 `gatekeeper.yaml` 读取。当根目录没有该文件、且命令未带 `--policy <path>` 时,gatekeeper 不会报错中止,而是按设计**回退到内置默认策略**(见 `REFERENCE.md` 的默认值:`pass=85 / request_changes=60`,权重 `40/20/15/15/10`)。
|
||||
|
||||
**解决**:
|
||||
- 若你确实想用默认策略 —— 这是正常行为,无需处理;评分卡页脚会标 `policy: built-in@v1`。
|
||||
- 若你有自定义策略 —— 确认文件确实在仓库根,或显式指定路径:
|
||||
|
||||
```bash
|
||||
# 显式指定策略文件(路径相对当前工作目录)
|
||||
gitlink-gatekeeper --policy ./policies/gatekeeper.strict.yaml --pr 42
|
||||
|
||||
# 把示例策略复制到仓库根,让默认查找命中
|
||||
cp skills/gitlink-gatekeeper/examples/gatekeeper.yaml ./gatekeeper.yaml
|
||||
```
|
||||
|
||||
> 注意:策略文件路径区分大小写;`Gatekeeper.yaml`、`gatekeeper.yml`(`.yml` 后缀)都不会被默认查找命中。
|
||||
|
||||
---
|
||||
|
||||
## 2. `weights` 之和不等于 100
|
||||
|
||||
**症状**:加载策略时报错「weights 之和必须为 100,当前为 `<n>`」,gatekeeper 拒绝按该策略评分。
|
||||
|
||||
**原因**:评分算法要求五个维度权重之和**严格等于 100**,这样 `total ∈ 0..100` 才有可比性与可复现性。常见错误是只改了一两个维度、忘了让其余维度补平。
|
||||
|
||||
**解决**:调整 `weights`,使五项相加正好 100。
|
||||
|
||||
```yaml
|
||||
weights:
|
||||
review_findings: 40
|
||||
test_coverage: 20
|
||||
pr_hygiene: 15
|
||||
commit_quality: 15
|
||||
ci_status: 10 # 40+20+15+15+10 = 100 ✅
|
||||
```
|
||||
|
||||
| 错误示例 | 和 | 问题 |
|
||||
|----------|:--:|------|
|
||||
| `40/20/15/15/5` | 95 | 少 5,需补到某一维度 |
|
||||
| `50/20/15/15/10` | 110 | 多 10,把 review 降回 40 |
|
||||
|
||||
> 注意:维度名必须是这五个固定键(`review_findings`/`test_coverage`/`pr_hygiene`/`commit_quality`/`ci_status`),多写或漏写键也会校验失败。某维度想「不计分」应把权重设 0 并把差额加到别处,而不是删除该键。
|
||||
|
||||
---
|
||||
|
||||
## 3. `401` / `403` 认证与权限错误
|
||||
|
||||
**症状**:任意数据采集命令返回
|
||||
```json
|
||||
{"ok": false, "error": {"code": 401, "message": "请登录后再操作", "suggestion": "请先运行 gitlink-cli auth login 登录"}}
|
||||
```
|
||||
或 `403`(拒绝访问)。
|
||||
|
||||
**原因**(详见 `gitlink-shared/SKILL.md`「认证错误处理」):
|
||||
- `401`:未登录或 Token 已过期。GitLink Token 有效期 **7 天**,过期需重新登录。
|
||||
- `403`:已登录但对该 `owner/repo` 无权限,或 owner/repo 解析错误。
|
||||
|
||||
**解决**:
|
||||
|
||||
```bash
|
||||
# 401:重新登录
|
||||
gitlink-cli auth login
|
||||
gitlink-cli auth status # 确认登录态
|
||||
|
||||
# 403:核对上下文(在仓库目录下可从 git remote 自动解析)
|
||||
gitlink-cli repo +info --owner <owner> --repo <repo> --format json
|
||||
```
|
||||
|
||||
> 注意:gatekeeper 全程**不回显 Token**。若 `auth status` 正常但仍 `403`,多半是 owner/repo 写错或你只有读权限——读权限足够采集与生成评分卡(dry-run),但写评论、打标签、合并需要写权限。
|
||||
|
||||
---
|
||||
|
||||
## 4. PR diff 过大需分段
|
||||
|
||||
**症状**:`gitlink-cli pr +diff -i <id> --format json` 返回内容极大,超出单次处理窗口,或采集很慢/报频率限制。
|
||||
|
||||
**原因**:大型 PR 的全量 diff 一次返回会非常大(`gitlink-shared` 与 `gitlink-code-review` 均提示 `pr +diff` 输出可能很大,需分段处理;files/diff 接口有频率限制)。
|
||||
|
||||
**解决**:先用 `pr +files` 拿到文件清单,只对**与评分相关**的源码/测试文件按文件取 diff:
|
||||
|
||||
```bash
|
||||
# 1. 先取文件清单(轻量),用于 test_coverage 维度与体量判断
|
||||
gitlink-cli pr +files -i <id> --format json
|
||||
|
||||
# 2. 对单个文件取差异(避免一次拉全量)
|
||||
gitlink-cli pr +version-diff -i <id> -v <version-id> -f path/to/file.go --format json
|
||||
# version-id 从 pr +versions -i <id> 获取(patchset 版本)
|
||||
```
|
||||
|
||||
> 注意:`test_coverage`、`pr_hygiene` 的体量判定只依赖**文件清单**(`changed_src`/`changed_tests`/`changed_files`),不需要全量 diff;只有 `review_findings`(AI 审查)才需要看 diff 内容。所以分段时优先保证文件清单完整,diff 可按文件懒加载。避免短时间内重复请求 files/diff 接口。
|
||||
|
||||
---
|
||||
|
||||
## 5. CI 状态取不到(按 `unknown` 处理)
|
||||
|
||||
**症状**:评分卡 `CI status` 行显示 `unknown`,得分为权重的一半(默认 `10 → 5`)。
|
||||
|
||||
**原因**:`gitlink-cli ci +builds` 返回的是仓库的构建列表,并不保证能定位到**正好对应当前 PR 头部 commit** 的那次构建——仓库可能没配 CI、构建尚未触发、或无法把构建与该 PR 的 commit 关联。此时 CI 既非「通过」也非「失败」,而是**未知**。
|
||||
|
||||
**原因细节与评分映射**(见 `REFERENCE.md` 3.5):
|
||||
|
||||
| CI 情况 | ci_status 得分(权重 10 时) |
|
||||
|---------|:---:|
|
||||
| 找到对应构建且通过 | 10 |
|
||||
| 找到对应构建且失败 | 0 |
|
||||
| 取不到 / 无 CI / 无法关联 | 5(`round(10 * 0.5)`) |
|
||||
|
||||
**解决**:
|
||||
- 这是**预期的降级行为**,不是 bug:取不到就当 `unknown`,给一半分,**不要**当成失败而误触发 `require_ci_pass` 硬门禁。
|
||||
- 若希望 `unknown` 也拦截,可在策略里收紧(但要清楚这会拦下没配 CI 的仓库)。默认 `require_ci_pass: true` 的语义是「**CI 明确失败**才拦」,`unknown` 不触发硬门禁。
|
||||
|
||||
```bash
|
||||
# 排查:先看仓库到底有没有构建记录
|
||||
gitlink-cli ci +builds --limit 20 --format json
|
||||
```
|
||||
|
||||
> 注意:硬门禁 `require_ci_pass` 只在 CI **明确失败**时命中;`unknown` 不算失败、不触发硬门禁,仅按 0.5×权重计分。
|
||||
|
||||
---
|
||||
|
||||
## 6. 如何表达 REQUEST_CHANGES 裁决(为何用评论而非 rejected)
|
||||
|
||||
**症状**:纠结要不要用 GitLink review 的 `rejected` 状态把 gatekeeper 的 `REQUEST_CHANGES` 裁决"硬"标到 PR 上。
|
||||
|
||||
**原因 / 设计选择**:GitLink 原生 PR review 的 `status` 为 `common`/`approved`/`rejected`(`rejected` 即"请求修改"的等价)。但 `approved`/`rejected` 是**强语义的人工授意动作**。gatekeeper 是自动门禁,**刻意不替人按下 approved/rejected** —— 所有自动裁决(含 REQUEST_CHANGES)一律以**建议性的 `common` 评论**回写,把强语义留给维护者。
|
||||
|
||||
**解决**:用「评论 + 标签」组合表达裁决(这正是本作品复用 `label` 命令的原因):
|
||||
1. 评分卡以 `pr +comment`(或 `pr +review --status common`)回写,**标题里明确标注裁决**(`Verdict: ❌ REQUEST_CHANGES`),一眼可读;
|
||||
2. 同时打上状态标签 `gatekeeper:needs-changes`,让状态可被列表/过滤识别。
|
||||
|
||||
```bash
|
||||
# 评分卡回写(标题已含裁决,正文是评分卡)—— 与 workflow 脚本一致,走 pr +comment
|
||||
gitlink-cli pr +comment -i <id> -b "$(cat scorecard.md)"
|
||||
# 也可作为评审记录:gitlink-cli pr +review -i <id> --status common --content "$(cat scorecard.md)"
|
||||
```
|
||||
|
||||
> 注意:gatekeeper **绝不**自动发 `approved`/`rejected` —— 那是维护者的权限。裁决的「拦截」语义靠标题文字 + `gatekeeper:needs-changes` 标签承载。
|
||||
|
||||
---
|
||||
|
||||
## 7. `label` 不存在,需先 `create`
|
||||
|
||||
**症状**:打标签时提示标签不存在,或 `issue_tag_ids` 里传了一个查不到的 ID。
|
||||
|
||||
**原因**:`gatekeeper.yaml` 里配置的 `labels`(`gatekeeper:pass` / `gatekeeper:needs-changes` / `gatekeeper:review`)只是名字,仓库里**未必已经创建**对应的 label 实体。给 PR/Issue 挂标签时用的是 label 的**数字 ID**,名字对不上数据库里就没有,自然挂不上。
|
||||
|
||||
**解决**:先查后建——先用 `label +list` 看标签是否存在并拿到 ID,缺的用 `label +create` 创建(注意 `label` 组**只有** `+list/+create/+update/+delete`,**没有** `+view`):
|
||||
|
||||
```bash
|
||||
# 1. 查标签是否已存在,拿 id(--only-name true 只返回 id+name,便于解析)
|
||||
gitlink-cli label +list --keyword gatekeeper --only-name true --format json
|
||||
|
||||
# 2. 缺失则创建(color 为十六进制,缺省有内置默认色)
|
||||
gitlink-cli label +create -n "gatekeeper:needs-changes" -d "Gatekeeper requested changes" -c "#D73A4A" --format json
|
||||
gitlink-cli label +create -n "gatekeeper:pass" -d "Gatekeeper passed" -c "#0E8A16" --format json
|
||||
gitlink-cli label +create -n "gatekeeper:review" -d "Gatekeeper left comments" -c "#FBCA04" --format json
|
||||
|
||||
# 3. 把标签 id 挂到 PR 背后的 issue(用 PR 关联的 issue.id,非 PR 号;先取:)
|
||||
ISSUE_ID=$(gitlink-cli pr +view -i <pr> --format json | jq -r '.data.issue.id')
|
||||
# 更新时需带 done_ratio/subject/description,见 gitlink-shared
|
||||
gitlink-cli api POST /:owner/:repo/issues/$ISSUE_ID --body '{
|
||||
"issue_tag_ids": [<tag_id>],
|
||||
"done_ratio": 0,
|
||||
"subject": "<原始标题>",
|
||||
"description": "<原始描述>"
|
||||
}'
|
||||
# 也可用 gitlink-cli issue +update,它会自动保留 subject/description,免手动回传。
|
||||
```
|
||||
|
||||
> 注意:标签是**幂等创建**——重复 `+create` 同名标签前应先 `+list` 检查,避免产生重名标签。更新 issue 挂标签时务必带上当前 `subject`/`description`,否则可能清空描述(`gitlink-shared` 已警示)。
|
||||
|
||||
---
|
||||
|
||||
## 8. `auto_merge` 想生效但没合并(需三者同时满足)
|
||||
|
||||
**症状**:策略里写了 `auto_merge: true`,跑完却没有合并 PR。
|
||||
|
||||
**原因**:这是**故意的安全设计**,不是 bug。gatekeeper 绝不轻易合并,合并必须**三个条件同时成立**:
|
||||
|
||||
| 条件 | 来源 | 缺了会怎样 |
|
||||
|------|------|-----------|
|
||||
| `verdict == PASS` | 评分结果 | 非 PASS(COMMENT/REQUEST_CHANGES)一律不合并 |
|
||||
| 命令带 `--apply` | 运行参数 | 不带则全程 dry-run,只打印不写不合并 |
|
||||
| 策略 `auto_merge: true` | `gatekeeper.yaml` | 默认 `false`,不会合并 |
|
||||
|
||||
三者缺一不可。最常见的是**忘了 `--apply`**(默认 dry-run),或裁决其实不是 PASS。
|
||||
|
||||
**解决**:
|
||||
|
||||
```bash
|
||||
# 先 dry-run 看裁决是不是 PASS(默认就是 dry-run,不写任何东西)
|
||||
gitlink-gatekeeper --pr <id>
|
||||
|
||||
# 确认 PASS 且策略 auto_merge: true 后,显式带 --apply 才会合并
|
||||
gitlink-gatekeeper --pr <id> --apply
|
||||
|
||||
# 底层合并命令(CLI flag 是 --method,不是 --do,详见 #12)
|
||||
gitlink-cli pr +merge -i <id> --method squash --format json
|
||||
```
|
||||
|
||||
> 注意:`merge_method`(`merge`/`rebase`/`squash`)由策略 `behavior.merge_method` 决定,映射到 `pr +merge --method`。即便三条件都满足,合并前也应向用户复述「将以 `<method>` 合并 PR #<id>」。
|
||||
|
||||
---
|
||||
|
||||
## 9. 草稿(draft)PR
|
||||
|
||||
**症状**:对一个还是草稿状态的 PR 跑出了正式裁决;作者反馈「我还没写完」。
|
||||
|
||||
**原因**:草稿 PR 通常尚未完成(描述未补、测试未加、commit 未整理),此时打门禁裁决意义不大,且容易误伤——`gitlink-code-review` 也建议对 draft PR 先提示作者转 Ready for Review。
|
||||
|
||||
**解决**:采集 PR 元信息后检查草稿标志(`pr +view` 返回里的 draft / WIP 标记,或标题含 `WIP`/`[Draft]`),命中则**不出裁决**,只提示:
|
||||
|
||||
```bash
|
||||
gitlink-cli pr +view -i <id> --format json # 检查是否 draft / 标题含 WIP
|
||||
```
|
||||
|
||||
> 注意:可在 dry-run 下对 draft PR 生成「预览评分卡」帮作者自查,但**不回写评论、不打标签、不合并**,直到 PR 转为 Ready for Review。
|
||||
|
||||
---
|
||||
|
||||
## 10. 分页 / 大仓库(列表只看到一页)
|
||||
|
||||
**症状**:大仓库里 `pr +list` / `ci +builds` / `label +list` 只返回 20 条,漏掉了你要找的 PR、构建或标签。
|
||||
|
||||
**原因**:列表类接口默认分页,`limit` 默认 20、`page` 默认 1。返回 Envelope 的 `meta` 里有 `page`/`limit`/`total_count`,但**不会自动翻页**。
|
||||
|
||||
**解决**:按 `meta.total_count` 判断是否还有下一页,循环翻页直到取全:
|
||||
|
||||
```bash
|
||||
# 第一页,先看 meta.total_count
|
||||
gitlink-cli pr +list --state open --page 1 --limit 50 --format json
|
||||
# 还有更多则继续翻页
|
||||
gitlink-cli pr +list --state open --page 2 --limit 50 --format json
|
||||
|
||||
# 构建、标签同理
|
||||
gitlink-cli ci +builds --page 1 --limit 50 --format json
|
||||
gitlink-cli label +list --page 1 --limit 50 --format json
|
||||
```
|
||||
|
||||
> 注意:把 `limit` 适当调大(如 50)可减少请求轮次,但 files/diff 接口有频率限制,翻页时不要过于密集。评分只针对**单个 PR**,分页主要用于「先在列表里定位到目标 PR 号」这一步。
|
||||
|
||||
---
|
||||
|
||||
## 11. 用 `--state` 过滤 PR 不准
|
||||
|
||||
**症状**:`gitlink-cli pr +list --state open` 返回的列表里混进了已合并 / 已关闭的 PR。
|
||||
|
||||
**原因**:GitLink 的真实行为(`gitlink-shared` 已记录)——`--state` 参数**仅影响统计计数**,返回的列表可能包含所有状态。不能只信 `--state`。
|
||||
|
||||
**解决**:在客户端用每条 PR 的 `pull_request_status` 字段二次过滤:
|
||||
|
||||
| `pull_request_status` | 含义 |
|
||||
|:---:|------|
|
||||
| `0` | open |
|
||||
| `1` | merged |
|
||||
| `2` | closed |
|
||||
|
||||
gatekeeper 只对 `pull_request_status == 0`(open)的 PR 做裁决;对已 merged/closed 的应跳过并提示。
|
||||
|
||||
> 注意:这是平台行为,不是 CLI bug。任何依赖「PR 是否仍 open」的逻辑(如批量门禁扫描)都必须以 `pull_request_status` 为准,而非 `--state`。
|
||||
|
||||
---
|
||||
|
||||
## 12. `pr +merge` 的 flag 是 `--method`,不是 `--do`
|
||||
|
||||
**症状**:执行 `gitlink-cli pr +merge -i <id> --do squash` 报错「未知 flag `--do`」。
|
||||
|
||||
**原因**:`do` 是 GitLink 合并 API(`POST .../pulls/:id/pr_merge`)的**底层请求字段名**;而 `gitlink-cli` 暴露给用户的 **flag 名是 `--method`(短选项 `-m`)**,默认值 `merge`。两者不要混淆——文档/笔记里若看到 `--do` 是记错了。
|
||||
|
||||
**解决**:
|
||||
|
||||
```bash
|
||||
# 正确:用 --method / -m
|
||||
gitlink-cli pr +merge -i <id> --method squash --format json
|
||||
gitlink-cli pr +merge -i <id> -m rebase --format json
|
||||
|
||||
# 合并方式取值:merge | rebase | squash(缺省 merge)
|
||||
```
|
||||
|
||||
> 注意:gatekeeper 的 `behavior.merge_method` 直接对应 `--method`。合并仍受 #8 的三条件约束(PASS + `--apply` + `auto_merge: true`)。
|
||||
|
||||
---
|
||||
|
||||
## 还没解决?
|
||||
|
||||
1. 加 `--debug` 重跑,看原始请求/响应(`gitlink-cli ... --debug`)。
|
||||
2. 用 `--format json` 拿结构化输出,核对 `error.code` / `error.suggestion` 与本文对照。
|
||||
3. 回到 [`SKILL.md`](./SKILL.md) 重走工作流,确认每步命令与参数;字段/算法/阈值一律以 [`REFERENCE.md`](./REFERENCE.md)(SSOT)与 [`REFERENCE.md`](./REFERENCE.md) 为准。
|
||||
4. 始终遵守安全默认:默认 dry-run、绝不自动合并、写操作前复述意图、不回显 Token。
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
# 裁决记录 — COMMENT(总分介于两阈值之间,无硬门禁失败)
|
||||
|
||||
> 模拟 `gitlink-gatekeeper` 的完整执行记录:采集 → 逐维算分 → 硬门禁 → 裁决 → 渲染评分卡 → dry-run 回写命令。
|
||||
> 策略:默认 `gatekeeper.yaml`(权重 40/20/15/15/10,severity_penalty blocker=100/major=25/minor=5/nit=1,thresholds pass=85 / request_changes=60,max_changed_files=80)。
|
||||
> 命令均来自 `REFERENCE.md` 第 7 节 CLI 映射,已对照真实 shortcut 核验。
|
||||
|
||||
**场景**:`Gitlink/forgeplus` 仓库 PR #277,给配置加载器增加默认值合并能力,改动 2 个源码文件,附 1 个测试文件,CI 通过;AI 审查只发现若干 minor/nit(无 blocker、无 major)。总分落在 60–85 之间 → COMMENT(建议性意见,不阻塞、不批准)。
|
||||
|
||||
---
|
||||
|
||||
## ① 采集上下文
|
||||
|
||||
```bash
|
||||
gitlink-cli pr +view -i 277 --owner Gitlink --repo forgeplus --format json
|
||||
gitlink-cli pr +files -i 277 --owner Gitlink --repo forgeplus --format json
|
||||
gitlink-cli pr +diff -i 277 --owner Gitlink --repo forgeplus --format json
|
||||
gitlink-cli api GET /Gitlink/forgeplus/pulls/277/commits --format json
|
||||
gitlink-cli ci +builds --owner Gitlink --repo forgeplus --format json
|
||||
```
|
||||
|
||||
**采集结果摘要**
|
||||
|
||||
| 信号 | 值 |
|
||||
|------|----|
|
||||
| 标题 | `feat(config): merge defaults on load` |
|
||||
| 描述 | 非空,长度 52 字符(≥30),**无** `#<n>` 关联 |
|
||||
| `changed_files` | 2 |
|
||||
| `changed_src` | 2(`loader.go` / `merge.go`) |
|
||||
| `changed_tests` | 1(`merge_test.go`) |
|
||||
| commits | 3 条,符合 Conventional Commits 的 2 条 → conforming=2 |
|
||||
| CI | passing |
|
||||
| AI 审查发现 | 0 blocker / 0 major / 3 minor / 2 nit |
|
||||
|
||||
---
|
||||
|
||||
## ② 逐维算分(公式代入,可复算)
|
||||
|
||||
**review_findings(权重 40)** — §3.1
|
||||
```
|
||||
penalty = 3×5(minor) + 2×1(nit) = 17
|
||||
score = 40 × max(0, 1 − 17/40) = 40 × (23/40) = 40 × 0.575 = 23
|
||||
```
|
||||
|
||||
**test_coverage(权重 20)** — §3.2
|
||||
```
|
||||
changed_src=2 > 0, changed_tests=1 > 0
|
||||
ratio = min(1, 1/2) = 0.5
|
||||
score = round(20 × (0.5 + 0.5×0.5)) = round(20 × 0.75) = 15
|
||||
```
|
||||
|
||||
**pr_hygiene(权重 15)** — §3.3
|
||||
```
|
||||
描述≥30 ✓ (+1/3)
|
||||
关联 Issue ✗ (0)
|
||||
体量 changed_files=2 ≤ max_changed_files/2 = 40 ✓ (+1/3)
|
||||
命中比例 = 2/3
|
||||
score = round(15 × 2/3) = round(10.0) = 10
|
||||
```
|
||||
|
||||
**commit_quality(权重 15)** — §3.4
|
||||
```
|
||||
conforming/total = 2/3
|
||||
score = round(15 × 2/3) = round(10.0) = 10
|
||||
```
|
||||
|
||||
**ci_status(权重 10)** — §3.5
|
||||
```
|
||||
CI passing → score = 10
|
||||
```
|
||||
|
||||
**总分**
|
||||
```
|
||||
total = 23 + 15 + 10 + 10 + 10 = 68
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ③ 硬门禁判定(§4)
|
||||
|
||||
| 门禁 | 配置 | 命中? |
|
||||
|------|------|:------:|
|
||||
| `forbid_blocker_findings` | true | 否(0 blocker) |
|
||||
| `require_ci_pass` | true | 否(passing) |
|
||||
| `require_tests_for_src_changes` | true | 否(changed_src=2>0 且 changed_tests=1>0) |
|
||||
| `require_linked_issue` | false | —(未启用) |
|
||||
| `max_changed_files` | 80 | 否(2 ≤ 80) |
|
||||
|
||||
`hard_gate_failed = false`。
|
||||
|
||||
---
|
||||
|
||||
## ④ 裁决(§5)
|
||||
|
||||
```
|
||||
hard_gate_failed = false
|
||||
total = 68:not (≥ pass 85),not (< request_changes 60)
|
||||
→ 介于两阈值之间 → verdict = COMMENT
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⑤ 渲染的评分卡
|
||||
|
||||
```markdown
|
||||
## 🛡️ Gatekeeper Report — PR #277 feat(config): merge defaults on load
|
||||
|
||||
**Verdict: 💬 COMMENT** · Score: 68/100 · policy: gatekeeper.yaml@v1
|
||||
|
||||
| Dimension | Weight | Score | Notes |
|
||||
|-----------|:------:|:-----:|-------|
|
||||
| Review findings | 40 | 23/40 | 0 blocker / 0 major / 3 minor / 2 nit |
|
||||
| Test coverage | 20 | 15/20 | 2 src / 1 test files |
|
||||
| PR hygiene | 15 | 10/15 | desc ✓ / linked issue ✗ / size ✓ |
|
||||
| Commit quality | 15 | 10/15 | 2/3 conventional |
|
||||
| CI status | 10 | 10/10 | passing |
|
||||
|
||||
### 🟡 Should fix (3)
|
||||
- [minor] `mergeDefaults` 对 nil map 未保护,可能 panic — merge.go:33
|
||||
- [minor] 深拷贝缺失,合并后仍共享底层 slice — merge.go:58
|
||||
- [minor] 加载失败时未记录原始路径,定位困难 — loader.go:21
|
||||
|
||||
### 🔵 Nits (2)
|
||||
- [nit] 导出函数 `Merge` 缺少 doc 注释 — merge.go:18
|
||||
- [nit] 一条 commit 未用 `type(scope):` 前缀 — (commit)
|
||||
|
||||
### ✅ Strengths
|
||||
- 改动小而聚焦,附带了测试,CI 全绿
|
||||
- 无 blocker、无 major,整体方向正确
|
||||
|
||||
### Next steps
|
||||
1. 处理 3 条 minor(尤其 nil map 与共享 slice),并补充关联 Issue 编号
|
||||
2. 修复后总分有望跨过 pass 阈值(85)转为 PASS;当前为非阻塞的 COMMENT,维护者可酌情合并
|
||||
---
|
||||
*Generated by gitlink-gatekeeper · policy-as-code PR gate · re-run after changes*
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⑥ 将执行的回写命令(dry-run 展示)
|
||||
|
||||
> 默认 dry-run,不传 `--apply` 时仅打印。COMMENT 裁决不阻塞、不批准、不合并。
|
||||
|
||||
```bash
|
||||
# [dry-run] 1) 评分卡作为评论回写(与 workflow 脚本一致,走 pr +comment)
|
||||
gitlink-cli pr +comment -i 277 -b "$(cat scorecard.md)" --owner Gitlink --repo forgeplus
|
||||
# 也可作为评审记录:gitlink-cli pr +review -i 277 --status common --content "$(cat scorecard.md)"
|
||||
|
||||
# [dry-run] 2) 打 review 标签(labels.comment = "gatekeeper:review")
|
||||
gitlink-cli label +create -n "gatekeeper:review" -c "#0969DA" \
|
||||
-d "Gatekeeper verdict: COMMENT" --owner Gitlink --repo forgeplus
|
||||
# 标签挂到 PR 背后的 issue.id(非 PR 号):
|
||||
ISSUE_ID=$(gitlink-cli pr +view -i 277 --owner Gitlink --repo forgeplus --format json | jq -r '.data.issue.id')
|
||||
gitlink-cli api POST /Gitlink/forgeplus/issues/$ISSUE_ID --body '{
|
||||
"issue_tag_ids": [<gatekeeper:review 的 tag_id>],
|
||||
"done_ratio": 0,
|
||||
"subject": "<PR 原标题>",
|
||||
"description": "<PR 原描述>"
|
||||
}'
|
||||
|
||||
# COMMENT 不合并、不创建 tracking issue(仅 REQUEST_CHANGES 才善后建 issue)。
|
||||
```
|
||||
|
||||
> 实际写回需追加 `--apply`,Agent 会先复述「将回写 1 条评论 + 打 gatekeeper:review 标签,不合并」并等待确认。
|
||||
|
|
@ -0,0 +1,178 @@
|
|||
# 裁决记录 — PASS
|
||||
|
||||
> 模拟 `gitlink-gatekeeper` 在一个真实风格 PR 上的完整执行记录:采集 → 逐维算分 → 硬门禁 → 裁决 → 渲染评分卡 → dry-run 回写命令。
|
||||
> 策略:默认 `gatekeeper.yaml`(权重 40/20/15/15/10,severity_penalty blocker=100/major=25/minor=5/nit=1,thresholds pass=85 / request_changes=60,max_changed_files=80)。
|
||||
> 命令均来自 `REFERENCE.md` 第 7 节 CLI 映射,已对照 `gitlink-cli pr/ci/label` 真实 shortcut 核验。
|
||||
|
||||
**场景**:`Gitlink/forgeplus` 仓库 PR #214,给搜索模块增加分页参数校验,附带单元测试,CI 全绿。
|
||||
|
||||
---
|
||||
|
||||
## ① 采集上下文
|
||||
|
||||
```bash
|
||||
# PR 元信息(标题/描述/作者/关联)
|
||||
gitlink-cli pr +view -i 214 --owner Gitlink --repo forgeplus --format json
|
||||
|
||||
# 变更文件列表
|
||||
gitlink-cli pr +files -i 214 --owner Gitlink --repo forgeplus --format json
|
||||
|
||||
# diff(供 AI 审查)
|
||||
gitlink-cli pr +diff -i 214 --owner Gitlink --repo forgeplus --format json
|
||||
|
||||
# commits
|
||||
gitlink-cli api GET /Gitlink/forgeplus/pulls/214/commits --format json
|
||||
|
||||
# CI 状态
|
||||
gitlink-cli ci +builds --owner Gitlink --repo forgeplus --format json
|
||||
```
|
||||
|
||||
**采集结果摘要**
|
||||
|
||||
| 信号 | 值 |
|
||||
|------|----|
|
||||
| 标题 | `feat(search): validate pagination params` |
|
||||
| 描述 | 非空,长度 142 字符(≥30),且含 `Closes #198` |
|
||||
| `changed_files` | 5 |
|
||||
| `changed_src`(匹配 `source_globs`) | 3(`search.go` / `paginate.go` / `params.go`) |
|
||||
| `changed_tests`(匹配 `test_globs`) | 2(`paginate_test.go` / `params_test.go`) |
|
||||
| commits | 4 条,全部 `type(scope): subject` 形式 → conforming=4 |
|
||||
| CI | passing |
|
||||
| AI 审查发现 | 0 blocker / 0 major / 1 minor / 2 nit |
|
||||
|
||||
---
|
||||
|
||||
## ② 逐维算分(公式代入,可复算)
|
||||
|
||||
**review_findings(权重 40)** — `REFERENCE.md` §3.1
|
||||
```
|
||||
penalty = 1×5(minor) + 2×1(nit) = 7
|
||||
score = 40 × max(0, 1 − 7/40) = 40 × (33/40) = 40 × 0.825 = 33
|
||||
```
|
||||
|
||||
**test_coverage(权重 20)** — §3.2
|
||||
```
|
||||
changed_src=3 > 0, changed_tests=2 > 0
|
||||
ratio = min(1, 2/3) = 0.6667
|
||||
score = round(20 × (0.5 + 0.5×0.6667)) = round(20 × 0.8333) = round(16.667) = 17
|
||||
```
|
||||
|
||||
**pr_hygiene(权重 15)** — §3.3
|
||||
```
|
||||
描述≥30 ✓ (+1/3)
|
||||
关联 Issue(body 含 #198) ✓ (+1/3)
|
||||
体量 changed_files=5 ≤ max_changed_files/2 = 40 ✓ (+1/3)
|
||||
命中比例 = 3/3 = 1
|
||||
score = round(15 × 1) = 15
|
||||
```
|
||||
|
||||
**commit_quality(权重 15)** — §3.4
|
||||
```
|
||||
conforming/total = 4/4
|
||||
score = round(15 × 4/4) = 15
|
||||
```
|
||||
|
||||
**ci_status(权重 10)** — §3.5
|
||||
```
|
||||
CI passing → score = 10
|
||||
```
|
||||
|
||||
**总分**
|
||||
```
|
||||
total = 33 + 17 + 15 + 15 + 10 = 90
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ③ 硬门禁判定(§4)
|
||||
|
||||
| 门禁 | 配置 | 命中? |
|
||||
|------|------|:------:|
|
||||
| `forbid_blocker_findings` | true | 否(0 blocker) |
|
||||
| `require_ci_pass` | true | 否(passing) |
|
||||
| `require_tests_for_src_changes` | true | 否(changed_src=3>0 但 changed_tests=2>0) |
|
||||
| `require_linked_issue` | false | —(未启用) |
|
||||
| `max_changed_files` | 80 | 否(5 ≤ 80) |
|
||||
|
||||
`hard_gate_failed = false`。
|
||||
|
||||
---
|
||||
|
||||
## ④ 裁决(§5)
|
||||
|
||||
```
|
||||
hard_gate_failed = false
|
||||
total = 90 ≥ thresholds.pass(85)
|
||||
→ verdict = PASS
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⑤ 渲染的评分卡
|
||||
|
||||
```markdown
|
||||
## 🛡️ Gatekeeper Report — PR #214 feat(search): validate pagination params
|
||||
|
||||
**Verdict: ✅ PASS** · Score: 90/100 · policy: gatekeeper.yaml@v1
|
||||
|
||||
| Dimension | Weight | Score | Notes |
|
||||
|-----------|:------:|:-----:|-------|
|
||||
| Review findings | 40 | 33/40 | 0 blocker / 0 major / 1 minor / 2 nit |
|
||||
| Test coverage | 20 | 17/20 | 3 src / 2 test files |
|
||||
| PR hygiene | 15 | 15/15 | desc ✓ / linked issue #198 ✓ / size ✓ |
|
||||
| Commit quality | 15 | 15/15 | 4/4 conventional |
|
||||
| CI status | 10 | 10/10 | passing |
|
||||
|
||||
### 🟡 Should fix (1)
|
||||
- [minor] `validatePage` 对负数与零分两个分支处理,可合并 — paginate.go:41
|
||||
|
||||
### 🔵 Nits (2)
|
||||
- [nit] 导出函数 `ValidateParams` 缺少 doc 注释 — params.go:18
|
||||
- [nit] 测试用例命名建议统一为 `TestXxx_Case` — params_test.go:7
|
||||
|
||||
### ✅ Strengths
|
||||
- 源码改动均有对应测试,覆盖良好
|
||||
- commit 全部符合 Conventional Commits,关联了 Issue #198
|
||||
- CI 全绿
|
||||
|
||||
### Next steps
|
||||
1. 评分达标且无硬门禁失败,可合并;如需自动合并请在策略中开启 `auto_merge` 并显式带 `--apply`
|
||||
2. 两个 nit 可在后续提交一并处理,不阻塞合并
|
||||
---
|
||||
*Generated by gitlink-gatekeeper · policy-as-code PR gate · re-run after changes*
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⑥ 将执行的回写命令(dry-run 展示)
|
||||
|
||||
> 默认 `behavior.dry_run_default: true` → 不传 `--apply` 时只打印以下命令,不真正回写。
|
||||
> `behavior.auto_merge: false` → 即便裁决 PASS 也**不会**触发合并。
|
||||
|
||||
```bash
|
||||
# [dry-run] 1) 把评分卡作为评论回写到 PR(与 workflow 脚本一致,走 pr +comment)
|
||||
gitlink-cli pr +comment -i 214 -b "$(cat scorecard.md)" --owner Gitlink --repo forgeplus
|
||||
# 也可作为评审记录回写:gitlink-cli pr +review -i 214 --status common --content "$(cat scorecard.md)"
|
||||
# (GitLink review status 为 common/approved/rejected;gatekeeper 自动裁决统一用建议性的 common,
|
||||
# 强语义 approved/rejected 留给人工授意。)
|
||||
|
||||
# [dry-run] 2) 打 PASS 标签(labels.pass = "gatekeeper:pass")
|
||||
# 2a. 确保标签存在(首次需创建;已存在则跳过)
|
||||
gitlink-cli label +create -n "gatekeeper:pass" -c "#2EA043" \
|
||||
-d "Gatekeeper verdict: PASS" --owner Gitlink --repo forgeplus
|
||||
# 2b. 标签挂到 PR 背后的 issue(GitLink PR 复用 issue 标签体系)。
|
||||
# 注意:用 PR 关联的 issue.id(非 PR 号),先从 pr +view 取:
|
||||
ISSUE_ID=$(gitlink-cli pr +view -i 214 --owner Gitlink --repo forgeplus --format json | jq -r '.data.issue.id')
|
||||
gitlink-cli api POST /Gitlink/forgeplus/issues/$ISSUE_ID --body '{
|
||||
"issue_tag_ids": [<gatekeeper:pass 的 tag_id>],
|
||||
"done_ratio": 0,
|
||||
"subject": "<PR 原标题>",
|
||||
"description": "<PR 原描述>"
|
||||
}'
|
||||
# 也可用 gitlink-cli issue +update,它会自动保留 subject/description,免手动回传。
|
||||
|
||||
# 合并默认禁用(auto_merge=false)。仅当策略 auto_merge=true 且裁决=PASS 且显式 --apply 时才会执行:
|
||||
# gitlink-cli pr +merge -i 214 --method squash --owner Gitlink --repo forgeplus
|
||||
```
|
||||
|
||||
> 实际写回需追加 `--apply`,且 Agent 会先向用户复述「将回写 1 条评论 + 打 1 个 gatekeeper:pass 标签,不合并」并等待确认。
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
# 裁决记录 — REQUEST_CHANGES(硬门禁失败 + 1 个 major)
|
||||
|
||||
> 模拟 `gitlink-gatekeeper` 的完整执行记录:采集 → 逐维算分 → 硬门禁 → 裁决 → 渲染评分卡 → dry-run 回写命令。
|
||||
> 策略:默认 `gatekeeper.yaml`(权重 40/20/15/15/10,severity_penalty blocker=100/major=25/minor=5/nit=1,thresholds pass=85 / request_changes=60,max_changed_files=80)。
|
||||
> 命令均来自 `REFERENCE.md` 第 7 节 CLI 映射,已对照真实 shortcut 核验。
|
||||
|
||||
**场景**:`Gitlink/forgeplus` 仓库 PR #305,重构订单结算逻辑,改动了 4 个源码文件但**未附带任何测试**,其中一处折扣计算有边界缺陷(major)。CI 通过。
|
||||
|
||||
---
|
||||
|
||||
## ① 采集上下文
|
||||
|
||||
```bash
|
||||
gitlink-cli pr +view -i 305 --owner Gitlink --repo forgeplus --format json
|
||||
gitlink-cli pr +files -i 305 --owner Gitlink --repo forgeplus --format json
|
||||
gitlink-cli pr +diff -i 305 --owner Gitlink --repo forgeplus --format json
|
||||
gitlink-cli api GET /Gitlink/forgeplus/pulls/305/commits --format json
|
||||
gitlink-cli ci +builds --owner Gitlink --repo forgeplus --format json
|
||||
```
|
||||
|
||||
**采集结果摘要**
|
||||
|
||||
| 信号 | 值 |
|
||||
|------|----|
|
||||
| 标题 | `refactor(billing): rework settlement pipeline` |
|
||||
| 描述 | 非空,长度 88 字符(≥30),**无** `#<n>` 关联 |
|
||||
| `changed_files` | 4 |
|
||||
| `changed_src` | 4(`settlement.go` / `discount.go` / `invoice.go` / `tax.go`) |
|
||||
| `changed_tests` | 0 |
|
||||
| commits | 3 条,符合 Conventional Commits 的 2 条(1 条为 `wip: fix`)→ conforming=2 |
|
||||
| CI | passing |
|
||||
| AI 审查发现 | 0 blocker / 1 major / 1 minor / 2 nit |
|
||||
|
||||
---
|
||||
|
||||
## ② 逐维算分(公式代入,可复算)
|
||||
|
||||
**review_findings(权重 40)** — §3.1
|
||||
```
|
||||
penalty = 25(major) + 1×5(minor) + 2×1(nit) = 32
|
||||
score = 40 × max(0, 1 − 32/40) = 40 × (1 − 0.8) = 40 × 0.2 = 8
|
||||
```
|
||||
|
||||
**test_coverage(权重 20)** — §3.2
|
||||
```
|
||||
changed_src=4 > 0, changed_tests=0
|
||||
→ score = 0
|
||||
```
|
||||
|
||||
**pr_hygiene(权重 15)** — §3.3
|
||||
```
|
||||
描述≥30 ✓ (+1/3)
|
||||
关联 Issue ✗ (0)
|
||||
体量 changed_files=4 ≤ max_changed_files/2 = 40 ✓ (+1/3)
|
||||
命中比例 = 2/3
|
||||
score = round(15 × 2/3) = round(10.0) = 10
|
||||
```
|
||||
|
||||
**commit_quality(权重 15)** — §3.4
|
||||
```
|
||||
conforming/total = 2/3
|
||||
score = round(15 × 2/3) = round(10.0) = 10
|
||||
```
|
||||
|
||||
**ci_status(权重 10)** — §3.5
|
||||
```
|
||||
CI passing → score = 10
|
||||
```
|
||||
|
||||
**总分**
|
||||
```
|
||||
total = 8 + 0 + 10 + 10 + 10 = 38
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ③ 硬门禁判定(§4)
|
||||
|
||||
| 门禁 | 配置 | 命中? |
|
||||
|------|------|:------:|
|
||||
| `forbid_blocker_findings` | true | 否(0 blocker) |
|
||||
| `require_ci_pass` | true | 否(passing) |
|
||||
| `require_tests_for_src_changes` | true | **是**(changed_src=4>0 且 changed_tests=0) |
|
||||
| `require_linked_issue` | false | —(未启用) |
|
||||
| `max_changed_files` | 80 | 否(4 ≤ 80) |
|
||||
|
||||
`hard_gate_failed = true`。
|
||||
|
||||
---
|
||||
|
||||
## ④ 裁决(§5)
|
||||
|
||||
```
|
||||
hard_gate_failed = true
|
||||
→ verdict = REQUEST_CHANGES (无视总分;本例 total=38 < request_changes(60) 也指向 REQUEST_CHANGES,结论一致)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⑤ 渲染的评分卡
|
||||
|
||||
```markdown
|
||||
## 🛡️ Gatekeeper Report — PR #305 refactor(billing): rework settlement pipeline
|
||||
|
||||
**Verdict: ❌ REQUEST_CHANGES** · Score: 38/100 · policy: gatekeeper.yaml@v1
|
||||
|
||||
| Dimension | Weight | Score | Notes |
|
||||
|-----------|:------:|:-----:|-------|
|
||||
| Review findings | 40 | 8/40 | 0 blocker / 1 major / 1 minor / 2 nit |
|
||||
| Test coverage | 20 | 0/20 | 4 src / 0 test files |
|
||||
| PR hygiene | 15 | 10/15 | desc ✓ / linked issue ✗ / size ✓ |
|
||||
| Commit quality | 15 | 10/15 | 2/3 conventional |
|
||||
| CI status | 10 | 10/10 | passing |
|
||||
|
||||
### ⛔ Hard gate failures (1)
|
||||
- `require_tests_for_src_changes`: 改动了 4 个源码文件,但本 PR 未包含任何测试文件
|
||||
|
||||
### 🔴 Must fix (1)
|
||||
- [major] 折扣金额未做下限保护,满减叠加时可算出负数总价 — discount.go:72
|
||||
|
||||
### 🟡 Should fix (1)
|
||||
- [minor] `settle()` 吞掉了 tax 计算的 error,应向上传播 — settlement.go:118
|
||||
|
||||
### 🔵 Nits (2)
|
||||
- [nit] commit `wip: fix` 不符合 Conventional Commits,建议 rebase 整理 — (commit)
|
||||
- [nit] 导出结构体 `Invoice` 字段缺少注释 — invoice.go:14
|
||||
|
||||
### ✅ Strengths
|
||||
- 改动聚焦、体量适中(4 文件),CI 全绿
|
||||
|
||||
### Next steps
|
||||
1. **补测试**(最高优先级):为 4 个改动源码文件补单元测试,解除 `require_tests_for_src_changes` 硬门禁
|
||||
2. 修复折扣下限的 major 缺陷,建议补一条针对负数总价的回归测试
|
||||
3. 整理 `wip: fix` commit、补充 error 传播后,重新触发 gatekeeper
|
||||
---
|
||||
*Generated by gitlink-gatekeeper · policy-as-code PR gate · re-run after changes*
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⑥ 将执行的回写命令(dry-run 展示)
|
||||
|
||||
> 默认 dry-run,不传 `--apply` 时仅打印。REQUEST_CHANGES 永不涉及合并。
|
||||
|
||||
```bash
|
||||
# [dry-run] 1) 评分卡作为评论回写(与 workflow 脚本一致,走 pr +comment)
|
||||
gitlink-cli pr +comment -i 305 -b "$(cat scorecard.md)" --owner Gitlink --repo forgeplus
|
||||
# 裁决=REQUEST_CHANGES 不直接用平台的强语义 rejected(留给人工);裁决靠评分卡标题 + 标签表达
|
||||
|
||||
# [dry-run] 2) 打 needs-changes 标签(labels.request_changes = "gatekeeper:needs-changes")
|
||||
gitlink-cli label +create -n "gatekeeper:needs-changes" -c "#D1242F" \
|
||||
-d "Gatekeeper verdict: REQUEST_CHANGES" --owner Gitlink --repo forgeplus
|
||||
# 标签挂到 PR 背后的 issue.id(非 PR 号):
|
||||
ISSUE_ID=$(gitlink-cli pr +view -i 305 --owner Gitlink --repo forgeplus --format json | jq -r '.data.issue.id')
|
||||
gitlink-cli api POST /Gitlink/forgeplus/issues/$ISSUE_ID --body '{
|
||||
"issue_tag_ids": [<gatekeeper:needs-changes 的 tag_id>],
|
||||
"done_ratio": 0,
|
||||
"subject": "<PR 原标题>",
|
||||
"description": "<PR 原描述>"
|
||||
}'
|
||||
|
||||
# [dry-run] 3)(子题三善后)创建 tracking issue 汇总必修项并关联 PR #305
|
||||
# 注意:用 $'...' 让 \n 成为真实换行(双引号里的字面 \n 不会换行)
|
||||
gitlink-cli issue +create --owner Gitlink --repo forgeplus \
|
||||
-t "[gatekeeper] PR #305 必修项跟踪" \
|
||||
-b $'由 gatekeeper 裁决 REQUEST_CHANGES 触发。\n硬门禁: require_tests_for_src_changes。\nMust fix: discount.go:72 折扣下限。\n关联 PR: #305'
|
||||
```
|
||||
|
||||
> 实际写回需追加 `--apply`,Agent 会先复述「将回写 1 条评论 + 打 gatekeeper:needs-changes 标签 + 建 1 条 tracking issue,不合并」并等待确认。
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
# gatekeeper.lenient.yaml — 宽松预设(低门槛,适合实验仓 / 早期项目 / 鼓励贡献的社区)
|
||||
|
||||
version: 1 # 策略 schema 版本,整数,当前为 1
|
||||
|
||||
# —— 加权评分维度,5 项权重之和必须 = 100 ——
|
||||
weights:
|
||||
review_findings: 40 # AI 代码审查发现(按严重度扣分)
|
||||
test_coverage: 20 # 改动的源码是否伴随测试
|
||||
pr_hygiene: 15 # PR 描述 / 关联 Issue / 体量
|
||||
commit_quality: 15 # commit 是否符合 Conventional Commits
|
||||
ci_status: 10 # CI 是否通过
|
||||
|
||||
# —— 硬门禁:只保留安全底线(blocker / CI),放开测试与 Issue 要求 ——
|
||||
hard_gates:
|
||||
forbid_blocker_findings: true # 出现 blocker 级发现即拦截(安全底线,保留)
|
||||
require_ci_pass: true # CI 未通过即拦截(安全底线,保留)
|
||||
require_tests_for_src_changes: false # 宽松:改源码不强制配测试
|
||||
require_linked_issue: false # 宽松:不强制关联 Issue
|
||||
max_changed_files: 200 # 宽松:放宽到 200 个文件
|
||||
|
||||
# —— 严重度 → 扣分(用于 review_findings 维度)——
|
||||
severity_penalty:
|
||||
blocker: 100 # 单条即清零本维度,且触发硬门禁
|
||||
major: 25
|
||||
minor: 5
|
||||
nit: 1
|
||||
|
||||
# —— 最终 0-100 总分 → 裁决阈值(整体下移,更容易 PASS)——
|
||||
thresholds:
|
||||
pass: 70 # 总分 ≥ 70 且无硬门禁失败 → PASS
|
||||
request_changes: 45 # 总分 < 45 → REQUEST_CHANGES
|
||||
# 介于两者之间 → COMMENT
|
||||
|
||||
# —— 各裁决对应回写的标签(依赖 gitlink-cli label)——
|
||||
labels:
|
||||
pass: "gatekeeper:pass"
|
||||
request_changes: "gatekeeper:needs-changes"
|
||||
comment: "gatekeeper:review"
|
||||
|
||||
# —— 源码判定:用于 test_coverage / require_tests_for_src_changes ——
|
||||
source_globs: ["**/*.go", "**/*.py", "**/*.js", "**/*.ts", "**/*.rs", "**/*.java"]
|
||||
test_globs: ["**/*_test.go", "**/test_*.py", "**/*.test.*", "**/*.spec.*", "tests/**"]
|
||||
|
||||
# —— 行为开关 ——
|
||||
behavior:
|
||||
dry_run_default: true # 默认只预览,不写任何东西
|
||||
post_comment: true # 把评分卡作为评论回写 PR
|
||||
apply_label: true # 按裁决打标签
|
||||
auto_merge: false # 仅当 true 且裁决=PASS 且显式 --apply 时才合并
|
||||
merge_method: squash # merge | rebase | squash
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
# gatekeeper.strict.yaml — 严格预设(高合并门槛,适合核心库 / 稳定分支 / 严格 review 团队)
|
||||
|
||||
version: 1 # 策略 schema 版本,整数,当前为 1
|
||||
|
||||
# —— 加权评分维度,5 项权重之和必须 = 100 ——
|
||||
weights:
|
||||
review_findings: 40 # AI 代码审查发现(按严重度扣分)
|
||||
test_coverage: 20 # 改动的源码是否伴随测试
|
||||
pr_hygiene: 15 # PR 描述 / 关联 Issue / 体量
|
||||
commit_quality: 15 # commit 是否符合 Conventional Commits
|
||||
ci_status: 10 # CI 是否通过
|
||||
|
||||
# —— 硬门禁:全部开启,命中任一 → 直接 REQUEST_CHANGES(无视总分)——
|
||||
hard_gates:
|
||||
forbid_blocker_findings: true # 出现 blocker 级发现即拦截
|
||||
require_ci_pass: true # CI 未通过即拦截
|
||||
require_tests_for_src_changes: true # 改了源码却没加测试即拦截
|
||||
require_linked_issue: true # 严格:PR 必须关联 Issue
|
||||
max_changed_files: 40 # 严格:PR 体量收紧到 40 个文件
|
||||
|
||||
# —— 严重度 → 扣分(用于 review_findings 维度)——
|
||||
severity_penalty:
|
||||
blocker: 100 # 单条即清零本维度,且触发硬门禁
|
||||
major: 25
|
||||
minor: 5
|
||||
nit: 1
|
||||
|
||||
# —— 最终 0-100 总分 → 裁决阈值(整体上移,更难 PASS、更易拦截)——
|
||||
thresholds:
|
||||
pass: 92 # 总分 ≥ 92 且无硬门禁失败 → PASS
|
||||
request_changes: 75 # 总分 < 75 → REQUEST_CHANGES
|
||||
# 介于两者之间 → COMMENT
|
||||
|
||||
# —— 各裁决对应回写的标签(依赖 gitlink-cli label)——
|
||||
labels:
|
||||
pass: "gatekeeper:pass"
|
||||
request_changes: "gatekeeper:needs-changes"
|
||||
comment: "gatekeeper:review"
|
||||
|
||||
# —— 源码判定:用于 test_coverage / require_tests_for_src_changes ——
|
||||
source_globs: ["**/*.go", "**/*.py", "**/*.js", "**/*.ts", "**/*.rs", "**/*.java"]
|
||||
test_globs: ["**/*_test.go", "**/test_*.py", "**/*.test.*", "**/*.spec.*", "tests/**"]
|
||||
|
||||
# —— 行为开关 ——
|
||||
behavior:
|
||||
dry_run_default: true # 默认只预览,不写任何东西
|
||||
post_comment: true # 把评分卡作为评论回写 PR
|
||||
apply_label: true # 按裁决打标签
|
||||
auto_merge: false # 仅当 true 且裁决=PASS 且显式 --apply 时才合并
|
||||
merge_method: squash # merge | rebase | squash
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
# gatekeeper.yaml — 默认策略(均衡基线,等同 SSOT 第2节内置默认策略)
|
||||
|
||||
version: 1 # 策略 schema 版本,整数,当前为 1
|
||||
|
||||
# —— 加权评分维度,5 项权重之和必须 = 100 ——
|
||||
weights:
|
||||
review_findings: 40 # AI 代码审查发现(按严重度扣分)
|
||||
test_coverage: 20 # 改动的源码是否伴随测试
|
||||
pr_hygiene: 15 # PR 描述 / 关联 Issue / 体量
|
||||
commit_quality: 15 # commit 是否符合 Conventional Commits
|
||||
ci_status: 10 # CI 是否通过
|
||||
|
||||
# —— 硬门禁:任一命中 → 直接 REQUEST_CHANGES(无视总分)——
|
||||
hard_gates:
|
||||
forbid_blocker_findings: true # 出现 blocker 级发现即拦截
|
||||
require_ci_pass: true # CI 未通过即拦截
|
||||
require_tests_for_src_changes: true # 改了源码却没加测试即拦截
|
||||
require_linked_issue: false # PR 是否必须关联 Issue
|
||||
max_changed_files: 80 # 超过该改动文件数即拦截(0 表示不限)
|
||||
|
||||
# —— 严重度 → 扣分(用于 review_findings 维度)——
|
||||
severity_penalty:
|
||||
blocker: 100 # 单条即清零本维度,且触发硬门禁
|
||||
major: 25
|
||||
minor: 5
|
||||
nit: 1
|
||||
|
||||
# —— 最终 0-100 总分 → 裁决阈值 ——
|
||||
thresholds:
|
||||
pass: 85 # 总分 ≥ pass 且无硬门禁失败 → PASS
|
||||
request_changes: 60 # 总分 < request_changes → REQUEST_CHANGES
|
||||
# 介于两者之间 → COMMENT
|
||||
|
||||
# —— 各裁决对应回写的标签(依赖 gitlink-cli label)——
|
||||
labels:
|
||||
pass: "gatekeeper:pass"
|
||||
request_changes: "gatekeeper:needs-changes"
|
||||
comment: "gatekeeper:review"
|
||||
|
||||
# —— 源码判定:用于 test_coverage / require_tests_for_src_changes ——
|
||||
source_globs: ["**/*.go", "**/*.py", "**/*.js", "**/*.ts", "**/*.rs", "**/*.java"]
|
||||
test_globs: ["**/*_test.go", "**/test_*.py", "**/*.test.*", "**/*.spec.*", "tests/**"]
|
||||
|
||||
# —— 行为开关 ——
|
||||
behavior:
|
||||
dry_run_default: true # 默认只预览,不写任何东西
|
||||
post_comment: true # 把评分卡作为评论回写 PR
|
||||
apply_label: true # 按裁决打标签
|
||||
auto_merge: false # 仅当 true 且裁决=PASS 且显式 --apply 时才合并
|
||||
merge_method: squash # merge | rebase | squash
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
# 评分卡样例(scorecard-sample)
|
||||
|
||||
> 这是 `gitlink-gatekeeper` 渲染到 PR 评论的 **评分卡** 的样例输出,严格套用 [`REFERENCE.md`](../REFERENCE.md) 第 6 节模板。
|
||||
> 本样例对应一个 **REQUEST_CHANGES**(含硬门禁失败)案例,用于展示模板的全部分区(硬门禁、Must fix、Should fix、Nits、Strengths、Next steps 同时出现)。
|
||||
> 数值来自默认策略 `gatekeeper.yaml`(权重 40/20/15/15/10,blocker=100/major=25/minor=5/nit=1,pass=85、request_changes=60),可按第 3–5 节算法逐项复算。
|
||||
|
||||
---
|
||||
|
||||
## 输入快照(用于复算)
|
||||
|
||||
| 信号 | 值 |
|
||||
|------|----|
|
||||
| 变更文件总数 `changed_files` | 6 |
|
||||
| 匹配 `source_globs` 的源码文件 `changed_src` | 4 |
|
||||
| 匹配 `test_globs` 的测试文件 `changed_tests` | 0 |
|
||||
| AI 审查发现 | 0 blocker / 1 major / 2 minor / 1 nit |
|
||||
| PR 描述 | 非空,长度 64 字符(≥30) |
|
||||
| 关联 Issue | 否(body 无 `#<n>`) |
|
||||
| commits | 共 4 条,符合 Conventional Commits 的 3 条 |
|
||||
| CI 状态 | passing |
|
||||
|
||||
## 逐维复算
|
||||
|
||||
- **review_findings(权重 40)**:`penalty = 25(major) + 2×5(minor) + 1(nit) = 36`;
|
||||
`score = 40 × max(0, 1 − 36/40) = 40 × 0.1 = 4`
|
||||
- **test_coverage(权重 20)**:`changed_src=4 > 0` 且 `changed_tests=0` → `score = 0`
|
||||
- **pr_hygiene(权重 15)**:描述≥30 ✓(+1/3);关联 Issue ✗(0);体量 `changed_files=6 ≤ max_changed_files/2 = 40` ✓(+1/3) → 命中比例 `2/3`;
|
||||
`score = round(15 × 2/3) = round(10.0) = 10`
|
||||
- **commit_quality(权重 15)**:`conforming/total = 3/4`;`score = round(15 × 3/4) = round(11.25) = 11`
|
||||
- **ci_status(权重 10)**:passing → `score = 10`
|
||||
- **总分**:`4 + 0 + 10 + 11 + 10 = 35`
|
||||
|
||||
## 硬门禁判定
|
||||
|
||||
| 门禁 | 配置 | 命中? |
|
||||
|------|------|:------:|
|
||||
| `forbid_blocker_findings` | true | 否(无 blocker) |
|
||||
| `require_ci_pass` | true | 否(CI passing) |
|
||||
| `require_tests_for_src_changes` | true | **是**(changed_src=4>0 且 changed_tests=0) |
|
||||
| `require_linked_issue` | false | —(未启用) |
|
||||
| `max_changed_files` | 80 | 否(6 ≤ 80) |
|
||||
|
||||
`hard_gate_failed = true` → 裁决直接 **REQUEST_CHANGES**(无视总分;本例总分 35 < 60 亦指向 REQUEST_CHANGES,结论一致)。
|
||||
|
||||
---
|
||||
|
||||
## 渲染的评分卡(回写到 PR 的正文)
|
||||
|
||||
```markdown
|
||||
## 🛡️ Gatekeeper Report — PR #128 feat(auth): add refresh-token rotation
|
||||
|
||||
**Verdict: ❌ REQUEST_CHANGES** · Score: 35/100 · policy: gatekeeper.yaml@v1
|
||||
|
||||
| Dimension | Weight | Score | Notes |
|
||||
|-----------|:------:|:-----:|-------|
|
||||
| Review findings | 40 | 4/40 | 0 blocker / 1 major / 2 minor / 1 nit |
|
||||
| Test coverage | 20 | 0/20 | 4 src / 0 test files |
|
||||
| PR hygiene | 15 | 10/15 | desc ✓ / linked issue ✗ / size ✓ |
|
||||
| Commit quality | 15 | 11/15 | 3/4 conventional |
|
||||
| CI status | 10 | 10/10 | passing |
|
||||
|
||||
### ⛔ Hard gate failures (1)
|
||||
- `require_tests_for_src_changes`: 改动了 4 个源码文件,但本 PR 未包含任何测试文件
|
||||
|
||||
### 🔴 Must fix (1)
|
||||
- [major] 刷新令牌轮换未对旧令牌做失效处理,存在重放风险 — internal/auth/refresh.go:88
|
||||
|
||||
### 🟡 Should fix (2)
|
||||
- [minor] `rotateToken` 缺少超时上下文,可能阻塞请求 — internal/auth/refresh.go:54
|
||||
- [minor] 错误信息直接回显内部字段名,建议脱敏 — internal/auth/handler.go:131
|
||||
|
||||
### 🔵 Nits (1)
|
||||
- [nit] 导出函数 `NewRotator` 缺少 doc 注释 — internal/auth/refresh.go:22
|
||||
|
||||
### ✅ Strengths
|
||||
- commit 历史清晰,3/4 符合 Conventional Commits
|
||||
- CI 全绿,PR 描述完整
|
||||
|
||||
### Next steps
|
||||
1. **补测试**:为改动的 4 个源码文件添加单元测试,解除 `require_tests_for_src_changes` 硬门禁
|
||||
2. 修复 major 重放风险后重新触发 gatekeeper
|
||||
---
|
||||
*Generated by gitlink-gatekeeper · policy-as-code PR gate · re-run after changes*
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> **复算提示**:把上表「输入快照」的数值代入 `REFERENCE.md` 第 3 节公式即可重现每一维得分;硬门禁按第 4 节逐项比对;最终裁决按第 5 节判定树得出。同策略 + 同输入 → 同评分卡(确定性)。
|
||||
Loading…
Reference in New Issue