feat: macOS 双架构分离构建 — x64 只签名,arm64 签名+条件公证 #55
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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[@]}" |