
no-mistakes PR 强制执行门禁共享 composite action 的设计、验证与迁移实战【免费下载链接】no-mistakesgit push no-mistakes项目地址: https://gitcode.com/GitHub_Trending/no/no-mistakes导读本文深入剖析 no-mistakes 项目中PR must be raised via no-mistakes门禁的共享实现——require-no-mistakescomposite action。它负责验证 PR 是否通过git push no-mistakes流水线提交、PR 正文是否携带签名行与绑定到当前 head 的 v1 pipeline-step attestation、以及review/test/document三步是否全部completed。读完本文你将掌握该 action 的判定逻辑、输入输出契约、为何必须固定 pin 而非main、如何理解 live lookup 的防过期机制以及迁移其他仓库时需要注意的坑位与验证手段。一、背景从内联门禁到共享 action在 no-mistakes 中所有 PR 都必须通过流水线提交。此前各强制执行仓库在自己的 workflow 里内联复制一段门禁 shell 脚本导致脚本在各仓库间漂移——verify.py模块注释明确指出多处内联副本从未获得head_sha绑定这正是共享 action 存在要解决的问题见 verify.py。共享实现将PR must be raised via no-mistakes门禁收敛为一处实现文件verify.py判定逻辑的唯一实现action 清单action.ymlcomposite action 定义action 使用说明README.md本仓库自己的调用者no-mistakes-required.yml从源码结构看共享 action 的核心设计意图有三个一是消除内联副本漂移二是把必须完成哪些步骤固化在实现里让任何调用者都无法在保持同名检查的前提下悄悄削弱门禁三是让调用者只能配置谁豁免而不能配置检查什么。二、判定规则一个纯函数式的四步验证verify.py的模块注释将判定定义为一个纯函数输入是 PR 正文与 PR 当前 head SHA输出是合规与否verify.py。判定顺序如下签名行PR 正文必须携带 no-mistakes 签名行Updates from [git push no-mistakes](https://github.com/kunchenguid/no-mistakes)定义于 verify.py可解析的 attestation 注释正文必须携带!-- no-mistakes-pipeline-attestation:v1 {...} --形式的注释head 绑定attestation 内的head_sha必须等于 PR 当前 head SHA防止后续 push 利用旧 attestation 蒙混过关必需步骤完成review、test、document三条记录的status必须全部为completed。配额跳过quota skip与 agent 跳过均不视为合规。REQUIRED_STEPS (review, test, document)在 verify.py 中是固定常量刻意不作为 action 输入暴露——这正是调用者可配置谁豁免、不可配置检查什么原则的代码体现。2.1 签名行与版本下限缺失签名行时check_signature输出::error::This PR was not raised through no-mistakes.并提示查看 CONTRIBUTING.mdverify.py。缺少或无法解析 attestation 时fail_missing_attestation会报告版本下限VERSION_FLOOR 1.46.0对应 PR #670并展示期望的注释格式verify.py。即旧版本 no-mistakes 只写签名行是不够的。2.2 head 绑定修复漂移副本缺失的关键一环check_head_bind的注释直言Without this the gate certifies a body, not a commitverify.py。attestation 的head_sha与当前 PR head SHA 不一致时门禁直接失败提示重新执行git push no-mistakes以重新绑定。这个绑定在流水线侧由 prsummary.go 的pipelineAttestation结构生成其HeadSHA与Steps字段正是验证方所解析的 JSON 内容而测试 ci_attestation_test.go 验证了旧 head 的过期 attestation 必须失败、新 head 的 attestation 可重新绑定的完整闭环。2.3 步骤判定LAST-WINS 是有意设计check_required_steps将同名步骤记录折叠成一张status_by_step映射重复记录后写覆盖先写LAST-WINS一个completed记录旁的skip 形状兄弟字段则刻意不被检查verify.py。迁移前某些内联门禁更严格要求同名的每条记录都completed但按 skill 文档的明确说明这种严格性不是标准迁移到 LAST-WINS 是有意的预期结果而非回归。未经 owner 决策不要加固这一点。缺失、非completed状态都会进入incomplete列表并在错误消息中逐一列出例如review (statusskipped)。三、Live Lookup为什么必须读取实时 PR 状态verify.py的模块注释用较长篇幅描述了一个真实事故场景verify.pyGitHub Actions 的job rerun 会重放原始触发时归档的 event payload而不是投递新事件因此重跑一个旧失败 run会用全新时间戳复现其过期判定GitHub 自身的 required-check 视图与本仓库的检查折叠逻辑collapseLatestByNamegithub.go都会把该结果当作当前状态导致一个本来已绿的提交旁永久挂着一个过期 FAILURE且不换新 SHA 就无法干净恢复。解决方式只要存在 GITHUB_TOKEN 与 PR 号live_pr_facts就通过 GitHub APIGET /repos/{repo}/pulls/{number}实时读取当前 body 与 head SHAverify.py超时 10 秒LIVE_LOOKUP_TIMEOUT_SECONDS显式传入pr-body/pr-head-sha输入时显式值永远优先且不会被 live lookup 或 event payload 覆盖verify.py关键行为fail-closed。当 live lookup 是必需的未转发显式输入却失败时main()直接让整个门禁失败关闭绝不回退到缓存的 event payload——因为用缓存 payload 评估合规恰恰就是上述过期漏洞本身verify.py。错误消息会指导调用者添加pull-requests: read或转发显式输入。四、Action 契约输入、输出与安全边界4.1 输入参数下表依据 action.yml 与 README.md 整理输入默认值用途pr-body取事件 payload待判定的 PR 正文普通pull_request调用者无需传入pr-head-sha取pull_request.head.sha当前 PR head 提交attestation 必须绑定到它pr-head-ref取pull_request.head.refhead 分支名用于匹配exempt-head-branchespr-author取pull_request.user.login作者登录名用于匹配exempt-authorspr-number取pull_request.number仅用于日志输出exempt-authors换行或逗号分隔的作者登录名单命中即豁免如github-actions[bot]代 release-please 开 PR豁免按仓库显式开启exempt-bot-authorsfalse为 true 时所有以[bot]结尾的作者登录名豁免比exempt-authors更宽默认关闭exempt-head-branches换行或逗号分隔的 glob 模式head 分支命中即豁免面向release-please--*这类结构性自动化分支github-token${{ github.token }}live lookup 用 token转发环境 token 本身不授予任何额外权限只使用调用方permissions:已允许的能力刻意不是输入的项哪些步骤是必需的REQUIRED_STEPS。调用者只能配置谁豁免不能配置门禁认证什么因此无法在保持同名检查的同时削弱门禁。4.2 输出输出含义compliant仅当 PR 正文结构上满足流水线门禁时为true豁免时保持false绕过 ≠ 验证通过exempt命中配置的豁免时为trueexempt-reason豁免原因人类可读被判定时为空实现细节exemption_reason按顺序检查作者名单、bot 名单、head 分支 globverify.py 中 verify.py豁免时main()输出exempttrue、compliantfalse并返回 0verify.py——豁免是显式调用方策略而非流水线跑过并满足的证据两个输出刻意保持区分。4.3 安全边界不检出、不执行、只读action 从不在 job setup 之外检出或执行仓库代码因此对 fork 的pull_requestrun 是安全的调用方应保持permissions: contents: read停留在pull_request而非pull_request_targetlive lookup 只用转发 token 执行GET /repos/{owner}/{repo}/pulls/{number}从不请求或要求写权限若调用方既省略pull-requests: read又不转发显式pr-body/pr-head-sha则每次运行都 fail-closed绝不静默降级到更弱的保护模式。4.4 非目标贡献者护栏而非防伪造边界必须强调的边界README.md 与 verify.py 均有明确声明attestation 是发布在 PR 正文中的确定性、提交绑定的声明不是密码学签名作者可以手写正文并复刻文档格式从而通过本检查——这是已知且被接受的限制且是从内联门禁原样继承的既有限制收敛共享实现并未引入或扩大它该 gate 可靠捕获的是它存在的原因意外绕过流水线的贡献者、格式错误或不完整的声明、被后续 push 遗留的过期 attestation有签名的 attestation 是稳健的修复方向已在 backlog 项nm-signed-attestations-r1中单独跟踪明确不纳入本文件。每次结构性通过时action 都会输出::warning::提示PR-body attestation 是作者可编辑的并非 no-mistakes 产物的密码学证明verify.py。五、调用者实践标准用法与本仓库自证5.1 最小调用示例来自 README.mdname: Require no-mistakes on: pull_request: types: [opened, edited, reopened] branches: [main] permissions: contents: read pull-requests: read # required unless the caller forwards explicit pr-body/pr-head-sha: the gate fails closed without it jobs: check: name: PR must be raised via no-mistakes runs-on: ubuntu-latest steps: - uses: kunchenguid/no-mistakes/.github/actions/require-no-mistakesrelease-tag-or-sha with: exempt-authors: | github-actions[bot] dependabot[bot]要点job 名必须严格保持PR must be raised via no-mistakes分支规则集ruleset才能跨仓库持续匹配同一个检查普通pull_request调用者不转发任何 PR 事实action 从事件 payload 读取 body、head SHA、head 分支、作者与编号仅当从其他事件驱动时才传pr-*输入uses:必须固定到 release tag 或 commit SHA绝不使用main——main正是被判定中的 PR 可编辑的引用。5.2 本仓库的自证调用者no-mistakes-required.yml 是 action 的瘦调用者固定到已发布的 commit SHA32d396ac0f29135daf7fcb9964aba9d5f4e796d6注释post-v1.57.1untaggedaction 在 PR #819 引入触发器types: [opened, edited, reopened]——刻意不含synchronize详情见下一节branches: [main]paths-ignore排除.release-please-manifest.json与CHANGELOG.mdpermissions: contents: readpull-requests: read豁免放在job 级if:github-actions[bot]、dependabot[bot]、release-please[bot]而不是 action 的exempt-authors输入。原因job 内豁免仍要求 run 启动而 GITHUB_TOKEN 发起的 PR run 以action_required创建、永不启动paths-ignore条目也是同理。没有该约束的仓库应优先使用 action 的输入。5.3 concurrency保持 body 事件身份唯一workflow 使用 concurrency groupconcurrency: group: no-mistakes-required-${{ github.event.pull_request.number }}-${{ (github.event.action opened || github.event.action edited) github.run_id || head-change }} cancel-in-progress: trueGitHub concurrency group 即便cancel-in-progress: false也只保留一个 pending run。这里为opened/edited携带 body 的事件提供基于 run_id 的不可变分组避免首次 fork 审批场景下 opened/edited 检查被互相取消reopened则保留原有合并语义。测试 workflow_no_mistakes_required_test.go 专门验证了body 事件分组必须唯一、reopened 分组保持合并。六、触发器陷阱为什么去掉synchronizeskill 文档与 workflow 注释共同说明了一个易踩的坑no-mistakes-required.yml判定是pull_request.body的纯函数push 不携带新 body却会移动 head SHA流水线的执行顺序是先 Push 再写确定性## Pipeline段PR step因此synchronize会把一个 FAILURE 检查 run 钉在本 run 即将修复的 body所对应的新 head 上GitHub 会把该失败保留在后续editedSUCCESS 旁边而非替换它而gh pr checks仅按startedAt折叠同名 check run——于是流水线自己的 CI monitor 可能读到过期失败把 run 永远挂红历史事故 PR #773。因此获得 head_sha 绑定的调用者必须同时从on.pull_request.types移除synchronize。该变更仅在没有 ruleset 或分支保护强制要求该检查时才安全——否则 push 后的 head 没有 run强制要求会永远阻塞合并。验证方式skill 文档给出gh api repos/owner/repo/rulesets gh api repos/owner/repo/branches/branch/protection在 fleet 迁移当时treehouse、sshhip、wheelhouse因强制要求该检查而保留synchronize。七、固定 Pin 即自证守卫本仓库 gate 固定到已发布的 commit SHA这一固定本身构成自证self-certification守卫README.mdGitHub 在 job setup 时下载uses:引用的 action因此pin 必须总是指向已携带 action 的 ref固定到早于 action 存在的 tag 会在每个 PR 上 fail-closed修改 action 的 PR 在其自身 head 上被完整测试本仓库 Go 测试执行工作区中的verify.py而判定该 PR 的必需检查运行的是已发布的固定副本——于是该变更无法重写自己的裁判升级 pin 是单独的、有意的 PR。八、迁移仓库不只是一处文件替换skill 文档明确警告迁移仓库很少是一处文件替换测试中提取并执行内联run:块的仓库extractGateScript()辅助函数及其 gate 测试在块消失后会于 import 处断裂仓库级AGENTS.md中让 agent 从兄弟仓库手工复制门禁的说明必须重写——手工复制正是共享 action 要消除的漂移来源迁移规则与自证相同固定已发布的 tag 或 commit SHA绝不main迁移到 head_sha 绑定时同步移除synchronize先按上文命令逐仓库核对 ruleset/branch protection。九、行为由测试固定两个根级测试共同固定了 action 与调用者的行为require_no_mistakes_action_test.go像 runner 一样执行 verify.py覆盖所有判定路径TestRequireActionEnforcesTheGate、豁免表面TestRequireActionExemptions作者豁免、逗号分隔名单、bot 豁免、分支 glob、人类作者必须被判定、live-lookup 失败的 fail-closed 路径并断言 action 必须暴露三个豁免输入与compliant/exempt输出TestRequireActionIsAComposite。测试还剥离了三个环境中的 live-lookup 变量避免测试误调真实 GitHub APIrequire_no_mistakes_action_test.go。workflow_no_mistakes_required_test.go从调用者视角固定——不可变 SHA pinimmutableActionPin为 40 位十六进制正则、单一委托步骤、豁免、触发器、concurrency 身份、fork 边界无pull_request_target、无 secrets、无 checkout、无写权限并通过事件 payload 驱动真实 action 完成端到端断言如 head 绑定失败消息包含head_sha与does not match。贡献者契约由 CONTRIBUTING.md 拥有所有 PR 必须通过no-mistakes提交若修订 PR 描述需保留生成的## Pipeline段整体替换正文会移除签名或 attestation使必需检查失败直到 no-mistakes 重新写入该段。十、故障排查速查现象原因与处置错误This PR was not raised through no-mistakes.正文缺签名行请用git push no-mistakes提交并让流水线重写## Pipeline段错误提示 no-mistakes 1.46.0缺或无法解析 attestation 注释旧版本只写签名行不够升级 no-mistakes 并重跑错误attestation head_sha does not match the current PR head后续 push 使 attestation 过期重新git push no-mistakes以绑定到当前 head错误Required no-mistakes pipeline steps are not completedreview/test/document存在缺失或非completed状态配额/agent 跳过不算合规错误提示需要pull-requests: read或显式pr-body/pr-head-shalive lookup 失败且为必需fail-closed请补齐权限或转发显式输入调用固定到早于 action 的 tagjob setup 下载失败所有 PR fail-closedpin 必须指向已携带 action 的 ref改 action 的 PR 无法自证预期行为必需检查运行固定副本修改自身 head 仅被测试升级 pin 需单独 PR结语require-no-mistakes共享 action 把PR 必须通过 no-mistakes 流水线提交这一门禁收敛为单一实现纯函数式四步判定、head_sha 绑定防过期、live lookup 防 rerun 陈旧、fail-closed 拒绝降级、固定 pin 自证防改写同时把豁免谁可配置与认证什么固化常量清晰分离。无论是本仓库的瘦调用者还是 fleet 内其他强制执行仓库遵循 pin 固定、移除synchronize、保持 job 名不变这三条纪律即可安全复用这套门禁并借助根级 Go 测试在每次改动时获得行为级回归保护。【免费下载链接】no-mistakesgit push no-mistakes项目地址: https://gitcode.com/GitHub_Trending/no/no-mistakes创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考