Skip to content

Commit dd41c5b

Browse files
authored
Merge pull request #444 from OpenPecha/develop
Develop
2 parents fef25de + e5da126 commit dd41c5b

10 files changed

Lines changed: 541 additions & 11 deletions

File tree

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
name: 🤖 Build & Distribute Android
2+
3+
# Reusable workflow. Called by release-dev.yml / release-prod.yml.
4+
# Builds a signed AAB for the requested flavor and (optionally) uploads it to
5+
# the given Google Play track. Android signing reuses the CM_* env vars that
6+
# android/app/build.gradle.kts already reads in CI mode.
7+
8+
on:
9+
workflow_call:
10+
inputs:
11+
flavor: { required: true, type: string } # dev | staging | prod
12+
entrypoint: { required: true, type: string } # lib/main_<flavor>.dart
13+
airbridge_app_name: { required: true, type: string } # webuddhistdev | webuddhist
14+
play_package_name: { required: true, type: string } # org.pecha.app[.dev]
15+
play_track: { required: false, type: string, default: internal }
16+
environment: { required: false, type: string, default: '' }
17+
upload_to_store: { required: false, type: boolean, default: true }
18+
19+
permissions:
20+
contents: read
21+
22+
jobs:
23+
android:
24+
name: Android • ${{ inputs.flavor }}
25+
runs-on: ubuntu-latest
26+
environment: ${{ inputs.environment }}
27+
timeout-minutes: 40
28+
steps:
29+
- name: 📂 Checkout
30+
uses: actions/checkout@v4
31+
32+
- name: ☕ Set up JDK 17
33+
uses: actions/setup-java@v4
34+
with:
35+
distribution: temurin
36+
java-version: '17'
37+
38+
- name: 🐦 Set up Flutter
39+
uses: subosito/flutter-action@v2
40+
with:
41+
channel: stable
42+
cache: true
43+
44+
- name: 📝 Create .env files
45+
env:
46+
DEV_BASE_API_URL: ${{ secrets.DEV_BASE_API_URL }}
47+
DEV_AI_URL: ${{ secrets.DEV_AI_URL }}
48+
DEV_AUTH0_AUDIENCE: ${{ secrets.DEV_AUTH0_AUDIENCE }}
49+
STAGING_BASE_API_URL: ${{ secrets.STAGING_BASE_API_URL }}
50+
STAGING_AI_URL: ${{ secrets.STAGING_AI_URL }}
51+
STAGING_AUTH0_AUDIENCE: ${{ secrets.STAGING_AUTH0_AUDIENCE }}
52+
BASE_API_URL: ${{ secrets.BASE_API_URL }}
53+
AI_URL: ${{ secrets.AI_URL }}
54+
AUTH0_AUDIENCE: ${{ secrets.AUTH0_AUDIENCE }}
55+
run: bash ci/scripts/create_env_files.sh
56+
57+
- name: 🔥 Inject google-services.json (only if provided)
58+
env:
59+
GOOGLE_SERVICES_JSON_BASE64: ${{ secrets.GOOGLE_SERVICES_JSON_BASE64 }}
60+
run: |
61+
if [ -n "$GOOGLE_SERVICES_JSON_BASE64" ]; then
62+
echo "$GOOGLE_SERVICES_JSON_BASE64" | base64 --decode > android/app/google-services.json
63+
echo "Injected android/app/google-services.json from secret"
64+
else
65+
echo "No GOOGLE_SERVICES_JSON_BASE64 secret — assuming the file is committed"
66+
fi
67+
68+
- name: 🔐 Decode Android keystore
69+
env:
70+
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
71+
run: |
72+
if [ -z "$ANDROID_KEYSTORE_BASE64" ]; then
73+
echo "::error::ANDROID_KEYSTORE_BASE64 is not set — release build cannot be signed."
74+
exit 1
75+
fi
76+
echo "$ANDROID_KEYSTORE_BASE64" | base64 --decode > "$RUNNER_TEMP/upload-keystore.jks"
77+
# build.gradle.kts treats a non-blank CM_KEYSTORE_PATH as "CI mode".
78+
echo "CM_KEYSTORE_PATH=$RUNNER_TEMP/upload-keystore.jks" >> "$GITHUB_ENV"
79+
80+
- name: 📦 flutter pub get
81+
run: flutter pub get
82+
83+
- name: 🏗️ Build App Bundle (${{ inputs.flavor }})
84+
env:
85+
AIRBRIDGE_APP_NAME: ${{ inputs.airbridge_app_name }}
86+
AIRBRIDGE_SDK_TOKEN: ${{ secrets.AIRBRIDGE_SDK_TOKEN }}
87+
CM_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
88+
CM_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
89+
CM_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
90+
run: |
91+
VERSION_NAME=$(grep '^version:' pubspec.yaml | sed 's/version: //' | cut -d'+' -f1)
92+
OFFSET="${{ vars.BUILD_NUMBER_OFFSET || '100' }}"
93+
BUILD_NUMBER=$(( ${{ github.run_number }} + OFFSET ))
94+
echo "Building $VERSION_NAME (+$BUILD_NUMBER)"
95+
flutter build appbundle --release \
96+
--flavor "${{ inputs.flavor }}" -t "${{ inputs.entrypoint }}" \
97+
--build-name "$VERSION_NAME" --build-number "$BUILD_NUMBER"
98+
99+
- name: ⬆️ Upload AAB artifact
100+
uses: actions/upload-artifact@v4
101+
with:
102+
name: android-${{ inputs.flavor }}
103+
path: |
104+
build/app/outputs/bundle/${{ inputs.flavor }}Release/*.aab
105+
build/app/outputs/mapping/${{ inputs.flavor }}Release/mapping.txt
106+
if-no-files-found: error
107+
retention-days: 14
108+
109+
- name: 🚀 Upload to Google Play (${{ inputs.play_track }})
110+
if: ${{ inputs.upload_to_store }}
111+
uses: r0adkll/upload-google-play@v1
112+
with:
113+
serviceAccountJsonPlainText: ${{ secrets.PLAY_SERVICE_ACCOUNT_JSON }}
114+
packageName: ${{ inputs.play_package_name }}
115+
releaseFiles: build/app/outputs/bundle/${{ inputs.flavor }}Release/*.aab
116+
track: ${{ inputs.play_track }}
117+
status: completed
118+
mappingFile: build/app/outputs/mapping/${{ inputs.flavor }}Release/mapping.txt

.github/workflows/build-ios.yml

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
name: 🍎 Build & Distribute iOS
2+
3+
# Reusable workflow. Called by release-dev.yml / release-prod.yml.
4+
# Builds a signed IPA for the requested flavor and (optionally) uploads it to
5+
# TestFlight via the App Store Connect API key.
6+
#
7+
# Signing model: distribution certificate imported from a base64 secret, the
8+
# flavor's provisioning profile installed from a base64 secret, and the Xcode
9+
# project switched to manual signing for the build. The "Generate Entitlements"
10+
# build phase in the Runner target reads AUTH0_DOMAIN + AIRBRIDGE_APP_NAME from
11+
# the environment, so both are exported before the build.
12+
13+
on:
14+
workflow_call:
15+
inputs:
16+
flavor: { required: true, type: string } # dev | staging | prod
17+
entrypoint: { required: true, type: string } # lib/main_<flavor>.dart
18+
airbridge_app_name: { required: true, type: string } # webuddhistdev | webuddhist
19+
auth0_domain: { required: true, type: string } # used by Generate Entitlements
20+
ios_bundle_id: { required: true, type: string } # org.pecha.app[.dev]
21+
environment: { required: false, type: string, default: '' }
22+
upload_to_store: { required: false, type: boolean, default: true }
23+
24+
permissions:
25+
contents: read
26+
27+
jobs:
28+
ios:
29+
name: iOS • ${{ inputs.flavor }}
30+
runs-on: macos-15
31+
environment: ${{ inputs.environment }}
32+
timeout-minutes: 75
33+
steps:
34+
- name: 📂 Checkout
35+
uses: actions/checkout@v4
36+
37+
- name: 🛠️ Select Xcode
38+
uses: maxim-lobanov/setup-xcode@v1
39+
with:
40+
xcode-version: latest-stable
41+
42+
- name: 🐦 Set up Flutter
43+
uses: subosito/flutter-action@v2
44+
with:
45+
channel: stable
46+
cache: true
47+
48+
- name: 📝 Create .env files
49+
env:
50+
DEV_BASE_API_URL: ${{ secrets.DEV_BASE_API_URL }}
51+
DEV_AI_URL: ${{ secrets.DEV_AI_URL }}
52+
DEV_AUTH0_AUDIENCE: ${{ secrets.DEV_AUTH0_AUDIENCE }}
53+
STAGING_BASE_API_URL: ${{ secrets.STAGING_BASE_API_URL }}
54+
STAGING_AI_URL: ${{ secrets.STAGING_AI_URL }}
55+
STAGING_AUTH0_AUDIENCE: ${{ secrets.STAGING_AUTH0_AUDIENCE }}
56+
BASE_API_URL: ${{ secrets.BASE_API_URL }}
57+
AI_URL: ${{ secrets.AI_URL }}
58+
AUTH0_AUDIENCE: ${{ secrets.AUTH0_AUDIENCE }}
59+
run: bash ci/scripts/create_env_files.sh
60+
61+
- name: 🔐 Create ios/Flutter/Secrets.xcconfig (Airbridge)
62+
env:
63+
AIRBRIDGE_SDK_TOKEN: ${{ secrets.AIRBRIDGE_SDK_TOKEN }}
64+
AIRBRIDGE_APP_NAME: ${{ inputs.airbridge_app_name }}
65+
run: bash ios/scripts/prepare_ci_secrets.sh
66+
67+
- name: 🔥 Inject GoogleService-Info.plist (only if provided)
68+
env:
69+
GOOGLE_SERVICE_INFO_PLIST_BASE64: ${{ secrets.GOOGLE_SERVICE_INFO_PLIST_BASE64 }}
70+
run: |
71+
if [ -n "$GOOGLE_SERVICE_INFO_PLIST_BASE64" ]; then
72+
echo "$GOOGLE_SERVICE_INFO_PLIST_BASE64" | base64 --decode > ios/Runner/GoogleService-Info.plist
73+
echo "Injected ios/Runner/GoogleService-Info.plist from secret"
74+
else
75+
echo "No secret — assuming GoogleService-Info.plist is committed"
76+
fi
77+
78+
- name: 🧩 Export entitlement env vars
79+
run: |
80+
# Consumed by the "Generate Entitlements" Xcode build phase.
81+
echo "AUTH0_DOMAIN=${{ inputs.auth0_domain }}" >> "$GITHUB_ENV"
82+
echo "AIRBRIDGE_APP_NAME=${{ inputs.airbridge_app_name }}" >> "$GITHUB_ENV"
83+
84+
- name: 📦 flutter pub get
85+
run: flutter pub get
86+
87+
- name: 🔥 Install FlutterFire CLI (Crashlytics dSYM upload phase)
88+
run: |
89+
dart pub global activate flutterfire_cli
90+
echo "$HOME/.pub-cache/bin" >> "$GITHUB_PATH"
91+
sudo ln -sf "$HOME/.pub-cache/bin/flutterfire" /usr/local/bin/flutterfire || true
92+
flutterfire --version || true
93+
94+
- name: 🍫 CocoaPods install
95+
run: |
96+
cd ios
97+
pod install --repo-update
98+
99+
- name: 🔑 Import distribution certificate
100+
uses: apple-actions/import-codesign-certs@v3
101+
with:
102+
p12-file-base64: ${{ secrets.IOS_DIST_CERT_BASE64 }}
103+
p12-password: ${{ secrets.IOS_DIST_CERT_PASSWORD }}
104+
105+
- name: 📄 Install provisioning profile
106+
id: profile
107+
env:
108+
PROFILE_DEV: ${{ secrets.IOS_PROVISION_PROFILE_DEV_BASE64 }}
109+
PROFILE_STAGING: ${{ secrets.IOS_PROVISION_PROFILE_STAGING_BASE64 }}
110+
PROFILE_PROD: ${{ secrets.IOS_PROVISION_PROFILE_PROD_BASE64 }}
111+
run: |
112+
case "${{ inputs.flavor }}" in
113+
dev) B64="$PROFILE_DEV" ;;
114+
staging) B64="$PROFILE_STAGING" ;;
115+
prod) B64="$PROFILE_PROD" ;;
116+
*) echo "::error::Unknown flavor"; exit 1 ;;
117+
esac
118+
if [ -z "$B64" ]; then
119+
echo "::error::No provisioning profile secret for flavor ${{ inputs.flavor }}"; exit 1
120+
fi
121+
PROFILES_DIR="$HOME/Library/MobileDevice/Provisioning Profiles"
122+
mkdir -p "$PROFILES_DIR"
123+
echo "$B64" | base64 --decode > "$RUNNER_TEMP/profile.mobileprovision"
124+
PLIST=$(security cms -D -i "$RUNNER_TEMP/profile.mobileprovision")
125+
UUID=$(/usr/libexec/PlistBuddy -c 'Print :UUID' /dev/stdin <<< "$PLIST")
126+
NAME=$(/usr/libexec/PlistBuddy -c 'Print :Name' /dev/stdin <<< "$PLIST")
127+
cp "$RUNNER_TEMP/profile.mobileprovision" "$PROFILES_DIR/$UUID.mobileprovision"
128+
echo "profile_name=$NAME" >> "$GITHUB_OUTPUT"
129+
echo "Installed profile '$NAME' ($UUID)"
130+
131+
- name: ✍️ Switch project to manual signing
132+
run: |
133+
fastlane run update_code_signing_settings \
134+
use_automatic_signing:false \
135+
path:"ios/Runner.xcodeproj" \
136+
team_id:"${{ secrets.IOS_TEAM_ID }}" \
137+
code_sign_identity:"Apple Distribution" \
138+
profile_name:"${{ steps.profile.outputs.profile_name }}" \
139+
targets:"Runner"
140+
141+
- name: 🧾 Generate ExportOptions.plist
142+
run: |
143+
cat > ios/ExportOptions.plist <<EOF
144+
<?xml version="1.0" encoding="UTF-8"?>
145+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
146+
<plist version="1.0">
147+
<dict>
148+
<key>method</key><string>app-store</string>
149+
<key>teamID</key><string>${{ secrets.IOS_TEAM_ID }}</string>
150+
<key>signingStyle</key><string>manual</string>
151+
<key>signingCertificate</key><string>Apple Distribution</string>
152+
<key>provisioningProfiles</key>
153+
<dict>
154+
<key>${{ inputs.ios_bundle_id }}</key>
155+
<string>${{ steps.profile.outputs.profile_name }}</string>
156+
</dict>
157+
<key>uploadSymbols</key><true/>
158+
<key>stripSwiftSymbols</key><true/>
159+
</dict>
160+
</plist>
161+
EOF
162+
163+
- name: 🏗️ Build IPA (${{ inputs.flavor }})
164+
env:
165+
AIRBRIDGE_SDK_TOKEN: ${{ secrets.AIRBRIDGE_SDK_TOKEN }}
166+
run: |
167+
VERSION_NAME=$(grep '^version:' pubspec.yaml | sed 's/version: //' | cut -d'+' -f1)
168+
OFFSET="${{ vars.BUILD_NUMBER_OFFSET || '100' }}"
169+
BUILD_NUMBER=$(( ${{ github.run_number }} + OFFSET ))
170+
echo "Building $VERSION_NAME (+$BUILD_NUMBER)"
171+
flutter build ipa --release \
172+
--flavor "${{ inputs.flavor }}" -t "${{ inputs.entrypoint }}" \
173+
--build-name "$VERSION_NAME" --build-number "$BUILD_NUMBER" \
174+
--export-options-plist=ios/ExportOptions.plist
175+
176+
- name: ⬆️ Upload IPA artifact
177+
uses: actions/upload-artifact@v4
178+
with:
179+
name: ios-${{ inputs.flavor }}
180+
path: build/ios/ipa/*.ipa
181+
if-no-files-found: error
182+
retention-days: 14
183+
184+
- name: 🛫 Upload to TestFlight
185+
if: ${{ inputs.upload_to_store }}
186+
env:
187+
ASC_KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_IDENTIFIER }}
188+
ASC_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
189+
ASC_KEY: ${{ secrets.APP_STORE_CONNECT_PRIVATE_KEY }}
190+
run: |
191+
KEY_B64=$(printf '%s' "$ASC_KEY" | base64 | tr -d '\n')
192+
cat > "$RUNNER_TEMP/asc_api_key.json" <<EOF
193+
{
194+
"key_id": "$ASC_KEY_ID",
195+
"issuer_id": "$ASC_ISSUER_ID",
196+
"key": "$KEY_B64",
197+
"is_key_content_base64": true,
198+
"in_house": false
199+
}
200+
EOF
201+
IPA=$(ls build/ios/ipa/*.ipa | head -n1)
202+
echo "Uploading $IPA to TestFlight"
203+
fastlane run upload_to_testflight \
204+
api_key_path:"$RUNNER_TEMP/asc_api_key.json" \
205+
ipa:"$IPA" \
206+
skip_waiting_for_build_processing:true

.github/workflows/ci.yml

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
name: ✅ CI
2+
3+
# Fast feedback on every PR into develop or main: format, static analysis, tests.
4+
# No signing, no store credentials — keeps PRs cheap and runnable from forks.
5+
6+
on:
7+
pull_request:
8+
branches: [ develop, main ]
9+
10+
permissions:
11+
contents: read
12+
13+
concurrency:
14+
group: ci-${{ github.ref }}
15+
cancel-in-progress: true
16+
17+
jobs:
18+
analyze-and-test:
19+
name: Analyze & Test
20+
runs-on: ubuntu-latest
21+
timeout-minutes: 30
22+
steps:
23+
- name: 📂 Checkout
24+
uses: actions/checkout@v4
25+
26+
- name: 🐦 Set up Flutter
27+
uses: subosito/flutter-action@v2
28+
with:
29+
channel: stable
30+
cache: true
31+
32+
- name: 📝 Create placeholder .env files
33+
run: |
34+
for e in dev staging prod; do
35+
printf 'BASE_API_URL=\nAI_URL=\nAUTH0_SCHEME=\nAUTH0_AUDIENCE=\nENVIRONMENT=%s\n' "$e" > ".env.$e"
36+
done
37+
38+
- name: 📦 flutter pub get
39+
run: flutter pub get
40+
41+
- name: 🎨 Verify formatting
42+
run: dart format --output=none --set-exit-if-changed .
43+
44+
- name: 🔍 Analyze
45+
run: flutter analyze
46+
47+
- name: 🧪 Test
48+
run: flutter test

0 commit comments

Comments
 (0)