release-pipeline
Skill0 starsChanged 46 days agoMITView on GitHub
把本地的东西整理成正确的版本、发布到 GitHub、逐项证明「线上与本地一致」,并让它在发布之后长期不腐坏。当需要把本地仓库上传到 GitHub、推送新版本与 tag、配置 CI 与自动 Release、为历史 tag 补建 Release、排查推送失败(认证 / 网络 / 权限)、核验线上产物,或手上只有一堆号位可疑的版本快照、需要先判出每个快照的真实版本号再重建成带 tag 的仓库时使用;也用于给已经发布出去的项目补上那些「枯燥但必须做」的长期维护配置 —— 信任发布、Action 固定到不可变引用、权限最小化、依赖更新分组、陈旧 issue 处理、低质贡献的应对政策。涵盖可发布性自检、快照号位取证、网络通路分层诊断、令牌权限配齐、凭据最小化、精确 ref 推送、CI 检出坑、发布物可由 tag 重现、推送之后的九项回验 + 四项附带核验(含平台安全设置只读核验)、发布之后的长期维护负担,以及发布物出了问题时的撤回与作废决策(下架 + 公告,不是删除)与发布前的隐私扫描。
- Commits and pushes
2 other projects use a version of this.
Adopted by
Most recently taken up by
- nesquena/steward-ostheir own version, first seen Nov 3, 2026
- NinjaSln-labs/agent-skillstheir own version, first seen Oct 2, 2026
Other versions2 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 2
- nesquena/steward-os★ 76same name
- NinjaSln-labs/agent-skills★ 1same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repositoryOne other file it gives its agents.
- AGENTS.mdAGENTS.md
The file
Files
发布流水线
把本地的东西变成线上一份可核验、且能长期活下去的发布:整理成正确的版本号 → 推上 GitHub → 让 CI 与 Release 自动跑 → 逐项证明「线上与本地真的一致」→ 再把「发布之后那些枯燥但必须做」的配置一次性配掉,让它不靠人记住也不会腐坏。
何时用
- 「把仓库传到 GitHub」「推上去」「上传到远端」
- 「推送报错」「推不上去」「认证失败」「refusing to allow a Personal Access Token…」
- 「配 CI」「打 tag 自动发 Release」「给历史版本补 Release」
- 「核验线上和本地一不一致」「刚才那下到底推成没成」
- 「这个包有什么问题」「版本号改成几点几」「重排版本历史」「帮我看看合不合格」
- 「发出去之后还要做什么」「依赖更新太吵」「issue 堆成山」「怎么防低质贡献」
先判断手里的东西是哪一种
这一步决定后面要走多长的路,别跳过:
| 输入形态 | 特征 | 走法 |
| 已是一个 git 仓库,只是想发出去 / 推新版本 | 仓库根有 .git,历史是线性的、tag 与内容对得上 | 直接进 阶段 2 |
| 一堆并列的版本快照(文件夹或 zip) | 号位来自「第几个保存的」而不是内容;序列可能有空洞 | 先做 阶段 1 的号位取证,再重建仓库 |
| 一个普通的软件项目(非 Agent Skill) | 有源码与构建脚本,但没有 SKILL.md 这种包结构 | 同样进阶段 2;只是阶段 2 的「命名三条」等 Skill 专属项可略过 |
「命名三条一致」「
name禁止点号」这类判据只对 Agent Skill 包成立, 换成普通项目时按你的项目自有约定替换,不要照搬。
先判断这个包要交给哪个渠道
同一批文件,交给不同的地方,规矩不一样 —— 而且两套规矩都硬,各有平台级报错作依据。 混起来就会出现「同一个问题、两个技能各说各话」,也会让自检给出假 FAIL。
| 渠道 | 版本号写在哪 | 形态 | 描述字段 | 同号位能否重发 |
| 发布到 Git 托管平台(Release) | metadata.version + git tag | 主.次 | 无硬约束 | 能,但会毁掉「一个号位一份内容」 |
| 上传到技能平台 | 表头顶层 version | 主.次.修订 | 每个 ≤ 1000 字符 | 不能,必须递增 |
- 两条渠道都走时要写两套编号,并显式绑定(如 tag
3.1↔ 平台版本3.1.0)—— 绝不让两个编号各自漂移,那正是「一堆号位对不上」的成因。 - 自检脚本按表头形态自动分流,也可用参数强制指定属于哪个渠道。
- 完整对照、以及「哪些问题该归哪个技能答」,见
references/channel-boundaries.md。
别做的事:把两套规则里的一套当成「更对的」,去统一另一套。 它们回答的不是同一个问题 —— 一个说「tag 叫什么」,一个说「上传时
version字段填什么」。
十二条铁律
- 推送成功 ≠ 推送正确。 每次线上写操作之后都逐项回验,不凭「应该没问题」收工。
- 断言打在内容层,不打在容器层。 比的是逐文件 Git 对象指纹
sha1(b"blob <len>\0" + data),不是 zip 文件的哈希 —— 后者随构建机器所在时区变, 会得到一个随机变红的流水线。 - 只推精确 ref,不用
--tags。 多做的动作就多一分意外。 - 令牌只走一次性凭据文件,推完立刻删除,删后逐处复查残留。
- 绝不把关键写操作接到可能失败的过滤器上。
git push … | sed …里过滤器一报错, 管道炸掉,git push的输出被吞,只剩exit 1+SIGTERM—— 极易误判为推送失败。 - 历史运行是只读快照。 平台上不重算历史 run,修好之后那次失败仍显示红色。
判断现状只看三件事:最新一次 run 的
conclusion、它的head_sha是否在当前分支祖先链上、 同一内容是否已有过一次success。 - 已发布的内容只顺延号位,不就地改。 只有「同一版本的收尾」才用
amend并进 (三条判据见阶段 6)。平台层的 Immutable Releases 已经把这条纪律变成强制,见阶段 8。 推错了也一样:只用「下架 + 公告」,绝不删除已发布的号位(见「发错了怎么办」)。 - 归档冻结。
history/里的旧快照与已发布的旧 tag 一律不动 —— 改了会破坏 「tag ↔ 归档快照逐文件一致」这条可验证保证,也等于伪造历史。 - 升级 CI 依赖取「刚好消除弃用警告的最小大版本」,不追最新 —— 大版本跳跃会带入 与本次修正无关的行为变化,而在无法本地复现 CI 的前提下,最小行为差就是最小风险。 这与「把 Action 固定到完整 SHA」不矛盾:前者决定选哪个版本, 后者决定用什么形态引用。两者同时满足 = 钉在这一档大版本所指向的 SHA 上, 再由更新机器人负责升 SHA。
- 权限一次配齐。 classic token 勾
repo+workflow,细粒度 token 勾Contents: RW+Workflows: RW。漏了不会立刻报错,而是「一半成功、一半被拒」。 - 发布物必须可由 tag 重现。 资产从 tag 现算(
git archive),不手工打包 —— 否则「我验收过的」和「CI 实际发布的」根本不是同一个东西。 - 能变成流水线里一步的,绝不留给记性。 靠人记得的检查一定会漏,而漏掉的代价 通常半年后才出现。枯燥不等于低回报 —— 这类事防的是灾难(见阶段 11)。
流水线总览
阶段 0 形态判断 手里是仓库,还是一堆号位可疑的快照?
├─ 快照堆 → 阶段 1 号位取证:三路证据判真实版本 → 重建 tag 历史
└─ 已是仓库 → 直接进阶段 2
阶段 2 可发布性自检 结构 · 自检器 · 测试 · 元数据 · 无环境残留
阶段 3 网络通路 分层定位 DNS → TCP → TLS(SNI) → HTTP → 认证
阶段 4 令牌与权限 repo + workflow,一次配齐,用完即吊销
阶段 5 凭据最小化 一次性凭据文件:不进 config / 不进 URL / 不回显
阶段 6 推送 只推精确 ref;先 main 后新 tag;分清单个 tag
阶段 7 CI 校验与发布分开;先跑一遍确认绿了再往下走
阶段 8 历史 Release 幂等回填;不可变发布 —— 发出去就锁死
阶段 9 九项回验 ref 对齐 · 历史零变动 · 逐文件指纹 · CI · 资产
阶段 10 留档 把踩坑也写进去,不美化
阶段 11 长期维护 一次性配掉能配的;剩下的交给机器定期跑
(发布之后持续生效,不是一次性的)阶段 1 · 快照号位取证(只在输入是快照堆时走)
核心问题:快照的目录名与外层文件名经常是错的(「另存为」只改外层名)。 所以号位必须按内容定,不能按保存顺序定。
三条判据合起来用,三路证据一致才下结论:
| 证据 | 做法 | 能回答什么 |
| 文件集合 | 比 same/added/removed/changed 计数 | 谁是谁的后继 |
| 特征串探针 | 搜只有某一版才有的字符串 | 内容属于哪个开发阶段 |
| 相似度 | 行级 difflib.SequenceMatcher 的 ratio | 分支点在哪 |
四条纪律:判定标准前后一致(用同一把尺子量所有快照)、号位序列不能有空洞 (有空洞通常是错标的信号)、改号位只动标签维度(不改正文内容)、 作废的号位不复用(否则旧引用会指向错内容)。
然后用 git tag 表达版本历史,不要并列文件夹 —— 并列文件夹会在平台上出现 N 份重复副本。
完整做法(含三路取证的操作细节、填实名时「标识符 vs 说明性句子」的分辨、
逐条复核外部评价的三分类、交付包文档层体检)见
references/audit-and-rebuild.md。
阶段 2 · 可发布性自检
推送之前先把「一个陌生人 clone 下来会碰到什么」过一遍。 在临时副本里跑,交付仓库一字节不碰。
| 组 | 检查 | 口径 |
| 链接 | 全部 .md 的相对链接与同文件/跨文件锚点 | 断链 0、悬空锚点 0 |
| 数字 | 文档与徽章里的数量声明 vs 实际 | 受控文件数、测试数、代码注释里的数字也算 |
| 元数据 | 版本号出现的每一处:包清单、引用文件(version 与 date-released)、README 徽章 | 只有一处跟着走是常态,其余全停在旧版 |
| 安装 | 复制成以包名命名的目录 → 跑自检 → 跑全部测试 → 编译检查 | 在旧目录名里绿不算数 |
| 身份 | 真实邮箱、占位符字面量、示例域名、TODO/FIXME | 要区分「历史引用」「示例语境」「真残留」三类 |
| 洁净 | 无编译缓存,git status 干净 | 忽略规则会遮蔽 git status,要用磁盘实际文件数对比受控文件数 |
关键的四条:
- 声称支持的版本必须由 CI 兑现。 文档写「支持 X 及以上」,CI 就得真跑那个版本; 跑不了就退而在文档里写清「按语法与标准库静态核查,CI 覆盖 A/B」,别留一句无凭据的断言。
- 命名三条必须完全一致:发布物文件名 / 仓库目录名 / 包清单里的
name(Skill 包是SKILL.md的name)。注意name禁止点号,所以2.0在name与目录名里写作2-0。 - 提交前删掉
.git/index强制重哈希。 体积相同 + 修改时间相同的文件会命中 stat cache,git add -A不会重新哈希,于是提交里留着上一版的内容, 而git status还显示「干净」。git status只是线索,不是证据。 - 行尾与
.gitattributes要同步:每新增一种文本文件类型,就在同一个提交里 给它补上显式的eol=lf,别指望* text=auto兜底(原因见阶段 8)。
有一条容易被忽略的自检项:包内不得残留某个执行环境的专属值 (绝对路径、IP 字面量、某台机器才有的端口与网络事实)。 这类内容换一台机器就变成误导,且往往被当成「无关紧要的说明」长期没人核。 本仓库的
scripts/selfcheck.py把这条做成了机器判据。同组还有一条发布前隐私扫描(凭据与个人信息)—— 它针对的是 「公开即已泄露、事后撤回治不了本」,见「发错了怎么办」那一节。
首次发布还要处理仓库的「门面」(可见性、许可、主题、简介、默认分支)
与身份设置(提交邮箱与贡献图的对应关系),见
references/first-publish.md。
阶段 3 · 网络通路
先分层定位,再选解法 —— 别一上来就写重试脚本。 判据阶梯(每层都能独立证伪上一层):
- 一次探针定全局:
git ls-remote <仓库URL> HEAD。能返回 sha ⇒ TLS 与读权限都通 (公开仓库这一条不需要认证)。失败则继续往下分层。 - DNS:解析结果是否落在服务方自己公布的网段里。权威依据在服务方的元数据端点 (现查,别背)—— 解析到该服务之外的地址段,说明 DNS 被改写。
- TCP:拿真实边缘地址做
connect_ex((地址, 443)),区分「连不上」与「连上但被切断」。 - TLS / SNI:同一个地址,分别用
server_hostname=<域名>与server_hostname=None握手。前者被重置、后者成功 ⇒ 阻断发生在 SNI 层(域名没发出去时反而通得过)。 - HTTP:直连真实地址时手工补
Host:头,看是否拿到正常状态码。 - 认证:走到这一步还失败,才是令牌 / 凭据的问题(进入阶段 4、5)。
节点不可达时,git ls-remote 会给出比通用 HTTP 客户端更可信的结论;
网络类命令要在无沙箱模式下执行,因为沙箱可能把出口改道,让客户端拿到占位 200 或 502。
解法按环境分三类(细节与命令见 references/network-diagnosis.md):
| 环境 | 做法 |
| 直连可用 | 直接推,不做任何证书配置 |
| 需要 HTTP(S) 代理 | 给 git 配 http.proxy,或用环境变量;用「禁用代理」做对照 |
| 出口存在中间人 | 把中间人的根证书合并进信任链 + 切不默认做吊销检查的后端,不要关闭证书校验 |
都不行时的兜底:换一条通道(平台连接器 / 让使用者在自己的终端推), 并如实说明代价 —— 某些通道只能按「写文件」方式推送,没有建 tag 的能力, 提交时间也会变成当前时间,于是「N 个 tag ↔ N 个归档快照逐文件一致」这条保证守不住。
探测时要注意:一个能显示内网地址的中间件(如本地代理)会让「能读到页面」与 「能推送」变成两件事。 读权限通不代表写权限通,见阶段 4。
阶段 4 · 令牌与权限
推送含 .github/workflows/ 的内容需要 workflow 权限,否则被拒:
refusing to allow a Personal Access Token to create or update workflow
'.github/workflows/validate.yml' without 'workflow' scope症状有欺骗性:不含工作流的旧 tag 会成功,含它的新 tag 与主分支失败, 仓库看起来「一半有内容一半没有」。所以一次就把权限配齐,别等报错再补。
| 目的 | classic token | 细粒度 token |
| 推代码 / 推 tag | repo | Contents: Read and write |
推 .github/workflows/ | workflow | Workflows: Read and write |
| 建 Release + 传资产 | repo 已含(Contents: write) | Contents: Read and write |
| 读 / 发包管理器产物 | write:packages / read:packages | 同名,缺了 API 一律 403 |
三条纪律:
- 失效与吊销是两回事。 补权限可以直接更新已有令牌(值不变);吊销后不可恢复,
且 classic token 上那个按钮叫
Delete,不叫 Revoke —— 提醒使用者时要说准。 - 绝不用密码做 git 认证。 平台自 2021-08 起已停用密码认证,密码对
git push无效。 使用者若在对话里贴了密码,要明确告知它未被使用、未写入任何文件,并建议尽快更换。 贴了令牌同理,用完即吊销;令牌一旦出现在对话或日志里,就按已泄露处理。 - 缺权限时读不到的东西,就说「没读到」。 例如缺读包权限时无法用 API 核实产物列表为空 —— 那就标注「这是界面观察,不是 API 核实」,别把观察写成核实。
两条等价的执行路径:纯
git命令 + 平台的 REST 接口,或官方gh命令行工具。 前者不依赖额外安装、判据更细;后者把建仓、建 Release、看 / 重跑 Actions 都收进一条命令, 少写很多取数代码。先探测再分支(command -v gh有则用之,无则走纯 git 路径), 不要假定它存在。两者在权限上的要求一致。
阶段 5 · 凭据最小化
首选一次性凭据文件:令牌不进 URL、不进 refspec、也不会被 git 回显。
CRED="$(mktemp)" # 临时文件;结束务必删除
printf 'https://x-access-token:%s@github.com\n' "$TOK" > "$CRED"
export GIT_TERMINAL_PROMPT=0 # 非交互环境里防止卡在提示
git -c credential.helper= -c credential.helper="store --file=$CRED" push origin main
git -c credential.helper= -c credential.helper="store --file=$CRED" push origin <新tag>
rm -f "$CRED"三个要点都是踩出来的:
-c credential.helper=是追加而非覆盖 —— 必须先插一个空值把列表清空, 否则系统自带的 helper 选择器与全局凭据管理器仍排在前面,照样弹窗挂起。- 必须配
GIT_TERMINAL_PROMPT=0,否则在无人值守环境里会一直等输入。 - 自写的
timeout N不要超过调用方自身的超时,否则内层没轮到、整条命令被杀, 连后续的读日志都执行不到,会把「命令没能交出结果」误读成「推送失败」。
推完立刻删,删后四处复查:仓库级配置、全局配置、凭据存储文件、仓库对象目录内,
四处均不得出现令牌前缀。细节见
references/token-and-credentials.md。
凭据管理器在桌面系统上会弹 GUI 等人,在无人值守环境里表现为 「零输出 + 被信号中断」。此时
git ls-remote因为不需要认证照样正常返回 ——ls-remote能通 ≠ 凭据可用。 这两个成因(弹窗等人 / 管道炸掉吞输出)症状相同, 分辨法是看有没有旁证:跑一次git ls-remote看远端 ref 是否真的变了。
阶段 6 · 推送
git push origin main # 先推分支
git push origin <新tag> # 再推这一个 tag- 不用
--tags:它会把全部 tag 都尝试推一遍,虽多为空操作,但没有必要。 - 推 tag 会触发发布工作流(若已配置),所以先把主分支推上去, 让校验在「新 tag 之前」先跑一遍,出问题好定位。
- 主分支是快进还是强推,看这一轮有没有重写历史。强推后必须用 「其余 tag 的 sha 与推送前逐一相同」来证明历史没被动过。
并进还是顺延:改已发布内容时的三条判据
只有同时满足三条才可以并进(amend + 强推):
① 仓库刚建、没人克隆过;② 改动未触及对外标识(包名 / 目录名 /
metadata.version / 能力清单 / 对外文档结构);③ 只动少数文件。
- 满足 →
git commit --amend(沿用原GIT_AUTHOR_DATE/GIT_COMMITTER_DATE)→git tag -f <版本>→ 强推主分支与那一个 tag。 - 重写了历史 tag 的内容,或等价于重新发布 → 必须顺延新号位。 否则同一个号位会同时指向两份内容,所有旧引用都变得不可信。 平台层还有一道强制:仓库若开了不可变发布,已发布的 tag 与资产根本改不动(见阶段 8)。
重建历史时若用脚本逐版提交,仓库级
user.*也要写进.git/config, 不能只靠GIT_AUTHOR_*/GIT_COMMITTER_*环境变量 —— 否则事后手工amend会报Committer identity unknown。脚本里用环境变量内联传日期,不要写进配置。
阶段 7 · CI
校验与发布分成两个工作流:建 Release 需要 contents: write,而结构校验只需读权限。
分开就能把写权限限制在真正需要的那条路径上,不放大例行检查的权限。
| 工作流 | 触发 | 做什么 |
validate | push 到主分支 / pull_request / 手动 | 矩阵跑「规范自检 + 编译检查 + 全部单元测试」 |
release | 推 tag / 手动 | 从该 tag 现算 zip,建 Release 并作为资产附上 |
完整模板见 references/ci-and-release.md。三条必须注意的:
- 检出目录名。若自检器里有「
name必须等于所在目录名」这条, CI 上它必然失败 —— 检出目录是仓库名(通常不带版本后缀),而name带。 修法是保住命名纪律:检出动作加path: _checkout→ 读出name→ 改名为同名目录 → 后续步骤统一working-directory。 同一个坑对直接git clone <url>的人也成立,所以这是真实的交付缺陷,不是 CI 抽风。 - 依赖版本取「最小必要大版本」。 工作流里
actions/*的大版本决定它跑在哪个运行时上, 停在旧大版本会带一条警告级的运行时弃用提示,之后按时变成失败。判断方法是 现查该 action 的最新版 + 读 release notes 找破坏性变更,据此挑「刚好够消除警告」的那一档; 当工作流只用到最基础的输入时,升一档、不跳档。 - 验收 CI 修复不能靠「本地跑一遍是绿的」。 本地绿、CI 红正是踩过的坑。正确做法是
复刻检出环境重放:用 YAML 解析器取出那一步的真实脚本 → 在临时工作区按
path:建出检出目录 → 跑那段脚本 → 核对输出变量与目录布局 → 在生成的同名目录里跑后续各步, 同时留一个「旧行为」对照组,确认它仍然失败。
阶段 8 · 历史 Release 回填与「发布出去就锁死」
推 tag 会自动建 Release,但「先有 tag、后才有发布工作流」的那些历史 tag 需要回填。
- 用一个手动触发的开关输入(如
backfill=all)驱动,并让流程幂等 (已存在的 Release 直接跳过),使它可重复触发。 - Release notes 从默认分支的 CHANGELOG 取,不要从该 tag 取:早期 tag 可能 根本没有 CHANGELOG,早期副本也可能被截断过。并做小节映射。
- Release 的时间戳取自各自 tag 的提交时间 ⇒ 回填不会抢走「latest」, 最新版标记会自动落在最新版,不需要额外设置。
- 平台为 tag 自动生成的源码归档不能当安装包:它的顶层目录是
{仓库名}-{tag}, 分隔符是点号;而包名与目录名要求用连字符。所以必须另附一个按包名打前缀的 zip。
⚠️ 不可变发布:发布出去就锁死
平台已有**不可变发布(Immutable Releases)**这一机制:仓库开启后, Release 一旦发布,它的资产与对应的 tag 就被锁定 —— 资产改不了也删不掉, tag 不能被重指、也不能删除。
它把「已发布内容只能顺延号位」从一条纪律变成了平台强制。 所以:
- 发布前的最后一次自检要格外认真 —— 发出去之后就再也没有「就地改一改」这个选项了。
- 想改已发布的内容,唯一的路径是顺延新号位;这与阶段 6 的三条判据方向一致, 只是不再有「并进」这个例外。
- 判定某个仓库有没有开启、以及开启方式,照全文的老规矩:现查官方文档与仓库设置, 不要背端点名与字段名;用 API 读仓库信息时留意返回值里有没有相关开关。
- ⚠️ 顺序:它只保护开启之后创建的 Release,不追溯。所以要在发第一个 Release 之前开;发布流程固定为「先建草稿 → 传完资产 → 再点发布」——发布之后补不了资产。
- 但它只锁资产与 tag 两样:标题、发布说明、预发布/Latest 标记仍可改 ⇒ 它不会堵死撤回路径。「发错了怎么办」那一节就建立在这个事实上。
阶段 9 · 九项回验
「本地全绿」不等于「线上正确」。 每一项的口径见
references/verification.md,
可直接用 scripts/verify_publish.py 跑。
| # | 检查项 | 口径 |
| 1 | 远端主分支 = 本地 HEAD = 本地新 tag | 三者 sha 相同 |
| 2 | tag 名集合(远端 vs 本地) | 对称差为空(比较前先剥掉 ^{} 解引用后缀) |
| 3 | 旧 tag 的 sha | 只比 refs/tags/*,且排除本次新推的那个 |
| 4 | 远端主分支树 vs 本地工作区 | 逐文件 blob 指纹,差异 0 |
| 5 | 远端新 tag 树 vs 本地同名 tag 树 | 差异 0 |
| 6 | 远端每个 tag 树 vs 本地归档目录 | 逐一一致;归档目录名不是本仓库 tag 的跳过并写明 |
| 7 | 全部 tag 本地树 vs 远端树 | 逐一一致 |
| 8 | 本次提交的 CI run | 结论 success,且 head_sha = 本次提交 |
| 9 | Release 资产 | 下载回来与 tag 做逐文件内容比对 |
外加两项附带核验:凭据残留复查(本地配置与凭据存储里无令牌残留),以及 平台安全设置只读核验 —— 密钥扫描 / 推送保护硬判(那是「三道闸」的第二道);规则集 / 外部贡献者批准 / 不可变发布等只报不判;读不到就写「无法核验」。
第 3 项要在推送前先取好基线,否则无从比较;首次推送没有「旧 tag」可比 ⇒ 应报跳过而非通过。第 8 项在 CI 未结束时报「进行中」(conclusion=None)—— 不计入失败也不算通过,等它跑完再跑一次即可。
三条「看着像线上坏了、其实全在工具侧」的实测坑(2026-09-29,跑在真实发布上)
- 「历史被动过」里必须没有一处是 tag。 拿全部 ref 比,推送本就移动
HEAD/main、关 PR 还会删pull/*(实测差异 5 处、没有一处是 tag)⇒ 只比refs/tags/*。 - 一项抛异常不许把后面的项带崩(实测:一个不是本仓库 tag 的归档目录名,就让第 7–9b 项连跑都没跑)⇒ 逐项隔离。判据:「失败」与「大段跳过」同现 ⇒ 先怀疑是同一件事。
- 注解按「归属」而非 app 名字统计:平台给机器人建的 check run app slug 也是
github-actions⇒ 拿details_url里的 run id 去比本次提交的 run。
三个会「骗过你」的取数陷阱,务必记住: ① 归档目录通常不含
.git,在里面跑git ls-tree会取到空集, 于是「差异数恰好等于待比文件总数」—— 差异数等于文件总数时,先怀疑自己取到了空集, 而不是先怀疑内容坏了。改用手算 Git 对象指纹,不依赖工作区里有没有.git。 ② 目录比对永远分开报两栏:「各自独有的路径」与「同名但内容不同」。 第二栏为空时,就能一眼判定是路径前缀写错了,而不是内容真的不一致。 ③ 归档目录里可能混着多种「一份内容」(解出的快照 / 目录本身 / 一个 zip)⇒ 形态要 按内容打分选;同名文件(如两边都有README.md)会让「有交集」判到错的那一份。
阶段 10 · 留档
- 每个阶段落成文档:交付总览、逐轮改动明细、推送记录(含踩坑与当时的现象)。
- 踩坑如实写,不美化。 失误清单本身就是资产 —— 尤其是「我一度判错」的那种, 它是下一个人(或下一次的自己)最快的路标。
- 凡「数量 / 行数 / 对比项数」这类声明,与版本号同级 —— 会随每次改动过期。 写数字的当下就跑命令核出来,并把命令附在变更记录里便于重放。
阶段 11 · 长期维护(发布之后一直生效)
这一阶段不是「做完就走」,而是「配完之后让机器替你做」。
每条问题配了哪些现成工具,见 references/maintenance-burden.md;
这里只放判据。
先给问题分两个坐标,再决定怎么办:
| 机器可判定 | 必须人类判断 | |
| 一次性成本 | 做完永久生效 ⇒ 现在花一次时间配好 | 只能靠降低触发频率 |
| 持续成本 | ⇒ 进流水线,定期跑 | ⇒ 只能靠政策与预期管理 |
- 枯燥 ≠ 低回报。 这类事防的是灾难:回报延迟、分散、不可见,代价却一次性集中。 ⇒ 判据不是「有没有意思」,而是**「能不能变成流水线里的一步」**。
- 落进「机器可判定」那一列的,绝不靠人记住。 人一定会漏,而漏掉的代价通常 半年后才出现 —— 那时已经没人记得当初为什么该做。
- 顺序是先政策、后工具。 涉及人的事(谁能提 PR、低质贡献怎么办)要先写成 一份一眼能找到的政策,再用规则集变成强制;反过来做,只会不断在评论区吵架。
⚠️ 一条容易忽略的反面教训:自动回复 / 自动分类机制本身可能制造新噪声 (有项目发现自己的机器人批量生成了低质 issue,外部贡献者又拿它们喂给工具生成 PR)。 上线任何自动回复机制前,先确认它不会自己造噪声。
让「没做到」这件事可见
这一阶段最容易出现的失败不是「漏做了几项」,而是「漏做了而没人知道」—— 自检器全绿、清单读过去也都对,因为没有任何东西在检查这份清单本身。 所以维护负担篇的检查清单带了第二列:本仓库现状; 读不到就写「无法核验」,绝不写成「已做」—— 但「无法核验」也不是终点: 能读到的开关就该做成机器判据(阶段 9 的平台只读核验就是这么来的)。
发错了怎么办:撤回与作废
撤回 = 下架 + 公告 —— 不是把东西收回来。 已下载的人手里那一份你追不回来, 所以绝不删除已发布的号位(那是别人依赖清单里的一个节点,2016 年的一次删除 让整个生态连锁停摆过)。
- 凭据类泄露的次序是「先轮换 → 再撤回 → 再公告」。 推上公开仓库就按已泄露 处理(永久存档、搜索索引、别人的 fork 都去不掉)—— 轮换是自保,撤回只是给人看。
- 下架不是修复:钉住那个版本的人照样装得上,所以仍然需要漏洞扫描。
- 判据可以简化成一句话:「已经下载的人手里那份,会不会害他」 —— 会才撤。
- ⚠️ 真正的防线在发布之前:本地隐私扫描 + 平台的推送保护。 公开即已泄露,事后撤回治不了本。
三类信息(凭据 / 个人信息 / 工程信息)各自的处置、三种手段的强度对照、
Release 页面上的三个动作与 Latest 被自动抢走的坑,见
references/withdrawal.md。
检查清单
- 先判断了输入形态(仓库 / 快照堆 / 普通项目),没有直接套 Skill 专属判据
- 先判断了包要交给哪个分发渠道,没有拿一条渠道的规则去判另一条
- 若是快照堆:号位按内容定,序列无空洞,作废号位没有复用
- 命名三条一致(发布物文件名 / 目录名 / 包清单里的
name) - 版本号出现的每一处都同步了,包括引用文件的
date-released与徽章 - 提交前删过
.git/index;磁盘实际文件数 == 受控文件数 - 仓库内无某个执行环境的专属残留(绝对路径、IP 字面量、机器专属网络事实)
- 发布前隐私扫描通过:无令牌 / 私钥块 / 真实邮箱 / 手机号 / 内网主机名
- 公开仓库的推送保护(密钥扫描 / 推送时拦截)已确认开启 —— 回验第 10b 项能只读核验它(无管理员权限时会明确报「无法核验」)
- 无编译缓存;行尾统一 LF,且每种文本类型都显式声明了
eol - 包内自检器 + 全部单元测试 + 声称支持的版本矩阵全绿
- 测试不依赖环境:断网或挂代理后结果不变
- 打包在跑编译检查 / 单测之后会带进缓存 ⇒ 打包前先清缓存
- 令牌权限配齐(
repo+workflow);用完已吊销 - 只推了精确 ref;凭据文件已删且四处无残留
- 推送后九项回验逐项有结论;跳过项单独计数、「进行中」不算失败也不算通过、且每项各自隔离(一项异常不许带崩后面的项)
- CI 结论取自最新一次 run 的
conclusion与head_sha - Release 资产与 tag 的比对打在内容层,不是容器哈希
- 若重写过历史 / 强推过:其余 tag 的 sha 与推送前逐一相同
- 未改动的归档快照与旧 tag 仍是原样(归档冻结)
- 发布前已确认目标仓库是否开启不可变发布(开了就没有「就地改」这条路)
- 若开不可变发布:在第一个 Release 之前开,且流程是「草稿 → 传资产 → 发布」
- 撤回门槛已定义:三类信息的处置与公告时限都写下来了(空着 = 那天临场发明)
- 已把长期维护项按「一次性 / 持续 × 机器判定 / 人类判断」分过类
- 「机器能查」的那份清单,每一条都真的是一条命令,而不是一条待办
- 若该注册表支持 OIDC 信任发布:发布凭据已不再依赖长期令牌
- 「所有外部贡献者都需批准」已打开(不是只卡首次贡献者)
- 依赖更新已分组,且安全更新单独可见
- 自动回复 / 自动分类机制确认过「不会自己造噪声」
- 工作流里的 Action 已固定到完整 SHA;工作流权限已在 job 级显式声明
失败特征速查
| 症状 | 最可能的根因 | 先做什么 |
零输出 + SIGTERM + exit 1 | ①凭据管理器弹 GUI 等人;②管道里的过滤器炸掉吞了输出 | 查旁证:git ls-remote 看远端 ref 真的变了吗 |
ls-remote 正常但 push 失败 | ls-remote 对公开仓库免认证 ⇒ 不能当凭据可用的证据 | 进入阶段 5,改用一次性凭据文件 |
| 旧 tag 推上去、新 tag 被拒 | 缺 workflow 权限 | 补齐后重推被拒的那些 ref |
| 强推 tag 被平台拒绝 | 仓库开了不可变发布,已发布的 tag 锁死 | 不要硬碰:顺延新号位,见阶段 8 |
| TLS 报错但浏览器能打开该站 | 出口有中间人(代理 / 加速器) | 阶段 3 的分层判据,别关证书校验 |
CI 报「目录名与 name 不一致」 | 检出目录是仓库名,不带版本后缀 | 加 path: _checkout + 改名,见阶段 7 |
| 每次运行都带一条运行时弃用警告 | actions/* 停在旧大版本 | 查最新版 + 读 release notes,升一档 |
| 列表里还挂着一个红色 run | 历史运行只读,修好也不会变绿 | 只看最新一次 run 的结论;必要时确认那次失败是否已是孤儿提交 |
| 归档比对报「全部文件都不一致」 | 在不含 .git 的目录里跑 ls-tree,取到空集 | 改用手算 Git 对象指纹 |
| 同一个 tag 在两台机器上打出的包哈希不同 | zip 条目时间戳跟随构建机器所在时区 | 断言改打内容层(逐文件指纹) |
改了文件却提交不上去(git status 说干净) | 体积与修改时间都相同 ⇒ 命中 stat cache | 删 .git/index 强制重哈希,见阶段 2 |
| 依赖更新 PR 堆积成山、没人处理 | 没配分组,一个包开一个 PR | 按生态与类型分组;安全更新不分组;把「队列能处理多少」当作配置前提,见阶段 11 |
| 自己的机器人似乎在制造低质 issue / 回复 | 自动回复把噪声放大了 | 先停掉自动回复,复核它生成内容的质量,再决定要不要重开 |
| 反复收到「读起来专业但经不起推敲」的贡献 | 提交成本趋零,评审成本不变 | 先写贡献政策(披露 + 人类负责 + 必须能解释自己的改动),再开「所有外部贡献者需批准」,见阶段 11 |
| 同一个问题,两个技能给的数字不一样 | 两条规则各自服务不同的分发渠道,但都没写清适用范围 | 先问「这个包要交给谁」,再决定用哪套规则,别去「统一」它们 |
| 新仓库首次推送后,一个 Release 都没建 | 分支与 tag 是同一次推的,tag 事件早于工作流注册被丢弃 | 用发布工作流内置的回填入口补,不必改任何配置 |
| 不可变发布明明开了,旧 Release 仍不受保护 | 该开关只保护开启之后创建的 Release,不追溯 | 顺序改成「先开开关、再发第一个 Release」;已存在的只能删了重建 |
一次性凭据文件不生效,报 terminal prompts disabled | 系统另装的凭据管理器仍排在 helper 列表前面 | 改用一次性认证 URL(用完复查四处无残留),或显式清空整条 helper 列表 |
坏版本被自动标成 Latest,成了仓库门面 | Latest 按日期与版本自动判定 | 改说明 + 抢回标记;彻底解决只能再发一版,见「发错了怎么办」 |
| 回验报「历史被动过」,可一个 tag 都没动 | 第 3 项拿全部 ref 当「旧 tag」比 —— 推送本就移动 HEAD/main,关掉 PR 还会删 pull/* | 只比 refs/tags/*,并排除本次新推的 tag |
| 回验只跑了几项,其余全是「未执行」 | 一项抛异常把整段远端检查带崩了 | 逐项隔离;「失败 + 大段跳过」同时出现时先怀疑是同一件事 |
| 注解数报错,点进去却是平台/机器人的通知 | 平台给机器人建的 check run,app 也是 github-actions | 按归属过滤(details_url 里的 run id),不按 app 名字 |
资产下载报 getaddrinfo failed | 资产 302 到不在 hosts 里的域名(加速器只转发已劫持的域名) | --connect-host 127.0.0.1:只改解析,SNI 与证书校验照旧 |
回验报 CI 失败,可结论是 conclusion=None | 该 run 还没跑完 —— None 不是 failure | 等它结束再跑一次;回验会把它单列成「进行中」,别当红叉处理 |
| 回验里平台设置那一项报「无法核验」 | 凭据没有管理员权限(security_and_analysis 只对管理员返回) | 换管理员凭据再跑,或去 Security 设置里人工核对 —— 它不是通过 |
| 归档比对报出一堆差异,但包明显是对的 | 归档目录里同时有交付文档与打包产物,且有同名文件(如 README.md) | 形态按内容打分选,别按形状猜 |
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

