Cline 桌面应用双渠道发布实战:Tag 契约、CI 签名公证流程与 Tauri 自动更新源
【免费下载链接】clineAutonomous coding agent as an SDK, IDE extension, or CLI assistant.项目地址: https://gitcode.com/GitHub_Trending/cl/cline
本文以 Cline 仓库中publish-desktop发布技能文档(SKILL.md)为主体,完整梳理 Cline 桌面应用(apps/examples/desktop-app,基于 Tauri)从打 tag 到发布的双渠道(stable / beta)发布契约、逐步操作流程,并结合 desktop-publish.yml 工作流与配套脚本,深入解析 macOS 通用二进制签名公证、Windows Azure Trusted Signing 签名以及 Tauri 自动更新源(desktop-latest/desktop-beta)的生成与防护机制。读完本文,你可以独立完成一次桌面版本发布,并理解其中每一道安全门禁(环境密钥隔离、渠道 fail-closed 校验、编译期内嵌更新地址断言)的设计原因。
发布契约:两个渠道,一个工作流
桌面应用发布完全在 GitHub Actions 中完成,没有本地发布路径。发布产物包含两个平台:
- macOS:单个签名 + 公证的 universal DMG,原生同时支持 Apple Silicon 与 Intel;
- Windows:经 Authenticode 签名的 NSIS 安装器(
<Product>_<version>_x64-setup.exe),在build-windows作业中通过 Azure Trusted Signing 签名(jsign 经 Tauri 的signCommand调用,见 tauri-sign-windows.ps1;需要仓库级AZURE_*密钥,其中包括AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_DESKTOP,以及cline-cli-signingEntra 应用上针对PublishDesktop环境的联邦凭据)。
已安装的 App 通过 Tauri updater 自动发现新版本,因此发布即更新——发布动作会把新版本推送给该渠道下的所有存量用户。
渠道、标签与更新源映射
两个渠道共用同一个工作流(desktop-publish.yml的channel输入):
| 渠道 | 标签格式 | 来源分支 | 滚动更新源(feed) | 产品名 / 包标识 |
|---|---|---|---|---|
| stable | desktop-vX.Y.Z(无后缀,工作流拒绝 prerelease 后缀) | main | desktop-latest | Cline /bot.cline.app |
| beta | desktop-vX.Y.Z-beta.N | desktop-experimental | desktop-beta | Cline Beta /bot.cline.app.beta,与 stable 并排安装 |
beta 构建额外叠加src-tauri/tauri.beta.conf.json配置层。beta 流程的完整背景见 EXPERIMENTAL.md。
版本来源与 beta 版本号规则
版本号的来源有两处,二者必须互相对齐、且与 tag 一致:
- apps/examples/desktop-app/package.json
- apps/examples/desktop-app/src-tauri/tauri.conf.json
(src-tauri/Cargo.toml有自己的版本号,但会被tauri.conf.json覆盖,无需改动。)
beta 版本是下一个stable 版本的 prerelease:stable0.0.13→ beta0.0.14-beta.1、-beta.2……一旦发出了版本 ≥ beta 基座的 stable,下一个 beta 就要抬升基座(stable0.0.14发布后,下一个 beta 是0.0.15-beta.1)。beta 的X.Y.Z基座绝不能与已发布的 stable 相同。
发布准备包含三部分:已批准的发布说明、两处版本文件升版、以及 CHANGELOG.md 的更新——stable 提交到main,beta 提交到desktop-experimental。
关键安全不变量:两个渠道都从 main 派发
Both channels dispatch frommain。这不是便利性设计,而是安全不变量:运行执行的是main上的工作流副本,只有 checkout 指向 tag。因此签名密钥的两道门禁——github.ref == main检查与PublishDesktop环境"仅允许 main 部署分支"策略——对 beta 同样成立。永远不要把desktop-experimental加入 PublishDesktop 的 deployment-branch 策略,否则在实验分支上编辑过的工作流文件就能接触到签名密钥。
工作流会为 tag 创建 GitHub release(universal DMG + macOS updater 制品 + 带 updater 签名的 Windows NSIS 安装器 +latest.json;beta 标记为 prerelease),并刷新渠道的滚动 feed release——这是该渠道所有已安装 App 轮询的静态自动更新源。永远不要删除desktop-latest或desktop-beta的 release 或 tag:feed URL 被编译进了二进制,updater 没有回退端点,删除会使所有存量安装永久失联。
CHANGELOG.md中## <version>小节(精确匹配,而非"顶部小节")会被逐字提取到 GitHub release 正文、Slack 公告和 updater manifest 的 notes 中。另外,推送 commit 或 tag 之前始终先询问。
逐步操作流程
以下命令都从仓库根目录执行(SKILL.md 明确要求 working directory 为 repo root)。
第 0 步:确认渠道
若用户未说明,先询问本次发布是stable 还是 beta。后续所有步骤都以此为分支条件,绝不要猜测。
第 1 步:收集上下文
git status --short --branch git fetch origin --tags git tag --list 'desktop-v*' --sort=-v:refname | head -10 node -p "require('./apps/examples/desktop-app/package.json').version" node -p "require('./apps/examples/desktop-app/src-tauri/tauri.conf.json').version"如果尚不存在任何desktop-v*tag,这是首个发布;以桌面应用的第一个 commit 作为基线,并明确告知"基线是推断的"。
对于beta发布:在desktop-experimental上工作(checkoutorigin/desktop-experimental;若它落后于 main,先把origin/main合入——冲突处理策略见 EXPERIMENTAL.md),并从该分支读取版本文件。上一个 tag 基线是两条渠道中最新的、且为当前分支祖先的desktop-v*tag。
第 2 步:收集发布区间内的提交
# stable(在 main 上): git log <last-desktop-tag>..HEAD --oneline --no-merges -- apps/examples/desktop-app sdk/packages .github/workflows/desktop-publish.yml # beta(在 desktop-experimental 上): git log <last-desktop-tag>..origin/desktop-experimental --oneline --no-merges -- apps/examples/desktop-app sdk/packages .github/workflows/desktop-publish.yml注意日志路径包含sdk/packages:桌面应用的 sidecar 会把 monorepo 里的@cline/core等 SDK 包打包进去,因此 SDK 变更也会随桌面应用一起发布。用户可见的 SDK 变更(provider、模型、行为修复)应并入发布说明;纯内部变更可以跳过。
第 3 步:起草面向用户的发布说明
扁平 bullet 列表、面向用户的语言。先呈现草稿并等待批准,再动任何文件。
第 4 步:确定版本升版
- Stable:询问本次是 patch、minor、major 还是显式版本号。用户没说清楚时不要猜。
- Beta:按版本号规则计算——基座 = 下一个 stable 版本,
N递增(0.0.14-beta.1→0.0.14-beta.2;stable0.0.14发布后则变为0.0.15-beta.1)。把计算出的版本与用户确认。
第 5 步:更新发布文件(stable 在main,beta 在desktop-experimental)
- package.json → 新版本号
- tauri.conf.json → 相同版本号
- 在 CHANGELOG.md 顶部插入
## X.Y.Z(不写日期;beta 为## X.Y.Z-beta.N),内容为已批准的说明
第 6 步:提交前验证
bun -F @cline/code typecheck bun test apps/examples/desktop-app/scripts/generate-update-manifest.test.ts完整的桌面 bundle 只能在 macOS 上构建,工作流的 build 作业才是真正的构建验证。在 Mac 上想要额外信心时,可在应用目录执行bun run package:desktop:mac --allow-unsigned-mac。
第 7 步:提交发布变更
git add apps/examples/desktop-app/package.json apps/examples/desktop-app/src-tauri/tauri.conf.json apps/examples/desktop-app/CHANGELOG.md git commit -m "chore(desktop): release vX.Y.Z"推送发布 commit 前询问一次,创建并推送 tag 前再询问一次:
git push origin HEAD git tag -a desktop-vX.Y.Z -m "Desktop vX.Y.Z" # beta: desktop-vX.Y.Z-beta.N / "Desktop vX.Y.Z-beta.N" git push origin refs/tags/desktop-vX.Y.Z第 8 步:派发工作流
前提:发布 commit 已在渠道分支上(stable 为main,beta 为desktop-experimental),且 tag 先推送。两个渠道都从main派发(原因见上文安全不变量):
# stable: gh workflow run desktop-publish.yml --ref main -f git_tag=desktop-vX.Y.Z -f channel=stable -f confirm_publish=publish # beta: gh workflow run desktop-publish.yml --ref main -f git_tag=desktop-vX.Y.Z-beta.N -f channel=beta -f confirm_publish=publish gh run list --workflow=desktop-publish.yml --limit=1 --json url,status,conclusion,createdAt --jq '.[0]'运行会暂停等待审批。validate立即执行,随后build作业等待PublishDesktop环境——直到必需审阅人批准,运行停留在waiting,这是预期行为而非卡死。可在运行的 Web UI("Review deployments")批准,或:
gh api repos/cline/cline/actions/runs/<run-id>/pending_deployments \ --method POST -f state=approved -f comment="desktop vX.Y.Z" \ -F 'environment_ids[]=19152605990' # PublishDesktop在此之前,validate之后什么都不会执行——签名密钥也读不到。
工作流随后执行的内容(与 desktop-publish.yml 对应):
- 构建一个 universal macOS bundle(
tauri build --target universal-apple-darwin对 aarch64 + x86_64 的 Rust 二进制做 lipo;Bun sidecar 由build-sidecar-bin.ts做 lipo;beta 叠加tauri.beta.conf.json),验证 bundle 内每个 Mach-O 都携带两个架构切片,并验证编译后的二进制恰好内嵌本渠道的 feed URL(Verify updater feed endpoint步骤用strings/grep断言:stable 必须含desktop-latest/latest.json且不含desktop-beta,反之亦然); - 用 Developer ID 证书签名、用 App Store Connect API key 公证、用 Tauri updater key 签名 updater 制品;
- 并行地,
build-windows在 Windows runner 上构建 x64 NSIS 安装器,通过 Azure Trusted Signing 对每个二进制做 Authenticode 签名(TaurisignCommand→ tauri-sign-windows.ps1),执行同样的 feed 端点与遥测护栏,并用Get-AuthenticodeSignature验证最终安装器; - release 作业创建 GitHub release(beta 为 prerelease)、刷新渠道 feed(
desktop-latest/latest.json或desktop-beta/latest.json)、发 Slack 公告。公证通常额外增加 2–10 分钟。
如果工作流因缺少凭据失败,见下文"Publish 密钥一次性配置"。
第 9 步:发布成功后验证更新源
curl -sL https://github.com/cline/cline/releases/download/desktop-latest/latest.json | head -30 # stable curl -sL https://github.com/cline/cline/releases/download/desktop-beta/latest.json | head -30 # betaversion字段必须等于新版本;darwin-aarch64和darwin-x86_64两个条目都必须指向该 release tag 下同一个新的 universal.app.tar.gz资产(因为 fat 二进制的每个切片在运行时各自请求自己的架构 key,所以两个 key 服务同一制品);windows-x86_64条目必须指向新的*_x64-setup.exe资产。该渠道的已安装 App(包括旧的按架构安装)会在下次启动或 2 小时内拿到更新。
beta 发布后还要确认 stable feed 未被触碰:desktop-latest/latest.json仍应服务上一个 stable 版本。工作流对此有 fail-closed 防护,但验证成本极低、漏检后果极重——updater 的比较器是朴素的 semver"大于",beta manifest 落到desktop-latest会让所有 stable 安装自动更新到 beta。
第 10 步:汇报
报告内容:渠道、版本、tag、changelog 是否更新、commit hash、推送了什么、工作流 URL、feed 验证结果。
工作流内部机制解析(desktop-publish.yml)
.github/workflows/desktop-publish.yml 由四个作业组成:validate→build(macOS)与build-windows(Windows)并行 →release。几个值得关注的实现细节:
validate 作业:fail-closed 的渠道映射
渠道映射是"承重墙":每个渠道定义自己的 tag 形状、祖先分支、feed 与产品名,未知渠道直接失败。源码中的注释点明了原因——updater 比较器是朴素的 semver"newer than",beta manifest 落到desktop-latest会让所有 stable 安装自动更新到 beta。stable 的正则拒绝 prerelease 后缀也是同一原因。validate 还做三件事:
- 密钥作用域检查(
Verify signing secrets are not repository-scoped):该作业不声明任何 environment,因此凡是能在此解析出的签名密钥只可能是 repository/organization 级密钥——意味着全仓库任何工作流都能读到它。这一步与 build 作业的存在性检查配合,共同证明密钥确实来自PublishDesktop环境; - tag 与版本一致性:checkout tag 后,比对
package.json与tauri.conf.json的版本与 tag 剥离desktop-v前缀后的值; - 祖先检查:tag 指向的 commit 必须可从渠道分支(
origin/main或origin/desktop-experimental)到达。
build 作业:macOS universal 构建与三道护栏
build作业声明environment: PublishDesktop且带if: github.ref == 'refs/heads/main'。工作流注释明确说明该if是"咨询性"的——真正强制的门禁是环境的 deployment-branch 策略,因为"被派发的分支会运行自己的工作流副本"。作业内的关键步骤:
- 密钥存在性前置检查:Tauri 在
APPLE_CERTIFICATE为空时会静默跳过代码签名、在APPLE_API_KEY为空时静默跳过公证,构建照样成功并产出一个未签名未公证的 bundle——所以必须在任何构建前 fail up front; - universal 校验:Tauri 自己 lipo 主二进制,但 sidecar 由自研的
build-sidecar-bin.ts合并,所以逐一把Contents/MacOS/*过lipo -archs,任一不是双架构即失败——单架构 sidecar 会"发布顺利、另一架构上崩溃"; - feed 端点断言:updater 端点由 tauri-build 以字符串字面量编入主二进制(合并配置经 codegen 内嵌),
strings+grep断言 bundle 恰好内嵌本渠道 feed URL,捕获--config叠加层静默未生效的场景; - 遥测自检:sidecar 以
--telemetry-selfcheck运行,断言"enabled":true且 OTLP endpoint host 可解析——--define内联是打包后 App(从 Finder/Dock 启动、无运行时环境变量)唯一能上报遥测的途径; - 刻意不使用 Rust 构建缓存:这是唯一能读到 Apple 证书与 updater key 的作业,Actions 缓存一旦中毒,恢复的缓存归档就是攻击者可控的。
产物收集阶段把 DMG 与 updater tarball 及其.sig复制为dist/publish/下的<PREFIX>_<VERSION>_universal.dmg/.app.tar.gz/.sig(Cline→Cline,Cline Beta→Cline-Beta)。
build-windows 作业:Azure Trusted Signing 全链签名
该作业需要id-token: write,通过PublishDesktop环境的联邦凭据(cline-cli-signingEntra 应用,subjectrepo:cline/cline:environment:PublishDesktop)走 OIDC 登录 Azure。设计要点:
- 全有或全无:未签名的 Windows 桌面构建绝不可接受(Smart App Control / WDAC 拦截未签名 exe,SmartScreen 标记未签名安装器),且 Tauri 在 updater key 缺失时会静默跳过 updater 制品签名——与 CLI 管线不同,这里没有未签名回退;
- 作业内所有 action 均SHA 钉死:它们持有
id-token: write与 updater 签名密钥,被劫持的上游 tag 不得触达签名身份; - 签名配置是运行时生成的 overlay(
signCommand需要签名脚本的绝对路径),指向 tauri-sign-windows.ps1。脚本本身:下载 jsign 7.5 并校验 SHA-256、每次调用时(Tauri 对主 exe、sidecar、NSIS 卸载器、安装器各调一次signCommand)从az account get-access-token现取短时效 token 经环境变量(而非 argv)传给 jsign、以SHA-256算法 + RFC3161 微软时间戳服务签名,并在签完立即Get-AuthenticodeSignature自验; - 独立的
Verify Authenticode signatures步骤对dist/publish/*.exe和 sidecar exe 再验一遍——即使某个安装器完全跳过了signCommand也能被捕获(sidecar 必须在场:WDAC 锁定的机器会在运行时拦截它,即使安装器本身完好)。
release 作业:changelog 提取、manifest 生成与 feed 刷新
- changelog 精确匹配:用 awk 抓取
## <version>到下一个## <数字>之间的内容并逐字写入 release 正文。注释解释了为何必须是精确匹配而非"顶部小节":main与desktop-experimental交叉合并后,stable 与 beta 小节会按时间交错,顶部小节可能属于另一渠道; - Slack 截断:Slack section 块拒绝超过 3000 字符的文本且不报错,因此对超长 changelog 发送带"Read the full release notes"链接的截断副本,而 GitHub release 正文与 updater manifest 保持完整;
- updater manifest:由 generate-update-manifest.ts 生成。它扫描制品目录,把
*_universal.app.tar.gz同时映射到darwin-aarch64与darwin-x86_64两个平台 key(指向同一资产与同一签名),*_x64-setup.exe映射到windows-x86_64,并拒绝同一平台 key 被多个制品声称; - feed 二次校验:
Update auto-update feed步骤从 channel 重新计算期望 feed 并要求与 validate 输出一致("belt and braces",防止单点线程化 bug 把发布指到另一渠道的 feed),再确保滚动 release 存在(不存在则创建,--latest=false——仓库级 "latest" release 归 CLI 发布所有),最后gh release upload "$FEED" dist/desktop/latest.json --clobber刷新滚动资产; - 创建 release 时
make_latest: false,beta 加prerelease。
自动更新源的配置与生成原理
stable 的 updater 配置在 tauri.conf.json 中:plugins.updater.pubkey(minisign 公钥,用于校验 updater 制品签名)与endpoints指向https://github.com/cline/cline/releases/download/desktop-latest/latest.json。beta 由 tauri.beta.conf.json 覆盖productName(Cline Beta)、identifier(bot.cline.app.beta)与 updater 端点(desktop-beta/latest.json)。
Tauri 按顺序合并重复的--config标志,因此 beta 叠加层(产品名、包标识、beta 更新源)覆盖在 release 叠加层之上而不必复制它;工作流的 build 步骤据此为 beta 传两个--config。
滚动 feed 的运作方式:desktop-latest/desktop-beta是滚动 release,其latest.json资产(由 release 作业--clobber上传)指向最新的按 tag 不可变资产。generate-update-manifest.ts中每个平台条目的url形如https://github.com/<repo>/releases/download/<tag>/<artifact>——即 manifest 每发版刷新一次,下载 URL 永远指向对应版本的 release tag。
EXPERIMENTAL.md 特别强调命名不对称的陷阱:desktop-latest就是stable feed,不要改名成desktop-stable——URL 被编译进每一个已发布的 stable 二进制,updater 没有回退端点,改名会让所有改名前安装永久停在死 feed 上;desktop-beta在首个 beta 发出后同理。
Publish 密钥一次性配置
以下密钥必须配置在PublishDesktop环境(Settings → Environments → PublishDesktop → Environment secrets),而不是仓库级,使得只有build作业能读到、且只在审批之后能读到。该环境同时限制部署来源为main并强制审阅人。
把它们配成仓库级密钥是常见错误。环境门禁的作业同样会解析仓库级密钥(环境值只是优先),构建照样成功——凭据就此静默暴露在整个仓库。validate作业专门在任何无 environment 的作业中探测这些密钥,一旦发现解析成功即失败整次运行。遇到该报错时,删除仓库级副本,而不是再复制一份。
若某处都缺失,build的前置检查会点名缺失项使运行失败。Apple 值来自用于手工签名的同一 Apple Developer 账户(获取方式见桌面应用 README 的 "macOS signing & notarization" 一节):
| 密钥 | 值 |
|---|---|
APPLE_CERTIFICATE | 从 Keychain Access 导出的Developer ID Application身份.p12(须含私钥)的 Base64:base64 -i certificate.p12 \| pbcopy |
APPLE_CERTIFICATE_PASSWORD | 导出.p12时设定的密码 |
APPLE_SIGNING_IDENTITY | Developer ID Application: <Team Name> (<TEAMID>)——来自security find-identity -v -p codesigning |
APPLE_API_KEY | App Store Connect APIKey ID(用于公证) |
APPLE_API_KEY_CONTENT | AuthKey_<KEYID>.p8文件内容 |
APPLE_API_ISSUER | App Store ConnectIssuer ID(Users and Access → Integrations 的 UUID) |
TAURI_SIGNING_PRIVATE_KEY | Tauri updater 私钥内容(tauri signer generate)。此钥一旦丢失,已发布的 App 将无法再验证更新——务必妥善保管 |
TAURI_SIGNING_PRIVATE_KEY_PASSWORD | 该密钥的密码 |
Windows 侧还需仓库级AZURE_CLIENT_ID、AZURE_TENANT_ID、AZURE_SUBSCRIPTION_ID、AZURE_TRUSTED_SIGNING_ENDPOINT、AZURE_TRUSTED_SIGNING_ACCOUNT_NAME、AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_DESKTOP(工作流注释:AZURE_*为 repository secret,TAURI_*在 PublishDesktop 环境)。
Slack 与遥测类密钥(SLACK_RELEASE_BOT_TOKEN、TELEMETRY_SERVICE_API_KEY、ERROR_SERVICE_API_KEY、OTEL 配置)与 CLI、SDK、扩展发布工作流共享且已配置。不要把它们移入PublishDesktop——一旦作用域限定到该环境,其他所有发布工作流中的这些值会被静默清空,除了"遥测缺失 + Slack 发不出"之外不会有任何报错。
Beta 渠道的分支模型与合并冲突策略(EXPERIMENTAL.md 要点)
EXPERIMENTAL.md 补充了 beta 渠道的流程背景,发布时需要知道:
- beta 是独立 App 而非 stable 的模式:产品名、包标识不同,两者并排安装以便直接对比;两个 App 共享
~/.cline(provider 凭据、全局设置、hub daemon),beta 需要更新版 hub 时可能触发 hub-update-required 流程——这是预期的版本偏斜; - 分支模型:特性 PR 目标
desktop-experimental并在其上迭代(对main的原始 PR 保持 draft 状态积累后续工作);毕业= 向main提全新 PR,按正常 main PR 标准走评审。同步方向单向:定期把main合入desktop-experimental(至少每个 stable 桌面版本发布后一次),绝不整支反合; - 合并冲突策略:
package.json/tauri.conf.json版本号冲突时保留分支的 beta 版本;CHANGELOG.md保留双方小节、按版本新者优先(stable 与 beta 小节按时间交错);特性代码在已毕业内容上以 main 为准; - 无自动降级:退出 beta 就是删掉 beta App(stable 从未被触碰),已毕业功能的 stable 版本通过 stable App 的正常发布获得;
tauri.beta.conf.json必须存在于被 tag 的 commit 上(build 作业 checkout 的是 tag),因此要在main与desktop-experimental两个分支上都保留它。
发布前自检清单
汇总 SKILL.md 全文的门禁要求,发布动作完成后可按此核对:
- 版本号在 package.json 与 tauri.conf.json 中一致,且与 tag 一致;
- CHANGELOG.md 存在精确的
## <version>小节(beta 为## X.Y.Z-beta.N),无日期; - 发布 commit 在渠道分支上,tag 已推送且可从渠道分支到达;
- 工作流从
main派发,PublishDesktop审批通过; - 渠道 feed 的
latest.json已刷新且版本正确,另一渠道 feed 未被触碰; - beta 基座未与任何已发布 stable 的
X.Y.Z相同; - 报告含渠道、版本、tag、commit hash、工作流 URL 与 feed 验证结果。
以上流程与护栏均以当前仓库中的 desktop-publish.yml、generate-update-manifest.ts、tauri-sign-windows.ps1 及两份 tauri 配置的实际实现为准;当前桌面应用版本号为0.0.22,可作为流程理解时的现实参照。
【免费下载链接】clineAutonomous coding agent as an SDK, IDE extension, or CLI assistant.项目地址: https://gitcode.com/GitHub_Trending/cl/cline
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考