Pass the Desktop build version as an argument instead of rewriting manifests
Desktop 发布版本改为命令参数,不再改写清单
[Deriving Desktop test versions](2026-09-16-desktop-release-version-derivation.md) fixed what a test build is called but not where that name lives. Publishing one meant running `release:dsh` to rewrite the version in every release-family manifest and the lockfile — 295 files — because packaging, the update feed, and upload validation each read the version from the checkout. Those edits are never committed, so a test
Source compatibility: Chinese title uses a compatible non-canonical header
English
Problem
Deriving Desktop test versions fixed what a test build is called but not where that name lives. Publishing one meant running release:dsh to rewrite the version in every release-family manifest and the lockfile — 295 files — because packaging, the update feed, and upload validation each read the version from the checkout. Those edits are never committed, so a test build left the working tree dirty until someone reverted it, and a reverted tree no longer describes the artifacts that were published. The index in <base>.YYYYMMDD.index was also chosen by hand against the records and the bucket, which is a lookup a script can do.
Two more facts about a build were unrecoverable afterwards. A build handed to a colleague or pushed to a test feed is reachable from no tag, and the build tree it came from is not a checkout, so nothing connected an installer to its sources. A production release was likewise recorded only in the bucket.
Decision
The version a build publishes is an argument. --build-version names it, --build-version auto proposes the next index for the day, and the value reaches electron-builder through extraMetadata, the update feed, and the upload validation as one input. Manifests keep the product version, so no packaging run modifies tracked files.
The version rules in the Desktop release rules are unchanged: production publishes the dsh base, a prerelease base takes .YYYYMMDD.index, and a stable base takes -test.YYYYMMDD.index. Validation uses semver itself, because electron-updater compares feed versions with semver.gt against the installed app.getVersion().
This supersedes two requirements of that note. Release-family manifests are no longer rewritten, and the shell version no longer equals the bundled runtime version: the runtime is the dsh package at the product version, and verifyDesktopRuntime receives the version whoever prepared that tree wrote, which installed-update qualification rewrites for its own materials. The application already reported the two separately to the mandatory update policy.
Every artifact records dshBuildCommit and dshBuildDirty in its manifest. A production upload tags the packaged commit as desktop-v<version> after the artifacts are public, and reports the command to run by hand rather than failing an upload that already completed; a build from a modified checkout is not tagged. Test and local builds are deliberately left untagged, because a tag per test build would bury the releases.
Upload reads the published version from the completion record packaging wrote, not from a variable, so release fields still come only from the target file.
Alternatives considered
Keep rewriting manifests and revert afterwards. The dirty tree is the defect, and a revert races anything else reading the workspace. It also leaves the published artifacts describing a state the repository no longer has.
Rewrite only the packed runtime's manifest in a temporary copy. The bundled dsh would then claim a version that does not exist on npm, and the copy would have to be kept consistent with the lockfile for no gain.
Number every build automatically without an argument. Operators confirm the version with the user before packaging. Automatic numbering is offered as auto, which prints what it chose, rather than as the default.
Tag every build. Test feeds receive builds continuously; tagging each one would make desktop-v* useless for finding releases. Artifacts carry their commit instead.
Consequences
A packaging run leaves the repository unchanged, so the tree that built an artifact is the tree the tag or the recorded commit names. auto contacts the destination bucket, including under --check, and falls back to the local output directory when no bucket is configured or the listing cannot complete within its deadline; a partial listing is discarded rather than risking a reused index that would overwrite a published installer. Uploading a production release now writes to the repository, which earlier release automation deliberately avoided; the write is one tag, performed by the operator's own command after the upload succeeds.
中文
问题
从完整 dsh 版本派生 Desktop 测试版本确定了测试构建叫什么,但没有确定这个名字存在哪里。发布一次测试构建要先运行 release:dsh,把版本写进所有发布家族清单和锁文件——295 个文件——因为打包、更新 feed 和上传校验都各自从工作区读取版本。这些改动从不提交,所以每次测试构建都会留下脏工作区直到有人还原,而还原后的工作区又不再描述已经发布出去的产物。<base>.YYYYMMDD.index 里的序号同样靠人对着发布记录和 bucket 挑选,而这是脚本可以完成的查询。
另有两件事在构建之后无法还原。交给同事或推到测试 feed 的构建没有任何标签可查,它来自的构建树也不是工作区,因此没有任何东西把安装包和源码连起来。production 发布同样只在 bucket 里留下痕迹。
决策
构建发布的版本是一个参数。--build-version 指定它,--build-version auto 给出当天的下一个序号,该值经 extraMetadata 进入 electron-builder,并作为同一个输入贯穿更新 feed 与上传校验。清单保留产品版本,因此任何打包运行都不修改被跟踪的文件。
Desktop 发布规则中的版本规则不变:production 发布 dsh 基础版本,预发布基础版本追加 .YYYYMMDD.index,稳定基础版本追加 -test.YYYYMMDD.index。校验直接使用 semver,因为 electron-updater 就是用 semver.gt 把 feed 版本与已安装的 app.getVersion() 比较。
本决策取代上述 Note 的两条要求。发布家族清单不再被改写;外壳版本也不再等于内置运行时版本:内置运行时就是产品版本的 dsh 包,verifyDesktopRuntime 接收准备该运行时树的一方写入的版本,而安装升级验证会为自己的材料改写这个版本。应用本来就把两者分别上报给强制更新策略。
所有产物的清单都记录 dshBuildCommit 与 dshBuildDirty。production 上传在产物公开后把打包所用 commit 打成 desktop-v<版本> 标签,失败时只打印需要手工执行的命令,而不让已完成的上传变成失败;来自有改动工作区的构建不打标签。test 与本地构建有意不留标签,因为为每个测试构建打标签会淹没真正的发布。
上传从打包写下的完成记录中读取发布版本,而不是从变量读取,因此发布字段仍然只来自目标文件。
考虑过的替代方案
继续改写清单,事后还原。 脏工作区本身就是缺陷,而还原会与任何同时读取工作区的操作竞争,也会让已发布产物所描述的状态在仓库中不复存在。
只在临时副本里改写打包运行时的清单。 内置 dsh 会因此宣称一个 npm 上不存在的版本,副本还得与锁文件保持一致,没有任何收益。
不要参数,全部自动编号。 操作者需要在打包前与用户确认版本。自动编号以 auto 提供并打印所选版本,而不是作为默认行为。
为每个构建打标签。 测试 feed 持续接收构建,逐个打标签会让 desktop-v* 无法再用于查找发布。改由产物自身携带 commit。
影响
打包运行不再改动仓库,因此构建产物的代码树就是标签或记录的 commit 所指的那棵树。auto 会访问目标 bucket,--check 下同样如此;未配置 bucket 或列举未能在期限内完成时回退到本地输出目录,不完整的列举结果会被丢弃,以免复用序号覆盖已发布的安装包。production 上传现在会写入仓库,而早先的发布自动化刻意避免这一点;这次写入只是一个标签,由操作者自己的命令在上传成功后执行。