From 97e5a2d196bdcfafecefda7029eaa852092f8320 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sat, 19 Sep 2026 20:27:02 -0700 Subject: [PATCH] Ship the watch app to Google Play's Wear OS tracks (#951) The Android lanes build and upload the wear bundle after the phone's, and prepareForRelease writes release notes under the wear version code too. --- build.gradle.kts | 12 ++++++---- docs/WEAR-OS.md | 23 +++++++++++------- fastlane/Fastfile | 59 ++++++++++++++++++++++++++++++++--------------- 3 files changed, 63 insertions(+), 31 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index e225e21a4..8f7e61804 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -11,6 +11,7 @@ import com.darkrockstudios.build.storeNotesLength import com.darkrockstudios.build.updateFlatpakFiles import com.darkrockstudios.build.updateIosShortVersion import com.darkrockstudios.build.updateSnapcraftYaml +import com.darkrockstudios.build.wearVersionCode import com.darkrockstudios.build.writeBakedChangelog import com.darkrockstudios.build.writeChangelogMarkdown import com.darkrockstudios.build.writeSemvar @@ -198,10 +199,13 @@ tasks.register("prepareForRelease") { val changelogsPath = "fastlane/metadata/android/en-US/changelogs".replace("/", File.separator) val changeLogsDir = rootDir.resolve(changelogsPath) - val changeLogFile = File(changeLogsDir, "$versionCode.txt") + // The watch app ships in the same listing under its own version code. + val changeLogFiles = listOf(versionCode, wearVersionCode(versionCode)).map { File(changeLogsDir, "$it.txt") } if (writeStoreNotes) { - changeLogFile.writeText(playChangelog) - println("Changelog for version ${releaseInfo.semVar} written to $changelogsPath/$versionCode.txt") + changeLogFiles.forEach { file -> + file.writeText(playChangelog) + println("Changelog for version ${releaseInfo.semVar} written to $changelogsPath/${file.name}") + } if (playNotesLength > PLAY_STORE_LIMIT) { println(" Google Play notes truncated to $PLAY_STORE_LIMIT characters; full text at $fullNotesUrl") } @@ -285,7 +289,7 @@ tasks.register("prepareForRelease") { // Commit the changes to the repo if (writeStoreNotes) { - git("add", changeLogFile.absolutePath) + changeLogFiles.forEach { git("add", it.absolutePath) } git("add", macReleaseNotesFile.absolutePath) git("add", iosReleaseNotesFile.absolutePath) git("add", flatpakManifestFile.absolutePath) diff --git a/docs/WEAR-OS.md b/docs/WEAR-OS.md index f51295b22..4449c42cf 100644 --- a/docs/WEAR-OS.md +++ b/docs/WEAR-OS.md @@ -262,11 +262,20 @@ Phase-gated behind the rest, but designed in now because it depends on synced co ## Packaging and release -- `versionName` comes from `libs.versions.toml` like every other target; `prepareForRelease` - needs no changes. -- `fastlane/Fastfile` gains wear lanes mirroring the phone lanes, and - `publish-google-play.yml` uploads the wear bundle alongside the phone bundle on the same - track. +- `versionName` comes from `libs.versions.toml` like every other target. `versionCode` is the + phone's plus 1,000,000,000 (`getWearVersionCode`), because every bundle in a Play listing needs + a unique code. +- The watch app is a second bundle in the phone's listing, released on Play's Wear OS form + factor tracks. Through the API those are `wear:qa` (internal testing), `wear:beta` and + `wear:production`. They exist only once Wear OS is added in the Play Console under Advanced + settings > Form factors; until then every upload to them fails. +- The Android lanes in `fastlane/Fastfile` build both bundles and upload the watch's after the + phone's, so a Wear OS failure never holds back a phone release. `internal`, `beta` and + `release` map to `wear:qa`, `wear:beta` and `wear:production`. `alpha` skips the watch: a + form factor has no default closed testing track, only custom-named ones. +- The watch upload sends release notes only. The listing is shared, and the phone upload already + sends its text and images, including any `images/wearScreenshots`. +- `prepareForRelease` writes the Play release notes under both version codes. - `build.yml` builds and unit-tests the wear module. ## Testing @@ -289,7 +298,7 @@ Phase-gated behind the rest, but designed in now because it depends on synced co 4. **Capture** (done): guards for unsynced captures, then the activity with `RemoteInput`, note and idea flows, tile, complication, WorkManager sync. 5. **Read aloud**: markdown to speech helper in `base`, playback service, controls. -6. **Release pipeline**: fastlane lanes, Play workflow, version code offset. +6. **Release pipeline** (done): fastlane lanes, release notes, version code offset. Phases 1 and 2 improve the phone and desktop clients on their own and should land even if the watch slips. @@ -298,7 +307,5 @@ watch slips. - Does dictation work offline on the target watch? Voice capture is the point of the feature and the emulator cannot answer this; it needs the real hardware. -- Whether Play accepts a wear bundle with a version-code offset scheme, or whether a separate - version stream is cleaner. - Whether backups on the watch stay on or get a no-op datasource once real usage shows the storage cost. diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 5e6775839..629c4bcab 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -15,6 +15,14 @@ default_platform(:android) +# The release bundle a Gradle module builds. Named rather than taken from the gradle +# action's lane context, which only remembers the last of several bundles. +def play_bundle(module_name) + path = File.expand_path("../#{module_name}/build/outputs/bundle/release/#{module_name}-release.aab", __dir__) + UI.user_error!("No release bundle at #{path}") unless File.exist?(path) + path +end + # Returns an App Store Connect API key hash for use in mac lanes. # The .p8 is gitignored in desktop/secrets/. private_lane :appstore_api_key do @@ -353,43 +361,56 @@ platform :android do desc "Submit a new Build to Google Play Internal" lane :internal do - gradle( - task: ":android:bundleRelease", - gradle_path: "./gradlew", - properties: { "channel" => "google-play" } - ) - upload_to_play_store(track: 'internal') + play_build + upload_to_play_store(track: 'internal', aab: play_bundle("android")) + upload_wear_bundle(track: 'wear:qa') end desc "Submit a new Alpha Build to Google Play Close" lane :alpha do - gradle( - task: ":android:bundleRelease", - gradle_path: "./gradlew", - properties: { "channel" => "google-play" } - ) - upload_to_play_store(track: 'alpha') + play_build + upload_to_play_store(track: 'alpha', aab: play_bundle("android")) + # Wear OS has no default closed testing track; closed tracks on a form factor are + # custom-named in the Play Console, so the watch app skips this lane. end desc "Submit a new Beta Build to Google Play Open" lane :beta do - gradle( - task: ":android:bundleRelease", - gradle_path: "./gradlew", - properties: { "channel" => "google-play" } - ) - upload_to_play_store(track: 'beta') + play_build + upload_to_play_store(track: 'beta', aab: play_bundle("android")) + upload_wear_bundle(track: 'wear:beta') end desc "Deploy a new version to the Google Play" lane :release do + play_build(clean: true) + upload_to_play_store( + root_url: "https://androidpublisher.googleapis.com/", + aab: play_bundle("android"), + skip_upload_images: true, + skip_upload_screenshots: true + ) + upload_wear_bundle(track: 'wear:production') + end + + private_lane :play_build do |options| gradle( - task: "clean :android:bundleRelease", + task: "#{options[:clean] ? 'clean ' : ''}:android:bundleRelease :wear:bundleRelease", gradle_path: "./gradlew", properties: { "channel" => "google-play" } ) + end + + # The watch app is a second bundle in the same listing, on Play's Wear OS form factor + # tracks. Those only exist once Wear OS is added under Advanced settings > Form factors. + # It uploads after the phone so a Wear OS failure never holds back a phone release. The + # listing is shared and the phone upload already sent it, so only the release notes go. + private_lane :upload_wear_bundle do |options| upload_to_play_store( root_url: "https://androidpublisher.googleapis.com/", + track: options[:track], + aab: play_bundle("wear"), + skip_upload_metadata: true, skip_upload_images: true, skip_upload_screenshots: true )