ci: Android build + Play Store upload workflow - #178
Conversation
Android twin of build-ios-from-expo.yml. Composes mieweb/actions building blocks (pinned to v2.3.0): stamp versionCode, expo prebuild, build-sign-android (direct-keystore), publish-android-to-play. Closes #52
There was a problem hiding this comment.
Pull request overview
Adds a manually triggered Android Expo workflow to build, sign, archive, and optionally publish an AAB to Google Play.
Changes:
- Sets up JDK, Node, dependencies, and Android prebuild.
- Stamps
versionCodeand signs the bundle using secret-based credentials. - Uploads artifacts and supports configurable Play Store tracks.
Suppressed comments (1)
.github/workflows/build-android-from-expo.yml:72
GITHUB_RUN_NUMBERdoes not change when a workflow run is re-run. Re-running an attempt that already reached Play therefore computes the sameversionCodeand Play rejects the bundle as a duplicate; this can also happen after an upload succeeds but the client loses the response. Allocate a version code per upload attempt (or persist/query the next Play version) instead of using only the run number.
node -e "const fs=require('fs');const j=JSON.parse(fs.readFileSync('app.json','utf8'));j.expo.android=j.expo.android||{};j.expo.android.versionCode=(j.expo.android.versionCode||0)+Number(process.env.GITHUB_RUN_NUMBER);fs.writeFileSync('app.json',JSON.stringify(j,null,2));console.log('Android versionCode =',j.expo.android.versionCode);"
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Overlapping workflow_dispatch runs could publish out of order, carrying a lower versionCode than an already-published later run.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.
Suppressed comments (2)
Previously missed (1) — in code that hasn't changed since the last review.
.github/workflows/build-android-from-expo.yml:77
- GitHub reruns keep the same
GITHUB_RUN_NUMBER(onlyGITHUB_RUN_ATTEMPTchanges), so rerunning an attempt that already reached Play uploads the identicalversionCodeand Google Play rejects it as already used. That prevents recovering from an upload that succeeded remotely but returned an error; incorporate the attempt into a collision-free allocation or persist/query the next Play versionCode before stamping.
node -e "const fs=require('fs');const j=JSON.parse(fs.readFileSync('app.json','utf8'));j.expo.android=j.expo.android||{};j.expo.android.versionCode=(j.expo.android.versionCode||0)+Number(process.env.GITHUB_RUN_NUMBER);fs.writeFileSync('app.json',JSON.stringify(j,null,2));console.log('Android versionCode =',j.expo.android.versionCode);"
.github/workflows/build-android-from-expo.yml:50
- This concurrency group does not provide FIFO ordering: GitHub allows only one pending run and may cancel/replace older pending runs, and scheduling order is not guaranteed. Two dispatches can therefore publish the higher
GITHUB_RUN_NUMBERfirst and then fail the lower-numbered bundle as a Play version downgrade (or silently lose the earlier requested run). Because the version code depends on this ordering, use an explicit FIFO/version-reservation mechanism rather than relying on this group alone.
concurrency:
group: ${{ github.workflow }}-android
cancel-in-progress: false
Verified the pipeline end-to-end locally with a throwaway keystore: versionCode stamp, expo prebuild, gradle bundleRelease with injected signing, and jarsigner verification all pass. - expo prebuild does not support --non-interactive (CI=1 covers it) - the Expo template's -Xmx2048m can OOM the Gradle daemon during the R8 release build; bump to 4g after prebuild regenerates the file
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
.github/workflows/build-android-from-expo.yml:77
GITHUB_RUN_NUMBERis reused when a workflow run is re-run. If an initial attempt has already been accepted by Google Play but fails or times out afterward, re-running this job stamps the sameversionCode, so Play rejects the retry as an already-used version code; this also breaks the uniqueness claim in the comment. Use a retry-safe unique allocation (for example, a bounded run ID scheme or a persisted counter) rather thanGITHUB_RUN_NUMBERalone.
node -e "const fs=require('fs');const j=JSON.parse(fs.readFileSync('app.json','utf8'));j.expo.android=j.expo.android||{};j.expo.android.versionCode=(j.expo.android.versionCode||0)+Number(process.env.GITHUB_RUN_NUMBER);fs.writeFileSync('app.json',JSON.stringify(j,null,2));console.log('Android versionCode =',j.expo.android.versionCode);"
Closes #52.
Adds
.github/workflows/build-android-from-expo.yml, the Android twin ofbuild-ios-from-expo.yml. It composes mieweb/actions building blocks pinned to v2.3.0 by SHA, mirroring the iOS workflow's pattern: secrets-based signing, no hardcoded credentials, no fastlane match.Pipeline
npm ci(ubuntu runner; the Android SDK is preinstalled)versionCode(app.jsonbase + run number), mirroring the iOSbuildNumberstampexpo prebuild --clean --platform androidto regenerateandroid/mieweb/actions/build-sign-android(direct-keystore mode) — builds and signs the release .aab via Gradle injected signing properties, verifies the signaturemieweb/actions/publish-android-to-play— publishes to the selected Google Play track (optional, on by default)Triggered via
workflow_dispatchwith anupload_to_playtoggle and a track picker (internal / alpha / beta / production, default internal).Required secrets (setup steps)
Five new repo secrets are needed before this workflow can run. None exist yet at repo or org level.
1. Upload keystore
The original upload keystore is not available, so a new one must be generated and registered via Play App Signing's upload-key reset.
Then in Play Console (requires account owner/admin):
upload_certificate.pem2. Play service account (for CI uploads)
3. Add the secrets
ANDROID_KEYSTORE_BASE64base64 -i upload.jksANDROID_KEYSTORE_PASSWORDANDROID_KEY_ALIASupload(or the alias used)ANDROID_KEY_PASSWORDPLAY_JSON_KEY_BASE64base64 -i service-account.json4. versionCode floor
The workflow stamps
versionCode = app.json base (20000) + run number. Check the highest existingversionCodein Play Console and bump the base inapp.jsonif any prior upload exceeds it.Verification
build-sign-android's Gradle auto-discovery requirements (gradlew+app/module +settings.gradle) matchexpo prebuildoutput for this projectapplicationId com.mieweb.pulseandversionCodeflow fromapp.jsonintoandroid/app/build.gradlevia prebuild360fec8)