macOS 应用发布到 GitHub 的完整流程:DMG、签名、公证与 Release

面向独立分发 macOS 应用的端到端发布手册:准备 Developer ID 与公证凭据、构建和验证可安装 DMG、先打版本 tag,再用 GitHub Draft Release 上传、回下载验收并正式发布。

  • 编程
  • 产品

适用范围:本文面向 不经过 Mac App Store、通过 GitHub Release 直接分发的 macOS 应用。最终产物是用户可直接安装的 DMG,而不是开发测试包或 GitHub 自动生成的源码压缩包。本文按 2026-08-02 可用的 Apple 与 GitHub 官方流程整理。

先理解这条发布链路

一次可信的直接分发,不是“把 App 压进 DMG 后上传”这么简单,而是让版本、代码来源、签名身份、公证结果和公开资产形成一条可追溯的链:

  1. 确定版本号和最终提交。
  2. 把最终提交推送到 GitHub。
  3. 先创建并推送版本 tag。
  4. 从这个 tag 对应的干净源码构建 Release 版本。
  5. 使用 Developer ID Application 签名 App 及其所有嵌套组件。
  6. 制作并签名 DMG。
  7. 把最终 DMG 提交给 Apple 公证,等待 Accepted。
  8. 将公证票据 staple 到 DMG。
  9. 对最终字节做签名、Gatekeeper、磁盘镜像和 SHA-256 验证。
  10. 创建 GitHub Draft Release,上传 DMG 与校验文件。
  11. 从 Draft 回下载资产,重新验收。
  12. 发布 Release,并从公开下载入口再做一次真实安装测试。

这里有五个容易混淆的概念:

  • Developer ID 签名:证明软件由哪个 Apple 开发团队发布,并保护签名后的内容不被篡改。
  • Apple 公证:Apple 的自动化恶意软件与签名检查,不等同于 App Review。
  • Stapling:把公证票据附着到 DMG,让离线 Mac 也能确认公证结果。
  • Git tag:固定仓库历史中的某个提交。
  • GitHub Release:基于 tag 的公开发布页,可附带说明和 DMG 等二进制资产。

一、开始前需要准备什么

1. Apple Developer Program 会员资格

为什么需要

免费 Apple 开发者账号可以本机开发测试,但不能完成面向公众的 Developer ID 直接分发。Developer ID 证书和 Apple 公证服务属于 Apple Developer Program 的分发能力。

在哪里获取

Apple Developer Program 注册页面以个人或组织身份加入。Apple 当前公开价格为每年 99 美元,实际金额、税费和本地货币以注册页为准。个人账号需要开启双重认证;组织账号还需要组织身份和 D-U-N-S 等材料。

2. Apple Account、Team ID、Bundle ID 与能力配置

为什么需要

  • Apple Account 用于登录开发者服务。
  • Team ID 标识签名所属团队。
  • Bundle ID 唯一标识 App;扩展、Helper 通常有各自的 Bundle ID。
  • App Group、iCloud、推送、File Provider 等能力需要在 App、扩展、Identifier 和 provisioning profile 之间保持一致。

在哪里查看或配置

  • Team ID:登录 Apple Developer Account 后查看 Membership details。
  • Identifiers 与能力:Apple Developer 的 Certificates, Identifiers & Profiles。
  • Xcode:Target → Signing & Capabilities。

如果应用包含扩展,必须逐个核对主 App 与扩展的 Team ID、Bundle ID、App Group 和 entitlement。只检查宿主 App 不够。

3. Developer ID Application 证书和匹配私钥

为什么需要

Developer ID Application 是直接分发 macOS App、扩展、框架、Helper 和 DMG 的签名身份。Gatekeeper 会据此确认开发团队与文件完整性。

不要选错证书

  • Developer ID Application:签 App、可执行代码和 DMG,本流程需要。
  • Developer ID Installer:签直接分发的 flat PKG;纯 DMG 发布不需要。
  • Apple Development:仅用于开发测试,不能作为公开 DMG 的正式身份。
  • Mac App Distribution / Mac Installer Distribution:用于 Mac App Store,不是 GitHub 直接分发。

在哪里获取

方式一是让 Xcode 管理:Xcode → Settings → Accounts → 选择团队 → Manage Certificates。

方式二是手动创建:

  1. 打开“钥匙串访问”。
  2. 选择“证书助理 → 从证书颁发机构请求证书”,生成 CSR。
  3. Certificates, Identifiers & Profiles 新建 Developer ID Application。
  4. 上传 CSR,下载 .cer 并双击安装。

Apple 的详细步骤见 Developer ID certificates创建 CSR

CSR 生成时,机器会创建私钥;下载回来的 .cer 只有证书,不包含私钥。因此“钥匙串里能看到证书”不代表能签名,必须在“我的证书”中看到证书下面展开出的匹配私钥。

security find-identity -v -p codesigning

输出中应存在目标团队的 Developer ID Application 且被识别为 valid identity。换 Mac 或在 CI 构建时,需要安全迁移包含证书和私钥的密码保护 .p12;不要把 .p12、私钥或密码提交到仓库。

4. Apple 公证凭据

公证凭据只负责让 notarytool 登录 Apple 公证服务,它不能代替 Developer ID 私钥。反过来,Developer ID 证书也不能代替公证凭据。

方案 A:Apple Account + App 专用密码

这是个人开发者最直观的方式。

为什么不是 Apple Account 主密码

Apple Account 开启双重认证后,命令行工具使用单独的 App 专用密码,避免主密码被脚本保存或收集。

在哪里获取

登录 account.apple.com → 登录与安全 → App 专用密码 → 生成。生成后立即放进密码管理器;修改或重置 Apple Account 主密码会使现有 App 专用密码全部失效。Apple 官方说明见 App 专用密码

不要把密码直接写进脚本。让 notarytool 安全提示输入,并保存到 macOS 钥匙串:

xcrun notarytool store-credentials "notary-prod" --apple-id "YOUR_APPLE_ACCOUNT" --team-id "YOUR_TEAM_ID"

其中 notary-prod 是本机自定义的 profile 名,不是 Apple 创建的资源。命令会验证凭据并把它存进钥匙串。之后先做连通性检查:

xcrun notarytool history --keychain-profile "notary-prod"
方案 B:App Store Connect API Key

更适合 CI 或不希望绑定个人 Apple Account 的自动化环境。

在哪里获取

App Store Connect → Users and Access → Integrations → App Store Connect API。团队首次使用 API 时可能需要 Account Holder 申请访问。创建后保存:

  • Key ID
  • Issuer ID
  • 只能下载一次的 AuthKey_*.p8 私钥文件

入口和角色说明见 App Store Connect API。p8 文件丢失后不能再次下载;泄露时应立即吊销并换新。

xcrun notarytool store-credentials "notary-ci" --key "/SECURE_PATH/AuthKey_KEY_ID.p8" --key-id "KEY_ID" --issuer "ISSUER_UUID"

无论使用哪种方案,都不要把 App 专用密码、p8、p12、解密密码或真实凭据写进 README、Release notes、构建日志或 Git 仓库。

5. Xcode、命令行工具和 GitHub 权限

需要:

  • 当前受支持的 Xcode 和 Xcode Command Line Tools。
  • Git。
  • GitHub 仓库的 write 权限。
  • GitHub CLI;macOS 可通过 Homebrew 安装,也可按官网方式安装。
  • 已完成 GitHub CLI 登录。
xcodebuild -version
xcrun notarytool --version
git --version
gh --version
gh auth login
gh auth status

gh auth login 的浏览器授权通常比手工创建长期 PAT 更省心。确需 PAT 时优先使用权限和仓库范围更小、带过期时间的 fine-grained token,并像密码一样保管。

二、把项目配置成可直接分发

1. 版本号与构建号

macOS App 通常至少有两组版本:

  • Marketing Version / CFBundleShortVersionString:用户看到的版本,例如 1.2.3。
  • Build Number / CFBundleVersion:每次构建递增的整数,例如 42。

主 App、扩展和 Helper 的版本应一致或遵循项目明确约定。DMG 文件名、tag 和 Release 标题也应对应同一 Marketing Version。

推荐规则:

  • tag:v1.2.3
  • Release 标题:AppName v1.2.3
  • DMG:AppName-1.2.3.dmg
  • 校验文件:AppName-1.2.3.dmg.sha256

GitHub 不会把名为“1”的 tag 自动显示成应用版本号。页面显示什么,取决于实际创建的 tag 名。如果显示“1”,就说明 tag 本身叫“1”,应在发布前删除错误 tag 并按统一规则重建;已经公开或启用不可变发布后,不应移动或复用 tag,而应发布新版本。

2. Release 配置、Hardened Runtime 与 entitlement

逐个 Target 检查:

  • 构建配置为 Release。
  • 签名团队正确。
  • 直接分发使用 Developer ID Application。
  • Hardened Runtime 已开启。
  • 所有嵌套组件都能独立签名。
  • entitlement 只包含实际需要的能力。
  • 正式包不存在 com.apple.security.get-task-allow=true
  • 受限能力有匹配的 Developer ID provisioning profile。
  • 最低 macOS 版本和 CPU 架构符合对外说明。

Apple 只公证启用 Hardened Runtime 的 macOS 应用。它与 App Sandbox 是两项不同能力:直接分发必须满足 Hardened Runtime 要求,但并非所有直接分发 App 都统一要求开启 App Sandbox。参见 Configuring the hardened runtime

查看最终签名里的 entitlement,而不是只看源码中的 entitlements 文件:

codesign -d --entitlements :- "/PATH/AppName.app" 2>/dev/null

如果手工签名,要从最深层的 Framework、XPC、Helper、Extension 开始,逐层向外签,最后签主 App。不要用 codesign --deep 代替正确的签名顺序;--deep 可以用于最终验证,但不适合拿来“自动修复”所有嵌套代码。

三、冻结最终提交并先打 tag

1. 为什么 tag 必须先于 Release

GitHub Release 建立在 tag 上。若直接运行 gh release create 而远端没有这个 tag,CLI 可以从默认分支自动创建 tag,容易把 Release 指向错误提交。正式流程应自己先创建、推送并核对 tag,然后使用 --verify-tag 阻止自动创建。

2. 版本 tag 规则

建议采用语义化版本:

  • 正式版:v1.2.3
  • 预览版:v1.2.3-beta.1,并把 Release 标为 prerelease

tag、版本号、DMG 文件名和 Release 标题必须互相匹配。新发布推荐 annotated tag:

VERSION="1.2.3"
TAG="v$VERSION"

git status --short
git push origin HEAD
git tag -a "$TAG" -m "$TAG"
git push origin "$TAG"

发布前确认工作区没有未纳入版本的发布代码。一个常见稳妥做法是从 tag 建立独立 worktree 构建,避免本地未提交文件混进产物:

git fetch origin --tags
git worktree add "../AppName-release-$TAG" "$TAG"

核对 tag 最终指向的 commit:

git rev-parse HEAD
git rev-parse "$TAG^{commit}"
git ls-remote origin "refs/tags/$TAG" "refs/tags/$TAG^{}"

annotated tag 在远端可能显示两行:第一行是 tag object 的 SHA,带 ^{} 的一行才是被 tag 指向的 commit SHA。这不是 tag 错乱。

不要把 Git tag 签名与 App 的 Developer ID 签名混为一谈。 Git tag 可以再使用 GPG、SSH 或 S/MIME 签名,证明 Git 元数据来源;Developer ID 签名保护的是 macOS 可执行产物。二者解决的问题不同。

四、构建 Developer ID 版本

推荐由 Xcode Archive + developer-id export 生成正式 App,而不是把 DerivedData 中的开发包直接拿来发布。

xcodebuild -project "AppName.xcodeproj" -scheme "AppName" -configuration Release -destination "generic/platform=macOS" -archivePath "build/AppName.xcarchive" archive
xcodebuild -exportArchive -archivePath "build/AppName.xcarchive" -exportPath "build/export" -exportOptionsPlist "ExportOptions.plist"

developer-id 导出配置的核心字段如下,实际项目还应按 Xcode 当前版本和签名方式调整:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>method</key>
  <string>developer-id</string>
  <key>signingStyle</key>
  <string>automatic</string>
  <key>teamID</key>
  <string>YOUR_TEAM_ID</string>
</dict>
</plist>

构建后,先验最内层扩展,再验宿主 App:

codesign --verify --strict --verbose=2 "AppName.app/Contents/PlugIns/Extension.appex"
codesign --verify --deep --strict --verbose=2 "AppName.app"
codesign -dv --verbose=4 "AppName.app" 2>&1

输出应能确认:

  • Authority 是 Developer ID Application。
  • TeamIdentifier 正确。
  • Runtime Version 存在。
  • 主 App 与扩展的版本、Team ID 和能力符合预期。

如果看到 Apple Development、adhoc,或缺少匹配私钥,应立即停止,不能继续制作公开 DMG。

五、制作并签名 DMG

一个面向普通用户的安装型 DMG 通常包含:

  • AppName.app
  • 指向 /Applications 的快捷方式
  • 可选的 Finder 背景、图标和窗口布局

先把这些内容放入 staging 目录,再创建压缩磁盘镜像:

mkdir -p "build/dmg-root"
ditto "build/export/AppName.app" "build/dmg-root/AppName.app"
ln -s /Applications "build/dmg-root/Applications"
hdiutil create -volname "AppName" -srcfolder "build/dmg-root" -format UDZO -ov "dist/AppName-1.2.3.dmg"

对最终 DMG 使用同团队的 Developer ID Application 签名:

codesign --force --timestamp --sign "Developer ID Application: YOUR NAME (YOUR_TEAM_ID)" "dist/AppName-1.2.3.dmg"
codesign --verify --verbose=2 "dist/AppName-1.2.3.dmg"

DMG 内部的 App 必须在打包前已经完成签名;给外层 DMG 签名不会自动修复内部 App。

六、提交 Apple 公证并 staple

Apple 已停止接受 altool 公证,应使用 notarytool。官方命令行流程见 Customizing the notarization workflow

xcrun notarytool submit "dist/AppName-1.2.3.dmg" --keychain-profile "notary-prod" --wait --output-format json

需要保存:

  • submission ID
  • status
  • 提交时间
  • 公证日志

只有 status 为 Accepted 才能继续。即便 Accepted,也建议下载日志检查 warning:

xcrun notarytool log "SUBMISSION_ID" --keychain-profile "notary-prod" "notary-log.json"

Apple 通过后,把票据附着到实际发布的 DMG:

xcrun stapler staple "dist/AppName-1.2.3.dmg"
xcrun stapler validate "dist/AppName-1.2.3.dmg"

顺序必须是:签名 App → 制作并签名 DMG → 公证 DMG → staple DMG → 最终校验 → 生成 SHA-256。Staple 会修改 DMG 的最终字节,因此 checksum 不能提前生成。

七、在上传前做完整验证

1. 容器和签名验证

hdiutil verify "dist/AppName-1.2.3.dmg"
codesign --verify --verbose=2 "dist/AppName-1.2.3.dmg"
xcrun stapler validate "dist/AppName-1.2.3.dmg"

挂载 DMG 后继续验证里面的 App:

MOUNT_DIR="$(mktemp -d)"
hdiutil attach -nobrowse -mountpoint "$MOUNT_DIR" "dist/AppName-1.2.3.dmg"
codesign --verify --deep --strict --verbose=2 "$MOUNT_DIR/AppName.app"
spctl --assess --type execute --verbose=2 "$MOUNT_DIR/AppName.app"
hdiutil detach "$MOUNT_DIR"

在支持的系统上还可用:

syspolicy_check distribution "/PATH/AppName.app"

理想结果应表明 App 被接受,来源为 Notarized Developer ID。

2. Gatekeeper 的真实首次启动测试

命令行验证不能完全替代真实用户路径。最可靠的验收方式是:

  1. 使用从未运行过该版本的干净 Mac、干净用户或 VM 快照。
  2. 从最终 GitHub 公开链接用 Safari 或 Chrome 下载,确保产生真实 quarantine 属性。
  3. 可在断网后挂载 DMG,以确认 stapled ticket 可离线工作。
  4. 把 App 拖到 Applications。
  5. 首次启动并实际使用核心功能。
  6. 每次重测恢复 VM 快照,避免 Gatekeeper 缓存造成假通过。

gh release download、curl、scp 下载的文件不一定带浏览器路径中的 quarantine,因此“CLI 下载后能打开”不能单独证明普通用户首次下载体验正常。

3. 对最终字节生成 SHA-256

必须在公证、staple 和所有最终修改完成后执行:

cd dist
shasum -a 256 "AppName-1.2.3.dmg" > "AppName-1.2.3.dmg.sha256"
shasum -a 256 -c "AppName-1.2.3.dmg.sha256"

校验文件应只记录相对文件名,避免把本机绝对路径写进公开资产。

4. Mirage 项目的现成命令

Mirage 当前的发布脚本已经串联 Archive、Developer ID 导出、App 与 File Provider 验签、DMG 制作、DMG 签名、公证、staple、Gatekeeper 检查、hdiutil verify 和 SHA-256:

NOTARY_KEYCHAIN_PROFILE="MirageNotary" RELEASE_MODE="developer-id" ./Scripts/build_dmg.sh

profile 名不是秘密,但必须换成当前 Mac 实际保存的 keychain profile。development 模式只能供本机测试,不能作为 GitHub 的正式可安装资产。

八、编写 Release notes

Release notes 应只描述当前版本真实存在并经过验证的内容。推荐固定结构:

## Summary

一句话说明本版本解决什么问题。

## Changelog

- 新增或调整的真实功能
- 修复的真实问题
- 必要的兼容性变化

## Installation

1. 下载 AppName-1.2.3.dmg。
2. 打开 DMG。
3. 将 AppName 拖到 Applications。
4. 从 Applications 启动。

## Requirements

- macOS 最低版本
- 支持的 CPU 架构
- 首次使用需要的系统权限或网络条件

## Assets

- AppName-1.2.3.dmg:已签名、公证并 stapled 的安装镜像。
- AppName-1.2.3.dmg.sha256:DMG 的 SHA-256 校验文件。

不要把计划中的功能、未验证的兼容性、没有实现的自动更新或“绝对安全”等表述硬写进 Release notes。

九、创建 GitHub Draft Release

1. 先确认 GitHub CLI 指向正确仓库

git remote -v
gh repo view
gh auth status

2. 创建 Draft 并上传资产

TAG="v1.2.3"

gh release create "$TAG" \
  --verify-tag \
  --draft \
  --title "AppName v1.2.3" \
  --notes-file "release-notes.md" \
  "dist/AppName-1.2.3.dmg#AppName 1.2.3 for macOS" \
  "dist/AppName-1.2.3.dmg.sha256#SHA-256 checksum"

--verify-tag 会在远端 tag 不存在时直接失败,避免 CLI 自动创建错误 tag。Draft 的意义是先把页面和资产完整组装好,再做回下载验收;它不是最终公开状态。

GitHub 每个 Release 最多可附加 1000 个上传资产,每个文件必须小于 2 GiB。对普通 DMG 足够,但仍应确认上传完成而不是只看命令退出。

3. 检查 Draft 元数据

gh release view "$TAG" --json tagName,targetCommitish,isDraft,isPrerelease,assets,url
gh api "repos/OWNER/REPO/releases/tags/$TAG" --jq '{tag_name,draft,prerelease,immutable,assets:[.assets[]|{name,size,state,content_type,digest,browser_download_url}]}'

至少核对:

  • tagName 与目标版本一致。
  • isDraft 为 true。
  • prerelease 状态符合版本类型。
  • DMG 与 sha256 两个资产都存在。
  • 资产 state 为 uploaded。
  • 文件名、大小、内容类型和服务端 digest 合理。
  • tag 的 peeled commit 与本地最终 commit 一致。

十、从 Draft 回下载并独立验收

上传成功不代表 GitHub 上的字节一定正确。应下载到空临时目录:

VERIFY_DIR="$(mktemp -d)"

gh release download "$TAG" --pattern "*.dmg" --pattern "*.sha256" --dir "$VERIFY_DIR"
(cd "$VERIFY_DIR" && shasum -a 256 -c "AppName-1.2.3.dmg.sha256")
cmp "dist/AppName-1.2.3.dmg" "$VERIFY_DIR/AppName-1.2.3.dmg"
hdiutil verify "$VERIFY_DIR/AppName-1.2.3.dmg"
codesign --verify --verbose=2 "$VERIFY_DIR/AppName-1.2.3.dmg"
xcrun stapler validate "$VERIFY_DIR/AppName-1.2.3.dmg"

然后挂载回下载的 DMG,重复 App 的 codesign、spctl 和核心功能检查。正式验收必须始终显式指定 tag;省略 tag 时,gh release download 可能下载当前 Latest,而不是正在检查的版本。

如果环境中安装了可复用发布 skill,也可以用独立验证脚本对最终 DMG 做一次与构建脚本分离的检查:

$HOME/.agents/skills/publish-macos-github-release/scripts/verify_macos_dmg.sh "/PATH/AppName-1.2.3.dmg"

独立验证的价值在于避免“生成产物的脚本和验证产物的脚本犯了同一个错误”。

十一、正式发布并做公开验证

Draft 验收通过后发布:

gh release edit "$TAG" --draft=false --latest

稳定版可显式设为 Latest;预览版应使用 prerelease,不应设为 Latest。GitHub 也可以按日期和语义化版本自动判断 Latest,但正式流程最好检查结果,而不是依赖列表顺序。

发布后继续验证:

gh release view "$TAG" --json tagName,targetCommitish,isDraft,isPrerelease,isImmutable,assets,url
gh release list --json tagName,isDraft,isPrerelease,isLatest,isImmutable

还要完成两项 Draft 阶段无法完全替代的检查:

  1. 使用未登录 GitHub 的浏览器确认 Release 页面和 DMG 可匿名下载。
  2. 在干净 Mac 或 VM 中通过浏览器下载、安装并首次启动。

GitHub 会自动在每个 Release 下生成 Source code (zip)Source code (tar.gz)。它们是 tag 对应的源码快照,不是可安装程序,也不需要手工上传。面向普通用户的安装说明必须明确指向 DMG。

十二、建议开启不可变 Release

在仓库 Settings 的 Releases 区域可启用 release immutability。它只保护启用之后发布的 Release。发布后:

  • 关联 tag 不能移动或删除。
  • Release assets 不能修改或删除。
  • GitHub 自动生成包含 tag、commit SHA 和资产摘要的 release attestation。
  • 标题和 Release notes 仍可编辑。

GitHub 官方推荐的流程正是:先建 Draft → 上传全部资产 → 验证 → 发布。参见 Immutable releasesPreventing changes to your releases

启用后可以验证发布证明与本地资产:

gh release verify "$TAG"
gh release verify-asset "$TAG" "$VERIFY_DIR/AppName-1.2.3.dmg"

不要把 gh release upload --clobber 纳入正式流程。它会先删除同名旧资产再上传新文件;新上传失败时,旧资产已经丢失。公开资产有误时,正确做法是发布新的 patch 版本,而不是静默替换同名 DMG。

十三、用户升级时出现“与之前打开的版本不同”是什么意思

macOS 可能提示:

“App 与之前打开的版本不同。打开将允许它访问之前版本中的数据。”

这通常表示新 App 的代码签名 designated requirement 或开发团队身份与本机之前运行过的同 Bundle ID 版本不同。常见情况:

  • 以前安装的是 Apple Development 签名,后来换成 Developer ID Application。
  • 以前是 ad-hoc 或未签名构建。
  • Team ID、Bundle ID 或签名链发生变化。
  • App 被第三方重新签名,或文件被修改。

它不是“公证失败”的固定提示,而是 macOS 在提醒用户:一个签名身份不同的程序正在尝试继承旧版本的数据访问关系。首次从开发版切换到稳定 Developer ID 版时可能只出现一次;以后持续使用同一团队、同一 Bundle ID 和稳定的 Developer ID 分发身份,通常不会反复出现。若正式版本之间仍频繁出现,应立即比较新旧 App 的签名详情、TeamIdentifier、Identifier 和 designated requirement。

codesign -dv --verbose=4 "/Applications/AppName.app" 2>&1
codesign -dr - "/Applications/AppName.app" 2>&1

十四、常见失败与定位方法

找不到 Developer ID identity

现象:security find-identity 没有目标证书,或 codesign 报 identity not found。

检查:证书是否过期或撤销;钥匙串“我的证书”中是否有匹配私钥;Team ID 是否正确。只有 .cer、没有私钥时不能签名。

notarytool 凭据无效

检查:

  • App 专用密码是否输错。
  • Apple Account 主密码是否刚被修改,导致 App 专用密码被全部撤销。
  • Team ID 是否属于该账号。
  • keychain profile 名是否拼写一致。
  • API Key 的 Key ID、Issuer ID 和 p8 是否匹配且未吊销。

先运行 notarytool history,确认凭据可用,再开始耗时构建。

公证状态为 Invalid

使用 submission ID 下载日志,不要只看终端最后一行。常见原因包括嵌套组件未签名、Hardened Runtime 缺失、secure timestamp 缺失、get-task-allow=true、entitlement 不匹配和签名后文件被修改。

Accepted 但用户仍被 Gatekeeper 拒绝

Accepted 只证明公证服务接受提交,不代表所有终端环境都一定通过。继续检查:

  • 是否把票据 staple 到实际上传的 DMG。
  • 上传后是否又修改或重签了 DMG。
  • App 和 DMG 的 codesign 是否都通过。
  • spctl 或 syspolicy_check 是否通过。
  • 浏览器下载的 quarantine 路径是否通过。
  • 用户下载到的是否真是当前 tag 的 DMG,而不是旧缓存或错误资产。

checksum 不一致

最常见原因是 checksum 在 staple 之前生成,或生成后 DMG 又被重签、重建、替换。删除错误的未公开 Draft 资产,重新从最终字节生成;已经公开时发布新 patch 版本,不覆盖原资产。

GitHub 上只有 Source code,没有可安装文件

GitHub 自动生成的 ZIP/TAR 只是源码快照。必须把最终 DMG 作为 Release asset 明确上传,并在 Release notes 的 Assets 与 Installation 中说明下载哪个文件。

tag 指向错误提交

如果还在未公开 Draft 阶段,停止发布,删除错误 Draft 和未公开 tag,再从正确提交重新创建。如果已公开,尤其启用了不可变发布,不要移动旧 tag;修正版本号并发布新 Release。

十五、可选的 CI 自动化

自动化不能降低签名和凭据安全要求。典型 CI 还需要:

  • 将 Developer ID 证书和私钥导出为强密码保护的 p12。
  • 把 p12、p12 密码和公证凭据放进 GitHub Actions encrypted secrets,而非仓库。
  • 在临时 keychain 中导入 identity,构建结束后清理 runner。
  • 公证优先使用可吊销、可独立管理的 App Store Connect API Key。
  • 使用最小 GitHub token 权限。
  • 让 workflow 只从受保护的版本 tag 触发。
  • 上传 Draft,完成独立验证后再发布。
  • 日志中屏蔽所有秘密,不输出 security dump 或环境变量全集。

CI 只是把同一发布契约自动化,不能省略 Developer ID、Hardened Runtime、公证、staple、checksum、回下载验收和真实首次启动测试。

官方资料

Apple

  • Apple Developer Program
  • Developer ID certificates
  • Notarizing macOS software before distribution
  • Customizing the notarization workflow
  • Configuring the hardened runtime
  • App 专用密码
  • App Store Connect API

GitHub

  • About releases
  • Managing releases
  • gh release create
  • gh release upload
  • gh release download
  • gh release edit
  • Immutable releases
  • Verifying release integrity