require 'supply' require 'supply/options' module Fastlane module Actions class DownloadUniversalApkFromGooglePlayAction < Action def self.run(params) package_name = params[:package_name] version_code = params[:version_code] destination = params[:destination] cert_sha = params[:certificate_sha256_hash] client = Supply::Client.make_from_config(params: params) UI.message("Fetching the list of generated APKs from the Google API...") all_universal_apks = client.list_generated_universal_apks(package_name: package_name, version_code: version_code) matching_apks = all_universal_apks.select { |apk| cert_sha.nil? || apk.certificate_sha256_hash&.casecmp?(cert_sha) } all_certs_printable_list = all_universal_apks.map { |apk| " - #{apk.certificate_sha256_hash}" } if matching_apks.count > 1 message = <<~ERROR We found multiple Generated Universal APK, with the following `certificate_sha256_hash`: #{all_certs_printable_list.join("\n")} Use the `certificate_sha256_hash` parameter to specify which one to download. ERROR UI.user_error!(message) elsif matching_apks.empty? # NOTE: if no APK was found at all to begin with, the client would already have raised a user_error!('Google Api Error ...') message = <<~ERROR None of the Universal APK(s) found for this version code matched the `certificate_sha256_hash` of `#{cert_sha}`. We found #{all_universal_apks.count} Generated Universal APK(s), but with a different `certificate_sha256_hash`: #{all_certs_printable_list.join("\n")} ERROR UI.user_error!(message) end UI.message("Downloading Generated Universal APK to `#{destination}`...") FileUtils.mkdir_p(File.dirname(destination)) client.download_generated_universal_apk(generated_universal_apk: matching_apks.first, destination: destination) UI.success("Universal APK successfully downloaded to `#{destination}`.") destination end ##################################################### # @!group Documentation ##################################################### def self.description "Download the Universal APK of a given version code from the Google Play Console" end def self.details <<~DETAILS Download the universal APK of a given version code from the Google Play Console. This uses fastlane `Supply` (and the `AndroidPublisher` Google API) to download the Universal APK generated by Google after you uploaded an `.aab` bundle to the Play Console. See https://developers.google.com/android-publisher/api-ref/rest/v3/generatedapks/list DETAILS end def self.available_options # Only borrow _some_ of the ConfigItems from https://github.com/fastlane/fastlane/blob/master/supply/lib/supply/options.rb # So we don't have to duplicate the name, env_var, type, description, and verify_block of those here. supply_borrowed_options = Supply::Options.available_options.select do |o| %i[package_name version_code json_key json_key_data root_url timeout].include?(o.key) end # Adjust the description for the :version_code ConfigItem for our action's use case supply_borrowed_options.find { |o| o.key == :version_code }&.description = "The versionCode for which to download the generated APK" [ *supply_borrowed_options, # The remaining ConfigItems below are specific to this action FastlaneCore::ConfigItem.new(key: :destination, env_name: 'DOWNLOAD_UNIVERSAL_APK_DESTINATION', optional: false, type: String, description: "The path on disk where to download the Generated Universal APK", verify_block: proc do |value| UI.user_error!("The 'destination' must be a file path with the `.apk` file extension") unless File.extname(value) == '.apk' end), FastlaneCore::ConfigItem.new(key: :certificate_sha256_hash, env_name: 'DOWNLOAD_UNIVERSAL_APK_CERTIFICATE_SHA256_HASH', optional: true, type: String, description: "The SHA256 hash of the signing key for which to download the Universal, Code-Signed APK for. " \ + "Use 'xx:xx:xx:…' format (32 hex bytes separated by colons), as printed by `keytool -list -keystore `. " \ + "Only useful to provide if you have multiple signing keys configured on GPC, to specify which generated APK to download", verify_block: proc do |value| bytes = value.split(':') next if bytes.length == 32 && bytes.all? { |byte| /^[0-9a-fA-F]{2}$/.match?(byte) } UI.user_error!("When provided, the certificate sha256 must be in the 'xx:xx:xx:…:xx' (32 hex bytes separated by colons) format") end) ] end def self.output # Define the shared values you are going to provide end def self.return_value 'The path to the downloaded Universal APK. The action will raise an exception if it failed to find or download the APK in Google Play' end def self.authors ['Automattic'] end def self.category :production end def self.is_supported?(platform) platform == :android end end end end