Skip to content

专题验证 · 发版链路

常驻清单,不归档。判断层专用——机械层先跑完再看这里。 对象:scripts/release.sh + .github/workflows/release.yml + scripts/gen-latest-json.mjs

  • updater endpoint(updater-latest 的 latest.json)+ 公开分发仓 + landing 下载页 + /release skill 体裁与纪律见 README.md
bash
./scripts/release-verify.sh                # 不下载安装包(R1-R15)
./scripts/release-verify.sh --deep         # 额外真的下一个制品验签
./scripts/release-verify.sh --static-only  # 只跑 R1-R4(零网络零凭据)—— CI 跑的就是这个

0. 这个域为什么危险

它的失效只有一种形状:存量用户停在旧版本上,而你看到的一切正常。

没有报错、没有告警、没有工单。他们的应用照常启动、照常用,只是再也不更新了。 而你这边:Release 页面好好的、下载链点得开、CI 全绿、Sentry 没有异常事件—— 因为根本没有事件可发,那台机器上什么都没发生。

链路有十来跳,任何一跳断掉都是同一个症状:

断在哪你会看到什么
忘跑 promote-updater什么都没有。新版发布了,没人收到
公开 Release 忘了从草稿转正什么都没有。endpoint 上线了,客户端下载 404
换版本标签没抬基版本号什么都没有。semver 认为新版更旧
某个平台的 updater 制品没产出什么都没有。而且只有那个平台,其余平台一切正常
签名密钥轮换了什么都没有。客户端验签失败,静默拒绝安装,且无法补救
启动时那句静默自检被删了什么都没有。设置页里手动点还能更新,但没人会去点

所以本清单的第一原则:

🔴 「我发出去了」不是证据。 每一格要问的是:如果这一跳断了,会有任何东西变红吗? 没有,那它就是靠运气在绿——而这个域的失效是不可观测的,运气用光了你也不会知道。

⚠️ 元前提:这个域的绿有两种,别混

release-verify.sh 验的是当前已发布的那一版这条链路通不通。它验不了 「下一次发版会不会出问题」——那要等到真的发版。两者的分界:

  • 现在能验:endpoint 可取、资产可下、签名密钥对得上、平台齐全、版本可升级、凭据在台账里
  • 只有发版当次能验:macOS 公证真的落地(release.sh verify-notarized)、 构建矩阵三条腿都出了制品、公开 Release 草稿转正之前那个窗口(见 K1)

报告里要写明这轮验的是哪一种。


1. 机械层已覆盖(不要在判断层重复)

🔒 R1-R4 自 2026-08-30 起在 CI 里跑.github/workflows/ci-verify.yml--static-only --max-skip=0),R5 起仍是手工 / /release 时才跑。 这条分界不是随手划的:整跑时 SKIP 数是网络可达性的函数——同机同码连跑两次 SKIP 分别是 4 和 11(第二次 latest.json 没取到 ⇒ R7-R11 连带全 SKIP), 钉任何基线都会得到一道随网络天气变红的门。拆完实测(代理指向关闭端口): 静态段 0.45s / SKIP=0,全量 20.1s / SKIP=10。 ⇒ 读下表时要分清哪几行是「每次提交都在验」,哪几行是「只有人记得跑时才验」 —— 后者的假绿风险高得多。

位置守什么
源码release-verify.sh R1(复用 set-version.mjs --check,CI 同款)四处版本号一致(tauri.conf / Cargo.toml / package.json / Cargo.lock 的 app)。第四处 2026-09-01 才补进断言 —— 此前它只靠「bump 之后碰巧跑过一次 cargo」,v0.1.0-dev.11 的 tag 里实测就落后一版(失效面 = 从该 tag 跑 cargo build --locked 会失败,而 CI 发版构建不带 --locked 故不会红)
源码release-verify.sh R2semver 判据自检(换标签陷阱 + 数字段向量的阳性对照)
源码release-verify.sh R3正式版 identifier 硬闸(不依赖 /release skill 被走到)
源码release-verify.sh R4updater 客户端接线五环(conf / rust plugin / capabilities / lib / 启动自检
源码release-verify.sh R5release.yml 引用的 secret 都真实存在(名单从 workflow 现推)
部署态release-verify.sh R6–R10endpoint 匿名可取 · 版本形状 · 平台键 vs 构建矩阵 · 资产匿名可下(含 404 阳性对照)· 签名 keyid vs 客户端 baked 公钥
部署态release-verify.sh R11工作树版本的 semver 可升级性(存量用户认不认)
发版前release.sh artifact-check/release Step 0)Actions 存储配额两个口径:快照(所有 artifact,含已过期 —— 过期≠已清除,账照算)+ 账单(billing/usage 的 GB-hours ÷ 已过去小时数 vs plan included)。🔒 问句:账单那半这次真的验了吗? 缺 keychain lampio-gh-billing 时它会说「本次没验」并放行 —— 那是刻意的(取不到 ≠ 值不对),但读的人要分得清「过了」和「没验」。假绿风险:① 只看快照 = 重演 dev.11(报 0 MB 却撞墙);② GB-hours 是均值不是快照,刚删完东西它仍偏高(偏保守,可接受),月初 <12h 直接判「太早」
部署态release-verify.sh R12–R14公开 Release 非草稿 + 产物集合 · landing 三条直链可下 · workflow 默认权限
部署态release-verify.sh R15会过期的发版凭据都在 ops_credentials 里有行
端到端release-verify.sh --deep R16真的下载制品并验 minisign(含翻转一字节的阳性对照)
别处CI check:schema-frozenv1 迁移本体冻结(红线 #11)——断链后 pending 迁移永不执行
别处release.sh verify-notarizedmacOS 签名 / 公证 / staple(发版当次,需 .dmg 在手)
别处admin/lib/ops-external.ts + release-consistency.spec.ts「最新 Release / updater 指向 / landing TAG」三路版本字符串比对,含「取不到 ≠ 一致」反向断言,显示在 /admin/ops
别处ops_daily_monitor()credential_expiry凭据到期告警
别处ops-monitoring.md J12/J13上面那张卡的看板显示侧判断层

R1–R16 全部做过注入式反向验证(2026-08-27,见 §3):12 处真实缺陷各注入一次, 全部变红,还原后 git diff 零残留。注入在独立 worktree 里做,没碰共享工作树。

⚠️ 三路字符串比对刻意不在本脚本里重做。 它已经有实现 + 单测 + 看板展示。 本脚本补的是那张卡够不到的一层:字符串对得上,文件不一定在—— TAG 写对、资产名写错,三路比对照样全绿,用户点下去 404(R13 下探到文件本身)。


2. 判断层逐格判据

2.1 更新真的送得到吗

#问句假绿风险
T1promote-updater人工闸门——这次发版跑了吗?✅ 机械层 R11/R12 能看出「endpoint 落后于最新 Release」。要人判的是这是不是故意的:护栏 #7 明说「先放着观察一晚」是合法用法。同一个数字既可能是事故也可能是策略,机器分不出来
T2公开仓那个版本的 Release,从草稿转正了吗?在跑 promote 之前?🔴 顺序反了会造出最隐蔽的一种断更:promote-updater你的 token 从草稿里就能取到 latest.json 并推上线,而客户端匿名去下资产是 404。两边都"成功"了,只有用户那头是坏的。R12 会在事后报出来,但代码里没有前置断言(见 §4 K2)
T3三个平台的制品产出了吗?🔴 少一个平台的后果是只有那个平台断更,其余一切正常——最难被发现的一种。upload-artifactif-no-files-found: warngen-latest-json.mjs 此前只在平台时才报错(见 §4 K3)。2026-08-30 起 gen-latest-json.mjs 自己也拿构建矩阵现推期望集合,与 R8 判据同构 —— 分工是它事前拦截(生成这一步就红)、R8 事后复核。两处都在
T4装了上一版的机器,semver 上认不认这一版是更新?🔴 这条是本域最贵的坑,且完全静默。prerelease 段里字母段按 ASCII 比:beta < dev < preview < rc0.1.0-dev.90.1.0-beta.1 会让所有存量用户永远收不到更新,而 Release 页、下载页、看板三路字符串全部一致。安全姿势是换标签的同时抬基版本号。机械层 R2+R11 钉住这条
T5客户端什么时候检查更新?启动时静默自检一次silentCheckOnce)。⚠️ 阅读器是那种会开着好几天不关的应用——开着不关就永远不再检查。这不算缺陷(下次启动就会检出),但决定了「promote 之后多久能铺开」的真实节奏,别按「一天内全量」估
T6自检失败时,有人会知道吗?🔴 silentCheckOnce 把异常吞进 logger.info刻意不打扰用户——这对用户是对的,对你是盲区:endpoint 挂了、证书过期、DNS 被污染,客户端侧一片安静。所以这个域的可观测性只能靠外部探针(本脚本),不能指望客户端上报

2.2 签名与信任

#问句假绿风险
T7签名私钥换过吗?🔴 这是本域唯一不可补救的操作。 客户端里的 pubkey 是编译期 baked 进二进制的——密钥一换,所有存量用户的客户端都会验签失败并静默拒绝,而你没有任何办法把新公钥推给他们(推更新正是被拒的那件事)。唯一出路是让用户手动重装。R10 比对 keyid,但真正的防线是「不要换
T8mac 的两个 tarball 在签名之后被改了名(gen-latest-json.mjs 插 arch 后缀),这安全吗?✅ 2026-08-27 实测坐实:下载 Lampio_aarch64.app.tar.gz 用客户端那把公钥验签通过,而签名的 trusted comment 里写的仍是改名前的 Lampio.app.tar.gz。安全的根据是 minisign 验的是内容不是文件名。⚠️ 这是一条关于 updater 实现的假设,不是契约——升级 tauri-plugin-updater 大版本时要重新确认它没开始校验文件名
T9macOS 包真的过了公证?两个 arch 都过了吗?verify-notarized 只下 aarch64 那个 .dmg 验(Intel 的从未验过,见 §4 K4)。且它必须验 DMG 内部的 .app——票据装订在 .app 上,直接对 DMG 容器跑 spctl 会恒报 Unnotarized(假阴性,2026-08-01 踩过)
T10Windows 仍未签名——后果讲清楚了吗?首次运行 SmartScreen 拦截,用户要点「更多信息 → 仍要运行」。⚠️ 这对更新也成立:updater 装 NSIS 包时同样会被拦。文案在 release.yml 的 releaseBody + landing。见 §4 K5

2.3 分发面

#问句假绿风险
T11公开仓是公开的、私有镜像仓是私有的吗?两个仓角色完全不同(Actions 在私有仓跑并建 draft;publish-public job 把产物发到公开仓)。公开仓一旦转私有,endpoint 与三条下载直链同时静默 404。R6/R9/R13 全部刻意匿名探测正是为此——带 token 验会把这种情况一起验绿
T12release.sh publish 到底发布了什么?🔴 它发布的是私有镜像仓那个 draft,对外可下载性一点没变。 名字叫 publish,做的事跟用户无关。对外要另外 gh release edit --repo …-releases --draft=false。SKILL.md Step 7 用一句 > 引注提了这件事——散文里的步骤会被跳过,这正是 K2 的成因
T13landing 那行 TAG 改了吗?改对了吗?✅ 三路字符串比对在 ops 卡(J12/J13),文件可达性在 R13。要人判的是第三层:下载页的文案与包的真实状态一致吗(「已签名+公证」是不是还成立、Windows 那段绕行说明该不该在)
T14私有仓的 artifact 配额还够吗?⚠️ 这条不在发版链路上,却会从外面把发版打红:2026-08-24 实测 artifact 累到 701MB 撑爆 500MB 免费额度,后果是全仓所有 workflow 的上传步骤失败——CI (admin) 在 typecheck/lint/E2E 全过的情况下照样报红。retention-days: 3 是为此设的,别随手调大

2.4 版本真相源与文档

#问句
T15版本真相源仍然只有 src-tauri/tauri.conf.json 一处吗?(package.json 是死字段,长期 0.0.0别据它判断;R1 复用 CI 那道判据)
T16本次 tag 动了 assets/sql/schema.sql 吗?(红线 #11:改它 = 存量库 VersionMismatch 断链,pending 迁移永不执行。CI check:schema-frozen + /release Step 1.5 双守)
T17/release 的 SKILL.md 里那些写死的数字与产物清单,还对吗?(它是操作真相源,过期会直接把人带偏——见 §4 K6)
T18发版依赖的凭据,下一次发版之前会不会有到期的?(R15 只管「有没有登记」,到期告警归 ops_daily_monitor;要人判的是「排期撞不撞」)

3. 反向验证配方

只验通过路径 = 没验。 以下全部不依赖「记得改回来」。

3.1 🔒 先说不要做的事

  • 不要为了验证而打 tag、触发 Actions、publish Release、或跑 promote-updater。 那些是 outward-facing 且部分不可逆(tag 不可复用、已送达的更新撤不回),归 /release skill 且必须由用户发起。本专题全程只读
  • 不要为了试一把而轮换签名密钥。见 T7——那是本域唯一不可补救的操作。
  • 不要在共享工作树里注入。注入必然要改 tauri.conf.json / release.yml 这些枢纽文件, 而并行会话的一次 checkout . 就会连你的注入一起卷走(或者反过来)。

3.2 注入 → 必须红 → 还原 → 复核零残留(在独立 worktree 里)

bash
git worktree add --detach /tmp/relverify-wt HEAD    # 隔离,不碰共享工作树

十二种值得常备的注入(2026-08-27 全部实测变红):

注入该红的检查
package.json 的 versionR1 三处一致
把 semver 比较器换成字符串比较R2 自检(阳性对照
版本去掉 -dev 后缀而 identifier 仍 .devR3 正式版硬闸
删掉 MainApp.tsx 里那句 silentCheckOnce()R4 接线(最容易漏的一环)
release.yml 引用一个不存在的 secretR5 名单对账
从构建矩阵里删掉一条 targetR8 平台键 vs 矩阵
翻转 tauri.conf 公钥里的一个 keyid 字节R10 签名密钥
版本从 dev.10 换成 beta.1真实陷阱R11 semver 可升级性
让 R12 去查一个产物不全的老版本R12 产物集合
landing TAG 指向不存在的 tagR13 下载直链
往必需凭据清单里加一个台账没有的名字R15 台账对账
endpoint 指向不存在的 URLR6 匿名可取

⚠️ 注入必须能通过编译/解析,否则脚本会在到达那条断言之前就挂掉——那既不是红也不是绿, 是没验,而输出里看起来只是一条报错。改 JSON 时用 json.load/json.dump 走一遍,别用裸 sed 碰结构。

3.3 🔴 阳性对照:证明扫描器不是瞎子

这个域有两条断言天生是「没找到问题」形式的,它们最危险的失效不是误判,而是根本没在看

  1. 「资产可下」——如果抓取路径本身恒返回 200(代理拦截、缓存、重定向到登录页), 三条「✅ 可下」会一直绿着而什么都没验证。故 R9 先探一个必然不存在的资产名, 它必须返回 404;不是 404 就把下面三条全部作废。
  2. 「签名通过」——一个恒返回 True 的验签器同样会一直全绿。故 minisign_verify.py selftest 在正向验完之后翻转一个字节再验一次,必须变 bad。 --deep 走的就是这条路径。

3.4 🔴 让字节真的走一趟(--deep

前面每一条都只是「拿到了一个成功的返回」。--deep 把制品真的下下来, 用客户端那把 baked 公钥做一遍 minisign 验签——客户端装不装得上,靠的正是这一步。

2026-08-27 实测:Windows 制品与 mac aarch64 制品(改名后的那个)都验签通过, 翻转一字节后都变 bad。这同时坐实了 T8 那条「改名安全」的假设。

⚠️ --deep 默认只验体积最小的那个平台。验的是同一把密钥、同一条链路, 不是每个平台各验一遍;要全验就手动逐个跑 minisign_verify.py

3.5 判据类不需要造故障

ops-monitoring.md §2.4 同法。本域唯一的纯判据是 semver 可升级性, 已做成 R2 的向量自检:正向(dev.10 > dev.9)+ 🔴 反向(beta.1 大于 dev.9, 把陷阱本身写进断言)。比较器一退化成字符串比较,R2 当场红。


4. 已知未修 / 有意接受

不要当成新发现报上来,但每轮仍要独立确认现状(还在?变严重了?条件变了?)。

#事情为什么现在不修什么条件下重新处理
K1R12 的「公开 Release 是草稿」那一支从未被真实草稿触发过。 库里没有草稿态的 release 可供验证,注入只验到了它的另一半(产物集合)造一个真草稿 = 真的发一次版,代价与风险都不对等🔴 下次发版时顺手做掉:在把公开仓那个 draft 转正之前跑一次 release-verify.sh,R12 必须报红。不红就说明这条从未工作过。这是免费的——那个窗口本来就会出现
K2已修(2026-08-30) —— cmd_promote_updater 开头加了 gh release view --json isDraft 前置断言:草稿即拒绝并提示 --draft=false。反向注入实测过(假 gh 顶掉真 ghisDraft=true → 拒绝且不进入 download;false → 放行)。原文留档:promote-updater 曾没有「目标版本的公开 Release 已 publish」前置断言。 它的注释写着「前置:该版本的公开 Release 已 publish」,但那是散文:脚本用你的 token 从草稿里就能取到 latest.json 并推上线,客户端匿名下载则 404顺序目前由 /release skill 的 Step 7 → 7.5 保证,且 R12 会在事后报出来已进 backlog。散文约束迟早会被跳过——/release 之外任何人手跑一次 promote-updater 就绕过了它。修法是在 cmd_promote_updater 里加一句 isDraft 断言
K3已修(2026-08-30) —— gen-latest-json.mjs 现从 release.yml 的矩阵 target 现推期望平台集合(判据与 R8 同构:同一张映射表、同一条正则),集合不等即 exit 1,把失败挡在生成这一步而不是发布之后。读不到 workflow / 推不出平台时也 exit 1(fail-closed,不能让它变成恒绿)。反向注入实测:抽掉 windows 那条腿的制品,旧版静默产出 2 平台 latest.json 且 rc=0,新版 rc=1。原文留档:gen-latest-json.mjs 曾只在平台数为 0 时报错,不检查是否等于构建矩阵。配合 upload-artifactif-no-files-found: warn,一个 2 平台的 latest.json 会静默上线当前有 needs: build 兜着(矩阵任一条腿失败就不会走到 publish)。漏网的是「腿成功了但没产出制品」已进 backlog。R8 目前是事后唯一会红的地方——但那是发布之后才发现。修法是在脚本里对矩阵求期望集合
K4verify-notarized 只验 aarch64 的 .dmg,Intel 那个从未被验过两个 mac job 用同一套证书与同一条公证路径,一个过了另一个大概率也过出现过一次「只有一个 arch 公证失败」时;或改动 macOS 签名/公证接线时
K5Windows 未签名,SmartScreen 首次运行拦截;updater 装 NSIS 包时同样被拦代码签名证书是年费成本,内测期不值得面向真实外部用户放量时(EV 证书或 Azure Trusted Signing)。届时 landing 与 releaseBody 的绕行文案要一起撤
K6已修(2026-08-30) —— 两处都改了,但改法不同.msi 那处是事实错误bundle.targets = nsis/app/dmg),直接改对并补一句为什么没有 msi;「当前 0.1.0-dev.8」那处没有更新成 dev.10,而是删掉了这个数(改成指向 next-version 现推 + 一条读现值的命令)—— 把它更新一次只会再漂一次,正是本条自己总结的教训。顺带把「汇总 4 包」改成 3 个安装包 + updater 制品 + latest.json。原文留档:SKILL.md 曾写着当前版本 0.1.0-dev.8(真相源已是 dev.10)、产物清单仍列 .msi三处 0.1.0-dev.8 刻意保留(冻结基线 ×2、首个带 updater 的版本 ×1):那些是历史事实,不是会漂的当前值
K7本地 main 常有未推送的 commit(本轮实测十余个)单机开发的常态,不是发版链路缺陷——git push <tag> 会连带推送可达 commit刻意不做成机械检查:它会近乎恒红,而「恒红的闸门与恒绿的一样坏」。双推备份的纪律归 CLAUDE.md §10

⚠️ 这张表本身也会腐烂。 每轮验收后更新它——一条「已知未修」若其实已被修, 会让下一轮跳过一次本该做的检查,那是最贵的一种过期。


5. 验收台账

每轮一行。不新建报告文档——产物按 README.md 的规矩拆散。

日期基线机械层判断层结论留痕
2026-08-010.1.0-dev.10尚无发版当次的真机验证:updater dev.8/9 → dev.10 检出+下载+应用 ✅;干净 Mac 首开无 Gatekeeper 拦截 ✅CHANGELOG;backlog §macOS CI 签名
2026-08-270.1.0-dev.10PASS=17 FAIL=0 SKIP=0--deep 时 18,首次)建成本专题。新增部署态脚本(R1–R16)+ 自带纯标准库 minisign 验签器。12 条守卫全部注入验证过鉴别力(独立 worktree,零残留),其中 dev.10 → beta.1 那条注入复现的是本域最贵的真实陷阱。--deep 让字节真的走一趟:Windows 与 mac aarch64 制品都验签通过、翻转一字节都变 bad——顺带坐实了 T8(mac tarball 在签名后被改名,安全性依赖「minisign 验内容不验文件名」,此前从未验证过)。新发现 K2(promote-updater 缺前置断言)+ K3(gen-latest-json 不核平台数)+ K6(SKILL.md 数字过期);K1 记为「下次发版顺手验」,K4/K5/K7 记为有意接受。本轮验的是「已发布那一版的链路通不通」,不是「下次发版会不会出问题」(见 §0 元前提)本目录;K2/K3 进 backlog