ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Context for Claude 独立发布指南:基于 GitHub Release 与 Sparkle 的 macOS 自更新发布管线

Context for Claude 独立发布指南:基于 GitHub Release 与 Sparkle 的 macOS 自更新发布管线 Context for Claude 独立发布指南基于 GitHub Release 与 Sparkle 的 macOS 自更新发布管线【免费下载链接】FriendAI that sees your screen, listens to your conversations and tells you what to do项目地址: https://gitcode.com/GitHub_Trending/fr/Friend导读本文聚焦 FriendOmi仓库中desktop/context-for-claude子项目的完整发布体系它如何以 GitHub Release 为唯一更新后端、通过 Sparkle appcast 向已安装用户推送增量更新以及如何在无平台端点、无需部署任何服务的前提下完成打 tag → 构建签名 → 公证 → 生成 feed → 发布的全流程。读完本文你将掌握该项目的发布脚本调用链release-context.sh→release-micro-app.sh→package-dmg.sh→generate-appcast.py、全部发布相关的环境变量与一次性密钥配置、--dry-run/--rehearsal/--publish三种运行模式的语义以及发布前的验证与回滚恢复策略可直接照此在 CI 或本地发起一次真实的版本发布。发布定位与 Omi Desktop 完全隔离的独立发布身份Context for Claude 是一个拥有独立产品身份的 macOS 应用它有自己的 Developer ID bundle 签名、独立的 Sparkle EdDSA 密钥对、独立的产物命名空间ContextForClaude-*和独立的发布 tag 命名空间context-for-claude-v*。这决定了它的发布路径与 Omi Desktop 的发布工作流完全分离不会互相干扰。这一点在 desktop/context-for-claude/Resources/Info.plist 中体现得最直接CFBundleIdentifier为com.omi.context-for-claudeSUFeedURL指向专属于该应用的 feed 地址SUPublicEDKey在源码中刻意保留占位符REPLACE_WITH_SUPublicEDKey_FROM_generate_keys由 desktop/context-for-claude/scripts/build.sh 在构建时注入真实公钥对应源码中PlistBuddy -c Set :SUPublicEDKey ...的逻辑。也就是说仓库本身不包含任何可用的发布密钥本地普通开发构建可以正常编译运行但无法进行自动更新。架构核心GitHub 是唯一的更新后端该项目的更新链路与常见的中心化后端下发 feed方案不同没有平台端点为这个应用提供 feed更新到达用户不需要部署任何服务应用只与github.com通信更新器完全内置于应用包中SUFeedURL指向一个作为 GitHub Release 资产存放的 Sparkle appcast而把 appcast 放到那个位置的正是scripts/release-context.sh。入口脚本是 desktop/context-for-claude/scripts/release-context.sh它只做一件事把本产品的名称和各密钥变量名tag 前缀、产品 slug、应用名、产物前缀、密钥环境变量名等传递给可复用的通用发布编排脚本 desktop/context-for-claude/scripts/release-micro-app.sh。后者负责 tag 校验、版本注入、Sparkle ZIP 签名、appcast 生成和 GitHub 发布desktop/context-for-claude/scripts/package-dmg.sh 负责组装并签名应用、内嵌 Sparkle 代码、DMG 与 ZIP 封装desktop/context-for-claude/scripts/generate-appcast.py 负责生成 feed XML。从release-context.sh的源码可以看出产品侧的全部命名参数这些命名参数是一个个独立事实集中管理exec $SCRIPT_DIR/release-micro-app.sh \ --tag-prefix ${CONTEXT_RELEASE_TAG_PREFIX:-context-for-claude-v} \ --product-slug context-for-claude \ --app-name Context for Claude \ --artifact-prefix ContextForClaude \ --package-script $SCRIPT_DIR/package-dmg.sh \ --public-key-env CONTEXT_SPARKLE_PUBLIC_KEY \ --private-key-env CONTEXT_SPARKLE_PRIVATE_KEY \ --version-env CONTEXT_VERSION \ --build-number-env CONTEXT_BUILD_NUMBER \ --release-identity-env CFC_SIGN_IDENTITY \ --github-token-env $GITHUB_TOKEN_ENV \ --github-repo $RELEASE_REPO \ --feed-url ${CONTEXT_APPCAST_FEED_URL:-$DEFAULT_FEED_URL} \ $其中RELEASE_REPO默认取BasedHardware/omi可由环境变量CONTEXT_RELEASE_REPO覆盖feed URL 默认推导为https://github.com/repo/releases/download/context-for-claude-appcast/appcast.xml。更新源feed的存放设计feed 的稳定地址feed 的正式地址是https://github.com/BasedHardware/omi/releases/download/context-for-claude-appcast/appcast.xml这条 URL 必须与 desktop/context-for-claude/Resources/Info.plist 中的SUFeedURL逐字符一致。发布助手直接从 feed URL 中解析出 holder tagcontext-for-claude-appcast和资产名appcast.xml保证feed 在哪这个事实只有一份拷贝并且当构建出的 bundle 的SUFeedURL与本次将要写入的 feed 不一致时拒绝发布见release-micro-app.sh中BUNDLE_FEED_URL与FEED_URL的比较逻辑。两个承重设计feed 不是被跟踪的仓库文件。enclosure 的 EdDSA 签名只有在 ZIP 打包完成后才存在因此 feed 只能在构建之后写入。如果appcast.xml被纳入版本控制就意味着每次发布都要向main提交一个 commit该仓库禁止此行为而走 PR 流程又会让一条命令的发布变成两步人工流程。因此 feed 被上传为一个固定的、可变的 release 上的资产使用--clobber原地覆盖。holder release 必须是 prerelease。如果没有--prerelease这个 release 会抢占仓库级的 Latest release 指针仓库首页就会把一个 XML 文件当作 Omi 的最新下载项展示。助手创建 holder release 时强制带--prerelease不要手动翻转。版本化 releasecontext-for-claude-vversion持有实际产物且不可变holder release 只持有 feed 并被原地重写除此之外不向它上传任何东西。为什么禁用releases/latest/downloadreleases/latest/download/...在这个仓库里任何地方都不允许使用它解析到整个仓库的最新 release而该仓库几乎每天都会发布 Omi Desktop 的 tag所以这个拼写要么 404要么把另一个产品的 bundle 交给用户。release-micro-app.sh通过正则约束 feed URL 必须形如https://github.com/owner/repo/releases/download/tag/name.xml同时校验 holder tag 必须在产品命名空间内context-for-claude-*、不能是版本化 taggenerate-appcast.py也拒绝包含releases/latest/download的 URL。这套拒绝逻辑被 desktop/context-for-claude/scripts/test-release-path.sh 作为行为契约反复测试。唯一一个瞬时故障模式gh release upload --clobber会先删除旧资产再上传新资产因此存在一个亚秒级的窗口feed URL 会 404。恰好在这个窗口内发起的 Sparkle 检查会向用户报告一次 feed 错误并在下一个计划检查时自动重试不会有任何数据损坏也不会让客户端装错东西。这是可变资产 稳定 URL的既定成本。一次性设置Sparkle 更新密钥对使用与锁定依赖匹配的 Sparkle 工具Package.resolved中锁定Sparkle 2.9.4。swift package resolve之后工具已经解包在.build/artifacts/sparkle/Sparkle/bin/下./.build/artifacts/sparkle/Sparkle/bin/generate_keysgenerate_keys会把私钥放入发布维护者的登录钥匙串并打印出公钥。私钥应当一直留在钥匙串里sign_update不带参数就能找到它因此本地发布从不移动私钥、从不把它放进环境变量、也从不把它写到磁盘。两个密钥相关的环境变量CONTEXT_SPARKLE_PUBLIC_KEY44 字符的 base64 公钥。不是机密但发布构建必须提供它——源码里的Resources/Info.plist有意保留占位符build.sh会把真实公钥注入到拷贝出来的 bundle plist。CONTEXT_SPARKLE_PRIVATE_KEY仅供没有钥匙串的 CI runner使用。设置时助手把它通过管道传给sign_update --ed-key-file -未设置时sign_update读钥匙串。导出方式为generate_keys -x /secure/offline/context-for-claude-eddsa.key并保留一份离线备份。私钥永远不会被打印、写入仓库或包含在发布产物中。轮换这把密钥会让所有已安装版本无法验证未来更新轮换意味着需要人工重装并配合更新器改造因此必须极其谨慎。发布模式采用失败即关闭fail closed策略公钥缺失或仍是占位符时直接失败。而普通本地开发构建不需要发布密钥也能继续工作只是无法自动更新。GitHub 发布凭证创建CONTEXT_GITHUB_TOKEN一个 fine-grained token作用域限定为发布仓库授予仓库 Contents 写权限含 releases和 Metadata 读权限。助手通过GH_TOKEN使用它绝不作为命令行参数传递。gh必须位于PATH中。值得注意的源码细节release-context.sh中GITHUB_TOKEN_ENV的解析逻辑会优先使用CONTEXT_GITHUB_TOKEN如果未设置则回退到已有的GH_TOKEN或GITHUB_TOKEN并在 stderr 打出提示。该选择以变量名的方式传递助手再自行解析值因此 token 永远不会出现在进程列表的参数中。test-release-path.sh专门验证了存在 scoped token 时不得静默使用共享 token这一契约。Apple 签名与公证可分发发布需要钥匙串中的 Developer ID Application 证书和公证凭据CFC_SIGN_IDENTITY— 形如Developer ID Application: NAME (TEAMID)。CFC_NOTARY_PROFILE— 一个notarytool钥匙串 profile或使用三个CFC_ASC_*App Store Connect 值CFC_ASC_PRIVATE_KEY、CFC_ASC_KEY_ID、CFC_ASC_ISSUER_ID。xcrun notarytool store-credentials context-notary \ --apple-id youexample.com --team-id TEAMID --password app-specific-passwordDeveloper ID 分发 bundle不需要 provisioning profile。自签名身份Omi Local Dev Signing永远无法产生真实发布——它只能用于下文提到的 Rehearsal。从package-dmg.sh源码看公证层还有一道重要防线assert_release_signing_authority会读取 bundle 的Authority链要求依次包含Developer ID Application:、Developer ID Certification Authority、Apple Root CA。原因很实际macOS 把 TCC 权限授予绑定在签名证书上若用一枚仅名字叫 Developer ID但实际是自签名的证书发布会写入任何未来版本都无法满足的权限记录导致用户的屏幕录制/麦克风权限永久性失效。从 CI 运行发布脚本对 CI 提供商零假设一台装有 Xcode、git、gh、python3的普通 macOS runner 即可运行。Job 必须提供的就是变量用途CONTEXT_SPARKLE_PUBLIC_KEY注入到构建出的 bundle缺失则发布失败关闭CONTEXT_SPARKLE_PRIVATE_KEYZIP 签名因为 CI runner 没有登录钥匙串CONTEXT_GITHUB_TOKEN创建版本化 release 并更新 feed 资产CFC_SIGN_IDENTITY将 Developer ID.p12导入 runner 钥匙串后设置CFC_ASC_PRIVATE_KEY、CFC_ASC_KEY_ID、CFC_ASC_ISSUER_IDnotarytool凭据CONTEXT_RELEASE_REPO可选默认BasedHardware/omiCI Job 的标准动作是检出 tag →xcrun swift package resolve→ 导入证书 → 在desktop/context-for-claude目录下执行./scripts/release-context.sh --tag $TAG --publish。发布流程tag 约定context-for-claude-vmajor.minor.patch例如context-for-claude-v1.1.0会发布ContextForClaude-1.1.0.dmg— 供手动安装ContextForClaude-1.1.0.zip— 作为 Sparkle enclosure。助手从 tag 推导出确定性的数字 Sparkle 构建号major * 1,000,000 minor * 1,000 patch并把两个版本字段都注入构建出的 plist。这样源码模板保持不动同时保证 Sparkle 看到的是随稳定版本单调递增的数字构建号release-micro-app.sh中BUILD_NUMBER$((10#$MAJOR * 1000000 10#$MINOR * 1000 10#$PATCH))。tag 版本必须是形如x.y.z的稳定语义化版本否则脚本拒绝执行。打 tag 前的无密钥本地校验cd desktop/context-for-claude ./scripts/test-release-path.sh ./scripts/release-context.sh --tag context-for-claude-v1.1.0 --dry-runtest-release-path.sh是一套不联网、不需要任何发布密钥的守护测试它真实运行 appcast 生成器并把发布助手的一系列拒绝行为当作契约断言占位公钥拒绝、外部仓库 feed 拒绝、releases/latest/download拒绝、版本化 tag 充当 holder 拒绝、无 Developer ID 拒绝发布、rehearsal 发布到生产仓库拒绝、非语义化版本拒绝等。--dry-run会校验 tag、产物名、feed URL、holder release、enclosure URL、打包入口以及任何已提供的密钥材料但不构建、不公证、不签名、不发布、不修改任何文件。从源码看dry-run 会把推导出的产品名、tag、版本、构建号、包脚本、产物文件名、feed URL、holder 信息、enclosure URL 和永久下载链接全部打印出来供人核对。正式发布维护者随后创建并推送 tag然后发布git tag context-for-claude-v1.1.0 git push origin context-for-claude-v1.1.0 CONTEXT_SPARKLE_PUBLIC_KEY44-char public key \ CONTEXT_GITHUB_TOKENtoken \ CFC_SIGN_IDENTITYDeveloper ID Application: NAME (TEAMID) \ CFC_NOTARY_PROFILEcontext-notary \ ./scripts/release-context.sh --tag context-for-claude-v1.1.0 --publish--publish依次执行以下序列并在第一步无法保证时直接拒绝先于构建做前置检查校验 token、确认该 tag 上尚无 release、确认HEAD就是 tag 指向的确切 commit——避免在四十分钟构建结束后才发现缺工具、token 失效或 tag 已发布。这一步还会单拉一次远端 tag 再比较 SHACI 检出通常只含 tag 指向的 commit 而不含 tag ref 本身。构建与签名构建 ContextApp 与context-for-claude-mcp把公钥和 tag 推导的版本注入构建出的Info.plist并用 Developer ID 身份从内到外签名 app、MCP 可执行文件、Sparkle 框架及每个内嵌的 Sparkle XPC/helper 组件。公证与打包公证并 stapling app创建 DMG然后对 DMG 本身签名、公证、stapling。package-dmg.sh中 app 以 ZIP 形式提交公证、DMG 以磁盘映像本身提交stapling 后首启可离线验证。生成 Sparkle ZIP从已 stapling 的 app 创建 ZIP并校验 bundle 的SUFeedURL与本次将要写入的 feed 一致。签名并验证用 EdDSA 私钥对 ZIP 签名然后针对 ZIP 的真实字节验证该签名——签名了一个文件和发布一个能通过校验的 feed是两个不同的主张必须都成立。追加 feed从 holder release 下载当前已发布的appcast.xml把新 item 追加进去。holder release 或资产尚不存在说明是首个版本会新建 feed资产存在但无法下载或解析则中止发布——丢掉所有旧客户端的更新历史永远不是更安全的那个分支。创建 GitHub release在该 tag 下创建 release附加 DMG 与 ZIP。更新 holder release首个版本用--prerelease创建 holder release后续版本把重新生成的appcast.xml用--clobber上传覆盖。产物先于 feed 上线一个 item 只有在其命名的 enclosure 真正可下载之后才可发布。重跑是安全的generate-appcast.py采用替换式插入如果 feed 中已存在相同数字构建号的 item就在原位置替换并保留其原始pubDate因此重试不会打乱 feed 顺序也不会给已发布的构建重新盖章日期。发布资产应当用脚本重新生成而非手工编辑——手工编辑正是更新历史被截断的常见原因。Rehearsal 演练模式不是每台机器都有 Developer ID 证书但发布路径仍然必须能端到端演练--rehearsal就是为此设计的# 本地构建、打包、签名 enclosure、生成 feed。不联网、不发布任何东西。 CONTEXT_SPARKLE_PUBLIC_KEY44-char public key \ ./scripts/release-context.sh --tag context-for-claude-v1.1.0 --rehearsal # 同上但用一份已发布 feed 的拷贝来演练追加路径 ... --rehearsal --existing-appcast /path/to/published-appcast.xml # 把演练发布到 fork演练 gh release create 与 feed 上传 CONTEXT_RELEASE_REPOyou/omi CONTEXT_GITHUB_TOKENtoken \ CONTEXT_APPCAST_FEED_URLhttps://github.com/you/omi/releases/download/context-for-claude-appcast/appcast.xml \ ./scripts/release-context.sh --tag context-for-claude-v1.1.0 --rehearsal --publishRehearsal可以构建并打包真实 app、产出真实 DMG、产出真正签名的 Sparkle ZIP、验证该签名、生成长度/签名/版本与磁盘上产物一致的合法 appcast并针对 fork 演练全部 GitHub 步骤。Rehearsal不可以产出任何别人能安装的东西也绝不能被误认为可以。它的产物命名为ContextForClaude-version-rehearsal.dmg/.zipfeed 写入dist/rehearsal/appcast.xml并带有REHEARSAL FEED — NOT DISTRIBUTABLE横幅演练版 GitHub release 标记为 prerelease 且标题为(REHEARSAL — not distributable)--rehearsal --publish完全拒绝面向BasedHardware/omi。它未经公证所以除了构建它的那台机器Gatekeeper 在任何机器上都会拒绝它真实安装的 Sparkle 客户端也无法安装它。反向同样成立不带--rehearsal的--publish在没有 Developer ID 身份时根本拒绝运行而package-dmg.sh --release在 Gatekeeper 拒绝结果时直接失败。两者不可能被混淆。CONTEXT_RELEASE_REPO同时重定向产物与 feed因此 fork 演练无需修改脚本。需要注意fork 演练的 feed 无法被生产 bundle 消费——应用读取的是烘焙进SUFeedURL的生产地址。把CONTEXT_APPCAST_FEED_URL指向 fork可以让助手的feed 与产物仓库一致性检查继续成立。发布前验证检查签名身份对照上一次发布检查实际发出的签名身份与 Team IDcodesign -dv --verbose2 build/Context for Claude.app 21 \ | grep -E ^(Identifier|Authority|TeamIdentifier|Runtime Version) xcrun stapler validate dist/ContextForClaude-1.1.0.dmg codesign --verify --deep --strict build/Context for Claude.app unzip -t dist/ContextForClaude-1.1.0.zipIdentifier必须是com.omi.context-for-claude。叶子 authority 与 Team ID 必须与上一个公开版本一致且TeamIdentifier不能是not set。Team ID 一旦改变会在 Sparkle 替换 bundle 后静默撤销用户的屏幕录制、麦克风和系统音频授权——macOS 把这些授权绑定在签名身份上更新后应用会带着零权限回来且没有任何提示。发现不一致必须中止发布。检查 feed从客户端的视角检查 feedcurl -fsSL https://github.com/BasedHardware/omi/releases/download/context-for-claude-appcast/appcast.xml \ | xmllint --format - curl -fsSLI $(curl -fsSL https://github.com/BasedHardware/omi/releases/download/context-for-claude-appcast/appcast.xml \ | xmllint --xpath string(/rss/channel/item[1]/enclosure/url) -) | head -1最新 item 的sparkle:version必须是该 tag 的数字构建号其length必须等于已发布 ZIP 的字节大小enclosure URL 必须可解析之前发布的每一个 item 都必须仍然存在。GitHub release 中必须恰好包含 Context 的 DMG 和 Context 的 Sparkle ZIP不得混入任何 Omi Desktop 资产。generate-appcast.py的源码为这些约束提供了背书length直接来自磁盘上 ZIP 的真实字节数os.path.getsizesparkle:edSignature来自sign_update的输出sparkle:minimumSystemVersion来自应用自己的Resources/Info.plist的LSMinimumSystemVersion——没有任何值是默认出来的缺失即报错因为能解析的 feed不等于描述用户下载产物的 feed。首次安装的升级约束Sparkle 只能升级一个已经具备相同产品身份与签名连续性的 Context 安装首次公开安装必须是已公证的 Developer ID Context DMG或由同一 Apple Team 签名的等效手动安装 bundle。本地Omi Local Dev Signing构建、rehearsal 构建、占位公钥构建或由其他 Team 签名的构建都无法通过 Sparkle 接收公开版本。首次公开版本必须手动安装之后的新版本才能自动更新。绝不要把 Context 指向 Omi 的 feed也不要复用 Omi 的 EdDSA 密钥。bundle ID、feed URL、证书 Team ID 与 EdDSA 密钥是四道独立的信任边界。客户端侧的实现证据在 desktop/context-for-claude/Sources/ContextApp/Update/UpdatePolicy.swiftdecide(...)依次拒绝非发布 bundle、feed 未配置/不是本产品精确 URLisUsableFeedURL逐字段比对 scheme、host、path必须精确等于com.omi.context-for-claude的 appcast 地址、更新公钥缺失/占位/不是 32 字节 base64isConfiguredPublicKey校验解码后长度恰为 32 字节、签名无 Team IDsignedWithoutATeam以及被环境变量CONTEXT_DISABLE_UPDATES关闭的情况。currentTeamIdentifier()通过 Security 框架读取运行中进程的签名 Team ID——这是本地构建永远不会满足的条件因此开发者的本地拷贝绝不会把自己悄悄替换成发布版本而丢失捕获权限。回滚与恢复feed 是撤回点若发布在用户安装前就被发现有问题重新生成不含坏 item 的 feed用--clobber上传到 holder release然后删除或取消发布版本化 release。客户端在下一次检查读到新 feed 后就不再被提供该版本。按策略要求保留 tag 与事故证据且不要复用该 tag 发布不同的 ZIP。若用户已经安装了坏版本Sparkle 不会安装更低的数字构建号。应当发布一个带相同 Context 身份、更高版本/构建号的修复版验证新 DMG 与 ZIP 后再宣布。因此回滚 对未安装客户端撤回 feed 对已安装客户端发布更高构建号的前向修复它不是降级。本地日常命令日常开发与发布密钥完全解耦./scripts/build.sh --no-install用开发身份做本地打包冒烟测试不做 Finder 自动化./scripts/package-dmg.sh --no-style这不是可分发的版本绝不能上传或作为 Sparkle 更新使用。相关文档导航发布全流程的权威文档desktop/context-for-claude/docs/releasing.md产品背景与为什么签名/更新如此谨慎desktop/context-for-claude/README.md客户端更新策略TCC 权限保护、feed 白名单、fail-closed 决策desktop/context-for-claude/Sources/ContextApp/Update/UpdatePolicy.swift发布助手的三件套release-context.sh、release-micro-app.sh、package-dmg.shfeed 生成器generate-appcast.py发布路径守护测试无密钥可跑test-release-path.sh【免费下载链接】FriendAI that sees your screen, listens to your conversations and tells you what to do项目地址: https://gitcode.com/GitHub_Trending/fr/Friend创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表