Skip to content

feat: macOS 双架构分离构建 — x64 只签名,arm64 签名+条件公证 #55

feat: macOS 双架构分离构建 — x64 只签名,arm64 签名+条件公证

feat: macOS 双架构分离构建 — x64 只签名,arm64 签名+条件公证 #55

Workflow file for this run

name: Build & Package
on:
workflow_dispatch:
inputs:
platform:
description: "Target platform"
required: true
default: "all"
type: choice
options:
- all
- windows
- macos
- linux
mac_notarize:
description: "mac notarization mode (arm64 only)"
required: false
default: "auto"
type: choice
options:
- auto
- "on"
- "off"
push:
tags:
- "v*"
permissions:
contents: write
jobs:
build-windows:
if: ${{ github.event_name == 'push' || inputs.platform == 'all' || inputs.platform == 'windows' }}
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- name: Install dependencies
run: npm ci
- name: Build Electron app
run: npm run electron:build
- name: Package for Windows
run: npx electron-builder --win --config electron-builder.yml --publish never
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: build-windows
path: |
release/*.exe
release/*.exe.blockmap
release/latest.yml
retention-days: 3
build-macos:
if: ${{ github.event_name == 'push' || inputs.platform == 'all' || inputs.platform == 'macos' }}
runs-on: macos-latest
env:
HAS_CERT: ${{ secrets.MAC_CERT_P12_BASE64 != '' && 'true' || 'false' }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- name: Install dependencies
run: npm ci
- name: Build Electron app
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: |
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
- 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)
timeout-minutes: 15
env:
CSC_LINK: ${{ env.HAS_CERT == 'true' && '/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
EXTRA_ARGS=()
if [[ "${{ steps.mac_mode.outputs.notarize }}" != "true" ]]; then
EXTRA_ARGS=(-c.mac.notarize=false)
fi
for i in 1 2 3; do
echo "electron-builder arm64 attempt $i/3 (notarize=${{ steps.mac_mode.outputs.notarize }})"
if npx electron-builder --mac --arm64 --config electron-builder.yml "${EXTRA_ARGS[@]}" --publish never; 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
- 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
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"
if [ "$FAILED" -ne 0 ]; then
echo "::error::One or more .app bundles failed signature verification"
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:
if: ${{ github.event_name == 'push' || inputs.platform == 'all' || inputs.platform == 'linux' }}
runs-on: ubuntu-latest
strategy:
matrix:
arch: [x64, arm64]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- name: Install dependencies
run: npm ci
- name: Build Electron app
run: npm run electron:build
- name: Package for Linux (${{ matrix.arch }})
run: npx electron-builder --linux --${{ matrix.arch }} --config electron-builder.yml --publish never
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: build-linux-${{ matrix.arch }}
path: |
release/*.AppImage
release/*.deb
release/*.rpm
release/latest-linux*.yml
retention-days: 3
release:
if: ${{ always() && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') && !contains(needs.*.result, 'cancelled') }}
needs: [build-windows, build-macos, build-linux]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
merge-multiple: true
- name: List artifacts
run: find artifacts -type f | sort
- name: Get version and previous tag
id: meta
run: |
VERSION=${GITHUB_REF_NAME#v}
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
PREV_TAG=$(git tag --sort=-creatordate | grep '^v' | sed -n '2p' || echo "")
echo "prev_tag=$PREV_TAG" >> "$GITHUB_OUTPUT"
- name: Generate changelog
id: changelog
run: |
PREV_TAG="${{ steps.meta.outputs.prev_tag }}"
if [ -n "$PREV_TAG" ]; then
RANGE="${PREV_TAG}..HEAD"
else
RANGE="HEAD"
fi
{
echo "changelog<<CHANGELOG_EOF"
git log "$RANGE" --pretty=format:'| `%h` | %s |'
echo ""
echo "CHANGELOG_EOF"
} >> "$GITHUB_OUTPUT"
- name: Create release and upload assets
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VERSION="${{ steps.meta.outputs.version }}"
PREV_TAG="${{ steps.meta.outputs.prev_tag }}"
cat > release-notes.md << 'NOTES_EOF'
## Downloads
**Windows**
- **CodePilot Setup ${{ steps.meta.outputs.version }}.exe** — Windows installer (x64 + arm64)
**macOS**
- **CodePilot-${{ steps.meta.outputs.version }}-arm64.dmg** — macOS Apple Silicon (M1/M2/M3/M4)
- **CodePilot-${{ steps.meta.outputs.version }}-x64.dmg** — macOS Intel
**Linux**
- **AppImage / deb / rpm** — x64 and arm64
## Installation
**Windows**: Download the exe installer, double-click to install.
**macOS**:
1. Download the DMG for your chip architecture
2. Open the DMG, drag CodePilot into Applications
3. If you see a security prompt on first launch, go to **System Settings > Privacy & Security** and click "Open Anyway"
4. Configure your Anthropic API Key in Settings
**Linux**: Run the AppImage directly, or install via deb/rpm package.
## Requirements
- Windows 10+ / macOS 12.0+ / Linux (glibc 2.31+)
- Anthropic API Key or `ANTHROPIC_API_KEY` environment variable
- Claude Code CLI recommended for code features
## Changelog
NOTES_EOF
if [ -n "$PREV_TAG" ]; then
echo "" >> release-notes.md
echo "| Commit | Description |" >> release-notes.md
echo "|--------|-------------|" >> release-notes.md
cat >> release-notes.md << 'CHANGELOG_INLINE_EOF'
${{ steps.changelog.outputs.changelog }}
CHANGELOG_INLINE_EOF
fi
# Collect all release files (handle filenames with spaces)
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)
# Create release and upload all assets in one shot
gh release create "${GITHUB_REF_NAME}" \
--title "CodePilot v${VERSION}" \
--notes-file release-notes.md \
--latest \
"${FILES[@]}"