mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-04-22 17:11:04 +08:00
feat(ci): add macOS code signing and Apple notarization to release workflow
- Import Developer ID Application certificate into temporary keychain - Inject APPLE_SIGNING_IDENTITY/APPLE_ID/APPLE_PASSWORD/APPLE_TEAM_ID into Tauri build step for automatic signing and notarization - Staple notarization tickets to both .app and .dmg (hard-fail) - Add verification step: codesign --verify + spctl -a + stapler validate for both .app and .dmg, gating the release on success - Collect .dmg alongside .tar.gz and .zip in release assets - Clean up temporary keychain with original default restored - Update release notes to recommend .dmg and note Apple notarization - Remove all xattr workarounds and "unidentified developer" warnings from README, README_ZH, installation guides, and FAQ (EN/ZH/JA)
This commit is contained in:
169
.github/workflows/release.yml
vendored
169
.github/workflows/release.yml
vendored
@@ -150,9 +150,56 @@ jobs:
|
||||
fi
|
||||
echo "✅ Tauri signing key prepared"
|
||||
|
||||
- name: Import Apple signing certificate
|
||||
if: runner.os == 'macOS'
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Decode .p12 certificate from base64
|
||||
CERT_PATH="$RUNNER_TEMP/certificate.p12"
|
||||
printf '%s' "${{ secrets.APPLE_CERTIFICATE }}" | (base64 --decode 2>/dev/null || base64 -D) > "$CERT_PATH"
|
||||
|
||||
# Save original default keychain for cleanup
|
||||
ORIGINAL_DEFAULT_KEYCHAIN=$(security default-keychain -d user | tr -d '"' | xargs)
|
||||
echo "ORIGINAL_DEFAULT_KEYCHAIN=$ORIGINAL_DEFAULT_KEYCHAIN" >> "$GITHUB_ENV"
|
||||
|
||||
# Create temporary keychain
|
||||
KEYCHAIN_PATH="$RUNNER_TEMP/build.keychain-db"
|
||||
security create-keychain -p "${{ secrets.KEYCHAIN_PASSWORD }}" "$KEYCHAIN_PATH"
|
||||
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
|
||||
security default-keychain -s "$KEYCHAIN_PATH"
|
||||
security unlock-keychain -p "${{ secrets.KEYCHAIN_PASSWORD }}" "$KEYCHAIN_PATH"
|
||||
|
||||
# Import certificate
|
||||
security import "$CERT_PATH" \
|
||||
-k "$KEYCHAIN_PATH" \
|
||||
-P "${{ secrets.APPLE_CERTIFICATE_PASSWORD }}" \
|
||||
-T /usr/bin/codesign \
|
||||
-T /usr/bin/security
|
||||
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "${{ secrets.KEYCHAIN_PASSWORD }}" "$KEYCHAIN_PATH"
|
||||
|
||||
# Dynamically resolve signing identity (must be "Developer ID Application")
|
||||
IDENTITY=$(security find-identity -v -p codesigning "$KEYCHAIN_PATH" \
|
||||
| grep "Developer ID Application" | grep -oE '"[^"]+"' | head -1 | tr -d '"')
|
||||
if [ -z "$IDENTITY" ]; then
|
||||
echo "❌ No 'Developer ID Application' identity found — listing all identities:" >&2
|
||||
security find-identity -v -p codesigning "$KEYCHAIN_PATH"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Signing identity: $IDENTITY"
|
||||
echo "APPLE_SIGNING_IDENTITY=$IDENTITY" >> "$GITHUB_ENV"
|
||||
|
||||
# Cleanup certificate file
|
||||
rm -f "$CERT_PATH"
|
||||
|
||||
- name: Build Tauri App (macOS)
|
||||
if: runner.os == 'macOS'
|
||||
run: pnpm tauri build --target universal-apple-darwin
|
||||
env:
|
||||
APPLE_SIGNING_IDENTITY: ${{ env.APPLE_SIGNING_IDENTITY }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
|
||||
- name: Build Tauri App (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
@@ -169,38 +216,127 @@ jobs:
|
||||
set -euxo pipefail
|
||||
mkdir -p release-assets
|
||||
VERSION="${GITHUB_REF_NAME}" # e.g., v3.5.0
|
||||
echo "Looking for updater artifact (.tar.gz) and .app for zip..."
|
||||
TAR_GZ=""; APP_PATH=""
|
||||
|
||||
# Locate bundle directory
|
||||
BUNDLE_DIR=""
|
||||
TAR_GZ=""; APP_PATH=""; DMG_PATH=""
|
||||
for path in \
|
||||
"src-tauri/target/universal-apple-darwin/release/bundle/macos" \
|
||||
"src-tauri/target/aarch64-apple-darwin/release/bundle/macos" \
|
||||
"src-tauri/target/x86_64-apple-darwin/release/bundle/macos" \
|
||||
"src-tauri/target/release/bundle/macos"; do
|
||||
if [ -d "$path" ]; then
|
||||
[ -z "$TAR_GZ" ] && TAR_GZ=$(find "$path" -maxdepth 1 -name "*.tar.gz" -type f | head -1 || true)
|
||||
BUNDLE_DIR="$path"
|
||||
[ -z "$TAR_GZ" ] && TAR_GZ=$(find "$path" -maxdepth 1 -name "*.tar.gz" -type f | head -1 || true)
|
||||
[ -z "$APP_PATH" ] && APP_PATH=$(find "$path" -maxdepth 1 -name "*.app" -type d | head -1 || true)
|
||||
[ -z "$DMG_PATH" ] && DMG_PATH=$(find "$path" -maxdepth 1 -name "*.dmg" -type f | head -1 || true)
|
||||
fi
|
||||
done
|
||||
|
||||
# Also search dmg directory (Tauri may put DMGs there)
|
||||
for path in \
|
||||
"src-tauri/target/universal-apple-darwin/release/bundle/dmg" \
|
||||
"src-tauri/target/aarch64-apple-darwin/release/bundle/dmg" \
|
||||
"src-tauri/target/x86_64-apple-darwin/release/bundle/dmg" \
|
||||
"src-tauri/target/release/bundle/dmg"; do
|
||||
if [ -d "$path" ] && [ -z "$DMG_PATH" ]; then
|
||||
DMG_PATH=$(find "$path" -maxdepth 1 -name "*.dmg" -type f | head -1 || true)
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$TAR_GZ" ]; then
|
||||
echo "No macOS .tar.gz updater artifact found" >&2
|
||||
exit 1
|
||||
fi
|
||||
# 重命名 tar.gz 为统一格式
|
||||
|
||||
# Staple notarization ticket (hard-fail — must succeed before release)
|
||||
if [ -n "$APP_PATH" ]; then
|
||||
xcrun stapler staple "$APP_PATH"
|
||||
echo "✅ .app stapled"
|
||||
fi
|
||||
if [ -n "$DMG_PATH" ]; then
|
||||
xcrun stapler staple "$DMG_PATH"
|
||||
echo "✅ .dmg stapled"
|
||||
fi
|
||||
|
||||
# 1) Collect .tar.gz (updater artifact)
|
||||
NEW_TAR_GZ="CC-Switch-${VERSION}-macOS.tar.gz"
|
||||
cp "$TAR_GZ" "release-assets/$NEW_TAR_GZ"
|
||||
[ -f "$TAR_GZ.sig" ] && cp "$TAR_GZ.sig" "release-assets/$NEW_TAR_GZ.sig" || echo ".sig for macOS not found yet"
|
||||
echo "macOS updater artifact copied: $NEW_TAR_GZ"
|
||||
|
||||
# 2) Collect .app as zip (absolute paths, no cd)
|
||||
if [ -n "$APP_PATH" ]; then
|
||||
APP_DIR=$(dirname "$APP_PATH"); APP_NAME=$(basename "$APP_PATH")
|
||||
NEW_ZIP="CC-Switch-${VERSION}-macOS.zip"
|
||||
cd "$APP_DIR"
|
||||
ditto -c -k --sequesterRsrc --keepParent "$APP_NAME" "$NEW_ZIP"
|
||||
mv "$NEW_ZIP" "$GITHUB_WORKSPACE/release-assets/"
|
||||
ditto -c -k --sequesterRsrc --keepParent "$APP_PATH" "release-assets/$NEW_ZIP"
|
||||
echo "macOS zip ready: $NEW_ZIP"
|
||||
else
|
||||
echo "No .app found to zip (optional)" >&2
|
||||
fi
|
||||
|
||||
# 3) Collect .dmg
|
||||
if [ -n "$DMG_PATH" ]; then
|
||||
NEW_DMG="CC-Switch-${VERSION}-macOS.dmg"
|
||||
cp "$DMG_PATH" "release-assets/$NEW_DMG"
|
||||
echo "macOS DMG ready: $NEW_DMG"
|
||||
else
|
||||
echo "No .dmg found (optional)" >&2
|
||||
fi
|
||||
|
||||
- name: Verify macOS code signing and notarization
|
||||
if: runner.os == 'macOS'
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
APP_PATH=""; DMG_PATH=""
|
||||
for path in \
|
||||
"src-tauri/target/universal-apple-darwin/release/bundle/macos" \
|
||||
"src-tauri/target/aarch64-apple-darwin/release/bundle/macos" \
|
||||
"src-tauri/target/x86_64-apple-darwin/release/bundle/macos" \
|
||||
"src-tauri/target/release/bundle/macos"; do
|
||||
if [ -d "$path" ]; then
|
||||
[ -z "$APP_PATH" ] && APP_PATH=$(find "$path" -maxdepth 1 -name "*.app" -type d | head -1 || true)
|
||||
fi
|
||||
done
|
||||
for path in \
|
||||
"src-tauri/target/universal-apple-darwin/release/bundle/dmg" \
|
||||
"src-tauri/target/aarch64-apple-darwin/release/bundle/dmg" \
|
||||
"src-tauri/target/x86_64-apple-darwin/release/bundle/dmg" \
|
||||
"src-tauri/target/release/bundle/dmg" \
|
||||
"src-tauri/target/universal-apple-darwin/release/bundle/macos" \
|
||||
"src-tauri/target/release/bundle/macos"; do
|
||||
if [ -d "$path" ] && [ -z "$DMG_PATH" ]; then
|
||||
DMG_PATH=$(find "$path" -maxdepth 1 -name "*.dmg" -type f | head -1 || true)
|
||||
fi
|
||||
done
|
||||
|
||||
# Verify .app
|
||||
if [ -z "$APP_PATH" ]; then
|
||||
echo "❌ No .app found for verification" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "=== Verifying .app: $APP_PATH ==="
|
||||
codesign --verify --deep --strict --verbose=2 "$APP_PATH"
|
||||
echo "✅ codesign verification passed"
|
||||
spctl -a -t exec -vv "$APP_PATH"
|
||||
echo "✅ spctl assessment passed"
|
||||
xcrun stapler validate "$APP_PATH"
|
||||
echo "✅ .app stapler validation passed"
|
||||
|
||||
# Verify .dmg
|
||||
if [ -n "$DMG_PATH" ]; then
|
||||
echo "=== Verifying .dmg: $DMG_PATH ==="
|
||||
codesign --verify --verbose=2 "$DMG_PATH"
|
||||
echo "✅ .dmg codesign verification passed"
|
||||
spctl -a -t open --context context:primary-signature -vv "$DMG_PATH"
|
||||
echo "✅ .dmg spctl assessment passed"
|
||||
xcrun stapler validate "$DMG_PATH"
|
||||
echo "✅ .dmg stapler validation passed"
|
||||
else
|
||||
echo "❌ No .dmg found for verification — release would ship without verified DMG" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Prepare Windows Assets
|
||||
if: runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
@@ -312,13 +448,15 @@ jobs:
|
||||
|
||||
### 下载
|
||||
|
||||
- **macOS**: `CC-Switch-${{ github.ref_name }}-macOS.zip`(解压即用)或 `CC-Switch-${{ github.ref_name }}-macOS.tar.gz`(Homebrew)
|
||||
- **macOS**: `CC-Switch-${{ github.ref_name }}-macOS.dmg`(推荐)或 `CC-Switch-${{ github.ref_name }}-macOS.zip`(解压即用)
|
||||
- **Windows**: `CC-Switch-${{ github.ref_name }}-Windows.msi`(安装版)或 `CC-Switch-${{ github.ref_name }}-Windows-Portable.zip`(绿色版)
|
||||
- **Linux (x86_64)**: `CC-Switch-${{ github.ref_name }}-Linux-x86_64.AppImage` / `.deb` / `.rpm`
|
||||
- **Linux (ARM64)**: `CC-Switch-${{ github.ref_name }}-Linux-arm64.AppImage` / `.deb` / `.rpm`
|
||||
|
||||
> `.tar.gz` 为 Tauri updater 自动更新专用,无需手动下载。
|
||||
|
||||
---
|
||||
提示:macOS 如遇"已损坏"提示,可在终端执行:`xattr -cr "/Applications/CC Switch.app"`
|
||||
macOS 版本已通过 Apple 代码签名和公证,可直接安装使用。
|
||||
files: release-assets/*
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -330,6 +468,17 @@ jobs:
|
||||
echo "Listing bundles in src-tauri/target..."
|
||||
find src-tauri/target -maxdepth 4 -type f -name "*.*" 2>/dev/null || true
|
||||
|
||||
- name: Clean up Apple signing keychain
|
||||
if: runner.os == 'macOS' && always()
|
||||
shell: bash
|
||||
run: |
|
||||
if [ -n "${ORIGINAL_DEFAULT_KEYCHAIN:-}" ]; then
|
||||
security default-keychain -s "$ORIGINAL_DEFAULT_KEYCHAIN" || true
|
||||
fi
|
||||
if [ -f "$RUNNER_TEMP/build.keychain-db" ]; then
|
||||
security delete-keychain "$RUNNER_TEMP/build.keychain-db" || true
|
||||
fi
|
||||
|
||||
assemble-latest-json:
|
||||
name: Assemble latest.json
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
Reference in New Issue
Block a user