forked from Gitlink/gitlink-cli
371 lines
18 KiB
Markdown
371 lines
18 KiB
Markdown
---
|
||
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`。
|