Ship the watch app to Google Play's Wear OS tracks (#951)
Some checks are pending
Build CI / build (push) Waiting to run
Build CI / static-analysis (push) Waiting to run
Build CI / android-instrumented-tests (push) Waiting to run
Build CI / iOS compile & test (push) Waiting to run
Build CI / iOS UI tests (push) Waiting to run
PublishInternal / publish-google-play (push) Waiting to run

The Android lanes build and upload the wear bundle after the phone's, and prepareForRelease writes release notes under the wear version code too.
This commit is contained in:
Adam Brown 2026-09-19 20:27:02 -07:00 • committed by GitHub
parent 6738235141
commit 97e5a2d196
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 63 additions and 31 deletions

View file

@ -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)

View file

@ -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.

View file

@ -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
)