diff --git a/.github/workflows/publish-store.yml b/.github/workflows/publish-store.yml index cd654fc64c..db39b4848b 100644 --- a/.github/workflows/publish-store.yml +++ b/.github/workflows/publish-store.yml @@ -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 "
Job Inputs" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo '```json' >> $GITHUB_STEP_SUMMARY + echo '${{ toJson(inputs) }}' >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "
" >> $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<> $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" diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 454b806547..803c0be24e 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -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 diff --git a/fastlane/patches/supply_custom_promote.rb b/fastlane/patches/supply_custom_promote.rb new file mode 100644 index 0000000000..e684dbc56a --- /dev/null +++ b/fastlane/patches/supply_custom_promote.rb @@ -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 diff --git a/fastlane/patches/supply_custom_promote_config.rb b/fastlane/patches/supply_custom_promote_config.rb new file mode 100644 index 0000000000..dc50163451 --- /dev/null +++ b/fastlane/patches/supply_custom_promote_config.rb @@ -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