Skip to content

Commit 0800119

Browse files
cwclaude
andcommitted
feat: add mobile app automated release system for iOS and Android
- Add Fastlane configuration for Android (Google Play) - Add Fastlane configuration for iOS (App Store) - Add GitHub Actions workflows for automated releases - Add comprehensive documentation and setup guides - Add interactive iOS secrets configuration script - Add app store submission materials and checklists Android releases are fully operational. iOS releases ready after configuring App Store Connect secrets. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 777903f commit 0800119

19 files changed

+5551
-0
lines changed
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
name: Android - Google Play Store Release
2+
3+
on:
4+
push:
5+
tags:
6+
- 'v*'
7+
workflow_dispatch:
8+
inputs:
9+
track:
10+
description: 'Release track (internal/beta/production)'
11+
required: true
12+
default: 'internal'
13+
type: choice
14+
options:
15+
- internal
16+
- beta
17+
- production
18+
19+
env:
20+
FLUTTER_VERSION: '3.32.2'
21+
JAVA_VERSION: '17'
22+
23+
jobs:
24+
build-and-release-android:
25+
name: Build and Release Android to Google Play
26+
runs-on: ubuntu-latest
27+
28+
steps:
29+
- name: Checkout code
30+
uses: actions/checkout@v4
31+
32+
- name: Setup Java
33+
uses: actions/setup-java@v4
34+
with:
35+
distribution: 'temurin'
36+
java-version: ${{ env.JAVA_VERSION }}
37+
38+
- name: Setup Flutter
39+
uses: subosito/flutter-action@v2
40+
with:
41+
flutter-version: ${{ env.FLUTTER_VERSION }}
42+
channel: 'stable'
43+
cache: true
44+
45+
- name: Get dependencies
46+
working-directory: opencli_mobile
47+
run: flutter pub get
48+
49+
- name: Run code analysis
50+
working-directory: opencli_mobile
51+
run: flutter analyze --no-fatal-infos --no-fatal-warnings
52+
53+
- name: Decode Keystore
54+
env:
55+
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
56+
run: |
57+
echo "$ANDROID_KEYSTORE_BASE64" | base64 --decode > opencli_mobile/android/app/release.keystore
58+
59+
- name: Create keystore.properties
60+
env:
61+
KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
62+
KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
63+
KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
64+
run: |
65+
cat > opencli_mobile/android/keystore.properties << EOF
66+
storeFile=app/release.keystore
67+
storePassword=$KEYSTORE_PASSWORD
68+
keyAlias=$KEY_ALIAS
69+
keyPassword=$KEY_PASSWORD
70+
EOF
71+
72+
- name: Build Release AAB
73+
working-directory: opencli_mobile
74+
run: |
75+
flutter build appbundle --release
76+
77+
- name: Upload AAB Artifact
78+
uses: actions/upload-artifact@v4
79+
with:
80+
name: android-release-aab
81+
path: opencli_mobile/build/app/outputs/bundle/release/app-release.aab
82+
retention-days: 30
83+
84+
- name: Setup Ruby (for Fastlane)
85+
uses: ruby/setup-ruby@v1
86+
with:
87+
ruby-version: '3.2'
88+
bundler-cache: false
89+
90+
- name: Install Fastlane
91+
run: |
92+
gem install fastlane
93+
94+
- name: Deploy to Google Play
95+
working-directory: opencli_mobile/android
96+
env:
97+
PLAY_STORE_JSON_KEY: ${{ secrets.PLAY_STORE_JSON_KEY }}
98+
run: |
99+
TRACK="${{ github.event.inputs.track || 'internal' }}"
100+
AAB_PATH="${{ github.workspace }}/opencli_mobile/build/app/outputs/bundle/release/app-release.aab"
101+
102+
echo "🚀 Deploying to track: $TRACK"
103+
echo "📦 AAB path: $AAB_PATH"
104+
echo "📊 AAB size: $(du -h "$AAB_PATH" | cut -f1)"
105+
106+
# Run fastlane lane based on track
107+
case "$TRACK" in
108+
internal)
109+
fastlane internal aab:"$AAB_PATH"
110+
;;
111+
beta)
112+
fastlane beta aab:"$AAB_PATH"
113+
;;
114+
production)
115+
fastlane production aab:"$AAB_PATH"
116+
;;
117+
*)
118+
echo "❌ Unknown track: $TRACK"
119+
exit 1
120+
;;
121+
esac
122+
123+
- name: Create GitHub Release
124+
if: startsWith(github.ref, 'refs/tags/')
125+
uses: softprops/action-gh-release@v1
126+
with:
127+
files: opencli_mobile/build/app/outputs/bundle/release/app-release.aab
128+
generate_release_notes: true
129+
env:
130+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
131+
132+
notify:
133+
name: Notify on completion
134+
needs: build-and-release-android
135+
runs-on: ubuntu-latest
136+
if: always()
137+
138+
steps:
139+
- name: Send notification
140+
run: |
141+
if [ "${{ needs.build-and-release-android.result }}" == "success" ]; then
142+
echo "✅ Android release to Google Play completed successfully!"
143+
else
144+
echo "❌ Android release to Google Play failed!"
145+
exit 1
146+
fi
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
name: iOS/Mac - App Store Release
2+
3+
on:
4+
push:
5+
tags:
6+
- 'v*'
7+
workflow_dispatch:
8+
inputs:
9+
submit_for_review:
10+
description: 'Submit to App Store Review after upload'
11+
required: false
12+
default: false
13+
type: boolean
14+
15+
env:
16+
FLUTTER_VERSION: '3.32.2'
17+
18+
jobs:
19+
build-and-release-ios:
20+
name: Build and Release iOS to App Store
21+
runs-on: macos-14
22+
23+
steps:
24+
- name: Checkout code
25+
uses: actions/checkout@v4
26+
27+
- name: Setup Flutter
28+
uses: subosito/flutter-action@v2
29+
with:
30+
flutter-version: ${{ env.FLUTTER_VERSION }}
31+
channel: 'stable'
32+
cache: true
33+
34+
- name: Get dependencies
35+
working-directory: opencli_mobile
36+
run: flutter pub get
37+
38+
- name: Run code analysis
39+
working-directory: opencli_mobile
40+
run: flutter analyze --no-fatal-infos --no-fatal-warnings
41+
42+
- name: Setup Xcode
43+
uses: maxim-lobanov/setup-xcode@v1
44+
with:
45+
xcode-version: latest-stable
46+
47+
- name: Install CocoaPods
48+
working-directory: opencli_mobile/ios
49+
run: |
50+
pod install --repo-update
51+
52+
- name: Setup App Store Connect API Key
53+
env:
54+
APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
55+
APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
56+
APP_STORE_CONNECT_API_KEY_BASE64: ${{ secrets.APP_STORE_CONNECT_API_KEY_BASE64 }}
57+
run: |
58+
mkdir -p ~/private_keys
59+
echo "$APP_STORE_CONNECT_API_KEY_BASE64" | base64 --decode > ~/private_keys/AuthKey_${APP_STORE_CONNECT_API_KEY_ID}.p8
60+
chmod 600 ~/private_keys/AuthKey_${APP_STORE_CONNECT_API_KEY_ID}.p8
61+
62+
echo "✅ App Store Connect API Key configured"
63+
ls -lh ~/private_keys/
64+
65+
- name: Import Signing Certificate
66+
env:
67+
DISTRIBUTION_CERTIFICATE_BASE64: ${{ secrets.DISTRIBUTION_CERTIFICATE_BASE64 }}
68+
DISTRIBUTION_CERTIFICATE_PASSWORD: ${{ secrets.DISTRIBUTION_CERTIFICATE_PASSWORD }}
69+
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
70+
run: |
71+
# Create temporary keychain
72+
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
73+
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
74+
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
75+
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
76+
77+
# Import certificate
78+
echo "$DISTRIBUTION_CERTIFICATE_BASE64" | base64 --decode > $RUNNER_TEMP/certificate.p12
79+
security import $RUNNER_TEMP/certificate.p12 \
80+
-P "$DISTRIBUTION_CERTIFICATE_PASSWORD" \
81+
-A -t cert -f pkcs12 -k $KEYCHAIN_PATH
82+
security list-keychain -d user -s $KEYCHAIN_PATH
83+
84+
# Allow codesign to access keychain
85+
security set-key-partition-list -S apple-tool:,apple:,codesign: \
86+
-s -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
87+
88+
echo "✅ Signing certificate imported"
89+
90+
- name: Install Provisioning Profile
91+
env:
92+
PROVISIONING_PROFILE_BASE64: ${{ secrets.PROVISIONING_PROFILE_BASE64 }}
93+
run: |
94+
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
95+
echo "$PROVISIONING_PROFILE_BASE64" | base64 --decode > ~/Library/MobileDevice/Provisioning\ Profiles/profile.mobileprovision
96+
97+
echo "✅ Provisioning profile installed"
98+
ls -lh ~/Library/MobileDevice/Provisioning\ Profiles/
99+
100+
- name: Create ExportOptions.plist
101+
run: |
102+
cat > opencli_mobile/ios/ExportOptions.plist << 'EOF'
103+
<?xml version="1.0" encoding="UTF-8"?>
104+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
105+
<plist version="1.0">
106+
<dict>
107+
<key>method</key>
108+
<string>app-store</string>
109+
<key>uploadBitcode</key>
110+
<false/>
111+
<key>uploadSymbols</key>
112+
<true/>
113+
<key>signingStyle</key>
114+
<string>automatic</string>
115+
<key>teamID</key>
116+
<string>G9VG22HGJG</string>
117+
</dict>
118+
</plist>
119+
EOF
120+
121+
echo "✅ ExportOptions.plist created"
122+
123+
- name: Build IPA
124+
working-directory: opencli_mobile
125+
run: |
126+
echo "🔨 Building iOS IPA..."
127+
flutter build ipa --release \
128+
--export-options-plist=ios/ExportOptions.plist
129+
130+
IPA_PATH=$(find build/ios/ipa -name "*.ipa" | head -n 1)
131+
if [ -z "$IPA_PATH" ]; then
132+
echo "❌ IPA file not found"
133+
exit 1
134+
fi
135+
136+
echo "✅ IPA built successfully: $IPA_PATH"
137+
echo "📊 IPA size: $(du -h "$IPA_PATH" | cut -f1)"
138+
139+
- name: Upload IPA Artifact
140+
uses: actions/upload-artifact@v4
141+
with:
142+
name: ios-release-ipa
143+
path: opencli_mobile/build/ios/ipa/*.ipa
144+
retention-days: 30
145+
146+
- name: Upload to App Store Connect
147+
env:
148+
APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
149+
APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
150+
run: |
151+
IPA_PATH=$(find opencli_mobile/build/ios/ipa -name "*.ipa" | head -n 1)
152+
153+
if [ -z "$IPA_PATH" ]; then
154+
echo "❌ IPA file not found"
155+
exit 1
156+
fi
157+
158+
echo "🚀 Uploading IPA to App Store Connect..."
159+
echo "📦 IPA path: $IPA_PATH"
160+
161+
xcrun altool --upload-app \
162+
--type ios \
163+
--file "$IPA_PATH" \
164+
--apiKey "$APP_STORE_CONNECT_API_KEY_ID" \
165+
--apiIssuer "$APP_STORE_CONNECT_ISSUER_ID" \
166+
--apiKeyPath ~/private_keys/AuthKey_${APP_STORE_CONNECT_API_KEY_ID}.p8
167+
168+
echo "✅ IPA uploaded successfully to App Store Connect!"
169+
170+
- name: Cleanup Keychain
171+
if: always()
172+
run: |
173+
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
174+
if [ -f "$KEYCHAIN_PATH" ]; then
175+
security delete-keychain $KEYCHAIN_PATH
176+
echo "🧹 Keychain cleaned up"
177+
fi
178+
179+
- name: Create GitHub Release
180+
if: startsWith(github.ref, 'refs/tags/')
181+
uses: softprops/action-gh-release@v1
182+
with:
183+
files: opencli_mobile/build/ios/ipa/*.ipa
184+
generate_release_notes: true
185+
env:
186+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
187+
188+
notify:
189+
name: Notify on completion
190+
needs: build-and-release-ios
191+
runs-on: ubuntu-latest
192+
if: always()
193+
194+
steps:
195+
- name: Send notification
196+
run: |
197+
if [ "${{ needs.build-and-release-ios.result }}" == "success" ]; then
198+
echo "✅ iOS release to App Store completed successfully!"
199+
else
200+
echo "❌ iOS release to App Store failed!"
201+
exit 1
202+
fi

0 commit comments

Comments
 (0)