Git 提交策略、审计与弃用治理
一次“全绿”合并为什么仍然留下违规历史
某个团队在仓库里放了 commit-msg Hook,每位开发者都看见过“提交主题不符合规范”的红字;Pull Request 的测试也全部通过。上线后,发布工具却无法从提交历史生成变更记录。追查发现,网页编辑产生了不合规提交,依赖机器人使用了另一套消息格式,还有人用 git commit --no-verify 绕过了本地 Hook。更糟的是,保护分支只要求“CI 成功”,CI 却只检查了最新一条提交,较早的违规 Commit 已随同合并进入主分支。
这不是正则表达式写得不够复杂,而是把不同信任等级的控制点混成了一层:
提交模板和本地 Hook 缩短反馈时间,但执行权在提交者手里。commitlint 把消息约定变成可测试规则,却不会自己决定在哪个入口强制执行。CI 能检查 Pull Request 的完整提交区间,但只有被保护规则设为必需检查时才具有准入意义。
自托管仓库的 pre-receive、托管平台的 Ruleset、Push Rule 或保护分支位于共享引用写入路径,才有资格作最终阻断。Commit 签名证明某个密钥签过对象,不等于完成评审,也不等于这次部署获批。审计回答“谁改了规则、谁使用了例外、哪个引用从哪个对象移动到哪个对象”,不能用当前分支状态或普通构建日志代替。
提交治理的目标不是让每条消息看起来整齐,而是回答一个更严格的问题:哪些对象能够进入关键引用,依据哪一版策略,失败时如何修复,绕过后如何追责,规则升级时如何迁移和回滚。
先把策略写成机器可以判定的契约
“提交要规范”无法测试,也无法审计。一条可以上线的策略至少应具备以下字段:
| 字段 | 示例 | 决定的问题 |
|---|---|---|
| 策略 ID | COMMIT-001 | 日志、告警和例外引用哪条规则 |
| 目标引用 | refs/heads/main | 哪些分支或 Tag 受约束 |
| 检查对象 | 本次新引入的每个 Commit | 检查 tip,还是完整提交集合 |
| 判定规则 | type(scope): subject | 什么输入允许或拒绝 |
| 失败证据 | 对象 ID、规则 ID、修复动作 | 开发者如何定位并修复 |
| 强制位置 | 必需 CI + 平台规则 | 哪一层不可由普通贡献者跳过 |
| 例外主体 | 限时服务账号或双人审批 | 谁可以绕过、多久失效 |
| owner | 研发效能或代码治理团队 | 谁维护规则和处理误报 |
| 回滚条件 | 拒绝率、延迟或业务阻塞异常 | 何时退回观测模式 |
消息规范宜从真正被消费的需求反推。若发布工具只依赖 type、scope 和破坏性变更标记,就不要把工单系统、部门编码、个人姓名都塞进一行标题。字段越多,机器人、网页编辑、合并队列和跨仓库搬运越容易失败。
下面以 Conventional Commits 风格为例,允许 feat、fix、docs、refactor、test、build、ci、chore,要求主题非空并限制标题长度。团队可以调整词表,但本地、CI 和服务端必须引用同一份版本化配置,不能复制三份逐渐漂移的正则。
安装 commitlint 并锁定规则版本
commitlint 官方入门文档要求同时安装 CLI 和一份可共享配置。把它们作为开发依赖写入仓库,提交 package.json 与锁文件:
npm install --save-dev @commitlint/cli @commitlint/config-conventionalCI 应使用 npm ci 从锁文件恢复已评审版本,不要每次运行时临时安装 latest。这样同一个 Commit 在开发机和流水线中得到相同规则。官方 commitlint 安装说明 还特别指出,Node.js 24 改变了配置模块的加载行为;没有合适 package.json 模块声明时,使用 commitlint.config.mjs 比含糊依赖 .js 推断更稳定。
创建 commitlint.config.mjs:
export default {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [2, 'always', [
'feat', 'fix', 'docs', 'refactor', 'test',
'build', 'ci', 'chore'
]],
'subject-empty': [2, 'never'],
'header-max-length': [2, 'always', 100]
}
};数组第一个值是严重级别:0 关闭、1 警告、2 错误;第二个值决定条件是 always 还是 never;后续值是规则参数。策略评审不能只看“启用了 commitlint”,还要审查每条规则的级别、词表、长度限制以及对历史消息和机器人提交的影响。
先直接验证规则引擎,不急着接 Hook:
printf 'feat(policy): add server-side gate\n' | npx --no-install commitlint --verbose
printf 'WIP: try bypass\n' | npx --no-install commitlint --verbose第一条应以退出码 0 结束;第二条应以非零退出码结束,并报告 type-empty、subject-empty 或同类具体规则。若两条都成功,优先检查配置文件是否被加载、执行目录是否正确,以及 CI 是否偷偷下载了另一版包,而不是继续增加正则。
本地 Hook 负责快,不能负责最终可信
版本库可以把 Hook 脚本放入 .githooks/commit-msg:
#!/bin/sh
exec ./node_modules/.bin/commitlint --edit "$1"然后由受审的初始化命令为当前 clone 启用:
git config --local core.hooksPath .githooks
git config --local commit.template .gitmessage
git config --show-origin --get core.hooksPath
git rev-parse --git-path hooksGit 的 githooks 文档 明确说明,commit-msg 接收消息文件路径,非零退出会终止提交,但可被 --no-verify 跳过。clone 也不会自动替一个仓库启用其版本化 Hook;这种显式启用避免了刚进入陌生仓库就执行任意脚本的供应链风险。
在一个可删除的分支上验证两条路径:
git switch -c lab/commit-policy
printf 'policy lab\n' > policy-lab.txt
git add policy-lab.txt
git commit -m 'WIP: local hook should reject'
git commit --no-verify -m 'WIP: local hook can be bypassed'第一条提交应失败且 HEAD 不移动;第二条应成功,证明本地层确实可绕过。这个反例不是漏洞演示后的尴尬补充,而是架构证据:凡是合规、安全或发布链真正依赖的规则,都必须在提交者控制范围之外重新执行。
恢复实验分支时先确认没有需要保留的工作,再执行:
git switch -
git branch -D lab/commit-policy若初始化脚本需要撤销,不要只删除 .githooks。应同时移除当前 clone 的配置,避免 Git 指向不存在目录后静默失去反馈:
git config --local --unset core.hooksPath
git config --local --unset commit.templateCI 必须检查本次变更引入的完整提交集合
只运行 commitlint --last 适合单提交 push,不足以证明一个多提交 Pull Request 合规。commitlint 的 CI 指南 对 GitHub Actions 使用基线 SHA 到 head SHA 的区间,对 GitLab CI 使用 Merge Request 的 diff base 到当前 SHA;共同前提是流水线拿得到完整区间。
平台无关的核心命令可以写成:
npm ci
BASE_SHA="$(git merge-base origin/main HEAD)"
npx --no-install commitlint --from "$BASE_SHA" --to HEAD --verbose这里有三个容易被忽略的判断点:
BASE_SHA 必须来自当前合并目标,不能永久写死 HEAD~1。浅克隆可能没有 merge base 或较早 Commit;流水线要获取足够历史,无法证明区间完整时应失败,而不是退化成只查最后一条。必需检查应绑定稳定且唯一的名称,例如 commit-policy。若两个工作流产生同名状态,平台可能无法判断哪个结果才是门禁依据。
构造“中间违规、tip 合规”的反向样例:
git switch -c lab/ci-range
printf 'one\n' > policy-range.txt
git add policy-range.txt
git commit --no-verify -m 'WIP: hidden in range'
printf 'two\n' >> policy-range.txt
git add policy-range.txt
git commit --no-verify -m 'fix(policy): make tip look valid'
npx --no-install commitlint --last --verbose
npx --no-install commitlint --from HEAD~2 --to HEAD --verbose--last 会通过,而区间检查应失败并指出前一条 Commit。修复不能靠新增一个“合规补丁提交”,因为违规对象仍在历史中;在共享前可交互式 rebase 或 amend,进入共享分支后则应按团队历史改写规则处理。
CI 成功仍不自动等于准入。以 GitHub 为例,状态检查只有被规则设为 required 后才会阻止合并;GitHub Rulesets 还能组合 Pull Request、签名、线性历史、创建、更新和删除限制。仓库管理员需要实际检查目标引用、enforcement 状态、bypass 列表和 required check 的来源应用,不能只确认 YAML 文件存在。
最终强制点必须位于共享引用写路径
自托管 Git:在 receive-pack 前拒绝对象
裸仓库先启用 Git 原生引用和对象基线:
git -C <bare-repository> config receive.denyNonFastForwards true
git -C <bare-repository> config receive.denyDeletes true
git -C <bare-repository> config receive.fsckObjects true这些配置分别限制非 fast-forward、引用删除和接收对象检查,不会理解 Conventional Commits、评审或工单语义。复杂策略要放在受版本控制、经过评审和回放测试的 pre-receive 或成熟代码托管平台中。
pre-receive 从标准输入逐行读取 <old-oid> <new-oid> <ref>。下面的最小脚本拒绝进入 main 的任何 WIP Commit,用于证明服务端强制点,而不是替代生产策略引擎:
#!/bin/sh
set -u
zero=$(git hash-object --stdin </dev/null | tr '0-9a-f' '0')
rejected="${TMPDIR:-/tmp}/commit-policy-rejected-$$"
: >"$rejected"
trap 'rm -f "$rejected"' EXIT HUP INT TERM
while read old new ref; do
[ "$ref" = "refs/heads/main" ] || continue
[ "$new" = "$zero" ] && continue
if [ "$old" = "$zero" ]; then
git rev-list "$new" --not --all
else
git rev-list "$old..$new"
fi | while read commit; do
subject=$(git show -s --format=%s "$commit")
case "$subject" in
WIP*|wip*) printf '%s\t%s\n' "$commit" "$subject" ;;
esac
done >>"$rejected"
done
if [ -s "$rejected" ]; then
echo 'COMMIT-001:关键引用包含 WIP 提交,请修复以下对象后重试:' >&2
cat "$rejected" >&2
exit 1
fi生产实现还要处理多引用和 atomic push、Tag、创建与删除引用、并发、对象格式、超大 push 的资源上限、临时文件隔离和结构化日志。不要把 SHA-1 的 40 个零写死;zero 应随仓库对象格式得到正确长度。
Git 官方 git-receive-pack 文档 说明,新对象先进入 quarantine 目录,pre-receive 成功后才迁入主对象库;失败的 push 不应留下接收对象。Hook 在 $GIT_DIR 中运行,而且不得让引用指向仍处于 quarantine 的对象。手工执行脚本成功并不能证明真实 push 路径正确,必须通过 git push 触发。
托管平台:用平台规则控制不可绕过性
托管平台通常不允许仓库成员修改服务器 Hook,应组合平台提供的强制点:
GitHub 使用 Ruleset 或保护规则要求 Pull Request、指定来源的 commit-policy 状态检查、必要审批和签名;规则的 bypass actor 应单独审批和定期盘点。
GitLab 的 Push Rules 可以在接收路径检查消息、邮箱、文件、DCO 和签名。官方 Push Rules 文档 当前把该能力标记在 Premium、Ultimate,并明确全局规则只是新项目模板,已有项目不会随全局修改自动继承;fork synchronization 还有绕过 Push Rules 的特殊路径。选型和巡检必须以实际实例版本、许可和写入路径为准。
其他平台要寻找等价的服务端对象检查、必需状态、保护引用、审计和 bypass 能力。只有“分支禁止普通成员直推”仍不足以证明合并队列、机器人、网页编辑和管理员通道都受控。
平台配置也应声明式管理或至少导出快照。只在网页上点一次开关,无法回答规则后来被谁关闭、已有仓库是否漏配以及灾备平台能否恢复同等准入。
把签名、DCO、审批和消息规范分开建模
一条 Commit 可以同时满足消息格式、密码学签名、DCO trailer、代码评审和 CI,但五者证明的是不同事实:
| 机制 | 能证明什么 | 不能证明什么 |
|---|---|---|
| commitlint | 消息符合约定 | 作者真实、代码正确、评审完成 |
| GPG/SSH/S/MIME 签名 | 对象由对应私钥签署且验证链可解释 | 签署者有合并权限、变更已审批 |
Signed-off-by / DCO | 提交者作出来源权利声明 | 密钥身份或代码质量 |
| Pull Request 审批 | 指定评审流程对某个 head SHA 作出决定 | 后续新提交仍被同一审批覆盖 |
| 必需状态检查 | 指定 SHA 的自动检查成功 | 规则未被管理员绕过或关闭 |
GitHub 官方 签名验证说明 支持 GPG、SSH 和 S/MIME,并解释平台保存的 persistent verification record:密钥以后撤销或过期,并不会自动改变仓库网络内既有 Commit 的已验证记录。若组织要求按当前撤销状态重新判断,就必须另建密钥状态和复核策略,不能把平台徽标当实时 PKI 查询。
GitLab 的“Reject unsigned commits”也有平台生成提交和 Web IDE 行为差异。启用签名强制前要逐一回放本地提交、网页编辑、API、机器人、合并队列、cherry-pick、rebase 与 squash merge,记录哪个主体签署最终对象。签名安装和密钥配置可参考相邻的 Git Commit 与 Tag 签名,准入策略则在这里把“Verified”或服务端验证结果变成关键引用条件。
用隔离裸仓库证明本地可绕过、服务端不可绕过
下面的实验只需要 Git 和 POSIX shell,Windows 可在 Git Bash 中执行。目录、身份和邮箱均为无效示例值。
建立远端、客户端和本地反馈
mkdir git-policy-lab
cd git-policy-lab
git init --bare remote.git
git init -b main client
cd client
git config user.name 'Example Developer'
git config user.email 'developer@example.invalid'
git remote add origin ../remote.git
mkdir .githooks创建 .githooks/commit-msg:
#!/bin/sh
subject=$(sed -n '1p' "$1")
case "$subject" in
WIP*|wip*) echo 'LOCAL-COMMIT-001:提交主题不能以 WIP 开头' >&2; exit 1 ;;
esac启用并执行正反路径:
chmod +x .githooks/commit-msg
git config core.hooksPath .githooks
printf 'policy lab\n' > README.md
git add README.md
git commit -m 'WIP: local rejection'
git commit --no-verify -m 'WIP: server must reject'第一条应失败,第二条应成功。运行 git log -1 --format='%H %s' 可以看见违规对象已经存在于本地。
部署接收端门禁并验证拒绝证据
把上一节最小 pre-receive 保存为 ../remote.git/hooks/pre-receive,再执行:
chmod +x ../remote.git/hooks/pre-receive
git push origin HEAD:mainpush 应返回非零,输出包含 COMMIT-001 和违规对象 ID。远端引用仍不存在:
git --git-dir=../remote.git show-ref --verify refs/heads/main该命令应返回非零。然后在尚未共享的实验历史上修复消息:
git commit --amend -m 'chore(policy): create policy lab'
git push -u origin HEAD:main
git --git-dir=../remote.git log -1 --format='%H %s' main
git --git-dir=../remote.git fsck --strict修复后的 push 应成功,远端主题符合约定,fsck 不报告对象损坏。这个闭环同时证明了拒绝、证据、修复和成功路径。
清理实验资产
退出实验目录并确认绝对路径仍指向 git-policy-lab 后再删除:
cd ../..
rm -rf git-policy-labPowerShell 可使用 Remove-Item -LiteralPath .\git-policy-lab -Recurse。不要把实验 Hook 投放到真实服务端,也不要在清理命令中使用未经检查的变量路径。
项目接入要把规则、证据和回滚同时上线
成熟的接入不是把 .githooks 复制到所有仓库,而是按风险逐步收紧:
观测:用 CI 或接收日志统计违规类型、仓库分布、机器人和网页编辑路径,不阻断写入。本地反馈:分发锁定版本的 commitlint 与 Hook,测量误报、执行耗时和未启用 clone 数量。CI 复核:检查完整变更区间,结果绑定 head SHA,先作为非必需检查运行。
服务端强制:将稳定检查设为关键引用的 required check,或在接收端重验不可委托的规则。持续运营:监控拒绝率、P95 延迟、超时、绕过次数、配置漂移和长期未升级仓库。
每次策略变更都应同时提交配置、正反夹具、影响分析、迁移说明和回滚开关。在影子仓库回放真实消息样本,再灰度到少量低风险仓库。服务端失败消息至少返回规则 ID、对象 ID 和修复动作;只输出“policy failed”会迫使开发者找管理员猜原因。
关键指标不应设成脱离基线的万能数字。拒绝率突然抬升可能是规则误报,也可能是某个机器人升级;P95 延迟持续增长说明门禁进入了仓库写路径的容量瓶颈;bypass 长期不归零说明例外正在变成永久接口。阈值由仓库规模、push 体量和可用性目标基于观测期确定。
审计要保存控制面历史,而不只是 Git 内容历史
Git Commit 保存内容和父子关系,当前 ref 只保存最后指向;它们无法独立回答谁修改了保护规则、谁批准了 bypass、谁关闭了 required check。平台审计、接收事件和策略仓库需要通过对象 ID、引用和策略版本关联。
一条可复核事件至少包含:
event_id, repository_id, actor_id, actor_type,
old_oid, new_oid, ref_name, policy_id, policy_version,
decision, exception_id, source_channel, occurred_at其中 actor_id 使用平台稳定标识,不把可修改昵称当身份;source_channel 区分本地 push、网页编辑、API、机器人、合并队列和镜像;exception_id 为空表示正常路径。日志不应记录 PAT、SSH 私钥、完整环境变量或源码正文,失败消息也不能泄露服务器路径和内部拓扑。
GitHub 的 组织审计日志说明 强调主体、动作和发生时间;GitLab 的 Audit events 则明确不同层级、角色和许可可见性存在差异,UI 搜索能力也有限。团队要先验证实际套餐和事件类型是否覆盖规则修改、权限变化及绕过,再决定是否将事件流送入受控存储。审计日志有自己的访问控制、完整性和保留策略,但它仍不是仓库备份。
例外机制必须比永久管理员权限更小
事故修复可能需要临时跳过耗时检查,但“把某人设为管理员”会同时绕过太多控制。一个可运营的例外至少包含:
只针对一个仓库、一条引用、一条策略和有限时间窗口。申请人不能单独批准,关键仓库采用双人审批。写明业务原因、风险、目标对象和补偿检查。
使用独立的 bypass actor 或临时授权,不共享万能账号和长期 Token。到期自动撤销,随后重放检查并关联事故或变更单。每次使用都产生审计事件,长期不用的例外入口也要定期删除。
门禁依赖不可用时采用 fail-open 还是 fail-closed,应按仓库风险预先决定。普通文档仓库可以降级成告警;生产基础设施或安全关键代码通常宁愿暂停合并。无论选择哪种,都要演练超时、队列堆积、规则服务回滚和人工恢复,而不是事故发生后临场修改服务器 Hook。
升级、弃用和退出是一条完整迁移链
提交策略同时依赖 Git、Node.js、commitlint、配置模块格式、CI 运行器和代码托管平台。任何一层升级都可能让原来通过的历史突然失败。
建立版本与假设清单
升级前记录实际执行来源:
git --version
git version --build-options
node --version
npm --version
npx --no-install commitlint --version
git config --show-origin --show-scope --get-regexp '^(core\.hooksPath|commit\.template|receive\.)'
git rev-parse --show-object-format
git fsck --strict重点搜索脚本中的隐藏假设:硬编码 master、40 位对象 ID、固定 Hook 目录、只支持 SHA-1 的正则、依赖交互式 shell profile、使用浮动容器标签、运行时在线安装依赖,以及把某个平台当前默认行为当成永久协议。
Git 官方维护 BreakingChanges 作为潜在破坏性变化入口,并明确大版本没有固定发布时间。团队应根据候选版本和实际发行说明做行为回放,不写死传闻中的升级日期。commitlint 升级则要同时回放配置加载、规则默认值、Node.js 模块模式和共享配置依赖。
用双轨规则完成迁移
规则从 v1 迁移到 v2 时,可以让 CI 同时运行两版:v1 继续阻断,v2 只记录差异。待误报、机器人路径和历史存量处理完成后,再把 required check 从 commit-policy-v1 切到 commit-policy-v2。切换后保留旧版可执行包和配置一个回滚窗口,而不是立即删除。
若新规则误伤生产修复,回滚顺序应是:先把服务端强制点退回上一版或观测模式,恢复关键写入;再修复本地 Hook 和 CI;最后分析差异并补测试。只回滚仓库里的配置而不修改平台 required check,仍会让所有合并卡住。
停用时清理所有引用关系
删除一个 Hook 文件不等于完成退出。还要盘点并清理:
各 clone 的 core.hooksPath 与提交模板配置。package 依赖、锁文件、共享配置和缓存镜像。CI 工作流、required check、Ruleset、Push Rule 和服务端 Hook。
bypass actor、机器人 Token、服务账号和审计告警。策略目录、运行手册、监控面板和历史证据的保留责任。
先停止新增依赖,再切换服务端强制点,确认没有关键引用仍消费旧结果,最后移除反馈层。历史审计证据按保留策略处理,不随工具卸载一起抹掉。
故障证据如何反推真正断点
仓库有 Hook,违规提交仍然成功
先运行:
git config --show-origin --get core.hooksPath
git rev-parse --git-path hooks
git hook list --show-scope commit-msg若当前 Git 不支持 git hook list,检查 git hook -h 和实际 Hook 目录。常见原因是 clone 未显式启用、脚本没有执行权限、依赖未安装或用户使用了 --no-verify。修复本地体验后,仍要以 CI 和服务端反例证明关键引用不可绕过。
CI 只看最后一条,违规 Commit 藏在中间
用 git log --format='%H%x09%s' <base>..<head> 与 CI 实际 --from/--to 对比。若 merge base 缺失,检查浅克隆深度;若区间正确但仍漏报,确认配置加载位置与 commitlint 版本。回归样例必须保留“违规中间提交 + 合规 tip”。
required check 显示成功,错误 SHA 却被合并
检查状态是否绑定当前 Pull Request head SHA、工作流名称是否唯一、规则是否处于 active/enforced、合并队列是否触发同一检查,以及管理员或 App 是否在 bypass 列表。配置文件存在只能证明流程被描述,不能证明平台把它当准入条件。
服务端脚本手工成功,真实 push 失败
接收 Hook 在 $GIT_DIR 中执行,输入来自 stdin,新对象位于 quarantine 环境。记录工作目录、受控 PATH、old/new/ref 和退出码,但不要把环境变量全集返回客户端。通过真实 push 验证允许与拒绝路径,不要只 ./pre-receive 空跑。
门禁启用后机器人、网页编辑或合并队列阻塞
按 source_channel 拆分证据,确认最终 Commit 由谁生成、是否签名、消息格式如何形成。不要为所有机器路径开放管理员绕过;为每种受信主体定义最小规则、测试夹具和失效时间,未知来源默认拒绝。
规则升级后全部仓库报告配置为空
检查 Node.js 模块模式、commitlint.config.js/.mjs、执行目录、共享配置解析和锁文件。先在影子仓库回退到旧版恢复服务,再按官方升级说明修复模块边界,不能通过 CI 临时安装浮动最新版掩盖差异。
架构师需要守住的深水区
接收端门禁是高价值代码执行面
服务端 Hook 继承 Git 服务进程权限,既能阻断全员 push,也可能读取服务器文件。脚本和依赖必须固定来源、评审、校验、最小权限和可回滚;仓库维护者不能直接修改。同步调用公网服务会把网络抖动放大成代码仓库不可写,外部分析宜异步产出状态检查,再由保护规则消费。
完整提交集合计算比 old..new 更复杂
已有分支的 old..new 容易理解,新分支没有 old tip;多引用和 atomic push 又改变“哪些对象在事务后可达”。简单 rev-list new 可能重扫全部历史,new --not --all 则要理解接收事务内的引用可见性。生产实现必须在 receive-pack 的 quarantine 语义下覆盖创建、删除、Tag、多引用、并发和强推;复杂需求优先采用成熟平台规则,避免无限扩张自制 shell。
规则成本会随 push 体量和仓库数量放大
逐 Commit 启动 Node.js、扫描全部历史或访问工单 API,会直接增加 push 延迟。门禁应只计算本次新引入对象,批量调用规则引擎,缓存由对象 ID 决定的纯函数结果,并设置 CPU、内存、对象数和超时预算。容量结论基于实际仓库分布和压测,不使用一个脱离上下文的“最大提交数”。
签名与平台徽标存在时间和身份边界
签名验证依赖密钥、邮箱、平台账户和验证时点。密钥轮换后,平台保存的历史 Verified 状态与组织当前信任名单可能不同;机器人和平台生成 Commit 也可能由平台密钥而非原作者签署。架构设计要明确验证的是提交者、作者、平台合并器还是发布主体,并为离职、密钥泄漏和重签迁移准备处置路径。
审计数据本身也受隐私和成本约束
长期保留对象 ID、账号、IP、仓库和规则事件会产生存储、检索、访问审批与个人数据治理成本。只收集能回答控制问题的字段,按风险设置保留期和分层存储,导出链路要校验完整性。既不能为了省钱只留“最后状态”,也不能把所有 Hook 环境和源码写进日志。
团队自检
每条策略都有 ID、目标引用、检查对象、失败证据、owner、例外和回滚条件。commitlint、共享配置与锁文件进入版本控制,CI 使用已评审依赖而非浮动最新版。正向消息退出码为 0,反向消息返回明确规则;配置未加载会被测试发现。
commit-msg 只承担快速反馈,团队已真实证明 --no-verify 可以绕过它。CI 检查 merge base 到 head 的完整区间,浅克隆无法证明区间时不会静默放行。必需状态绑定当前 SHA、唯一检查名称和可信来源,合并队列也执行同一门禁。
自托管接收端遍历本次引入的所有 Commit,并覆盖新建、删除、强推、多引用和 Tag。托管平台规则已核对实际版本、许可、继承语义、fork/机器人/网页编辑和 bypass 路径。消息规范、签名、DCO、审批和状态检查分别建模,没有把一个徽标当成全部证明。
审计事件能关联主体、来源通道、old/new OID、引用、策略版本、决定和例外。bypass 小于管理员全集,限仓库、限规则、限时间,并有审批、到期撤销和事后复核。门禁超时的 fail-open/fail-closed 决策经过演练,接收路径不依赖不稳定公网同步调用。
升级回放覆盖 Node.js 模块加载、commitlint 规则、Git 对象格式、Hook 环境和平台写入路径。规则迁移采用观测、双轨、切换和回滚窗口,required check 与服务端规则同步变更。停用时会清理本地配置、依赖、CI、平台规则、服务端 Hook、机器人权限与监控,同时保留必要审计证据。
