From 3255884c6e5fbb883abacc7a80a33f9ad1ab3109 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BD=95=E5=BC=80=E5=85=83?= Date: Thu, 4 Jun 2026 00:05:29 -0700 Subject: [PATCH] =?UTF-8?q?feat(skills):=20add=20gitlink-gatekeeper=20?= =?UTF-8?q?=E2=80=94=20Policy-as-Code=20PR=20merge=20gate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 gitlink-gatekeeper Skill:版本化 gatekeeper.yaml 策略 → 5 维 0-100 评分卡 → 三态裁决(PASS/REQUEST_CHANGES/COMMENT)→ 建议性 common 评论回写。默认 dry-run、 绝不自动合并。复用 pr +view/+files/+diff、pr +comment、ci +builds、label(已并入 master)。 - SKILL.md(六步工作流)+ REFERENCE.md(gatekeeper.yaml 全字段 + 评分算法 + CLI 映射) + TROUBLESHOOTING.md(12 问) - examples/:3 套策略预设 + 评分卡样例 + 3 个可逐位复算的裁决记录(90/38/68) - skills/README.md 登记一行 按 #90 review 重做:基于最新 master 的单个干净 commit,仅含 skill 文档, 不含 gitlink-cli 二进制(.gitignore 已排除)。 Co-Authored-By: Claude Opus 4.8 (1M context) --- skills/README.md | 1 + skills/gitlink-gatekeeper/REFERENCE.md | 244 ++++++++++++ skills/gitlink-gatekeeper/SKILL.md | 370 ++++++++++++++++++ skills/gitlink-gatekeeper/TROUBLESHOOTING.md | 319 +++++++++++++++ .../examples/decision-comment.md | 163 ++++++++ .../examples/decision-pass.md | 178 +++++++++ .../examples/decision-request-changes.md | 169 ++++++++ .../examples/gatekeeper.lenient.yaml | 50 +++ .../examples/gatekeeper.strict.yaml | 50 +++ .../examples/gatekeeper.yaml | 50 +++ .../examples/scorecard-sample.md | 88 +++++ 11 files changed, 1682 insertions(+) create mode 100644 skills/gitlink-gatekeeper/REFERENCE.md create mode 100644 skills/gitlink-gatekeeper/SKILL.md create mode 100644 skills/gitlink-gatekeeper/TROUBLESHOOTING.md create mode 100644 skills/gitlink-gatekeeper/examples/decision-comment.md create mode 100644 skills/gitlink-gatekeeper/examples/decision-pass.md create mode 100644 skills/gitlink-gatekeeper/examples/decision-request-changes.md create mode 100644 skills/gitlink-gatekeeper/examples/gatekeeper.lenient.yaml create mode 100644 skills/gitlink-gatekeeper/examples/gatekeeper.strict.yaml create mode 100644 skills/gitlink-gatekeeper/examples/gatekeeper.yaml create mode 100644 skills/gitlink-gatekeeper/examples/scorecard-sample.md diff --git a/skills/README.md b/skills/README.md index f7e10ea..9a6e519 100644 --- a/skills/README.md +++ b/skills/README.md @@ -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` | --- diff --git a/skills/gitlink-gatekeeper/REFERENCE.md b/skills/gitlink-gatekeeper/REFERENCE.md new file mode 100644 index 0000000..b19a54f --- /dev/null +++ b/skills/gitlink-gatekeeper/REFERENCE.md @@ -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 ` 指定;找不到时回退到内置默认策略(即下表「默认值」列)。 + +### 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 含 `#` 或 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 --format json` | `+view`,`-i`/`--id` | +| 变更文件 | 文件路径列表 | `gitlink-cli pr +files -i --format json` | `+files` | +| Diff | 变更内容供 AI 审查 | `gitlink-cli pr +diff -i --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 -b ""` | `+comment` 底层走 issue journals(评论流);评审记录形式用 `pr +review -i -s common -c "..."`(走 reviews 端点,payload 字段是 `content`/`status`,status 取 `common`/`approved`/`rejected`)。评分卡作为建议性回写,二者均用 `common` | +| 创建标签 | 裁决标签 | `gitlink-cli label +create -n "" -c "#RRGGBB"` | `+create`(本作品子题一新增;`label +list/+update/+delete` 同组) | +| 挂标签 | 把标签挂到 PR 对应 Issue | `ISSUE_ID=$(gitlink-cli pr +view -i --format json \| jq -r '.data.issue.id')` 后 `gitlink-cli api POST /:owner/:repo/issues/$ISSUE_ID --body '{"issue_tag_ids":[],"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 --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 不回显。 diff --git a/skills/gitlink-gatekeeper/SKILL.md b/skills/gitlink-gatekeeper/SKILL.md new file mode 100644 index 0000000..ce31ddb --- /dev/null +++ b/skills/gitlink-gatekeeper/SKILL.md @@ -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 ` 指定。找不到时**回退到下文内置默认策略**(务必在评分卡 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 标记为 `@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 --format json` | +| 变更文件 | 文件路径列表 | `gitlink-cli pr +files -i --format json` | +| Diff | 变更内容供 AI 审查 | `gitlink-cli pr +diff -i --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: "", line: , 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 含 `#` 或 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 # + +**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`。 diff --git a/skills/gitlink-gatekeeper/TROUBLESHOOTING.md b/skills/gitlink-gatekeeper/TROUBLESHOOTING.md new file mode 100644 index 0000000..4272934 --- /dev/null +++ b/skills/gitlink-gatekeeper/TROUBLESHOOTING.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。 diff --git a/skills/gitlink-gatekeeper/examples/decision-comment.md b/skills/gitlink-gatekeeper/examples/decision-comment.md new file mode 100644 index 0000000..41ae723 --- /dev/null +++ b/skills/gitlink-gatekeeper/examples/decision-comment.md @@ -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 标签,不合并」并等待确认。 diff --git a/skills/gitlink-gatekeeper/examples/decision-pass.md b/skills/gitlink-gatekeeper/examples/decision-pass.md new file mode 100644 index 0000000..e47001c --- /dev/null +++ b/skills/gitlink-gatekeeper/examples/decision-pass.md @@ -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 标签,不合并」并等待确认。 diff --git a/skills/gitlink-gatekeeper/examples/decision-request-changes.md b/skills/gitlink-gatekeeper/examples/decision-request-changes.md new file mode 100644 index 0000000..34d78d0 --- /dev/null +++ b/skills/gitlink-gatekeeper/examples/decision-request-changes.md @@ -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,不合并」并等待确认。 diff --git a/skills/gitlink-gatekeeper/examples/gatekeeper.lenient.yaml b/skills/gitlink-gatekeeper/examples/gatekeeper.lenient.yaml new file mode 100644 index 0000000..fae0129 --- /dev/null +++ b/skills/gitlink-gatekeeper/examples/gatekeeper.lenient.yaml @@ -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 diff --git a/skills/gitlink-gatekeeper/examples/gatekeeper.strict.yaml b/skills/gitlink-gatekeeper/examples/gatekeeper.strict.yaml new file mode 100644 index 0000000..2701a0f --- /dev/null +++ b/skills/gitlink-gatekeeper/examples/gatekeeper.strict.yaml @@ -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 diff --git a/skills/gitlink-gatekeeper/examples/gatekeeper.yaml b/skills/gitlink-gatekeeper/examples/gatekeeper.yaml new file mode 100644 index 0000000..92ff8a6 --- /dev/null +++ b/skills/gitlink-gatekeeper/examples/gatekeeper.yaml @@ -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 diff --git a/skills/gitlink-gatekeeper/examples/scorecard-sample.md b/skills/gitlink-gatekeeper/examples/scorecard-sample.md new file mode 100644 index 0000000..661bc9e --- /dev/null +++ b/skills/gitlink-gatekeeper/examples/scorecard-sample.md @@ -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 节判定树得出。同策略 + 同输入 → 同评分卡(确定性)。