Two things the v0.4.15 cut got wrong were things this doc told it to do. - PUBLISHING.md step 1 said not to bother hand-bumping `android.versionCode`. That is true for Play (CI overrides it with run_number+100) and false for the two channels that carry most of the install base: F-Droid and direct APK key upgrades off versionCode, so reusing the previous one means the release is never offered to anyone who already has that code. Step 1 now lists all four places to bump plus the changelog named after the code, and step 5 adds the two channel checks (GitHub release — the source the in-app update check polls — and the F-Droid index) that were previously implied to be unnecessary. - docs/playstore.md release history: v0.4.15 at production versionCode 153, the first release to reach production from a tag push alone (#177), plus the superseded 152 from the pre-#180 tag. Co-authored-by: engineer <engineer@gray-knight-m1.local> Co-authored-by: Paperclip <noreply@paperclip.ing>
8.5 KiB
Publishing to Google Play Store
Required GitHub Secrets
Configure these in Settings > Secrets and variables > Actions:
| Secret | Description |
|---|---|
PLAY_STORE_SERVICE_ACCOUNT_JSON |
Google Play Console service account JSON key (full JSON content) |
KEYSTORE_BASE64 |
Base64-encoded release keystore (base64 -w0 release.keystore) |
KEYSTORE_PASSWORD |
Keystore password |
KEY_ALIAS |
Key alias in the keystore |
KEY_PASSWORD |
Key password |
Setup Steps
1. Create a release keystore
keytool -genkeypair -v -storetype PKCS12 \
-keystore release.keystore -alias release \
-keyalg RSA -keysize 2048 -validity 10000
Encode it for GitHub secrets:
base64 -w0 release.keystore
2. Create a Google Play service account
- Go to Google Cloud Console > IAM > Service Accounts
- Create a service account and download the JSON key
- In Google Play Console > Settings > API access, link the service account
- Grant it release management permissions for your app
3. Workflow triggers
The publish workflow runs on:
- GitHub Release publish events
- Tag pushes matching
v*
It builds an AAB (Android App Bundle), signs it with the release keystore, and uploads to the production track (status: completed). Manual workflow_dispatch runs still honour the track/status inputs and default to internal.
Releasing (proven runbook)
- Bump the version in four places, then run
npm run check:versionsbefore you push:package.jsonversion,app.jsonexpo.version, andandroid/app/build.gradleversionName— plusversionCodein bothapp.json(expo.android.versionCode) andbuild.gradle, incremented by one.- Play does not need the hand-bumped
versionCode(the publish workflow overrides it withgithub.run_number + 100, so the Play code is e.g.152, unrelated to theapp.jsonnumber). F-Droid and direct-APK installs do. They key upgrades offversionCode, so a release that reuses the previous one is silently never offered to anyone who already has that code installed. v0.4.15 shipped with v0.4.14'sversionCode 41for exactly this reason (fixed in #180) — the F-Droid publish failed outright and the GitHub release never got created.
- Play does not need the hand-bumped
- Add the changelog for the new versionCode in both
distribution/changelogs/<versionCode>.txtandfastlane/metadata/android/en-US/changelogs/<versionCode>.txt(F-Droid reads the fastlane copy), and update the Play release notes indistribution/whatsnew/whatsnew-en-US(single file, applied to the build being uploaded; max 500 chars).whatsnew— not the fastlanechangelogs/*.txt— is what the Play publish uses (whatsNewDirectoryin the workflow).check:versionsenforces that the changelog named after the versionCode describes the version you are releasing. Merge tomain. - Tag the release:
git tag -a vX.Y.Z <sha> -m "..." && git push origin vX.Y.Z. This triggers the publish workflow → production track,status: completed. - Verify the publish run is green, and read the run summary — it records the event, resolved track/status, and the real Play
versionCode(run_number+100, not theapp.jsonnumber). - Check the other two channels, because Play is only one of three and the smaller share of the install base: the GitHub release exists with both APKs (
build.yml'sreleasejob — this is what the in-app update check polls viareleases/latest), and the F-Droid index lists the new version (https://dzianisv.github.io/opencode-mobile/fdroid/repo/index-v1.json). - Nothing else to do. There is no second promotion step.
Promoting to production
CI does this for you on every release tag (AGE-110). The service account does hold "Release to production" — verified by run 31807432647 (Validating tracks: 'production' → committed edit 02632873494323676310, 2026-08-14 14:22 UTC), so the older "internal only" note here was stale.
Manual paths, still available when you need them:
- Recommended — Play Console: Production → Create release → Add from library → select the build by its versionName (e.g.
0.4.10) and confirm itsversionCode(the run_number-derived one, e.g.142— not theapp.jsonnumber) → review → roll out. If the "What's new" field is empty, paste fromdistribution/whatsnew/whatsnew-en-US. No rebuild. - Fully automated (optional):
workflow_dispatchwithtrack=production,status=completed— same thing the tag push does, useful for re-shippingmainwithout cutting a tag.
Resubmitting after a Data Safety rejection (issue #143)
Google Play rejected cc.agentlabs.opencode on 2026-07-22 because the app's Data Safety
declaration did not disclose collection of Email Address. Root cause: the "OpenCode
Connect" waitlist card on the Connect screen (app/connection/add.tsx →
src/lib/waitlist.ts → POST https://opencode.agentlabs.cc/api/beta-signup) collects an
email address when a user opts in, and the backend forwards it to Brevo (email
marketing/CRM). This was true collection that the Data Safety form did not declare — Play
requires all personal-info collection to be declared, even when it's optional and
unrelated to the app's core function.
The repo-side declaration is now fixed (this PR): distribution/play-listing.md Data Safety
table, distribution/privacy-policy.md/.html, and docs/privacy/index.html all disclose
the email collection. To resubmit:
- Play Console → your app → App content → Data safety → Manage.
- Under Data types → Personal info, check Email address.
- Is this data collected, shared, or both? → Collected and shared.
- Is this data processed ephemerally? → No.
- Is data collection required for your app, or can users choose whether this data is collected? → Users can choose whether this data is collected (optional — only collected if the user opts into the waitlist).
- Why is this user data collected? → check Account management (the waitlist is a signup for the not-yet-launched hosted service). Optionally also check App functionality if Console requires at least one additional purpose.
- Under sharing: declare it is shared with a third party (Brevo) for the same purpose.
- Re-verify the existing declared types are still accurate (unchanged by this fix):
App activity (PostHog analytics), App info and performance / Crash logs (Sentry),
and Diagnostics — user-submitted reports (Chatwoot) — all opt-in, default OFF, shared
with the named third parties. See the full table in
distribution/play-listing.md→ "Data safety form". - Confirm the Privacy policy URL field still points at
https://dzianisv.github.io/opencode-mobile/privacy/(now updated with the email disclosure —docs/privacy/index.html, mirrored fromdistribution/privacy-policy.md). - Save, then Send for review (Play re-reviews Data Safety changes; this is separate from a binary/release review since no code changed).
- Once Data Safety is approved, resume any blocked release rollout (e.g. 0.4.12 and pending retention fixes) — those builds themselves did not need to change, only the Console-side declaration.
Known earlier blocker (if it resurfaces): a prior release (v0.4.5) was blocked by Google
with a "Missing sign-in details" rejection under App access, not Data Safety — Play
reviewers could not exercise the app because it requires the user's own opencode server and
Play had no way to sign in / connect one. That was resolved (see HANDOFF.md) by providing
reviewer instructions plus a temporary demo server URL in the App access form (see the
reviewer-instructions block in distribution/play-listing.md → "App access"). If a future
review flags "sign-in details" again, the fix is the same: confirm App access still has
either working temporary credentials/demo server or an accurate "all functionality available
without sign-in" declaration — this is unrelated to the Data Safety fix in this PR, but is
the other known rejection mode for this app and worth checking in the same Console pass.
Fastlane (Alternative)
A Fastlane setup is included for local publishing:
bundle install
bundle exec fastlane android deploy
Set environment variables: SUPPLY_JSON_KEY, RELEASE_STORE_FILE, RELEASE_STORE_PASSWORD, RELEASE_KEY_ALIAS, RELEASE_KEY_PASSWORD.