[BRE-768] Automate Google Play publishing (#5256)

Co-authored-by: Álison Fernandes <vvolkgang@users.noreply.github.com>
This commit is contained in:
Amy Galles 2025-07-21 10:11:30 -04:00 committed by GitHub
parent 6454dc1a58
commit f22643fec1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 345 additions and 7 deletions

View File

@ -1,16 +1,146 @@
name: Publish
name: Publish to Google Play
run-name: "Promoting ${{ inputs.product }} ${{ inputs.version-code }} from ${{ inputs.track-from }} to ${{ inputs.track-target }}"
on:
workflow_dispatch:
inputs:
product:
description: "Which app is being released."
type: choice
options:
- Password Manager
- Authenticator
version-name:
description: "Version name to promote to production ex 2025.1.1"
type: string
version-code:
description: "Build number to promote to production."
required: true
type: string
rollout-percentage:
description: "Percentage of users who will receive this version update."
required: true
type: choice
options:
- 10%
- 30%
- 50%
- 100%
default: 10%
release-notes:
description: "Change notes to be included with this release."
type: string
default: "Bug fixes."
required: true
track-from:
description: "Track to promote from."
type: choice
options:
- internal
- Fastlane Automation Source
required: true
default: "internal"
track-target:
description: "Track to promote to."
type: choice
options:
- production
- Fastlane Automation Target
required: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_ACTION_RUN_URL: "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
permissions: {}
permissions:
contents: read
packages: read
jobs:
publish:
promote:
runs-on: ubuntu-24.04
name: Promote build to Production in Play Store
steps:
- name: TEST STEP
run: exit 0
- name: Log inputs to job summary
run: |
echo "<details><summary>Job Inputs</summary>" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo '```json' >> $GITHUB_STEP_SUMMARY
echo '${{ toJson(inputs) }}' >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo "</details>" >> $GITHUB_STEP_SUMMARY
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Configure Ruby
uses: ruby/setup-ruby@ca041f971d66735f3e5ff1e21cc13e2d51e7e535 # v1.233.0
with:
bundler-cache: true
- name: Install Fastlane
run: |
gem install bundler:2.2.27
bundle config path vendor/bundle
bundle install --jobs 4 --retry 3
- name: Log in to Azure
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
- name: Retrieve secrets
env:
ACCOUNT_NAME: bitwardenci
CONTAINER_NAME: mobile
run: |
mkdir -p ${{ github.workspace }}/secrets
mkdir -p ${{ github.workspace }}/app/src/standardRelease
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
--name play_creds.json --file ${{ github.workspace }}/secrets/play_creds.json --output none
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
--name authenticator_play_store-creds.json --file ${{ github.workspace }}/secrets/authenticator_play_store-creds.json --output none
- name: Format Release Notes
run: |
FORMATTED_MESSAGE="$(echo "${{ inputs.release-notes }}" | sed 's/ /\n/g')"
echo "RELEASE_NOTES<<EOF" >> $GITHUB_ENV
echo "$FORMATTED_MESSAGE" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
- name: Promote Play Store version to production
env:
PLAY_KEYSTORE_PASSWORD: ${{ secrets.PLAY_BETA_KEYSTORE_PASSWORD }}
PLAY_KEY_PASSWORD: ${{ secrets.PLAY_BETA_KEY_PASSWORD }}
VERSION_CODE_INPUT: ${{ inputs.version-code }}
VERSION_NAME: ${{inputs.version-name}}
ROLLOUT_PERCENTAGE: ${{ inputs.rollout-percentage }}
PRODUCT: ${{ inputs.product }}
TRACK_FROM: ${{ inputs.track-from }}
TRACK_TARGET: ${{ inputs.track-target }}
run: |
if [ "$PRODUCT" = "Password Manager" ]; then
PACKAGE_NAME="com.x8bit.bitwarden"
elif [ "$PRODUCT" = "Authenticator" ]; then
PACKAGE_NAME="com.bitwarden.authenticator"
else
echo "Unsupported product: $PRODUCT"
exit 1
fi
VERSION_CODE=$(echo "${VERSION_CODE_INPUT}" | tr -d ',')
decimal=$(echo "scale=2; ${ROLLOUT_PERCENTAGE/\%/} / 100" | bc)
bundle exec fastlane updateReleaseNotes \
releaseNotes:"$RELEASE_NOTES" \
versionCode:"$VERSION_CODE"
bundle exec fastlane promoteToProduction \
versionCode:"$VERSION_CODE" \
versionName:"$VERSION_NAME" \
rolloutPercentage:"$decimal" \
packageName:"$PACKAGE_NAME" \
releaseNotes:"$RELEASE_NOTES" \
track:"$TRACK_FROM" \
trackPromoteTo:"$TRACK_TARGET"

View File

@ -13,6 +13,9 @@
# Uncomment the line if you want fastlane to automatically update itself
# update_fastlane
require_relative 'patches/supply_custom_promote_config'
require_relative 'patches/supply_custom_promote'
default_platform(:android)
platform :android do
@ -418,4 +421,74 @@ platform :android do
mapping: "authenticator/build/outputs/mapping/release/mapping.txt",
)
end
desc "Retrieve build from Github releases"
lane :retrieveBuildFromGithub do |options|
version_codes = Actions.lane_context[SharedValues::GOOGLE_PLAY_TRACK_VERSION_CODES]
UI.message("Version codes in beta track: #{version_codes}")
end
desc "Update release notes for all locales."
lane :updateReleaseNotes do |options|
changelog = options[:releaseNotes]
version_code = options[:versionCode]
auth_locales = ["en-US"]
pw_manager_locales = ["ca", "cs-CZ", "da-DK", "de-DE", "en-US", "es-ES", "et", "fr-FR", "hr", "hu-HU", "id", "it-IT", "iw-IL", "ja-JP", "nl-NL", "pl-PL", "pt-BR", "pt-PT", "ro", "ru-RU", "sk", "sv-SE", "tr-TR", "uk", "vi", "zh-CN", "zh-TW"]
if options[:packageName] == "com.bitwarden.authenticator"
locales = auth_locales
else
locales = pw_manager_locales
end
locales.each do |locale|
dir = "metadata/android/#{locale}/changelogs"
FileUtils.mkdir_p(dir)
File.write("#{dir}/#{version_code}.txt", changelog)
end
end
desc "Promote to production."
lane :promoteToProduction do |options|
release_options = {
package_name: options[:packageName],
version_code: options[:versionCode].to_i,
version_name: options[:versionName],
track: options[:track],
track_promote_to: options[:trackPromoteTo],
skip_release_verification: true,
skip_upload_apk: true,
skip_upload_aab: true,
}
if options[:releaseNotes].nil? or options[:releaseNotes].to_s.empty?
release_options[:skip_upload_metadata] = true
else
release_options[:skip_upload_metadata] = false
end
if options[:rolloutPercentage].to_f < 1
release_options[:track_promote_release_status] = "inProgress"
release_options[:rollout] = options[:rolloutPercentage]
else
release_options[:release_status] = "completed"
end
begin
UI.message("🚀 Starting release to #{options[:trackPromoteTo]}...")
supply(release_options)
rescue => error
message = error.to_s
if message.include?("You cannot rollout this release because it does not allow any existing users to upgrade to the newly added APKs")
UI.error("❌ App Store Error: Cannot rollout release because no existing users can upgrade to the new build.")
UI.error("This might mean the version is older than what is currently available on the track, pick a newer build.")
else
UI.error("❌ Unexpected error during release: #{message}")
end
raise
end
end
end

View File

@ -0,0 +1,97 @@
# Patch Description:
# Fixes issue where Fastlane 'Supply' doesn't recognize previous builds
# when promoting to another track.
#
# Source: https://github.com/artsy/eigen/pull/10262
# Author: Brian Beckerle (@brainbicycle)
#
module Supply
class Uploader
alias_method :original_promote_track, :promote_track
def promote_track
if Supply.config[:skip_release_verification]
custom_promote_track
else
original_promote_track
end
end
def custom_promote_track
UI.message("Using custom promotion logic")
track_from = client.tracks(Supply.config[:track]).first
unless track_from
UI.user_error!("Cannot promote from track '#{Supply.config[:track]}' - track doesn't exist")
end
releases = track_from.releases
UI.message("Track contents: #{track_from.to_json}")
version_code = Supply.config[:version_code].to_s
if !Supply.config[:skip_release_verification]
if version_code != ""
releases = releases.select do |release|
release.version_codes.include?(version_code)
end
else
releases = releases.select do |release|
release.status == Supply.config[:release_status]
end
end
if releases.size == 0
if version_code != ""
UI.user_error!("Cannot find release with version code '#{version_code}' to promote in track '#{Supply.config[:track]}'")
else
UI.user_error!("Track '#{Supply.config[:track]}' doesn't have any releases")
end
elsif releases.size > 1
UI.user_error!("Track '#{Supply.config[:track]}' has more than one release - use :version_code to filter the release to promote")
end
else
UI.message("Skipping release verification as per configuration.")
if version_code == ""
UI.user_error!("Must provide a version code when release verification is skipped.")
end
if Supply.config[:version_name].nil?
UI.user_error!("To force promote a :version_code, it is mandatory to enter the :version_name")
end
release = AndroidPublisher::TrackRelease.new(
name: Supply.config[:version_name],
version_codes: [version_code],
status: Supply.config[:track_promote_release_status] || Supply::ReleaseStatus::COMPLETED
)
end
release = releases.first unless Supply.config[:skip_release_verification]
track_to = client.tracks(Supply.config[:track_promote_to]).first || AndroidPublisher::Track.new(
track: Supply.config[:track_promote_to],
releases: []
)
rollout = (Supply.config[:rollout] || 0).to_f
if rollout > 0 && rollout < 1
release.status = Supply::ReleaseStatus::IN_PROGRESS
release.user_fraction = rollout
else
release.status = Supply.config[:track_promote_release_status]
release.user_fraction = nil
end
if track_to
# Its okay to set releases to an array containing the newest release
# Google Play will keep previous releases there this release is a partial rollout
track_to.releases = [release]
else
track_to = AndroidPublisher::Track.new(
track: Supply.config[:track_promote_to],
releases: [release]
)
end
client.update_track(Supply.config[:track_promote_to], track_to)
UI.message("confirmed that update_track was reached: #{Supply.config[:track_promote_to]} #{release}")
end
end
end

View File

@ -0,0 +1,38 @@
# Patch Description:
# Fixes issue where Fastlane 'Supply' doesn't recognize previous builds
# when promoting to another track.
#
# Source: https://github.com/artsy/eigen/pull/10262
# Author: Brian Beckerle (@brainbicycle)
#
module Supply
class Options
class << self
alias_method :original_available_options, :available_options
def available_options
original_options = original_available_options
custom_options = [
FastlaneCore::ConfigItem.new(
key: :skip_release_verification,
env_name: "SUPPLY_SKIP_RELEASE_VERIFICATION",
description: "If set to true, skips checking if the version code exists in the track before promoting",
type: Boolean,
default_value: false,
optional: true
)
]
# Only add custom options if they aren't already present
custom_options.each do |custom_option|
unless original_options.any? { |option| option.key == custom_option.key }
original_options << custom_option
end
end
original_options
end
end
end
end