chore(desktop-client): add macOS code-signing and notarization scaffolding
Backend CI / Backend (push) Successful in 13m6s
Deployment Config CI / Deployment Config (push) Successful in 17s
Frontend CI / Frontend (push) Successful in 2m24s

- Move the build config out of package.json into electron-builder.yml so
  the mac block can carry hardenedRuntime, entitlements, universal arch,
  and usage-description Info.plist keys. Drop identity:null (which
  forces unsigned builds) and let CSC_NAME or the keychain decide.
- Add entitlements plist files and three helper scripts: sign-setup.sh
  (one-time keychain prep with notarytool store-credentials),
  sign-mac.sh (sign + notarize + staple, supports keychain or CI env
  vars), and ci-import-cert.sh (CI keychain bootstrap).
- Ignore .env.signing so local credentials don't sneak into the repo,
  and pin @emnapi/{core,runtime} as devDependencies so universal builds
  resolve them deterministically.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-01 16:02:14 +08:00
parent 9c58fe2312
commit a9af6c634c
11 changed files with 336 additions and 86 deletions
+63
View File
@@ -0,0 +1,63 @@
#!/usr/bin/env bash
# CI 专用:把 base64 编码的 .p12 证书导入到一个临时钥匙串
# 仅在 CI 环境调用,本地不要跑(会污染你的登录钥匙串体验)
#
# 必需环境变量:
# CSC_LINK - base64 编码的 .p12 文件内容
# CSC_KEY_PASSWORD - .p12 文件的密码
# KEYCHAIN_PASSWORD - 临时钥匙串自身的密码(任意字符串,例如 secrets.GITHUB_TOKEN 截断都行)
#
# 副作用:创建 ~/Library/Keychains/build.keychain-db,已加入搜索路径并解锁
# 销毁:脚本注册了 trap,进程退出时自动 delete-keychain
set -euo pipefail
: "${CSC_LINK:?CI 必须提供 CSC_LINK (base64 .p12)}"
: "${CSC_KEY_PASSWORD:?CI 必须提供 CSC_KEY_PASSWORD}"
: "${KEYCHAIN_PASSWORD:?CI 必须提供 KEYCHAIN_PASSWORD(用于临时钥匙串自身)}"
KEYCHAIN_NAME="build.keychain"
CERT_PATH="$RUNNER_TEMP/build_certificate.p12"
[ -z "${RUNNER_TEMP:-}" ] && CERT_PATH="/tmp/build_certificate.p12"
# 1. 解码证书到临时文件
echo "$CSC_LINK" | base64 --decode > "$CERT_PATH"
# 2. 创建临时钥匙串
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_NAME"
security set-keychain-settings -lut 21600 "$KEYCHAIN_NAME" # 6 小时不锁
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_NAME"
# 3. 导入 .p12(允许 codesign 和 productsign 不弹密码框)
security import "$CERT_PATH" \
-P "$CSC_KEY_PASSWORD" \
-A -t cert -f pkcs12 \
-k "$KEYCHAIN_NAME"
# 4. 关键:允许 codesign 不交互访问私钥
security set-key-partition-list \
-S apple-tool:,apple:,codesign: \
-s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_NAME" >/dev/null
# 5. 把临时钥匙串加进搜索路径(放第一位,登录钥匙串保留作 fallback)
security list-keychain -d user -s "$KEYCHAIN_NAME" $(security list-keychain -d user | tr -d '"')
# 6. 立即清理证书文件
rm -f "$CERT_PATH"
# 7. 输出实际可用的 identity 名(让 sign-mac.sh 拿到精确的 CSC_NAME
IDENTITY=$(security find-identity -v -p codesigning "$KEYCHAIN_NAME" \
| grep "Developer ID Application" | head -n1 \
| sed -E 's/.*"(Developer ID Application: [^"]+)".*/\1/')
if [ -z "$IDENTITY" ]; then
echo "✗ 导入后未在临时钥匙串里找到 Developer ID Application" >&2
exit 1
fi
echo "✓ 已导入证书到临时钥匙串: $KEYCHAIN_NAME"
echo "✓ Identity: $IDENTITY"
# 在 GitHub Actions 里把 CSC_NAME 写入后续步骤的 env
if [ -n "${GITHUB_ENV:-}" ]; then
echo "CSC_NAME=$IDENTITY" >> "$GITHUB_ENV"
echo "✓ 已写入 \$GITHUB_ENVCSC_NAME"
fi
+110
View File
@@ -0,0 +1,110 @@
#!/usr/bin/env bash
# 一键:构建 → 签名 → 公证 → 装订 → 验证
# 用法: pnpm sign:mac
set -euo pipefail
cd "$(dirname "$0")/.."
R=$'\033[31m'; G=$'\033[32m'; Y=$'\033[33m'; B=$'\033[1m'; N=$'\033[0m'
ok() { echo " ${G}${N} $1"; }
fail() { echo " ${R}${N} $1"; }
step() { echo ""; echo "${B}── $1 ──${N}"; }
# ── 前置检查(本地/CI 双模式)─────────────────────────────────────
# 本地模式:读 .env.signing,公证走钥匙串 profile
# CI 模式:靠环境变量 CSC_NAME / APPLE_ID / APPLE_APP_SPECIFIC_PASSWORD / APPLE_TEAM_ID
NOTARY_ARGS=()
if [ -f .env.signing ] && [ -z "${CI:-}" ]; then
# shellcheck source=/dev/null
set -a; source .env.signing; set +a
: "${CSC_NAME:?.env.signing 缺少 CSC_NAME}"
: "${APPLE_TEAM_ID:?.env.signing 缺少 APPLE_TEAM_ID}"
: "${NOTARY_KEYCHAIN_PROFILE:?.env.signing 缺少 NOTARY_KEYCHAIN_PROFILE}"
NOTARY_ARGS=(--keychain-profile "$NOTARY_KEYCHAIN_PROFILE")
ok "模式: 本地(钥匙串 profile"
elif [ -n "${CSC_NAME:-}" ] && [ -n "${APPLE_ID:-}" ] && [ -n "${APPLE_APP_SPECIFIC_PASSWORD:-}" ] && [ -n "${APPLE_TEAM_ID:-}" ]; then
NOTARY_ARGS=(--apple-id "$APPLE_ID" --password "$APPLE_APP_SPECIFIC_PASSWORD" --team-id "$APPLE_TEAM_ID")
ok "模式: CI(环境变量)"
else
fail "未找到签名配置"
echo " 本地: 运行 ${B}pnpm sign:setup${N}"
echo " CI: 设置 ${B}CSC_NAME / APPLE_ID / APPLE_APP_SPECIFIC_PASSWORD / APPLE_TEAM_ID${N} 环境变量"
exit 1
fi
ok "签名身份: $CSC_NAME"
ok "Team ID: $APPLE_TEAM_ID"
# ── 1. 构建 ──────────────────────────────────────────────────────
step "[1/5] 构建 Electron 产物"
pnpm build
ok "构建完成"
# ── 2. 打包 + 签名 ───────────────────────────────────────────────
step "[2/5] 打包 + 签名 (electron-builder)"
# 清理旧产物,避免 spctl 把上次未公证的 dmg 认成本次产物
rm -f release/*.dmg release/*-mac.zip release/*.blockmap 2>/dev/null || true
pnpm exec electron-builder --mac --arm64
ok "打包完成"
DMG=$(ls -t release/*.dmg 2>/dev/null | head -n1 || true)
ZIP=$(ls -t release/*-mac.zip 2>/dev/null | head -n1 || true)
APP=$(ls -td release/mac*/*.app 2>/dev/null | head -n1 || true)
[ -z "$DMG" ] && { fail "未找到 .dmg 产物"; exit 1; }
ok "DMG: $DMG"
[ -n "$ZIP" ] && ok "ZIP: $ZIP"
[ -n "$APP" ] && ok "APP: $APP"
# 验证签名是否真的盖上了
if [ -n "$APP" ]; then
if ! codesign --verify --deep --strict --verbose=2 "$APP" >/dev/null 2>&1; then
fail ".app 签名验证失败,停止公证"
codesign --verify --deep --strict --verbose=2 "$APP" || true
exit 1
fi
ok ".app 签名校验通过"
fi
# ── 3. 公证 ──────────────────────────────────────────────────────
step "[3/5] 提交公证 (notarytool, 通常 1-5 分钟)"
echo " 正在提交 DMG..."
xcrun notarytool submit "$DMG" \
"${NOTARY_ARGS[@]}" \
--wait
ok "DMG 公证通过"
if [ -n "$ZIP" ]; then
echo " 正在提交 ZIP(用于自动更新)..."
xcrun notarytool submit "$ZIP" \
--keychain-profile "$NOTARY_KEYCHAIN_PROFILE" \
--wait
ok "ZIP 公证通过"
fi
# ── 4. 装订 ticket ───────────────────────────────────────────────
step "[4/5] 装订 ticket (stapler)"
xcrun stapler staple "$DMG"
ok "DMG ticket 已装订(断网也能验证)"
# zip 不能 staple,但用户解压后里面的 .app 已经被 staple 过
# ── 5. 验证 ──────────────────────────────────────────────────────
step "[5/5] 最终验证 (Gatekeeper)"
if spctl -a -vv -t install "$DMG" 2>&1 | tee /tmp/spctl.log | grep -q "accepted"; then
ok "Gatekeeper 验证通过"
grep -E "source=|origin=" /tmp/spctl.log | sed 's/^/ /'
else
fail "Gatekeeper 拒绝了产物"
cat /tmp/spctl.log
exit 1
fi
# ── 完成 ────────────────────────────────────────────────────────
SIZE=$(du -h "$DMG" | awk '{print $1}')
echo ""
echo "${G}${B}━━━━━━━━━━ 全部完成 ━━━━━━━━━━${N}"
echo " 📦 $DMG (${SIZE})"
[ -n "$ZIP" ] && echo " 📦 $ZIP"
echo ""
echo " 这份 DMG 可以直接发给任何 macOS 用户,"
echo " 双击安装,首次打开${B}不会${N}弹\"无法验证开发者\"或被 Gatekeeper 拦截。"
+105
View File
@@ -0,0 +1,105 @@
#!/usr/bin/env bash
# 一次性签名环境配置:检查证书 + 录入 Apple ID 凭证 + 写入 .env.signing
# 用法: pnpm sign:setup
set -euo pipefail
cd "$(dirname "$0")/.."
KEYCHAIN_PROFILE="GeoRanklySign"
ENV_FILE=".env.signing"
R=$'\033[31m'; G=$'\033[32m'; Y=$'\033[33m'; B=$'\033[1m'; N=$'\033[0m'
ok() { echo " ${G}${N} $1"; }
fail() { echo " ${R}${N} $1"; }
warn() { echo " ${Y}!${N} $1"; }
hr() { echo ""; echo "${B}── $1 ──${N}"; }
# ── 1. Xcode CLT ────────────────────────────────────────────────
hr "1/3 检查 Xcode Command Line Tools"
if xcode-select -p >/dev/null 2>&1; then
ok "已安装:$(xcode-select -p)"
else
fail "未安装"
echo " 请在终端运行 ${B}xcode-select --install${N},弹窗装好后再来"
exit 1
fi
# ── 2. Developer ID 证书 ────────────────────────────────────────
hr "2/3 检查 Developer ID Application 证书"
IDENTITIES=$(security find-identity -v -p codesigning | grep "Developer ID Application" || true)
if [ -z "$IDENTITIES" ]; then
fail "钥匙串里没找到 Developer ID Application 证书"
cat <<EOF
${B}申请步骤(一次性,约 10 分钟):${N}
${Y}1.${N} 浏览器登录 https://developer.apple.com/account/resources/certificates
${Y}2.${N} 点 ${B}+${N} 新建证书 → 选 ${B}Developer ID Application${N} → Continue
${Y}3.${N} 它要你上传 CSR,按下面方法生成:
打开 macOS ${B}钥匙串访问.app${N}
菜单栏 → 钥匙串访问 → 证书助理 → ${B}从证书颁发机构请求证书…${N}
邮箱填你的 Apple ID,常用名随意,选 ${B}存储到磁盘${N},保存为 .certSigningRequest
${Y}4.${N} 把这个 .certSigningRequest 上传,下载得到 ${B}developerID_application.cer${N}
${Y}5.${N} 双击 .cer,导入到 ${B}登录${N} 钥匙串
${Y}6.${N} 重新运行 ${B}pnpm sign:setup${N}
EOF
exit 1
fi
# 多张证书时取第一张
IDENTITY_LINE=$(echo "$IDENTITIES" | head -n1)
IDENTITY_NAME=$(echo "$IDENTITY_LINE" | sed -E 's/.*"(Developer ID Application: [^"]+)".*/\1/')
# CSC_NAME 必须去掉 "Developer ID Application: " 前缀,否则 electron-builder 会拒绝
CSC_NAME_VALUE=$(echo "$IDENTITY_NAME" | sed -E 's/^Developer ID Application: //')
TEAM_ID=$(echo "$IDENTITY_NAME" | sed -E 's/.*\(([A-Z0-9]+)\).*/\1/')
ok "找到证书:${B}$IDENTITY_NAME${N}"
ok "Team ID${B}$TEAM_ID${N}"
if [ "$(echo "$IDENTITIES" | wc -l | tr -d ' ')" -gt 1 ]; then
warn "钥匙串里有多张 Developer ID 证书,本脚本默认用上面这一张"
fi
# ── 3. Apple ID 凭证(公证用)─────────────────────────────────────
hr "3/3 录入 Apple ID 凭证(用于公证 notarize"
NEED_CREDENTIALS=1
if security find-generic-password -s "com.apple.gke.notary.tool.saved-creds" -a "$KEYCHAIN_PROFILE" >/dev/null 2>&1; then
ok "钥匙串里已有 profile$KEYCHAIN_PROFILE"
read -r -p " 是否重新录入?(y/N): " RECONFIG
[[ "${RECONFIG:-n}" =~ ^[Yy]$ ]] || NEED_CREDENTIALS=0
fi
if [ "$NEED_CREDENTIALS" = "1" ]; then
echo ""
echo " 需要 ${B}Apple ID 邮箱${N} + ${B}App 专用密码${N}(不是登录密码!)"
echo " 生成 App 专用密码: https://appleid.apple.com → 登录与安全 → App 专用密码 → +"
echo ""
read -r -p " Apple ID 邮箱: " APPLE_ID
read -r -s -p " App 专用密码 (xxxx-xxxx-xxxx-xxxx): " APPLE_PWD
echo ""
if ! xcrun notarytool store-credentials "$KEYCHAIN_PROFILE" \
--apple-id "$APPLE_ID" \
--team-id "$TEAM_ID" \
--password "$APPLE_PWD" >/dev/null; then
fail "凭证录入失败,常见原因:密码格式错(需带短横线),或 Team ID 不匹配"
exit 1
fi
ok "凭证已存入钥匙串(profile: ${KEYCHAIN_PROFILE}"
fi
# ── 写入 .env.signing ───────────────────────────────────────────
cat > "$ENV_FILE" <<EOF
# Auto-generated by scripts/sign-setup.sh — DO NOT COMMIT
# 让 electron-builder 自动选用钥匙串里的证书
# 注意:CSC_NAME 不能带 "Developer ID Application:" 前缀
CSC_NAME="$CSC_NAME_VALUE"
APPLE_TEAM_ID="$TEAM_ID"
NOTARY_KEYCHAIN_PROFILE="$KEYCHAIN_PROFILE"
EOF
ok "写入 ${B}apps/desktop-client/$ENV_FILE${N}"
echo ""
echo "${G}${B}━━━ 配置完成 ━━━${N}"
echo " 下一步:运行 ${B}pnpm sign:mac${N} 一键打包 + 签名 + 公证"