refactor: 简化 macOS 构建 — 只签名不公证,禁用原生热更新

CI (.github/workflows/build.yml):
- 删除 mac_notarize 输入、notarization strategy/credentials 步骤
- 双架构回归单步构建,统一 -c.mac.notarize=false
- 证书缺失时硬失败,不 fallback ad-hoc
- 上传产物移除 blockmap / latest-mac.yml
- release 收集资产移除 blockmap / yml 元数据

electron-builder.yml:
- mac.notarize 固定为 false

Electron 主进程 (main.ts):
- 移除 initAutoUpdater / setUpdaterWindow 调用和 import

Preload (preload.ts):
- 移除 updater bridge,不再暴露 electronAPI.updater

Updater (updater.ts):
- 恢复为 no-op stub

前端 AppShell 无需改动 — isNativeUpdater 运行时检测 updater 不存在,
自动走 /api/app/updates 浏览器模式(全量下载链接)。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
7418andClaude Sonnet 4.6 committed 2026-03-03 13:27:35 +08:00
1 parent 5629da9571
commit 5413eb55ba
5 files changed
+25 -233

No files matched your search

+12 -138
View File
@@ -13,15 +13,6 @@ on:
- windows
- macos
- linux
mac_notarize:
description: "mac notarization mode (arm64 only)"
required: false
default: "auto"
type: choice
options:
- auto
- "on"
- "off"
push:
tags:
- "v*"
@@ -56,8 +47,6 @@ jobs:
name: build-windows
path: |
release/*.exe
release/*.exe.blockmap
release/latest.yml
retention-days: 3
build-macos:
@@ -80,154 +69,51 @@ jobs:
run: npm run electron:build
- name: Decode signing certificate
if: ${{ env.HAS_CERT == 'true' }}
env:
MAC_CERT_P12_BASE64: ${{ secrets.MAC_CERT_P12_BASE64 }}
MAC_CERT_PASSWORD: ${{ secrets.MAC_CERT_PASSWORD }}
run: |
if [ "${HAS_CERT}" != "true" ]; then
echo "::error::Missing signing certificate (MAC_CERT_P12_BASE64 secret not set)"
exit 1
fi
printf '%s' "$MAC_CERT_P12_BASE64" | tr -d '\r\n' | base64 --decode > /tmp/cert.p12
openssl pkcs12 -in /tmp/cert.p12 -passin pass:"$MAC_CERT_PASSWORD" -noout
echo "Certificate decoded and validated"
- name: Resolve arm64 notarization strategy
id: mac_mode
shell: bash
env:
MANUAL_MODE: ${{ github.event_name == 'workflow_dispatch' && inputs.mac_notarize || 'auto' }}
REF_NAME: ${{ github.ref_name }}
run: |
set -euo pipefail
MODE="${MANUAL_MODE:-auto}"
NOTARIZE=false
if [[ "$MODE" == "on" ]]; then
NOTARIZE=true
elif [[ "$MODE" == "off" ]]; then
NOTARIZE=false
else
# auto: vX.Y.0 公证;vX.Y.Z(Z>0) 只签名
if [[ "$REF_NAME" =~ ^v([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
PATCH="${BASH_REMATCH[3]}"
if [[ "$PATCH" == "0" ]]; then
NOTARIZE=true
fi
fi
fi
echo "mode=$MODE" >> "$GITHUB_OUTPUT"
echo "notarize=$NOTARIZE" >> "$GITHUB_OUTPUT"
echo "Resolved arm64 notarization: $NOTARIZE (mode=$MODE, ref=$REF_NAME)"
- name: Validate notarization credentials
if: ${{ env.HAS_CERT == 'true' && steps.mac_mode.outputs.notarize == 'true' }}
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: |
echo "APPLE_ID len=${#APPLE_ID}, TEAM=$APPLE_TEAM_ID, APP_PWD len=${#APPLE_APP_SPECIFIC_PASSWORD}"
xcrun notarytool history \
--apple-id "$APPLE_ID" \
--password "$APPLE_APP_SPECIFIC_PASSWORD" \
--team-id "$APPLE_TEAM_ID" >/dev/null
# ── x64: sign only, no notarization ──────────────────────────────────
- name: Package for macOS (x64, sign only)
- name: Package for macOS (x64 + arm64, sign only)
timeout-minutes: 15
env:
CSC_LINK: ${{ env.HAS_CERT == 'true' && '/tmp/cert.p12' || '' }}
CSC_LINK: /tmp/cert.p12
CSC_KEY_PASSWORD: ${{ secrets.MAC_CERT_PASSWORD }}
run: |
set -euo pipefail
for i in 1 2 3; do
echo "electron-builder x64 attempt $i/3 (notarize=false)"
if npx electron-builder --mac --x64 --config electron-builder.yml -c.mac.notarize=false --publish never; then
exit 0
fi
if [ "$i" -lt 3 ]; then sleep $((i * 30)); fi
done
exit 1
# Move x64 artifacts aside so arm64 build produces clean latest-mac.yml
- name: Stash x64 artifacts
run: |
mkdir -p release/x64-stash
mv release/*.dmg release/*.zip release/*.blockmap release/x64-stash/ 2>/dev/null || true
rm -f release/latest-mac.yml
# ── arm64: sign + conditional notarization ───────────────────────────
- name: Package for macOS (arm64)
timeout-minutes: 35
env:
CSC_LINK: ${{ env.HAS_CERT == 'true' && '/tmp/cert.p12' || '' }}
CSC_KEY_PASSWORD: ${{ secrets.MAC_CERT_PASSWORD }}
APPLE_ID: ${{ steps.mac_mode.outputs.notarize == 'true' && secrets.APPLE_ID || '' }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ steps.mac_mode.outputs.notarize == 'true' && secrets.APPLE_APP_SPECIFIC_PASSWORD || '' }}
APPLE_TEAM_ID: ${{ steps.mac_mode.outputs.notarize == 'true' && secrets.APPLE_TEAM_ID || '' }}
DEBUG: electron-notarize*
run: |
set -euo pipefail
NOTARIZE="${{ steps.mac_mode.outputs.notarize }}"
for i in 1 2 3; do
echo "electron-builder arm64 attempt $i/3 (notarize=$NOTARIZE)"
if [[ "$NOTARIZE" == "true" ]]; then
npx electron-builder --mac --arm64 --config electron-builder.yml --publish never
else
npx electron-builder --mac --arm64 --config electron-builder.yml -c.mac.notarize=false --publish never
fi
if [ $? -eq 0 ]; then
exit 0
fi
if [ "$i" -lt 3 ]; then sleep $((i * 60)); fi
done
exit 1
# Restore x64 artifacts alongside arm64 output
- name: Restore x64 artifacts
run: mv release/x64-stash/* release/ && rmdir release/x64-stash
run: npx electron-builder --mac --config electron-builder.yml --publish never
- name: Verify code signature
if: ${{ env.HAS_CERT == 'true' }}
run: |
# Verify every .app bundle produced (arm64 + x64)
APPS=$(find release -name "CodePilot.app" -maxdepth 3)
if [ -z "$APPS" ]; then
echo "::warning::No CodePilot.app found, skipping signature verification"
exit 0
echo "::error::No CodePilot.app found after build"
exit 1
fi
FAILED=0
while IFS= read -r APP_PATH; do
echo "========== Verifying: $APP_PATH =========="
# Display signature details
codesign -dv --verbose=4 "$APP_PATH" 2>&1 | tee /tmp/codesign-info.txt || true
# Assert Developer ID authority
if ! grep -q 'Authority=Developer ID Application' /tmp/codesign-info.txt; then
echo "::error::$APP_PATH is NOT signed with Developer ID Application"
FAILED=1
continue
fi
# Assert correct Team Identifier
if ! grep -q 'TeamIdentifier=K9X599X9Q2' /tmp/codesign-info.txt; then
echo "::error::$APP_PATH has unexpected TeamIdentifier"
FAILED=1
continue
fi
# Strict verification (must pass)
codesign --verify --deep --strict --verbose=4 "$APP_PATH"
# Gatekeeper assessment (best-effort, does not fail the build)
spctl -a -vvv --type execute "$APP_PATH" || echo "::warning::spctl assessment did not pass for $APP_PATH (may be expected in CI)"
echo "✓ $APP_PATH passed all checks"
done <<< "$APPS"
@@ -236,23 +122,13 @@ jobs:
exit 1
fi
- name: Print mac release mode
run: |
echo "mac_notarize_mode=${{ steps.mac_mode.outputs.mode }}"
echo "arm64_notarize=${{ steps.mac_mode.outputs.notarize }}"
echo "x64_notarize=false (always sign-only)"
echo "latest-mac.yml comes from arm64 build"
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: build-macos
path: |
release/*.dmg
release/*.dmg.blockmap
release/*.zip
release/*.zip.blockmap
release/latest-mac.yml
retention-days: 3
build-linux:
@@ -286,7 +162,6 @@ jobs:
release/*.AppImage
release/*.deb
release/*.rpm
release/latest-linux*.yml
retention-days: 3
release:
@@ -381,13 +256,12 @@ jobs:
CHANGELOG_INLINE_EOF
fi
# Collect all release files (handle filenames with spaces)
# Collect release files: installers only (no blockmap / update metadata)
FILES=()
while IFS= read -r f; do
FILES+=("$f")
done < <(find artifacts -type f \( -name "*.dmg" -o -name "*.zip" -o -name "*.exe" -o -name "*.AppImage" -o -name "*.deb" -o -name "*.rpm" -o -name "*.blockmap" -o -name "*.yml" \) | sort)
done < <(find artifacts -type f \( -name "*.dmg" -o -name "*.zip" -o -name "*.exe" -o -name "*.AppImage" -o -name "*.deb" -o -name "*.rpm" \) | sort)
# Create release and upload all assets in one shot
gh release create "${GITHUB_REF_NAME}" \
--title "CodePilot v${VERSION}" \
--notes-file release-notes.md \
+1 -1
View File
@@ -52,7 +52,7 @@ mac:
gatekeeperAssess: false
entitlements: build/entitlements.mac.plist
entitlementsInherit: build/entitlements.mac.inherit.plist
notarize: true
notarize: false
target:
- target: dmg
arch: [x64, arm64]
-17
View File
@@ -4,7 +4,6 @@ import { execFileSync, spawn, ChildProcess } from 'child_process';
import fs from 'fs';
import net from 'net';
import os from 'os';
import { initAutoUpdater, setUpdaterWindow } from './updater';
let mainWindow: BrowserWindow | null = null;
let serverProcess: Electron.UtilityProcess | null = null;
@@ -163,9 +162,6 @@ function createTray(): void {
click: () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow(`http://127.0.0.1:${serverPort || 3000}`);
if (!isDev && mainWindow) {
setUpdaterWindow(mainWindow);
}
} else {
mainWindow?.focus();
}
@@ -194,9 +190,6 @@ function createTray(): void {
tray.on('double-click', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow(`http://127.0.0.1:${serverPort || 3000}`);
if (!isDev && mainWindow) {
setUpdaterWindow(mainWindow);
}
} else {
mainWindow?.focus();
}
@@ -938,12 +931,6 @@ app.whenReady().then(async () => {
autoStartReq.end();
}
// Initialize auto-updater in packaged mode only.
// Disabled on macOS x64 (Intel) — those users get browser-mode update check.
const skipNativeUpdater = process.platform === 'darwin' && process.arch === 'x64';
if (!isDev && mainWindow && !skipNativeUpdater) {
initAutoUpdater(mainWindow);
}
} catch (err) {
console.error('Failed to start:', err);
dialog.showErrorBox(
@@ -990,10 +977,6 @@ app.on('activate', async () => {
createWindow(`http://127.0.0.1:${serverPort || 3000}`);
}
// Re-attach updater to the new window
if (!isDev && mainWindow) {
setUpdaterWindow(mainWindow);
}
} catch (err) {
console.error('Failed to restart server:', err);
}
-14
View File
@@ -28,18 +28,4 @@ contextBridge.exposeInMainWorld('electronAPI', {
bridge: {
isActive: () => ipcRenderer.invoke('bridge:is-active'),
},
// Native updater: disabled on macOS x64 (Intel) — those users get browser-mode
// update check (download link to GitHub Releases) instead.
...(process.platform === 'darwin' && process.arch === 'x64' ? {} : {
updater: {
checkForUpdates: () => ipcRenderer.invoke('updater:check'),
downloadUpdate: () => ipcRenderer.invoke('updater:download'),
quitAndInstall: () => ipcRenderer.invoke('updater:quit-and-install'),
onStatus: (callback: (data: unknown) => void) => {
const listener = (_event: unknown, data: unknown) => callback(data);
ipcRenderer.on('updater:status', listener);
return () => { ipcRenderer.removeListener('updater:status', listener); };
},
},
}),
});
+12 -63
View File
@@ -1,70 +1,19 @@
import { autoUpdater } from 'electron-updater';
import { BrowserWindow, ipcMain } from 'electron';
// =============================================================================
// Native auto-updater (electron-updater) — DISABLED
//
// macOS builds are signed with Developer ID but not notarized. Users update by
// downloading the latest DMG from GitHub Releases. The browser-mode update
// check (/api/app/updates) in the frontend notifies users of new versions.
// =============================================================================
let win: BrowserWindow | null = null;
function sendStatus(data: {
status: string;
info?: unknown;
progress?: unknown;
error?: string;
}) {
if (win && !win.isDestroyed()) {
win.webContents.send('updater:status', data);
}
}
import type { BrowserWindow } from 'electron';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export function initAutoUpdater(_win: BrowserWindow) {
win = _win;
autoUpdater.autoDownload = false;
autoUpdater.autoInstallOnAppQuit = true;
autoUpdater.on('checking-for-update', () => {
sendStatus({ status: 'checking' });
});
autoUpdater.on('update-available', (info) => {
sendStatus({ status: 'available', info });
});
autoUpdater.on('update-not-available', (info) => {
sendStatus({ status: 'not-available', info });
});
autoUpdater.on('download-progress', (progress) => {
sendStatus({ status: 'downloading', progress });
});
autoUpdater.on('update-downloaded', (info) => {
sendStatus({ status: 'downloaded', info });
});
autoUpdater.on('error', (err) => {
sendStatus({ status: 'error', error: err?.message ?? String(err) });
});
// IPC handlers
ipcMain.handle('updater:check', async () => {
return autoUpdater.checkForUpdates();
});
ipcMain.handle('updater:download', async () => {
return autoUpdater.downloadUpdate();
});
ipcMain.handle('updater:quit-and-install', () => {
autoUpdater.quitAndInstall();
});
// Delayed initial check (10 seconds after launch)
setTimeout(() => {
autoUpdater.checkForUpdates().catch((err) => {
console.log('[updater] Initial update check failed:', err?.message ?? err);
});
}, 10_000);
console.log('[updater] Native auto-updater is disabled. Users should download updates from GitHub Releases.');
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export function setUpdaterWindow(_win: BrowserWindow) {
win = _win;
// no-op while native updater is disabled
}