4f8ee0a8c5
按项目方给的规则重做 CHANGELOG 与检查器(本轮不推、不打 tag、不部署): - 条目形状:四个自定义必填节(改了什么/兼容性与迁移影响/怎么验收/已知问题) → 固定四类 Added/Changed/Fixed/Security;分类可省略,**不许空栏目**;每个版本块 至少一个非空分类;`[Unreleased]` 段不得写日期。 - 破坏性变化:固定写在 Changed 里、以 `**破坏性**` 开头,**必须链接发布文档**; 仓内 `docs/….md` 链接**对账**(不存在 ⇒ 红)。写在别的分类里也判红。 - 结构判据补强:非法日期、重复版本、顺序必须严格递减、前导零;围栏代码块里的 `##`/`###` 不算标题;**块正文以任意二级标题为界**(「## 能力基线(未发布)」这类 不带方括号的标题原先不是边界,会把后文算成上一节的内容 ⇒ 空栏目被兜成"有内容", 夹具真的抓到了这个假绿)。 - tag 一致性:远端专属清单 `.ci-remote-tags` → `ls-remote` → 本地 refs 三级来源; **没核对远端就不许说校验成功**(CI/预检用 `--require-remote`,拿不到清单即红), 非正式 tag(如本地 `x`)不当作发布版本、不判红,没有 tag 不判红。 - `0.1.0` 定为**未发布的能力基线**(远端无 tag、无 Release):改成不带方括号的 「## 能力基线(未发布)」,不补造 tag/发布日期/发布事实。 - 过程记录迁出:审查轮次与命令输出 → `docs/archive/CI_接入留档.md`(原文保留); 未合并事项 → `docs/BACKLOG.md`;补 `docs/RELEASE_RUNBOOK.md`(破坏性变化的链接目标, 同时修掉 CHANGELOG 里指向不存在文档的悬空引用)。 - 发版预检:无版本基线时把「首个正式版本号由人指定」列成阻塞项(不再要求先有基线才能跑); 打印破坏性变化条数;远端 tag 查不到时明说未核对并阻塞;不再硬读已删除的『已知问题』节。 - traps trap-5 口径修正:不是"不用 tag",而是"构建时不要用 git describe 取版本号"。 验证:check_changelog --self-test 全过;反仓夹具 80/0(含五条真改文件的结构负例、 六条只在临时仓库里造 tag 的场景);make verify 15/15 success。
348 lines
20 KiB
Markdown
348 lines
20 KiB
Markdown
# 已知陷阱(全文)
|
||
|
||
> 这份文件是 `AGENTS.md` 第 4 节那张索引的**全文**。拆成两份是有原因的:
|
||
> `AGENTS.md` 作为"工作区指令"会被自动加载,而**指令有字节预算** —— 超出的部分会被
|
||
> **静默截断**,没有任何报错。索引留在指令文件里(一行一条,一眼扫到),全文放这里。
|
||
>
|
||
> 纪律:**只追加、不重排、不复用旧号** —— 编号会被代码注释、测试、文档引用(形如"见 trap-3")。
|
||
> 新增一条要改两处:这里加全文 + `AGENTS.md` 的索引表加一行。
|
||
|
||
## trap-1:`.gitignore` 对**已入库**的文件无效
|
||
|
||
**症状**:规则明明写了 `__pycache__/`,`git ls-files` 里却仍有 8 个
|
||
`scripts/__pycache__/*.pyc`。
|
||
|
||
**根因**:`.gitignore` 只影响"未跟踪"的文件。文件一旦进了索引,规则对它就完全不起作用,
|
||
而且**不会有任何提示**。
|
||
|
||
**判据**:`git ls-files | grep -E '__pycache__/|\.pyc$'` 输出非空即是这条。
|
||
|
||
**修法**:规则 + `git rm -r --cached <path>`(保留磁盘文件)。
|
||
规则齐全不等于没被跟踪 —— 断言要查**两件事**:规则在、且 `git ls-files` 里没有。
|
||
|
||
**反例**:只加 `.gitignore` 规则就以为修好了。下次 `git add -A` 时它照样进来。
|
||
|
||
## trap-2:`git rm --cached` 之后跑 `git add -A`,文件会被搬回索引
|
||
|
||
**症状**:执行了 `git rm --cached node_modules`,`git status` 也显示删除了,
|
||
提交后 `git ls-files` 里那个文件**仍在**,净效果为零。
|
||
|
||
**根因**:`--cached` 只从索引移除、**磁盘文件留着**。如果此刻忽略规则**还没生效**
|
||
(本仓当时只写了 `/web/node_modules/`,管不到根级 `node_modules`),
|
||
紧接着的 `git add -A` 会把这个"存在且未被忽略"的文件重新加回索引。
|
||
|
||
**判据**:`git rm --cached X` 后、提交前,先跑 `git check-ignore -v X` ——
|
||
必须命中一条规则,否则 `add -A` 会把它捡回来。
|
||
|
||
**修法**:**先让忽略规则生效,再撤出索引**,并且此后只 `git add <具体文件>`,
|
||
不要用 `-A`。
|
||
|
||
**反例**:把两步写成一条 `git rm --cached X && git add -A && git commit`。
|
||
|
||
## trap-3:用子串判断代替整行判断
|
||
|
||
**症状**:脚本判断"规则已存在"时写 `if "/node_modules/" not in 全文`,
|
||
而文件里已有 `/web/node_modules/` —— **它包含这个子串**,于是误判为"已存在",
|
||
跳过了追加。结果规则根本没加上,而脚本打印"规则已存在"。
|
||
|
||
**根因**:`.gitignore` 的 `/web/node_modules/` 与 `/node_modules/` 语义完全不同
|
||
(前者只管 `web/` 下),但作为字符串是**包含关系**。
|
||
|
||
**判据**:任何"这条规则在不在"的判断,都要对**行集合**做精确比较
|
||
(`{l.strip() for l in lines}`),不要对全文做 `in`。
|
||
|
||
**反例**:`if "xxx" not in text` 这类判断在多条相似规则并存时必然出错。
|
||
|
||
## trap-4:Gitea 合并接口成功时返回**空 body**
|
||
|
||
**症状**:脚本调用合并接口,HTTP 200,但解析 JSON 抛
|
||
`json.decoder.JSONDecodeError: Expecting value: line 1 column 1`,
|
||
看起来像"合并失败",于是重试 —— 而 PR 其实已经合并了。
|
||
|
||
**根因**:Gitea 的 `POST /pulls/{n}/merge` 成功时 body 为空(不是 `{"merged":true}`)。
|
||
把"HTTP 200 + 空 body"当成失败是客户端的问题,不是服务端的问题。
|
||
|
||
**判据**:合并后 **不要**信 POST 的返回,改 `GET /pulls/{n}`,
|
||
要求 `state=closed` + `merged=true` + `merge_commit_sha` 非空。
|
||
|
||
**反例**:在合并接口上做重试。**永不重试** —— 会得到"第二次合并失败"的假故障。
|
||
|
||
## trap-5:浅克隆里 **`git describe --tags` 会静默退化**(≠ 不能用 tag)
|
||
|
||
**症状**:想让版本号来自 `git describe --tags`,在 CI 上失败或给出误导结果。
|
||
|
||
**根因**:工作流里是 `git fetch -q --depth 50 origin "$BR"`,浅克隆不带 tag。
|
||
`git describe` 要么报 `No names found`,要么退回到一个和预期无关的短 sha。
|
||
⚠️ 这是"**不要在构建时靠 `git describe` 取版本号**",**不是**"本仓不用 tag" —— 正式版本
|
||
**就是**用注释 tag `vX.Y.Z` 标记的(由人批准后创建)。
|
||
|
||
**判据**:任何依赖 tag 历史的**自动**步骤,必须在 CI 里显式
|
||
`git fetch --tags` / `--unshallow`;否则就是这条。
|
||
|
||
**修法(本仓采用)**:
|
||
- **版本号的唯一来源**是仓库里的 `CHANGELOG.md`(`python3 scripts/version.py`),文件一定在,不依赖 tag;
|
||
构建身份(含开发标识)用 `scripts/version.py --identity`,`make version` 打印它。
|
||
- **tag 一致性由人可核验的方式检查**:检出步骤专门落一份远端 tag 清单
|
||
(`.ci-remote-tags`)并 `git fetch --tags` 取对象,`scripts/check_changelog.py --tags` 再核
|
||
「tag 版本 == 它指到那个提交里的版本记录」、且必须是**注释 tag**;
|
||
**没有 tag 不判红**,但**没核对远端就不许说"校验成功"**(CI 用 `--require-remote` 强制)。
|
||
|
||
**反例**:`VERSION=$(git describe --tags --always)` —— 本地看着正常,CI 上静默退化。
|
||
反例二:检查器在拿不到远端 tag 清单时照打 `✅ 校验通过` —— 那是"假绿",现在会明确写「未核对远端」。
|
||
|
||
## trap-6:act_runner **不会**自动检出代码
|
||
|
||
**症状**:工作流第一步就报找不到文件 / 空工作区。
|
||
|
||
**根因**:本仓的 act_runner 不会替作业做 `git clone`,工作区起始为空。
|
||
|
||
**判据**:工作流里若没有 `git init` + `git remote add` + `git fetch` + `git checkout`,
|
||
就是这条。
|
||
|
||
**修法**:照 `.gitea/workflows/build.yml` 的「检出代码」步写(带 `x-access-token` 的
|
||
remote,令牌来自 `secrets.GITHUB_TOKEN`)。
|
||
|
||
**反例**:以为 runner 会像 GitHub Actions 那样自动 checkout,直接写业务步骤。
|
||
|
||
## trap-7:`grep -q` 在 `pipefail` 下会被 SIGPIPE 谎报失败
|
||
|
||
**症状**:`echo "$OUT" | grep -q 'PASS'` 明明匹配上了,整条命令却返回非 0
|
||
(141),在 `set -o pipefail` 的作业里表现为"检查失败"。
|
||
|
||
**根因**:`grep -q` 一命中就退出并关闭管道,上游 `echo` 随即收到 SIGPIPE,
|
||
整条管道返回 141。
|
||
|
||
**判据**:`set -o pipefail` + `... | grep -q ...` 同时出现即是这条。
|
||
|
||
**修法**:用 `case "$OUT" in *PASS*) ... ;; esac`,或让 `grep` 直接读文件;
|
||
非用管道不可时用 `set +e; set +o pipefail` + `${PIPESTATUS[0]}`。
|
||
|
||
**反例**:`if echo "$x" | grep -q y; then` —— 时红时绿,且方向完全错。
|
||
|
||
## trap-8:`gofmt -l` 列出文件时**退出码仍是 0**(只看退出码=假绿)
|
||
|
||
**症状**:门禁里写着 `gofmt -l .`(或 `step gofmt -l . || fail=1`),格式乱掉的文件明明被列出来了,
|
||
这一步却**通过**。整条门禁全绿,而仓库里躺着一堆没格式化的文件。
|
||
|
||
**根因**:`gofmt -l` 的正常行为就是"列出不合规的文件并返回 0" ——
|
||
它的退出码表达的是"我跑完了",不是"都合规"。用退出码当判据,等于没判。
|
||
|
||
**判据**:任何"输出即问题"的工具(`gofmt -l`、`prettier --list-different`、`git status --porcelain`)
|
||
都必须**判输出是否为空**,不能判退出码。
|
||
|
||
**修法**:`[ -z "$(gofmt -l <文件...>)" ]`,或者像本仓 `scripts/check_format.py` 那样解析输出;
|
||
并且给它一条**故意弄坏的夹具**证明它真的会红(否则"永远返回 0 的校验器"会一路绿灯)。
|
||
|
||
**反例**:`step gofmt -l . || fail=1` —— 生成器里就是这么写的,一直没人发现。
|
||
|
||
## trap-9:`tar -C dir .` 产生 `.` 根成员 ⇒ 预览 404
|
||
|
||
**症状**:打包与上传都成功、通知也发了,点开预览链接却是 404 或报错。
|
||
|
||
**根因**:`tar -czf x.tgz -C dir .` 会把 `.`(以及 `./index.html`)写进成员名。
|
||
严格的解包器要么拒绝整包,要么解出一层多余的 `.`,于是 `index.html` 不在预期的位置上。
|
||
|
||
**判据**:`tar -tzf <包>` 的第一行若是 `.` 或 `./…`,即是这条。
|
||
|
||
**修法**:先 `cd` 进产物目录,再用通配展开成员名(`( cd "$WEB" && { shopt -s dotglob; tar -czf "$TAR" *; } )`),
|
||
并在发之前断言包里真的有 `index.html`(`tar -tzf "$TAR" | grep -qx 'index.html'`)。
|
||
|
||
**反例**:只看"打包成功 / 上传 HTTP 200"就当预览发好了 —— 那两步都不会因为包结构不对而失败。
|
||
|
||
## trap-10:依赖清单被静默改写
|
||
|
||
**症状**:一次 CI 通过之后,`go.mod` / `go.sum` / `package-lock.json` 与提交里的不一样了;
|
||
本地跑一遍还多了/少了几个依赖,没人改过它们。
|
||
|
||
**根因**:两个"顺手"的默认行为 —— `GOFLAGS=-mod=mod`(构建时自动补全/更新 `go.mod`)、
|
||
以及缺锁文件时回落到 `npm install`(按 `package.json` 重算整棵依赖树并重写锁文件)。
|
||
两者都让"依赖清单"变成**运行时产物**,而它本该是人 review 过的契约。
|
||
|
||
**判据**:夹具 = 门禁开始前后对这几个文件取 sha256;任何一步改写了它们 ⇒ 红。
|
||
|
||
**修法**:`GOFLAGS=-mod=readonly`;缺锁**直接失败**(不接受无约束安装);
|
||
`npm ci`(按锁装、锁与 `package.json` 不一致就报错)。见门禁的 `deps-install` 与 `manifests-locked` 两项。
|
||
|
||
**反例**:`if [ -f package-lock.json ]; then npm ci; else npm install; fi` —— 看起来"更健壮",
|
||
实际是给漂移开了后门。
|
||
|
||
## trap-11:汇总把「未运行 / 被跳过」当「通过」
|
||
|
||
**症状**:某个必需检查压根没跑(失败即停的后果、或运行时被 `if:` 跳过),
|
||
汇总却报"全绿";事后看报告,那一项**根本不在报告里**,谁都没发现。
|
||
|
||
**根因**:汇总逻辑只遍历"跑过的项"。没跑的项不出现 ⇒ 集合变小 ⇒ 全绿。
|
||
另一种写法是 `skipped` 与 `success` 一起放行。
|
||
|
||
**判据**:给汇总喂一个"必需项缺失 / 必需项 skipped"的夹具,它**必须**非绿。
|
||
|
||
**修法**:清单里声明必需项集合,汇总按**清单**(不是按结果数组)逐项核对;
|
||
缺项记 `not_run` 并判红;`aggregate()` 只认 `success`。
|
||
不适用要**在清单里声明**(本仓的 `required_in`),不能在运行时悄悄 skip。
|
||
|
||
**反例**:`status = "failure" if any(x == "failure" for x in results) else "success"`。
|
||
|
||
## trap-12:事件 SHA ≠ 检出 SHA
|
||
|
||
**症状**:CI 里跑的是"分支当前 HEAD",而这次运行的事件指向的是**另一个提交**。
|
||
表现是"我刚推的那一步好像没跑",或者更糟 —— 拿新代码的结论去解释旧代码的产物。
|
||
|
||
**根因**:工作流写 `git fetch --depth 50 origin "$BR" && git checkout -B "$BR" FETCH_HEAD`,
|
||
取的是**当时**的分支 HEAD。只要在事件产生之后又有人推了一次(或重跑旧运行),两者就分叉。
|
||
|
||
**判据**:`git rev-parse HEAD` 必须等于 `${GITHUB_SHA}`(**完整** SHA,不是短 sha)。
|
||
不一致 ⇒ 红。
|
||
|
||
**修法**:检出后立刻核对;不一致时改按精确 SHA 取(`git fetch --depth 50 origin "$EVENT_SHA"`),
|
||
再取不到就失败。门禁的 `identity` 项会**再核对一次**(两道,故意的)。
|
||
|
||
**反例**:报告里只写"分支 main 的构建成功" —— 这个结论没有对应的提交。
|
||
|
||
## trap-13:密钥扫描规则过宽 ⇒ 永远的假红
|
||
|
||
**症状**:接入密钥扫描后立刻红,命中全是**正常代码**:`postgres://{u.quote(user)}:{u.quote(password)}@host/db`
|
||
(模板表达式)、`postgres://governance:***@postgres/governance`(脱敏占位)、
|
||
`X-CI-Token: ${BRIDGE_TOKEN}`。
|
||
|
||
**根因**:规则用 `[^@\s]{6,}` 这种"很宽"的字符类去抓口令段,于是把变量插值、脱敏星号、
|
||
模板表达式全部当成密钥。假红一旦出现,人就会把整个检查注释掉 —— 那比没有检查更糟。
|
||
|
||
**判据**:扫描器必须自带**双向夹具**:合成的真密钥要命中,**良性对照要零误报**,
|
||
而良性对照里要**钉住踩过的误报形状**(防回归)。
|
||
|
||
**修法**:只收"干净字符"的口令段(`[A-Za-z0-9+/=_\-]{6,}`),并对命中值做占位符判定
|
||
(`${...}`、`<...>`、`***`、`changeme`…)。见 `scripts/check_secrets.py` 的 `RULES` 与 `--self-test`。
|
||
|
||
**反例**:为了让门禁变绿去放宽/注释规则 —— 那是降低规则换绿灯,规范 §4.2 明确禁止。
|
||
|
||
---
|
||
|
||
### trap-14|"这个语言我没写检查" = 假报成功(通用模板的 Python 门禁空跑)
|
||
|
||
**症状**:通用模板套到一个 Python 项目上,`deps-install / format / static / build / tests` 五项**全绿**,
|
||
但一个 Python 检查都没跑 —— 函数体只打印一句话就 `return SUCCESS`。
|
||
|
||
**根因**:模板最初只为 Go+Node 写实现,Python 分支留了"占位返回成功"。占位返回成功不是"还没做",
|
||
是**假报成功**:它会让人以为这个项目被检查过。审查是靠"模拟一个 Python 项目,看那五项是不是真的执行了"
|
||
才发现的 —— 只看 YAML/看代码是看不出来的。
|
||
|
||
**判据**:每一门识别到的语言都必须**真的执行**;工具缺失、依赖没按锁装、输出解析不了 ⇒ **失败**。
|
||
|
||
**修法**:`uv sync --frozen` / `uv run ruff format --check` / `compileall` / `uv run pytest`;
|
||
`need()` 找不到工具就抛错(转成 FAILURE)。前端同理:测试**进程退出码非零**必须红
|
||
(报告写 pass 不算数),框架按 `test-scope.json` 声明,认不出来就失败。
|
||
|
||
### trap-15|预览放在"报告核对"之前 = 发出去就收不回来
|
||
|
||
**症状**:预览步骤只读 `overall_status` + `commit_sha` 就发布;完整报告核对在后面的通知步骤。
|
||
报告缺项时,**预览已经发出去了**,几分钟后才报错 —— 那时 URL 可能已经被人打开/转群。
|
||
|
||
**根因**:把"发布"当成"通知"的一环。预览是**不可撤回**的对外动作,不能排在质量结论之前。
|
||
|
||
**判据**:发布前必须调用**同一份**核对逻辑(`ci_report.verify()`),有任何问题 ⇒ **一个 publish 请求都不发**。
|
||
|
||
**修法**:`.gitea/scripts/ci_preview.py` 内部先核对再发布;`scripts/ci_preview_negative_test.py`
|
||
用假 bridge **数请求**证明坏报告 0 次、好报告 1 次。
|
||
|
||
### trap-16|用"观察到的 context"代替"要求哪些检查"
|
||
|
||
**症状**:合并前没指定必需 context 时,脚本拿"体检时在该仓看到过哪些状态"当必需列表。
|
||
|
||
**根因**:`combined state` 只报"报过状态的都成功",一个**无关**的检查成功就能让它变成 `success`。
|
||
于是"没跑过必需检查"的 PR 会被合进去。
|
||
|
||
**判据**:要求哪些检查只能来自**明确策略**(`--require-context` 或 `.gitea/required-contexts.txt`);
|
||
策略缺失或为空 ⇒ 拒绝合并(空不等于"没有要求")。
|
||
|
||
**修法**:见 `scripts/adopt-existing.py` 的 `resolve_required_contexts()` 与 `--self-test-chain`
|
||
(假 Gitea + 真实 `merge_pr()`,断言 merge 请求发没发)。同时核对 base 分支/PR 状态/head 仓库。
|
||
|
||
### trap-17|remote 协议被"顺手"拼成 http
|
||
|
||
**症状**:注释说"保留服务端给的协议",代码却是 `git remote add origin "http://${HOSTPORT}/..."` ——
|
||
服务端给的是 https 时会**静默降级**成 http(凭据与产物走明文)。
|
||
|
||
**根因**:把 `${SRV#*://}` 取出来的 host:port 再拼一个固定的 scheme。取 host 的动作本身就把协议丢了。
|
||
|
||
**判据**:remote 直接用 `GITHUB_SERVER_URL` 原样拼,并**断言**最终 scheme 与服务端一致;
|
||
未知 scheme 直接拒绝;URL 里不许带凭据。
|
||
|
||
**修法**:`.gitea/scripts/ci_checkout.sh`(`--print-remote` 可本地验证协议保留);
|
||
YAML 里只调用它,不再内联这套拼接。
|
||
|
||
### trap-18|范围校验回退成"最近 N 条"或"接受截断的列表"
|
||
|
||
**症状**:范围算不出来时退化成 `git log -n 5`(和历史无关),或直接采信事件负载里那串 commits。
|
||
|
||
**根因**:事件负载里的 commits **可能是截断的**(实测:声明 6 条、列表只给 1 条),
|
||
浅克隆里 `before` 可能根本不存在 —— 两种都会让"本次改动"少核几条,而且全绿。
|
||
|
||
**判据**:以**准确 Git 范围**为准(push 用 `before..after`,新分支用 `after --not --remotes`);
|
||
事件声明条数与 Git 范围不一致 / 空范围 / 浅克隆取不到 before / 列表提交不在检出里 ⇒ 全部**拒绝**;
|
||
手工运行比不出范围时只核 HEAD,但必须**写明**。
|
||
|
||
**修法**:`.gitea/scripts/ci_gate.py` 的 `resolve_scope()`(`--self-test` 11 个分支)+
|
||
检出脚本顺带把事件 `before` 取下来。
|
||
|
||
---
|
||
|
||
### trap-19|第一步就调用仓内脚本 = 整条流水线起不来
|
||
|
||
**症状**:workflow 第一步写 `bash .gitea/scripts/ci_checkout.sh`。本地"已经有代码的目录"里跑得好好的,
|
||
真到 Runner 上返回 **127**(`bash: .gitea/scripts/ci_checkout.sh: 没有那个文件或目录`)——
|
||
因为这个 Runner **不自动检出**,工作区初始是空的(trap-6),此时仓内脚本还不存在。
|
||
|
||
**根因**:把"检出"写成了仓内脚本,而"检出"恰恰是**让仓内文件出现**的那一步 —— 循环依赖。
|
||
本地验证只在有代码的目录里做,就永远看不到这个形状。
|
||
|
||
**判据**:第一步必须是**最小 bootstrap**:只依赖 runner 给的环境变量(`GITHUB_SERVER_URL` /
|
||
`GITHUB_REPOSITORY` / `GITHUB_SHA` / `GITHUB_TOKEN`),自己 `git init` + `remote add` + 取**事件完整 SHA**
|
||
+ checkout + 断言 HEAD==事件 SHA。检出完成、脚本就位之后,**才**调用仓内脚本做后续校验与补取。
|
||
|
||
**修法**:`build.yml` 第一步内联 bootstrap → 然后 `bash .gitea/scripts/ci_checkout.sh --post-checkout`。
|
||
`scripts/ci_chain_fixture.py` 会把那段 `run:` **原样抽出来**在空目录里执行(YAML 一改坏就红)。
|
||
|
||
### trap-20|`fetch origin <SHA>` 取任意提交(服务端默认不允许)
|
||
|
||
**症状**:分支 HEAD 已经前进时,bootstrap 想"按精确 SHA 取",执行 `git fetch origin $GITHUB_SHA`
|
||
→ `服务器不允许请求未公开的对象`(upload-pack 默认不开放按任意 SHA 取)。
|
||
|
||
**根因**:把 `--depth` 取分支的写法套到按 SHA 取上。这个能力在默认配置下**不存在**。
|
||
|
||
**判据**:事件 SHA 必须能从**已取到的分支历史**里找到;不够深就 `--deepen` 再找;
|
||
仍找不到才是失败(提示提高深度或等重跑)。失败要明确,不许"取不到就换个分支糊过去"。
|
||
|
||
**修法**:见 bootstrap 里的 `git cat-file -e "${GITHUB_SHA}^{commit}"` + `fetch --deepen 200` 分支。
|
||
|
||
### trap-21|`after --not --remotes` 在检出后把范围排成空集
|
||
|
||
**症状**:新分支推送(`before` 为全 0)用 `after --not --remotes` 算范围。CI 检出时
|
||
`refs/remotes/origin/<本分支>` 往往已经等于 `after` —— 那个引用把自身排除了,范围恒为 **0 条**,
|
||
闸门直接失败(或更糟:被当成"没有要检查的提交"放过去)。
|
||
|
||
**根因**:拿"刚 fetch 下来的同名远端分支"当排除集。它是**被检出的那个分支**,不是基线。
|
||
|
||
**判据**:新分支的基准只能是**远端默认分支**(事件里的 `repository.default_branch`),
|
||
范围 = `refs/remotes/origin/<default>..<after>`;取不到基准 ⇒ **明确失败**,不许退化成整部历史或空集。
|
||
|
||
**修法**:`ci_gate.resolve_scope()`;`ci_checkout.sh --post-checkout` 负责把默认分支引用取下来。
|
||
`--self-test` 里有专门的回归用例(`origin/<本分支> == after` 时必须仍算出那几条提交)。
|
||
|
||
---
|
||
|
||
### trap-22|把事件里的 `total_commits` 当成"声明条数"
|
||
|
||
**症状**:新分支推送的真实事件里 Gitea 给的是 `total_commits=0`、`commits` 列表被截断(实测 5 条),
|
||
而 Git 范围算出 12 条。严判"声明 ≠ Git 范围"⇒ 门禁在**第一次真实 CI 就红了**,
|
||
红的原因与代码质量无关(本地夹具用的是"声明 6 条/列表 1 条"的自造形状,覆盖不到这一种)。
|
||
|
||
**根因**:`total_commits` 在 Gitea 的语义不是"这次推送的提交条数",**新分支推送时就是 0**
|
||
(等于"没声明")。把它当权威声明去比对,等于用一个不存在的约束判红。
|
||
|
||
**判据**:**Git 范围是唯一权威**。`total_commits > 0` 时才拿来交叉核对(不一致 ⇒ 拒绝,
|
||
覆盖"声明 6 条 / 列表 1 条 / Git 2 条"那一类);`<=0` 或缺失 ⇒ 视为未声明,
|
||
以 Git 范围为准,并把"负载条数不可信、列表被截断"**写进来源说明**里。
|
||
|
||
**修法**:`ci_gate.resolve_scope()`(`--self-test` 里有专门的 `total_commits=0` 分支)。
|
||
教训:负例要**照真实事件负载的形状**造,不能只造自己想的形状。
|