Skip to content

fix ios export signing #21

fix ios export signing

fix ios export signing #21

# 打 tag(如 v1.2.3)后自动构建 Windows / macOS 桌面与 Android APK,并发布到 GitHub Release。
# 手动运行 workflow 仅上传 Actions Artifact,不创建 Release(便于试打)。
# Android 产物为 release APK,需在仓库 Secrets 配置签名密钥:
# ANDROID_KEYSTORE_BASE64 / ANDROID_KEY_ALIAS / ANDROID_KEY_PASSWORD / ANDROID_STORE_PASSWORD
name: Client Release
on:
push:
tags:
- "v*"
workflow_dispatch:
# 仅写 contents 会把其余权限置为 none,会导致 download-artifact 找不到同 run 上传的产物
permissions:
contents: write
actions: write
concurrency:
group: electron-release-${{ github.ref }}
cancel-in-progress: true
env:
NODE_VERSION: "20"
RUBY_VERSION: "3.3"
CSC_IDENTITY_AUTO_DISCOVERY: false
IOS_BUNDLE_ID: "com.monkeycode.mobile"
jobs:
electron-windows:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: pnpm
cache-dependency-path: desktop/pnpm-lock.yaml
- name: Cache Electron binary
uses: actions/cache@v4
with:
path: ~\AppData\Local\electron\Cache
key: electron-win-${{ hashFiles('desktop/package.json') }}
- name: Cache electron-builder tools
uses: actions/cache@v4
with:
path: ~\AppData\Local\electron-builder\Cache
key: electron-builder-win-${{ hashFiles('desktop/package.json') }}
- name: Set package.json version
shell: bash
working-directory: desktop
run: |
if [[ "${{ github.ref }}" == refs/tags/v* ]]; then
V="${GITHUB_REF_NAME#v}"
else
V="0.0.0-ci.${{ github.run_number }}"
fi
# electron-builder 要求严格 semver(至少 major.minor.patch 三段)
if [[ "$V" =~ ^[0-9]+\.[0-9]+$ ]]; then
V="${V}.0"
fi
node -e "const fs=require('fs');const p='package.json';const j=JSON.parse(fs.readFileSync(p,'utf8'));j.version=process.argv[1];fs.writeFileSync(p,JSON.stringify(j,null,2)+'\n');" "$V"
# windows-latest 默认 run 用 pwsh;pnpm/electron-builder 在 Git Bash 下 cwd 更可靠
- name: Install dependencies
working-directory: desktop
shell: bash
run: pnpm install --frozen-lockfile
# 用 bash + pnpm exec,避免 pwsh 下子进程 cwd/退出码异常;ci-win.json 显式 directories.output
- name: Build Windows (portable + NSIS)
working-directory: desktop
shell: bash
run: |
set -euxo pipefail
pwd
test -f package.json
test -f electron/main.cjs
pnpm exec electron-builder --win portable --x64 --publish never -c electron-builder.ci-win.json
if [[ ! -d release ]]; then
echo "electron-builder 已结束但 ./release 不存在,当前目录:"
ls -la
exit 1
fi
ls -laR release
- name: Rename Windows artifact
shell: bash
run: cp "$(ls desktop/release/*.exe | head -1)" desktop/release/MonkeyCode-windows-x86.exe
- uses: actions/upload-artifact@v4
with:
name: electron-windows-x64
path: desktop/release/MonkeyCode-windows-x86.exe
if-no-files-found: error
electron-macos:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: pnpm
cache-dependency-path: desktop/pnpm-lock.yaml
- name: Cache Electron binary
uses: actions/cache@v4
with:
path: ~/Library/Caches/electron
key: electron-mac-${{ hashFiles('desktop/package.json') }}
- name: Cache electron-builder tools
uses: actions/cache@v4
with:
path: ~/Library/Caches/electron-builder
key: electron-builder-mac-${{ hashFiles('desktop/package.json') }}
- name: Set package.json version
working-directory: desktop
run: |
if [[ "${{ github.ref }}" == refs/tags/v* ]]; then
V="${GITHUB_REF_NAME#v}"
else
V="0.0.0-ci.${{ github.run_number }}"
fi
if [[ "$V" =~ ^[0-9]+\.[0-9]+$ ]]; then
V="${V}.0"
fi
node -e "const fs=require('fs');const p='package.json';const j=JSON.parse(fs.readFileSync(p,'utf8'));j.version=process.argv[1];fs.writeFileSync(p,JSON.stringify(j,null,2)+'\n');" "$V"
- name: Install dependencies
working-directory: desktop
run: pnpm install --frozen-lockfile
# 目标格式(dmg / zip)取自 package.json 的 build.mac.target
- name: Build macOS (arm64 + x64)
working-directory: desktop
run: |
set -euo pipefail
pwd
pnpm run electron:ci:mac
test -d release || (echo "missing desktop/release"; ls -la; exit 1)
ls -laR release
- name: Rename macOS artifacts
run: |
ARM64=$(ls desktop/release/*arm64*.dmg 2>/dev/null | head -1)
X64=$(ls desktop/release/*.dmg | grep -v arm64 | head -1)
[ -n "$ARM64" ] && cp "$ARM64" desktop/release/MonkeyCode-macos-arm64.dmg
[ -n "$X64" ] && cp "$X64" desktop/release/MonkeyCode-macos-x86.dmg
- uses: actions/upload-artifact@v4
with:
name: electron-macos-universal
path: |
desktop/release/MonkeyCode-macos-arm64.dmg
desktop/release/MonkeyCode-macos-x86.dmg
if-no-files-found: error
capacitor-android:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: pnpm
cache-dependency-path: |
frontend/pnpm-lock.yaml
mobile/pnpm-lock.yaml
- uses: actions/setup-java@v4
with:
distribution: "temurin"
java-version: "21"
- name: Set versions (tag / CI)
shell: bash
run: |
set -euo pipefail
if [[ "${{ github.ref }}" == refs/tags/v* ]]; then
V="${GITHUB_REF_NAME#v}"
else
V="0.0.0-ci.${{ github.run_number }}"
fi
if [[ "$V" =~ ^[0-9]+\.[0-9]+$ ]]; then
V="${V}.0"
fi
CODE=${{ github.run_number }}
node -e "const fs=require('fs');const p='mobile/package.json';const j=JSON.parse(fs.readFileSync(p,'utf8'));j.version=process.argv[1];fs.writeFileSync(p,JSON.stringify(j,null,2)+'\n');" "$V"
sed -i 's/versionCode [0-9][0-9]*/versionCode '"$CODE"'/' mobile/android/app/build.gradle
sed -i 's/versionName ".*"/versionName "'"$V"'"/' mobile/android/app/build.gradle
- name: Install frontend deps
working-directory: frontend
run: pnpm install --frozen-lockfile
- name: Install mobile deps
working-directory: mobile
run: pnpm install --frozen-lockfile
- name: Build web + Capacitor sync
run: |
set -euxo pipefail
(cd frontend && ELECTRON=true pnpm run build)
(cd mobile && pnpm exec cap sync)
- name: Build Android APK (release)
working-directory: mobile/android
env:
ANDROID_KEYSTORE_PATH: ${{ runner.temp }}/release.jks
ANDROID_STORE_PASSWORD: ${{ secrets.ANDROID_STORE_PASSWORD }}
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
run: |
set -euxo pipefail
echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 -d > "$ANDROID_KEYSTORE_PATH"
chmod +x gradlew
./gradlew assembleRelease --no-daemon --stacktrace -Dorg.gradle.jvmargs=-Xmx4096m
- name: Stage APK for artifact / release
shell: bash
run: |
mkdir -p mobile/release-apk
cp mobile/android/app/build/outputs/apk/release/app-release.apk mobile/release-apk/MonkeyCode-android-arm64.apk
ls -lh mobile/release-apk/
- uses: actions/upload-artifact@v4
with:
name: capacitor-android-apk
path: mobile/release-apk/*.apk
if-no-files-found: error
capacitor-ios:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: pnpm
cache-dependency-path: |
frontend/pnpm-lock.yaml
mobile/pnpm-lock.yaml
- uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ env.RUBY_VERSION }}
- name: Install fastlane gems
working-directory: mobile/ios
run: bundle install --jobs 4 --retry 3
- name: Set versions (tag / CI)
shell: bash
run: |
set -euo pipefail
if [[ "${{ github.ref }}" == refs/tags/v* ]]; then
V="${GITHUB_REF_NAME#v}"
else
V="0.0.0-ci.${{ github.run_number }}"
fi
if [[ "$V" =~ ^[0-9]+\.[0-9]+$ ]]; then
V="${V}.0"
fi
BUILD_NUMBER="${{ github.run_number }}"
echo "APP_VERSION=$V" >> "$GITHUB_ENV"
echo "BUILD_NUMBER=$BUILD_NUMBER" >> "$GITHUB_ENV"
echo "IPA_NAME=MonkeyCode-${V}-ios-release.ipa" >> "$GITHUB_ENV"
node -e "const fs=require('fs');const p='mobile/package.json';const j=JSON.parse(fs.readFileSync(p,'utf8'));j.version=process.argv[1];fs.writeFileSync(p,JSON.stringify(j,null,2)+'\n');" "$V"
perl -0pi -e "s/MARKETING_VERSION = [^;]+;/MARKETING_VERSION = ${V};/g; s/CURRENT_PROJECT_VERSION = [^;]+;/CURRENT_PROJECT_VERSION = ${BUILD_NUMBER};/g" mobile/ios/App/App.xcodeproj/project.pbxproj
- name: Install frontend deps
working-directory: frontend
run: pnpm install --frozen-lockfile
- name: Install mobile deps
working-directory: mobile
run: pnpm install --frozen-lockfile
- name: Build frontend
working-directory: frontend
run: pnpm run build
- name: Capacitor sync iOS
working-directory: mobile
run: pnpm exec cap sync ios
- name: Install signing assets
shell: bash
env:
IOS_CERTIFICATE_BASE64: ${{ secrets.IOS_CERTIFICATE_BASE64 }}
IOS_CERTIFICATE_PASSWORD: ${{ secrets.IOS_CERTIFICATE_PASSWORD }}
IOS_PROVISIONING_PROFILE_BASE64: ${{ secrets.IOS_PROVISIONING_PROFILE_BASE64 }}
run: |
set -euo pipefail
: "${IOS_CERTIFICATE_BASE64:?Missing IOS_CERTIFICATE_BASE64}"
: "${IOS_CERTIFICATE_PASSWORD:?Missing IOS_CERTIFICATE_PASSWORD}"
: "${IOS_PROVISIONING_PROFILE_BASE64:?Missing IOS_PROVISIONING_PROFILE_BASE64}"
CERT_PATH="$RUNNER_TEMP/build_certificate.p12"
PROFILE_PATH="$RUNNER_TEMP/build_profile.mobileprovision"
KEYCHAIN_PATH="$RUNNER_TEMP/app-signing.keychain-db"
KEYCHAIN_PASSWORD="$(openssl rand -base64 24)"
echo "$IOS_CERTIFICATE_BASE64" | base64 -D > "$CERT_PATH"
echo "$IOS_PROVISIONING_PROFILE_BASE64" | base64 -D > "$PROFILE_PATH"
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security import "$CERT_PATH" -P "$IOS_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH"
security list-keychains -d user -s "$KEYCHAIN_PATH"
security default-keychain -s "$KEYCHAIN_PATH"
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
mkdir -p "$HOME/Library/MobileDevice/Provisioning Profiles"
security cms -D -i "$PROFILE_PATH" > "$RUNNER_TEMP/profile.plist"
PROFILE_UUID=$(/usr/libexec/PlistBuddy -c "Print UUID" "$RUNNER_TEMP/profile.plist")
PROFILE_NAME=$(/usr/libexec/PlistBuddy -c "Print Name" "$RUNNER_TEMP/profile.plist")
cp "$PROFILE_PATH" "$HOME/Library/MobileDevice/Provisioning Profiles/$PROFILE_UUID.mobileprovision"
echo "IOS_KEYCHAIN_PATH=$KEYCHAIN_PATH" >> "$GITHUB_ENV"
echo "IOS_PROFILE_NAME=$PROFILE_NAME" >> "$GITHUB_ENV"
- name: Build iOS IPA
working-directory: mobile/ios
env:
IOS_TEAM_ID: ${{ secrets.IOS_TEAM_ID }}
IOS_BUNDLE_ID: ${{ env.IOS_BUNDLE_ID }}
IOS_PROFILE_NAME: ${{ env.IOS_PROFILE_NAME }}
IOS_OUTPUT_NAME: ${{ env.IPA_NAME }}
run: bundle exec fastlane ios build_release
- name: Stage IPA for artifact / release
shell: bash
run: |
test -f "mobile/ios/App/output/${IPA_NAME}"
mkdir -p mobile/release-ios
cp "mobile/ios/App/output/${IPA_NAME}" "mobile/release-ios/${IPA_NAME}"
- uses: actions/upload-artifact@v4
with:
name: capacitor-ios-ipa
path: mobile/release-ios/*.ipa
if-no-files-found: error
- name: Cleanup signing keychain
if: always() && env.IOS_KEYCHAIN_PATH != ''
shell: bash
run: |
security delete-keychain "$IOS_KEYCHAIN_PATH" || true
publish-release:
needs: [electron-windows, electron-macos, capacitor-android, capacitor-ios]
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
name: electron-windows-x64
path: release-assets/windows
- uses: actions/download-artifact@v4
with:
name: electron-macos-universal
path: release-assets/macos
- uses: actions/download-artifact@v4
with:
name: capacitor-android-apk
path: release-assets/android
- uses: actions/download-artifact@v4
with:
name: capacitor-ios-ipa
path: release-assets/ios
- name: List release files
run: find release-assets -type f -exec ls -lh {} \;
- uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.ref_name }}
name: MonkeyCode ${{ github.ref_name }}
generate_release_notes: true
fail_on_unmatched_files: false
files: |
release-assets/windows/*
release-assets/macos/*
release-assets/android/*
release-assets/ios/*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ios-distribute:
needs: [capacitor-ios, publish-release]
if: startsWith(github.ref, 'refs/tags/v')
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ env.RUBY_VERSION }}
- name: Install fastlane gems
working-directory: mobile/ios
run: bundle install --jobs 4 --retry 3
- uses: actions/download-artifact@v4
with:
name: capacitor-ios-ipa
path: mobile/release-ios
- name: Resolve IPA path
shell: bash
run: |
set -euo pipefail
IPA_PATH="$(find "$GITHUB_WORKSPACE/mobile/release-ios" -name '*.ipa' | head -1)"
test -n "$IPA_PATH"
test -f "$IPA_PATH"
echo "IOS_IPA_PATH=$IPA_PATH" >> "$GITHUB_ENV"
- name: Upload to TestFlight
working-directory: mobile/ios
env:
IOS_IPA_PATH: ${{ env.IOS_IPA_PATH }}
APP_STORE_CONNECT_KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }}
APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
APP_STORE_CONNECT_PRIVATE_KEY_BASE64: ${{ secrets.APP_STORE_CONNECT_PRIVATE_KEY_BASE64 }}
run: bundle exec fastlane ios upload_testflight ipa:"$IOS_IPA_PATH"
- name: Submit to App Store
working-directory: mobile/ios
env:
IOS_IPA_PATH: ${{ env.IOS_IPA_PATH }}
APP_STORE_CONNECT_KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }}
APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
APP_STORE_CONNECT_PRIVATE_KEY_BASE64: ${{ secrets.APP_STORE_CONNECT_PRIVATE_KEY_BASE64 }}
run: bundle exec fastlane ios submit_app_store ipa:"$IOS_IPA_PATH"