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:
1 parent
5629da9571
commit
5413eb55ba
5 files changed
+25
-233
No files matched your search
+12
-138
@@ -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 \
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user