feat(desktop): add client release updater
Desktop Client Build / Resolve Build Metadata (push) Successful in 19s
Frontend CI / Frontend (push) Successful in 3m20s
Backend CI / Backend (push) Successful in 16m48s
Desktop Client Build / Build Desktop Client (push) Successful in 24m46s
Desktop Client Build / Publish Client Artifacts to NAS (push) Successful in 37s
Desktop Client Build / Resolve Build Metadata (push) Successful in 19s
Frontend CI / Frontend (push) Successful in 3m20s
Backend CI / Backend (push) Successful in 16m48s
Desktop Client Build / Build Desktop Client (push) Successful in 24m46s
Desktop Client Build / Publish Client Artifacts to NAS (push) Successful in 37s
This commit is contained in:
@@ -10,6 +10,14 @@ ok() { echo " ${G}✓${N} $1"; }
|
||||
fail() { echo " ${R}✗${N} $1"; }
|
||||
step() { echo ""; echo "${B}── $1 ──${N}"; }
|
||||
|
||||
LOCK_DIR="${TMPDIR:-/tmp}/geo-rankly-desktop-sign-mac.lock"
|
||||
if ! mkdir "$LOCK_DIR" 2>/dev/null; then
|
||||
fail "已有 sign:mac 正在运行,避免多个构建同时写 release/"
|
||||
echo " 如确认没有进程,可删除: $LOCK_DIR"
|
||||
exit 1
|
||||
fi
|
||||
trap 'rm -rf "$LOCK_DIR"' EXIT
|
||||
|
||||
# ── 前置检查(本地/CI 双模式)─────────────────────────────────────
|
||||
# 本地模式:读 .env.signing,公证走钥匙串 profile
|
||||
# CI 模式:靠环境变量 CSC_NAME / APPLE_ID / APPLE_APP_SPECIFIC_PASSWORD / APPLE_TEAM_ID
|
||||
@@ -35,6 +43,12 @@ fi
|
||||
ok "签名身份: $CSC_NAME"
|
||||
ok "Team ID: $APPLE_TEAM_ID"
|
||||
|
||||
CODESIGN_IDENTITY="${CODESIGN_IDENTITY:-$CSC_NAME}"
|
||||
if [[ ! "$CODESIGN_IDENTITY" =~ ^[0-9A-Fa-f]{40}$ && "$CODESIGN_IDENTITY" != Developer\ ID\ Application:* ]]; then
|
||||
CODESIGN_IDENTITY="Developer ID Application: $CODESIGN_IDENTITY"
|
||||
fi
|
||||
ok "codesign 身份: $CODESIGN_IDENTITY"
|
||||
|
||||
# ── 1. 构建 ──────────────────────────────────────────────────────
|
||||
step "[1/5] 构建 Electron 产物"
|
||||
pnpm build
|
||||
@@ -43,68 +57,98 @@ ok "构建完成"
|
||||
# ── 2. 打包 + 签名 ───────────────────────────────────────────────
|
||||
step "[2/5] 打包 + 签名 (electron-builder)"
|
||||
# 清理旧产物,避免 spctl 把上次未公证的 dmg 认成本次产物
|
||||
rm -rf release/mac-* 2>/dev/null || true
|
||||
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)
|
||||
shopt -s nullglob
|
||||
DMGS=(release/*.dmg)
|
||||
ZIPS=(release/*-mac.zip)
|
||||
APPS=(release/mac*/*.app)
|
||||
shopt -u nullglob
|
||||
|
||||
[ -z "$DMG" ] && { fail "未找到 .dmg 产物"; exit 1; }
|
||||
ok "DMG: $DMG"
|
||||
[ -n "$ZIP" ] && ok "ZIP: $ZIP"
|
||||
[ -n "$APP" ] && ok "APP: $APP"
|
||||
[ "${#DMGS[@]}" -eq 0 ] && { fail "未找到 .dmg 产物"; exit 1; }
|
||||
[ "${#APPS[@]}" -eq 0 ] && { fail "未找到 .app 产物"; exit 1; }
|
||||
for DMG in "${DMGS[@]}"; do ok "DMG: $DMG"; done
|
||||
for ZIP in "${ZIPS[@]}"; do ok "ZIP: $ZIP"; done
|
||||
for APP in "${APPS[@]}"; do ok "APP: $APP"; done
|
||||
|
||||
# 验证签名是否真的盖上了
|
||||
if [ -n "$APP" ]; then
|
||||
for APP in "${APPS[@]}"; do
|
||||
if ! codesign --verify --deep --strict --verbose=2 "$APP" >/dev/null 2>&1; then
|
||||
fail ".app 签名验证失败,停止公证"
|
||||
fail ".app 签名验证失败,停止公证: $APP"
|
||||
codesign --verify --deep --strict --verbose=2 "$APP" || true
|
||||
exit 1
|
||||
fi
|
||||
ok ".app 签名校验通过"
|
||||
fi
|
||||
ok ".app 签名校验通过: $APP"
|
||||
done
|
||||
|
||||
# electron-builder 会签 .app,但 DMG 本体在部分配置/版本下不会自动签。
|
||||
# Gatekeeper 对安装包执行 `spctl -t install` 时要求外层 DMG 也有可用签名。
|
||||
for DMG in "${DMGS[@]}"; do
|
||||
echo " 正在签名 DMG: $DMG"
|
||||
codesign --force --timestamp --sign "$CODESIGN_IDENTITY" "$DMG"
|
||||
if ! codesign --verify --verbose=2 "$DMG" >/dev/null 2>&1; then
|
||||
fail "DMG 签名验证失败,停止公证: $DMG"
|
||||
codesign --verify --verbose=2 "$DMG" || true
|
||||
exit 1
|
||||
fi
|
||||
ok "DMG 签名校验通过: $DMG"
|
||||
done
|
||||
|
||||
# ── 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" \
|
||||
for DMG in "${DMGS[@]}"; do
|
||||
echo " 正在提交 DMG: $DMG"
|
||||
xcrun notarytool submit "$DMG" \
|
||||
"${NOTARY_ARGS[@]}" \
|
||||
--wait
|
||||
ok "ZIP 公证通过"
|
||||
fi
|
||||
ok "DMG 公证通过: $DMG"
|
||||
done
|
||||
|
||||
for ZIP in "${ZIPS[@]}"; do
|
||||
echo " 正在提交 ZIP(用于自动更新): $ZIP"
|
||||
xcrun notarytool submit "$ZIP" \
|
||||
"${NOTARY_ARGS[@]}" \
|
||||
--wait
|
||||
ok "ZIP 公证通过: $ZIP"
|
||||
done
|
||||
|
||||
# ── 4. 装订 ticket ───────────────────────────────────────────────
|
||||
step "[4/5] 装订 ticket (stapler)"
|
||||
xcrun stapler staple "$DMG"
|
||||
ok "DMG ticket 已装订(断网也能验证)"
|
||||
for DMG in "${DMGS[@]}"; do
|
||||
xcrun stapler staple "$DMG"
|
||||
ok "DMG ticket 已装订(断网也能验证): $DMG"
|
||||
done
|
||||
# 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
|
||||
for DMG in "${DMGS[@]}"; do
|
||||
LOG_FILE=$(mktemp "${TMPDIR:-/tmp}/spctl.XXXXXX")
|
||||
if spctl -a -vv -t install "$DMG" 2>&1 | tee "$LOG_FILE" | grep -q "accepted"; then
|
||||
ok "Gatekeeper 验证通过: $DMG"
|
||||
grep -E "source=|origin=" "$LOG_FILE" | sed 's/^/ /'
|
||||
else
|
||||
fail "Gatekeeper 拒绝了产物: $DMG"
|
||||
cat "$LOG_FILE"
|
||||
rm -f "$LOG_FILE"
|
||||
exit 1
|
||||
fi
|
||||
rm -f "$LOG_FILE"
|
||||
done
|
||||
|
||||
# ── 完成 ────────────────────────────────────────────────────────
|
||||
SIZE=$(du -h "$DMG" | awk '{print $1}')
|
||||
echo ""
|
||||
echo "${G}${B}━━━━━━━━━━ 全部完成 ━━━━━━━━━━${N}"
|
||||
echo " 📦 $DMG (${SIZE})"
|
||||
[ -n "$ZIP" ] && echo " 📦 $ZIP"
|
||||
for DMG in "${DMGS[@]}"; do
|
||||
SIZE=$(du -h "$DMG" | awk '{print $1}')
|
||||
echo " 📦 $DMG (${SIZE})"
|
||||
done
|
||||
for ZIP in "${ZIPS[@]}"; do
|
||||
echo " 📦 $ZIP"
|
||||
done
|
||||
echo ""
|
||||
echo " 这份 DMG 可以直接发给任何 macOS 用户,"
|
||||
echo " 双击安装,首次打开${B}不会${N}弹\"无法验证开发者\"或被 Gatekeeper 拦截。"
|
||||
|
||||
Reference in New Issue
Block a user