From 5413eb55bab1266f7016dd0a9c912f1707cf8c0c Mon Sep 17 00:00:00 2001 From: 7418 <7418@openclaw.ai> Date: Tue, 3 Mar 2026 13:27:35 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E7=AE=80=E5=8C=96=20macOS=20?= =?UTF-8?q?=E6=9E=84=E5=BB=BA=20=E2=80=94=20=E5=8F=AA=E7=AD=BE=E5=90=8D?= =?UTF-8?q?=E4=B8=8D=E5=85=AC=E8=AF=81=EF=BC=8C=E7=A6=81=E7=94=A8=E5=8E=9F?= =?UTF-8?q?=E7=94=9F=E7=83=AD=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .github/workflows/build.yml | 150 +++--------------------------------- electron-builder.yml | 2 +- electron/main.ts | 17 ---- electron/preload.ts | 14 ---- electron/updater.ts | 75 +++--------------- 5 files changed, 25 insertions(+), 233 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 13a6c92..8872e21 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 \ diff --git a/electron-builder.yml b/electron-builder.yml index 84b53cf..4002b8d 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -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] diff --git a/electron/main.ts b/electron/main.ts index 37c059b..ba5ed4d 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -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); } diff --git a/electron/preload.ts b/electron/preload.ts index 5c290e1..2da9136 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -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); }; - }, - }, - }), }); diff --git a/electron/updater.ts b/electron/updater.ts index 8dbb341..944d8ed 100644 --- a/electron/updater.ts +++ b/electron/updater.ts @@ -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 }