1010 options :
1111 - testflight-android
1212 - production
13+ workflow_call :
14+ inputs :
15+ profile :
16+ type : string
17+ description : Build profile to use
18+ required : true
19+ outputs :
20+ package-version :
21+ description : Version from package.json
22+ value : ${{ jobs.build.outputs.package-version }}
23+ version-code :
24+ description : Android version code
25+ value : ${{ jobs.build.outputs.version-code }}
1326
1427# Deploys happen via EAS using EXPO_TOKEN; the GITHUB_TOKEN only checks out code
1528permissions :
2336 concurrency :
2437 group : android-build
2538 cancel-in-progress : false
39+ outputs :
40+ package-version : ${{ steps.get-build-info.outputs.PACKAGE_VERSION }}
41+ version-code : ${{ steps.get-build-info.outputs.BSKY_ANDROID_VERSION_CODE }}
42+ apk-artifact-name : build-${{ steps.timestamp.outputs.time }}.apk
2643 steps :
2744 - name : Check for EXPO_TOKEN
2845 run : >
6683 run : pnpm intl:build 2>&1 | tee i18n.log
6784
6885 - name : Check for i18n compilation errors
69- run : if grep -q "invalid syntax" "i18n.log"; then echo "\n\nFound compilation errors!\n\n" && exit 1; else echo "\n\nNo compilation errors!\n\n"; fi
86+ run : if grep -q "invalid syntax" "i18n.log"; then echo "\n\nFound compilation
87+ errors!\n\n" && exit 1; else echo "\n\nNo compilation errors!\n\n"; fi
7088
7189 # EXPO_PUBLIC_ENV is handled in eas.json
7290 - name : Env
@@ -96,93 +114,72 @@ jobs:
96114 --profile $PROFILE
97115 --local --output build.aab --non-interactive
98116
99- - name : ✍️ Rename Testflight bundle
100- if : ${{ inputs.profile != 'production' }}
101- run : mv build.aab build.apk
102-
103- - name : ⏰ Get a timestamp
104- id : timestamp
105- run : echo "time=$(date -u +'%m-%d-%H-%M-%S')" >> "$GITHUB_OUTPUT"
106-
107- - name : 🚀 Upload Production Artifact
108- id : upload-artifact-production
109- if : ${{ inputs.profile == 'production' }}
110- uses : actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
111- with :
112- retention-days : 30
113- compression-level : 6
114- name : build-${{ steps.timestamp.outputs.time }}.aab
115- path : build.aab
116-
117- - name : 🚀 Upload Testflight Artifact
118- id : upload-artifact-testflight
119- if : ${{ inputs.profile != 'production' }}
120- uses : actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
121- with :
122- retention-days : 30
123- compression-level : 6
124- name : build-${{ steps.timestamp.outputs.time }}.apk
125- path : build.apk
126-
127117 - name : 📚 Get version from package.json
128118 id : get-build-info
129119 run : bash scripts/setGitHubOutput.sh
130120
131- - name : 🔔 Notify Slack of Production Build
121+ - name : 🚀 Submit to Google Play
122+ env :
123+ PROFILE : ${{ inputs.profile || 'testflight-android' }}
124+ run : pnpm eas submit -p android --profile $PROFILE --non-interactive --path build.aab
125+
126+ - name : 🔔 Notify Slack of Play Store Submission
132127 if : ${{ inputs.profile == 'production' }}
133128 uses : slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3
134129 with :
135130 webhook : ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
136131 webhook-type : incoming-webhook
137132 payload-templated : true
138133 payload : |
139- {"text": "Android production build for Google Play Store submission is ready !\n```Artifact: ${{ steps.upload-artifact-production.outputs.artifact-url }}\nVersion Number: ${{ steps.get-build-info.outputs.PACKAGE_VERSION }}\nBuild Number: ${{ steps.get-build-info.outputs.BSKY_ANDROID_VERSION_CODE }}```"}
134+ {"text": "Android ${{ inputs.profile || 'testflight-android' }} build submitted to Google Play !\n```Version Number: ${{ steps.get-build-info.outputs.PACKAGE_VERSION }}\nBuild Number: ${{ steps.get-build-info.outputs.BSKY_ANDROID_VERSION_CODE }}```"}
140135
141- - name : 🔔 Notify Slack of Testflight Build
142- if : ${{ inputs.profile != 'production' }}
143- uses : slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3
136+ - name : 🔧 Setup bundletool
137+ uses : amyu/setup-bundletool@cc2e1857284660bd625e43f2c8a45626f034302f # v1.1
144138 with :
145- webhook : ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
146- webhook-type : incoming-webhook
147- payload-templated : true
148- payload : |
149- {"text": "Android build is ready for testing. Download the artifact here: ${{ steps.upload-artifact-testflight.outputs.artifact-url }}"}
139+ bundletool-version : " 1.17.2"
150140
151- - name : 🧹 Clear Metro cache
152- if : ${{ inputs.profile == 'production' }}
153- # https://github.com/expo/eas-cli/issues/2959#issuecomment-2749791326
154- run : rm -rf ${TMPDIR:-/tmp}/metro-cache
141+ - name : 🔑 Decode keystore
142+ run : echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 --decode >
143+ keystore.jks
155144
156- - name : 🏗️ Build Production APK
157- if : ${{ inputs.profile == 'production' }}
158- run : >
159- SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
160- SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }}
161- SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }}
162- pnpm use-build-number-with-bump
163- pnpm eas build -p android
164- --profile production-apk
165- --local --output build.apk --non-interactive
145+ - name : 📦 Build signed universal APK
146+ run : |
147+ bundletool build-apks \
148+ --bundle=build.aab \
149+ --output=universal.apks \
150+ --mode=universal \
151+ --ks=keystore.jks \
152+ --ks-pass=pass:${{ secrets.ANDROID_KEYSTORE_PASSWORD }} \
153+ --ks-key-alias=${{ secrets.ANDROID_KEY_ALIAS }} \
154+ --key-pass=pass:${{ secrets.ANDROID_KEY_PASSWORD }}
166155
167- - name : 🚀 Upload Production APK Artifact
168- id : upload-artifact-production-apk
169- if : ${{ inputs.profile == 'production' }}
156+ - name : 📋 Rename to .zip for extraction
157+ run : mv universal.apks universal.zip
158+
159+ - name : 📦 Extract universal APK
160+ run : unzip -p universal.zip universal.apk > build.apk
161+
162+ - name : ⏰ Get a timestamp
163+ id : timestamp
164+ run : echo "time=$(date -u +'%m-%d-%H-%M-%S')" >> "$GITHUB_OUTPUT"
165+
166+ - name : 🚀 Upload APK Artifact
167+ id : upload-artifact
170168 uses : actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
171169 with :
172170 retention-days : 30
173171 compression-level : 6
174172 name : build-${{ steps.timestamp.outputs.time }}.apk
175173 path : build.apk
176174
177- - name : 🔔 Notify Slack of Production APK Build
178- if : ${{ inputs.profile == 'production' }}
175+ - name : 🔔 Notify Slack of APK Artifact
179176 uses : slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3
180177 with :
181178 webhook : ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
182179 webhook-type : incoming-webhook
183180 payload-templated : true
184181 payload : |
185- {"text": "Android production build for GitHub/Obtanium is ready!\n```Artifact: ${{ steps.upload-artifact-production-apk .outputs.artifact-url }}\nVersion Number: ${{ steps.get-build-info.outputs.PACKAGE_VERSION }}\nBuild Number: ${{ steps.get-build-info.outputs.BSKY_ANDROID_VERSION_CODE }}```"}
182+ {"text": "Android ${{ inputs.profile || 'testflight-android' }} APK is ready for testing !\n```Artifact: ${{ steps.upload-artifact.outputs.artifact-url }}\nVersion Number: ${{ steps.get-build-info.outputs.PACKAGE_VERSION }}\nBuild Number: ${{ steps.get-build-info.outputs.BSKY_ANDROID_VERSION_CODE }}```"}
186183
187184 - name : ⬇️ Restore Cache
188185 id : get-base-commit
@@ -197,3 +194,62 @@ jobs:
197194 env :
198195 GITHUB_SHA : ${{ github.sha }}
199196 run : echo $GITHUB_SHA > most-recent-testflight-commit.txt
197+
198+ # Releases are cut from tags named after the version (e.g. "1.124.0"), so when a production
199+ # build is dispatched against such a tag we attach the APK to the matching release. This runs
200+ # as a separate job so that `contents: write` is isolated here and the build job stays read-only.
201+ attachToRelease :
202+ name : Attach APK to GitHub Release
203+ runs-on : ubuntu-latest
204+ needs : [build]
205+ if : ${{ inputs.profile == 'production' && github.ref_type == 'tag' && github.repository == 'bluesky-social/social-app' }}
206+ permissions :
207+ contents : write
208+ steps :
209+ # We only attach to a release that already exists — never create one.
210+ - name : 🔎 Check for matching GitHub Release
211+ id : release-check
212+ env :
213+ GH_TOKEN : ${{ github.token }}
214+ TAG : ${{ github.ref_name }}
215+ run : |
216+ status=$(curl -sS -o /dev/null -w '%{http_code}' \
217+ -H "Authorization: Bearer $GH_TOKEN" \
218+ -H "Accept: application/vnd.github+json" \
219+ "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases/tags/${TAG}")
220+ if [ "$status" = "200" ]; then
221+ echo "Found GitHub Release for tag $TAG"
222+ echo "exists=true" >> "$GITHUB_OUTPUT"
223+ else
224+ echo "No GitHub Release found for tag $TAG (HTTP $status); skipping APK attachment."
225+ echo "exists=false" >> "$GITHUB_OUTPUT"
226+ fi
227+
228+ - name : ⬇️ Download APK artifact
229+ if : ${{ steps.release-check.outputs.exists == 'true' }}
230+ uses : actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
231+ with :
232+ name : ${{ needs.build.outputs.apk-artifact-name }}
233+
234+ - name : 🏷️ Rename APK for release
235+ if : ${{ steps.release-check.outputs.exists == 'true' }}
236+ run : cp build.apk "Bluesky-${{ needs.build.outputs.package-version }}.apk"
237+
238+ - name : 📎 Attach APK to GitHub Release
239+ id : attach
240+ if : ${{ steps.release-check.outputs.exists == 'true' }}
241+ uses : softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
242+ with :
243+ tag_name : ${{ github.ref_name }}
244+ files : Bluesky-${{ needs.build.outputs.package-version }}.apk
245+ fail_on_unmatched_files : true
246+
247+ - name : 🔔 Notify Slack of Release Attachment
248+ if : ${{ steps.release-check.outputs.exists == 'true' }}
249+ uses : slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3
250+ with :
251+ webhook : ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
252+ webhook-type : incoming-webhook
253+ payload-templated : true
254+ payload : |
255+ {"text": "Android APK attached to GitHub Release ${{ github.ref_name }}!\n```Asset: Bluesky-${{ needs.build.outputs.package-version }}.apk\nRelease: ${{ steps.attach.outputs.url }}```"}
0 commit comments