From a4ea3212e3f9057146e05a94a215e9cffb2390ec Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Fri, 21 Aug 2026 14:06:58 +0200 Subject: [PATCH 1/2] Promote to open testing from the command line Open testing was the one wider track reached only by clicking through the console. It is the same promotion either way - one upload, then an assignment of the bundle already in the artifact library - so it may as well be a lane. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01CAS3exxPhGS3iBjj7K9JVZ --- README.md | 19 +++++++++++++++---- fastlane/Fastfile | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index bacdf58a5fb6..f15a8101b46d 100644 --- a/README.md +++ b/README.md @@ -110,10 +110,21 @@ the APK on the GitHub release. That is what keeps a version on a single commit - the `v*` tag names and F-Droid builds. Internal is the only track it uploads to. Anything wider - closed, open, production - -is a promotion in the Play Console, which moves the same bundle and version code that -was tested onto the wider track instead of uploading a second one. It is also where the -review that a production release waits on actually happens, so the workflow finishing is -not the same as the release being out. +is a promotion, which moves the same bundle and version code that was tested onto the +wider track instead of uploading a second one. A version code is spent the moment it +goes up, so there is one upload per release and every track after it is an assignment +of what is already there. + +`fastlane android openTestingPro` and `openTestingLite` promote to open testing, given +the version whose code should move: + +```sh +fastlane android openTestingLite version:v4.17.0 +``` + +Closed testing and production are still the Play Console. Play reviews a promotion like +any other release - only the internal track is immediate - so neither the workflow +finishing nor the promotion running is the same as the release being out. The listing goes up in its own job, behind the bundle: the title, both descriptions, the release notes of that version, the screenshots and the feature graphic, in all fifteen diff --git a/fastlane/Fastfile b/fastlane/Fastfile index bbda403a07b0..d20c0d3de859 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -10,6 +10,10 @@ PACKAGE_NAMES = { DEFAULT_TRACK = "internal".freeze +# what the console calls open testing. Supply kept the older track names, so the +# one a promotion has to name is not the one the console shows. +OPEN_TESTING_TRACK = "beta".freeze + # absolute, because a lane and an action do not agree on what the working directory # is, and every path below is handed to something else ROOT = File.expand_path("..", __dir__).freeze @@ -174,6 +178,16 @@ platform :android do uploadListing(flavor: "Lite", track: options[:track], version: options[:version]) end + desc "Move the Pro version already on the internal track to open testing" + lane :openTestingPro do |options| + promoteBundle(flavor: "Pro", version: options[:version]) + end + + desc "Move the Lite version already on the internal track to open testing" + lane :openTestingLite do |options| + promoteBundle(flavor: "Lite", version: options[:version]) + end + lane :tests do gradle(task: "connectedAndroidTest") end @@ -252,6 +266,38 @@ platform :android do ) end + # A promotion, not a second upload: the bundle is already in the artifact library + # and a version code is spent the moment it goes up, so the wider track is handed + # the build the narrower one tested rather than one built again beside it. + # + # Play reviews this like any other release - what the lane saves is the console, + # not the wait. The source track keeps serving the release it is promoted from. + private_lane :promoteBundle do |options| + flavor = options[:flavor] + version = require_version(options[:version]) + + upload_to_play_store( + track: DEFAULT_TRACK, + track_promote_to: OPEN_TESTING_TRACK, + # spelled out rather than left to the default: a promotion that lands as a + # draft, or at a percent of the testers, is the failure that looks like success + track_promote_release_status: "completed", + package_name: PACKAGE_NAMES.fetch(flavor), + json_key: ENV["ODR_PLAY_JSON_KEY"] || + CredentialsManager::AppfileConfig.try_fetch_value(:json_key_file), + # which release moves. There is no bundle to read it off, as uploadBundle has + version_code: version_code(version), + # nothing new goes up here - the listing and its notes are already against + # this version code, and resending them is uploadListing's job + skip_upload_aab: true, + skip_upload_apk: true, + skip_upload_metadata: true, + skip_upload_changelogs: true, + skip_upload_images: true, + skip_upload_screenshots: true + ) + end + desc "Take the play store screenshots of one device, in every locale the listing is written in" lane :screenshots do device = screenshot_device From 6600f7085190fe6311e43db49594a4b22096b471 Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Fri, 21 Aug 2026 14:16:17 +0200 Subject: [PATCH 2/2] Say why version_code is enough to name the release It reads as a label on the promotion, and a review took it for one. In supply it is the filter: the source track is narrowed to the release carrying the code and a version that is not there fails the lane. The pair of skips is the other half - a binary to upload would take the update path, where track_promote_to is never read. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01M54PU7ztmSDrCZwCpMsLto --- fastlane/Fastfile | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index d20c0d3de859..28204ca10a56 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -285,10 +285,15 @@ platform :android do package_name: PACKAGE_NAMES.fetch(flavor), json_key: ENV["ODR_PLAY_JSON_KEY"] || CredentialsManager::AppfileConfig.try_fetch_value(:json_key_file), - # which release moves. There is no bundle to read it off, as uploadBundle has + # which release moves, and there is no bundle to read it off as uploadBundle + # has. It is a filter, not a label: supply keeps the source track's release + # carrying this code and errors when none does, so a version that never + # reached internal fails the lane rather than promoting what it is serving. version_code: version_code(version), # nothing new goes up here - the listing and its notes are already against - # this version code, and resending them is uploadListing's job + # this version code, and resending them is uploadListing's job. Skipping both + # binaries is what puts supply on the promotion path at all: hand it one to + # upload and it updates the source track and never reads track_promote_to. skip_upload_aab: true, skip_upload_apk: true, skip_upload_metadata: true,