fix ios export signing #21
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
| # 打 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" |