# Releasing ## One-time setup ### 1. Generate an upload key Google holds the real app signing key under Play App Signing and re-signs every release. What you generate here is only the *upload* key — if it's lost, Play support can reset it, which is not true of the app signing key. ```bash keytool -genkeypair -v \ -keystore upload-keystore.jks \ -alias upload \ -keyalg RSA -keysize 2048 -validity 10000 ``` Then create `keystore.properties` in the repo root: ```properties storeFile=upload-keystore.jks storePassword= keyAlias=upload keyPassword= ``` Both files are gitignored. Keep a backup somewhere other than this machine — without them you can still recover, but only by going through Play support. Release builds without `keystore.properties` are left unsigned rather than failing, so a fresh clone and CI can still compile the release variant. ### 2. Play Console - Create the app. The package name **`social.grain` is permanent** once the first build is uploaded and cannot be changed afterwards. - Under **Setup → App signing**, opt into Play App Signing (the default for new apps) and let Google generate the app signing key. - Fill in the **Data safety** form. The app collects the account handle and DID, the photos a user chooses to publish, and their EXIF when the poster leaves camera data enabled. All of it goes to the user's own PDS via grain.social. It requests `INTERNET` and, for the story composer, `CAMERA`; photos taken there are only ever sent when the user posts them. - Content rating questionnaire, target audience, and a privacy policy URL are all required before any track can go live. Note for personal developer accounts created after 13 Nov 2023: production is locked until a closed test has run with at least 12 testers opted in for 14 continuous days. Check **Testing → Closed testing** — a requirement card there means the rule applies. ## Cutting a release Bump `versionCode` and `versionName` in `app/build.gradle.kts`. `versionCode` must increase with every upload, even for a build that never ships. ```bash ./gradlew bundleRelease ``` The bundle lands at `app/build/outputs/bundle/release/app-release.aab`. Upload that, not the APK — Play requires the App Bundle format. To sanity-check a release build on a device first, the APK variant can be signed with the debug key and installed locally: ```bash ./gradlew assembleRelease zipalign -f 4 app/build/outputs/apk/release/app-release-unsigned.apk /tmp/aligned.apk apksigner sign --ks ~/.android/debug.keystore --ks-pass pass:android \ --ks-key-alias androiddebugkey --key-pass pass:android \ --out /tmp/release-test.apk /tmp/aligned.apk adb install -r /tmp/release-test.apk ``` This is worth doing at least once per dependency bump. R8 strips kotlinx.serialization's generated `$$serializer` classes without the keep rules in `proguard-rules.pro`, and the failure is release-only: every network decode throws and the feed simply comes back empty. ## Store assets - Icon: `store/play-store-icon-512.png` (regenerate with `python3 tools/generate_icons.py`). - Feature graphic: `store/play-feature-graphic-1024x500.png` (regenerate with `python3 tools/generate_feature_graphic.py`). - Screenshots: `store/play-screenshot-*.png`. Capture with `adb exec-out screencap -p > shot.png`, then fit them to Play's rules with `python3 tools/prepare_screenshots.py shot.png --out store/` — a raw capture is 2.06:1, which is past the 2:1 Play accepts, and the script pads each to 1080x1920 rather than cropping the app's own chrome away. Sign in on an emulator rather than using a phone: a locked screen returns a black `screencap` and blocks instrumented tests. Use your own galleries — the feed shows other people's photographs, which aren't yours to put in a store listing.