Skip to content

ci: Android build + Play Store upload workflow - #178

Merged
morepriyam merged 3 commits into
mainfrom
ci/android-play-workflow
Aug 28, 2026
Merged

ci: Android build + Play Store upload workflow#178
morepriyam merged 3 commits into
mainfrom
ci/android-play-workflow

Conversation

@morepriyam

Copy link
Copy Markdown
Collaborator

Closes #52.

Adds .github/workflows/build-android-from-expo.yml, the Android twin of build-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

  1. Checkout + JDK 17 + Node 20 + npm ci (ubuntu runner; the Android SDK is preinstalled)
  2. Stamp a monotonically increasing versionCode (app.json base + run number), mirroring the iOS buildNumber stamp
  3. expo prebuild --clean --platform android to regenerate android/
  4. mieweb/actions/build-sign-android (direct-keystore mode) — builds and signs the release .aab via Gradle injected signing properties, verifies the signature
  5. Uploads the signed .aab as a run artifact
  6. mieweb/actions/publish-android-to-play — publishes to the selected Google Play track (optional, on by default)

Triggered via workflow_dispatch with an upload_to_play toggle 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.

# Generate the keystore (keep it backed up — losing it means another reset)
keytool -genkeypair -v -keystore upload.jks -alias upload \
  -keyalg RSA -keysize 2048 -validity 10000

# Export the certificate needed for the reset request
keytool -export -rfc -keystore upload.jks -alias upload \
  -file upload_certificate.pem

Then in Play Console (requires account owner/admin):

  1. Pulse → Test and release → Setup → App integrity → App signing
  2. Confirm Play App Signing is enabled
  3. Request upload key reset → reason: lost keystore → upload upload_certificate.pem
  4. Wait for Google's confirmation email (typically within ~48 h). Uploads signed with the new key are rejected until the reset is processed.

Before resetting: check whether anyone still has the original upload.jks — a reset permanently invalidates the old key.

2. Play service account (for CI uploads)

  1. Google Cloud Console → IAM & Admin → Service Accounts → create one (no GCP roles needed)
  2. Keys → Add key → JSON → download the key file
  3. Play Console → Users and permissions → Invite new users → paste the service account email → grant app access to Pulse with release permissions (at minimum "Release to testing tracks" for the internal track)

3. Add the secrets

Secret Value
ANDROID_KEYSTORE_BASE64 base64 -i upload.jks
ANDROID_KEYSTORE_PASSWORD keystore password chosen in step 1
ANDROID_KEY_ALIAS upload (or the alias used)
ANDROID_KEY_PASSWORD key password (same as keystore password if Enter was pressed at that prompt)
PLAY_JSON_KEY_BASE64 base64 -i service-account.json

4. versionCode floor

The workflow stamps versionCode = app.json base (20000) + run number. Check the highest existing versionCode in Play Console and bump the base in app.json if any prior upload exceeds it.

Verification

  • YAML lint passes
  • build-sign-android's Gradle auto-discovery requirements (gradlew + app/ module + settings.gradle) match expo prebuild output for this project
  • applicationId com.mieweb.pulse and versionCode flow from app.json into android/app/build.gradle via prebuild
  • All action inputs/outputs used here verified against the v2.3.0 tag (360fec8)

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
Copilot AI lite review requested due to automatic review settings August 21, 2026 15:01

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 versionCode and 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_NUMBER does not change when a workflow run is re-run. Re-running an attempt that already reached Play therefore computes the same versionCode and 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.

Comment thread .github/workflows/build-android-from-expo.yml
Overlapping workflow_dispatch runs could publish out of order,
carrying a lower versionCode than an already-published later run.
Copilot AI review requested due to automatic review settings August 21, 2026 15:10

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 (only GITHUB_RUN_ATTEMPT changes), so rerunning an attempt that already reached Play uploads the identical versionCode and 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_NUMBER first 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
Copilot AI review requested due to automatic review settings August 21, 2026 15:32

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_NUMBER is 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 same versionCode, 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 than GITHUB_RUN_NUMBER alone.
          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);"

@morepriyam
morepriyam merged commit 13fbb92 into main Aug 28, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ci: Android build + Play Store Internal Testing deploy workflow

2 participants