bash 数组展开在 GitHub Actions 中不稳定, 改为 NOTARIZE 变量 + if/else 直接调用不同命令行。 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
396 lines
13 KiB
YAML
396 lines
13 KiB
YAML
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
|
|
|
|
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
|
|
|
|
- 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[@]}"
|