33# Android 产物为 release APK,需在仓库 Secrets 配置签名密钥:
44# ANDROID_KEYSTORE_BASE64 / ANDROID_KEY_ALIAS / ANDROID_KEY_PASSWORD / ANDROID_STORE_PASSWORD
55
6- name : Desktop & Android Release
6+ name : Client Release
77
88on :
99 push :
@@ -22,7 +22,9 @@ concurrency:
2222
2323env :
2424 NODE_VERSION : " 20"
25+ RUBY_VERSION : " 3.3"
2526 CSC_IDENTITY_AUTO_DISCOVERY : false
27+ IOS_BUNDLE_ID : " com.monkeycode.mobile"
2628
2729jobs :
2830 electron-windows :
@@ -248,8 +250,128 @@ jobs:
248250 path : mobile/release-apk/*.apk
249251 if-no-files-found : error
250252
253+ capacitor-ios :
254+ runs-on : macos-latest
255+ steps :
256+ - uses : actions/checkout@v4
257+
258+ - uses : pnpm/action-setup@v4
259+ with :
260+ version : 9
261+
262+ - uses : actions/setup-node@v4
263+ with :
264+ node-version : ${{ env.NODE_VERSION }}
265+ cache : pnpm
266+ cache-dependency-path : |
267+ frontend/pnpm-lock.yaml
268+ mobile/pnpm-lock.yaml
269+
270+ - uses : ruby/setup-ruby@v1
271+ with :
272+ ruby-version : ${{ env.RUBY_VERSION }}
273+
274+ - name : Install fastlane gems
275+ working-directory : mobile/ios
276+ run : bundle install --jobs 4 --retry 3
277+
278+ - name : Set versions (tag / CI)
279+ shell : bash
280+ run : |
281+ set -euo pipefail
282+ if [[ "${{ github.ref }}" == refs/tags/v* ]]; then
283+ V="${GITHUB_REF_NAME#v}"
284+ else
285+ V="0.0.0-ci.${{ github.run_number }}"
286+ fi
287+ if [[ "$V" =~ ^[0-9]+\.[0-9]+$ ]]; then
288+ V="${V}.0"
289+ fi
290+ BUILD_NUMBER="${{ github.run_number }}"
291+ echo "APP_VERSION=$V" >> "$GITHUB_ENV"
292+ echo "BUILD_NUMBER=$BUILD_NUMBER" >> "$GITHUB_ENV"
293+ echo "IPA_NAME=MonkeyCode-${V}-ios-release.ipa" >> "$GITHUB_ENV"
294+ 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"
295+ 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
296+
297+ - name : Install frontend deps
298+ working-directory : frontend
299+ run : pnpm install --frozen-lockfile
300+
301+ - name : Install mobile deps
302+ working-directory : mobile
303+ run : pnpm install --frozen-lockfile
304+
305+ - name : Build frontend
306+ working-directory : frontend
307+ run : pnpm run build
308+
309+ - name : Capacitor sync iOS
310+ working-directory : mobile
311+ run : pnpm exec cap sync ios
312+
313+ - name : Install signing assets
314+ shell : bash
315+ env :
316+ IOS_CERTIFICATE_BASE64 : ${{ secrets.IOS_CERTIFICATE_BASE64 }}
317+ IOS_CERTIFICATE_PASSWORD : ${{ secrets.IOS_CERTIFICATE_PASSWORD }}
318+ IOS_PROVISIONING_PROFILE_BASE64 : ${{ secrets.IOS_PROVISIONING_PROFILE_BASE64 }}
319+ run : |
320+ set -euo pipefail
321+ : "${IOS_CERTIFICATE_BASE64:?Missing IOS_CERTIFICATE_BASE64}"
322+ : "${IOS_CERTIFICATE_PASSWORD:?Missing IOS_CERTIFICATE_PASSWORD}"
323+ : "${IOS_PROVISIONING_PROFILE_BASE64:?Missing IOS_PROVISIONING_PROFILE_BASE64}"
324+ CERT_PATH="$RUNNER_TEMP/build_certificate.p12"
325+ PROFILE_PATH="$RUNNER_TEMP/build_profile.mobileprovision"
326+ KEYCHAIN_PATH="$RUNNER_TEMP/app-signing.keychain-db"
327+ KEYCHAIN_PASSWORD="$(openssl rand -base64 24)"
328+ echo "$IOS_CERTIFICATE_BASE64" | base64 -D > "$CERT_PATH"
329+ echo "$IOS_PROVISIONING_PROFILE_BASE64" | base64 -D > "$PROFILE_PATH"
330+ security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
331+ security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
332+ security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
333+ security import "$CERT_PATH" -P "$IOS_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH"
334+ security list-keychains -d user -s "$KEYCHAIN_PATH"
335+ security default-keychain -s "$KEYCHAIN_PATH"
336+ security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
337+ mkdir -p "$HOME/Library/MobileDevice/Provisioning Profiles"
338+ security cms -D -i "$PROFILE_PATH" > "$RUNNER_TEMP/profile.plist"
339+ PROFILE_UUID=$(/usr/libexec/PlistBuddy -c "Print UUID" "$RUNNER_TEMP/profile.plist")
340+ PROFILE_NAME=$(/usr/libexec/PlistBuddy -c "Print Name" "$RUNNER_TEMP/profile.plist")
341+ cp "$PROFILE_PATH" "$HOME/Library/MobileDevice/Provisioning Profiles/$PROFILE_UUID.mobileprovision"
342+ echo "IOS_KEYCHAIN_PATH=$KEYCHAIN_PATH" >> "$GITHUB_ENV"
343+ echo "IOS_PROFILE_NAME=$PROFILE_NAME" >> "$GITHUB_ENV"
344+
345+ - name : Build iOS IPA
346+ working-directory : mobile/ios
347+ env :
348+ IOS_TEAM_ID : ${{ secrets.IOS_TEAM_ID }}
349+ IOS_BUNDLE_ID : ${{ env.IOS_BUNDLE_ID }}
350+ IOS_PROFILE_NAME : ${{ env.IOS_PROFILE_NAME }}
351+ IOS_OUTPUT_NAME : ${{ env.IPA_NAME }}
352+ run : bundle exec fastlane ios build_release
353+
354+ - name : Stage IPA for artifact / release
355+ shell : bash
356+ run : |
357+ test -f "mobile/ios/App/output/${IPA_NAME}"
358+ mkdir -p mobile/release-ios
359+ cp "mobile/ios/App/output/${IPA_NAME}" "mobile/release-ios/${IPA_NAME}"
360+
361+ - uses : actions/upload-artifact@v4
362+ with :
363+ name : capacitor-ios-ipa
364+ path : mobile/release-ios/*.ipa
365+ if-no-files-found : error
366+
367+ - name : Cleanup signing keychain
368+ if : always() && env.IOS_KEYCHAIN_PATH != ''
369+ shell : bash
370+ run : |
371+ security delete-keychain "$IOS_KEYCHAIN_PATH" || true
372+
251373 publish-release :
252- needs : [electron-windows, electron-macos, capacitor-android]
374+ needs : [electron-windows, electron-macos, capacitor-android, capacitor-ios ]
253375 if : startsWith(github.ref, 'refs/tags/v')
254376 runs-on : ubuntu-latest
255377 steps :
@@ -268,6 +390,11 @@ jobs:
268390 name : capacitor-android-apk
269391 path : release-assets/android
270392
393+ - uses : actions/download-artifact@v4
394+ with :
395+ name : capacitor-ios-ipa
396+ path : release-assets/ios
397+
271398 - name : List release files
272399 run : find release-assets -type f -exec ls -lh {} \;
273400
@@ -281,5 +408,52 @@ jobs:
281408 release-assets/windows/*
282409 release-assets/macos/*
283410 release-assets/android/*
411+ release-assets/ios/*
284412 env :
285413 GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }}
414+
415+ ios-distribute :
416+ needs : [capacitor-ios, publish-release]
417+ if : startsWith(github.ref, 'refs/tags/v')
418+ runs-on : macos-latest
419+ steps :
420+ - uses : actions/checkout@v4
421+
422+ - uses : ruby/setup-ruby@v1
423+ with :
424+ ruby-version : ${{ env.RUBY_VERSION }}
425+
426+ - name : Install fastlane gems
427+ working-directory : mobile/ios
428+ run : bundle install --jobs 4 --retry 3
429+
430+ - uses : actions/download-artifact@v4
431+ with :
432+ name : capacitor-ios-ipa
433+ path : mobile/release-ios
434+
435+ - name : Resolve IPA path
436+ shell : bash
437+ run : |
438+ set -euo pipefail
439+ IPA_PATH="$(find mobile/release-ios -name '*.ipa' | head -1)"
440+ test -n "$IPA_PATH"
441+ echo "IOS_IPA_PATH=$IPA_PATH" >> "$GITHUB_ENV"
442+
443+ - name : Upload to TestFlight
444+ working-directory : mobile/ios
445+ env :
446+ IOS_IPA_PATH : ${{ env.IOS_IPA_PATH }}
447+ APP_STORE_CONNECT_KEY_ID : ${{ secrets.APP_STORE_CONNECT_KEY_ID }}
448+ APP_STORE_CONNECT_ISSUER_ID : ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
449+ APP_STORE_CONNECT_PRIVATE_KEY_BASE64 : ${{ secrets.APP_STORE_CONNECT_PRIVATE_KEY_BASE64 }}
450+ run : bundle exec fastlane ios upload_testflight ipa:"$IOS_IPA_PATH"
451+
452+ - name : Submit to App Store
453+ working-directory : mobile/ios
454+ env :
455+ IOS_IPA_PATH : ${{ env.IOS_IPA_PATH }}
456+ APP_STORE_CONNECT_KEY_ID : ${{ secrets.APP_STORE_CONNECT_KEY_ID }}
457+ APP_STORE_CONNECT_ISSUER_ID : ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
458+ APP_STORE_CONNECT_PRIVATE_KEY_BASE64 : ${{ secrets.APP_STORE_CONNECT_PRIVATE_KEY_BASE64 }}
459+ run : bundle exec fastlane ios submit_app_store ipa:"$IOS_IPA_PATH"
0 commit comments