lib/fastlane/plugin/polidea/actions/fota_s3.rb in fastlane-plugin-polidea-3.0.1 vs lib/fastlane/plugin/polidea/actions/fota_s3.rb in fastlane-plugin-polidea-4.0.0.pre

- old
+ new

@@ -1,22 +1,17 @@ -# rubocop:disable Metrics/AbcSize -# rubocop:disable Metrics/ClassLength + require 'fastlane/erb_template_helper' require 'ostruct' require 'securerandom' module Fastlane module Actions module SharedValues S3_IPA_OUTPUT_PATH = :S3_IPA_OUTPUT_PATH S3_DSYM_OUTPUT_PATH = :S3_DSYM_OUTPUT_PATH - S3_PLIST_OUTPUT_PATH = :S3_PLIST_OUTPUT_PATH S3_APK_OUTPUT_PATH = :S3_APK_OUTPUT_PATH S3_MAPPING_OUTPUT_PATH = :S3_MAPPING_OUTPUT_PATH - S3_HTML_OUTPUT_PATH = :S3_HTML_OUTPUT_PATH - S3_VERSION_OUTPUT_PATH = :S3_VERSION_OUTPUT_PATH - S3_ICON_OUTPUT_PATH = :S3_ICON_OUTPUT_PATH end class FotaS3Action < Action def self.run(config) Fastlane::Polidea.session.action_launched("fota_s3", config) @@ -25,429 +20,113 @@ # Calling fetch on config so that default values will be used params = {} params[:ipa] = config[:ipa] params[:apk] = config[:apk] - params[:icon] = config[:icon] params[:dsym] = config[:dsym] params[:mapping] = config[:mapping] - params[:access_key] = config[:access_key] - params[:secret_access_key] = config[:secret_access_key] - params[:bucket] = config[:bucket] - params[:region] = config[:region] - params[:acl] = config[:acl] - params[:upload_metadata] = config[:upload_metadata] - params[:plist_template_path] = config[:plist_template_path] - params[:html_template_path] = config[:html_template_path] - params[:html_file_name] = config[:html_file_name] - params[:version_template_path] = config[:version_template_path] - params[:version_file_name] = config[:version_file_name] - params[:acl] = config[:acl] - params[:release_notes] = config[:release_notes] - params[:treat_bucket_as_domain_name] = config[:treat_bucket_as_domain_name] + params[:environment] = config[:environment] + params[:api_token] = config[:api_token] + params[:app_identifier] = config[:app_identifier] + params[:prefix_schema] = config[:prefix_schema] + params[:build_number] = config[:build_number] + shuttle_client = Shuttle::Client.new(base_url(params[:environment]), params[:api_token]) + case platform when :ios - upload_ios(params) + upload_ios(params, shuttle_client) when :android - upload_android(params) + upload_android(params, shuttle_client) end Fastlane::Polidea.session.action_completed("fota_s3") return true end - def self.upload_ios(params) + def self.upload_ios(params, shuttle_client) # Pulling parameters for other uses - s3_region = params[:region] - s3_subdomain = params[:region] ? "s3-#{params[:region]}" : "s3" - s3_access_key = params[:access_key] - s3_secret_access_key = params[:secret_access_key] - s3_bucket = params[:bucket] ipa_file = params[:ipa] - icon_file = params[:icon] dsym_file = params[:dsym] - acl = params[:acl] - release_notes = params[:release_notes] - treat_bucket_as_domain_name = params[:treat_bucket_as_domain_name] + app_identifier = params[:app_identifier] + prefix_schema = params[:prefix_schema] - validate(params) UI.user_error!("No IPA file path given, pass using `ipa: 'ipa path'`") unless ipa_file.to_s.length > 0 - UI.message("Will transform S3 urls from https://s3.amazonaws.com/#{s3_bucket} to https://#{s3_bucket}") if treat_bucket_as_domain_name + upload_urls = shuttle_client.get_upload_urls('ios', app_identifier, prefix_schema) + upload_build_url = upload_urls['buildUrl'] + self.upload_file(upload_build_url, ipa_file) - bucket = get_bucket(s3_access_key, s3_secret_access_key, s3_region, s3_bucket) - - # Gets info used for the plist - info = FastlaneCore::IpaFileAnalyser.fetch_info_plist_file(ipa_file) - - build_number = info['CFBundleVersion'] - bundle_id = info['CFBundleIdentifier'] - bundle_version = info['CFBundleShortVersionString'] - app_name = info['CFBundleDisplayName'] || info['CFBundleName'] - full_version = "#{bundle_version}.#{build_number}" - url_part = get_url_part(app_name, "ios", bundle_version, build_number) - - plist_template_path = params[:plist_template_path] - html_file_name = params[:html_file_name] - version_template_path = params[:version_template_path] - version_file_name = params[:version_file_name] - - ipa_file_basename = File.basename(ipa_file) - ipa_file_name = "#{url_part}#{ipa_file_basename}" - ipa_file_data = File.open(ipa_file, 'rb') - - ipa_url = self.upload_file(bucket, ipa_file_name, ipa_file_data, acl, treat_bucket_as_domain_name) - # Setting action and environment variables - Actions.lane_context[SharedValues::S3_IPA_OUTPUT_PATH] = ipa_url - ENV[SharedValues::S3_IPA_OUTPUT_PATH.to_s] = ipa_url + Actions.lane_context[SharedValues::S3_IPA_OUTPUT_PATH] = upload_build_url + ENV[SharedValues::S3_IPA_OUTPUT_PATH.to_s] = upload_build_url if dsym_file - dsym_file_basename = File.basename(dsym_file) - dsym_file_name = "#{url_part}#{dsym_file_basename}" - dsym_file_data = File.open(dsym_file, 'rb') - - dsym_url = self.upload_file(bucket, dsym_file_name, dsym_file_data, acl, treat_bucket_as_domain_name) - - dsym_file_data.close + upload_dsym_url = upload_urls['debugFileUrl'] + self.upload_file(upload_url, dsym_file) end - if params[:upload_metadata] == false - return true - end + UI.success("Successfully uploaded ipa file") - ##################################### - # - # html and plist building - # - ##################################### - - # Creating plist and html names - plist_file_name = "#{url_part}manifest.plist" - plist_url = "https://#{s3_subdomain}.amazonaws.com/#{s3_bucket}/#{plist_file_name}" - - html_file_name ||= "#{url_part}index.html" - html_resources_name = "#{url_part}installation-page" - - version_file_name ||= "#{url_part}version.json" - - # grabs module - eth = Fastlane::ErbTemplateHelper - - # Creates plist from template - if plist_template_path && File.exist?(plist_template_path) - plist_template = eth.load_from_path(plist_template_path) - else - plist_template = eth.load("s3_plist_template") + if upload_dsym_url + Actions.lane_context[SharedValues::S3_DSYM_OUTPUT_PATH] = upload_dsym_url + ENV[SharedValues::S3_DSYM_OUTPUT_PATH.to_s] = upload_dsym_url + UI.success("Successfully uploaded dsym file") end - plist_render = eth.render(plist_template, { - url: ipa_url, - ipa_url: ipa_url, - bundle_id: bundle_id, - build_number: build_number, - bundle_version: bundle_version, - title: app_name - }) - - # Gets icon from ipa and uploads it - icon_url = self.upload_icon(icon_file, url_part, bucket, acl, treat_bucket_as_domain_name) - - # Creates html from template - html_render = PageGenerator.installation_page({ - url: "itms-services://?action=download-manifest&url=#{URI.encode_www_form_component(plist_url)}", - app_version: bundle_version, - build_number: build_number, - app_name: app_name, - app_icon: icon_url, - platform: "ios", - release_notes: release_notes - }) - - # Creates version from template - if version_template_path && File.exist?(version_template_path) - version_template = eth.load_from_path(version_template_path) - else - version_template = eth.load("s3_version_template") - end - version_render = eth.render(version_template, { - url: plist_url, - plist_url: plist_url, - ipa_url: ipa_url, - build_number: build_number, - bundle_version: bundle_version, - full_version: full_version - }) - - ##################################### - # - # html and plist uploading - # - ##################################### - - plist_url = self.upload_file(bucket, plist_file_name, plist_render, acl, treat_bucket_as_domain_name) - html_url = self.upload_file(bucket, html_file_name, html_render, acl, treat_bucket_as_domain_name) - self.upload_directory(bucket, html_resources_name, "#{__dir__}/../templates/installation-page", acl) - version_url = self.upload_file(bucket, version_file_name, version_render, acl, treat_bucket_as_domain_name) - - # Setting action and environment variables - Actions.lane_context[SharedValues::S3_PLIST_OUTPUT_PATH] = plist_url - ENV[SharedValues::S3_PLIST_OUTPUT_PATH.to_s] = plist_url - - Actions.lane_context[SharedValues::S3_HTML_OUTPUT_PATH] = html_url - ENV[SharedValues::S3_HTML_OUTPUT_PATH.to_s] = html_url - - Actions.lane_context[SharedValues::S3_VERSION_OUTPUT_PATH] = version_url - ENV[SharedValues::S3_VERSION_OUTPUT_PATH.to_s] = version_url - - UI.success("Successfully uploaded ipa file to '#{Actions.lane_context[SharedValues::S3_IPA_OUTPUT_PATH]}'") - UI.success("Successfully uploaded plist file to '#{Actions.lane_context[SharedValues::S3_PLIST_OUTPUT_PATH]}'") - UI.success("Successfully uploaded html file to '#{Actions.lane_context[SharedValues::S3_HTML_OUTPUT_PATH]}'") - UI.success("Successfully uploaded version file to '#{Actions.lane_context[SharedValues::S3_VERSION_OUTPUT_PATH]}'") - - if icon_url - Actions.lane_context[SharedValues::S3_ICON_OUTPUT_PATH] = icon_url - ENV[SharedValues::S3_ICON_OUTPUT_PATH.to_s] = icon_url - UI.success("Successfully uploaded icon file to '#{Actions.lane_context[SharedValues::S3_ICON_OUTPUT_PATH]}'") - end - if dsym_url - Actions.lane_context[SharedValues::S3_DSYM_OUTPUT_PATH] = dsym_url - ENV[SharedValues::S3_DSYM_OUTPUT_PATH.to_s] = dsym_url - UI.success("Successfully uploaded dsym file to '#{Actions.lane_context[SharedValues::S3_DSYM_OUTPUT_PATH]}'") - end end - def self.upload_android(params) + def self.upload_android(params, shuttle_client) # Pulling parameters for other uses - s3_region = params[:region] - s3_access_key = params[:access_key] - s3_secret_access_key = params[:secret_access_key] - s3_bucket = params[:bucket] apk_file = params[:apk] - icon_file = params[:icon] mapping_file = params[:mapping] - acl = params[:acl] - release_notes = params[:release_notes] - treat_bucket_as_domain_name = params[:treat_bucket_as_domain_name] + app_identifier = params[:app_identifier] + build_number = params[:build_number] - validate(params) UI.user_error!("No APK file path given, pass using `apk: 'apk path'`") unless apk_file.to_s.length > 0 - UI.message("Will transform S3 urls from https://s3.amazonaws.com/#{s3_bucket} to https://#{s3_bucket}") if treat_bucket_as_domain_name + upload_urls = shuttle_client.get_upload_urls('android', app_identifier, build_number) + upload_build_url = upload_urls['buildUrl'] + self.upload_file(upload_build_url, apk_file) - bucket = get_bucket(s3_access_key, s3_secret_access_key, s3_region, s3_bucket) - - # Gets info used from the apk manifest - manifest = Android::Apk.new(apk_file).manifest - - app_name = manifest.label - build_number = manifest.version_code - app_version = manifest.version_name - url_part = get_url_part(app_name, "android", app_version, build_number) - - html_file_name = params[:html_file_name] - - apk_file_basename = File.basename(apk_file) - apk_file_name = "#{url_part}#{apk_file_basename}" - apk_file_data = File.open(apk_file, 'rb') - - apk_url = self.upload_file(bucket, apk_file_name, apk_file_data, acl, treat_bucket_as_domain_name) - # Setting action and environment variables - Actions.lane_context[SharedValues::S3_APK_OUTPUT_PATH] = apk_url - ENV[SharedValues::S3_APK_OUTPUT_PATH.to_s] = apk_url + Actions.lane_context[SharedValues::S3_APK_OUTPUT_PATH] = upload_build_url + ENV[SharedValues::S3_APK_OUTPUT_PATH.to_s] = upload_build_url if mapping_file - mapping_file_basename = File.basename(mapping_file) - mapping_file_name = "#{url_part}#{mapping_file_basename}" - mapping_file_data = File.open(mapping_file, 'rb') + upload_mapping_file_url = upload_urls['debugFileUrl'] + self.upload_file(upload_mapping_file_url, mapping_file) - mapping_url = self.upload_file(bucket, mapping_file_name, mapping_file_data, acl, treat_bucket_as_domain_name) - # Setting action and environment variables - Actions.lane_context[SharedValues::S3_MAPPING_OUTPUT_PATH] = mapping_url - ENV[SharedValues::S3_MAPPING_OUTPUT_PATH.to_s] = mapping_url + Actions.lane_context[SharedValues::S3_MAPPING_OUTPUT_PATH] = upload_mapping_file_url + ENV[SharedValues::S3_MAPPING_OUTPUT_PATH.to_s] = upload_mapping_file_url - mapping_file_data.close end - ##################################### - # - # html building - # - ##################################### + UI.success("Successfully uploaded apk file") - # Creating html names - - html_file_name ||= "#{url_part}index.html" - html_resources_name = "#{url_part}installation-page" - - # Gets icon from ipa and uploads it - icon_url = self.upload_icon(icon_file, url_part, bucket, acl, treat_bucket_as_domain_name) - - # Creates html from template - html_render = PageGenerator.installation_page({ - url: apk_url, - app_version: app_version, - build_number: build_number, - app_name: app_name, - app_icon: icon_url, - platform: "android", - release_notes: release_notes - }) - - html_url = self.upload_file(bucket, html_file_name, html_render, acl, treat_bucket_as_domain_name) - self.upload_directory(bucket, html_resources_name, "#{__dir__}/../templates/installation-page", acl) - - Actions.lane_context[SharedValues::S3_HTML_OUTPUT_PATH] = html_url - ENV[SharedValues::S3_HTML_OUTPUT_PATH.to_s] = html_url - - UI.success("Successfully uploaded apk file to '#{Actions.lane_context[SharedValues::S3_APK_OUTPUT_PATH]}'") - UI.success("Successfully uploaded html file to '#{Actions.lane_context[SharedValues::S3_HTML_OUTPUT_PATH]}'") - - if icon_url - Actions.lane_context[SharedValues::S3_ICON_OUTPUT_PATH] = icon_url - ENV[SharedValues::S3_ICON_OUTPUT_PATH.to_s] = icon_url - UI.success("Successfully uploaded icon file to '#{Actions.lane_context[SharedValues::S3_ICON_OUTPUT_PATH]}'") + if upload_mapping_file_url + UI.success("Successfully uploaded mapping file") end - if mapping_url - UI.success("Successfully uploaded mapping file to '#{Actions.lane_context[SharedValues::S3_MAPPING_OUTPUT_PATH]}'") - end end - def self.validate(params) - s3_access_key = params[:access_key] - s3_secret_access_key = params[:secret_access_key] - s3_bucket = params[:bucket] - - UI.user_error!("No S3 access key given, pass using `access_key: 'key'`") unless s3_access_key.to_s.length > 0 - UI.user_error!("No S3 secret access key given, pass using `secret_access_key: 'secret key'`") unless s3_secret_access_key.to_s.length > 0 - UI.user_error!("No S3 bucket given, pass using `bucket: 'bucket'`") unless s3_bucket.to_s.length > 0 + def self.upload_file(url, build) + client = S3::Client.new + client.upload_file(url, build) end - def self.get_url_part(app_name, platform, app_version, build_number) - random_part = SecureRandom.hex(10) - "#{app_name}/#{platform}/#{app_version}_#{build_number}/#{random_part}/" - end - - def self.get_bucket(s3_access_key, s3_secret_access_key, s3_region, s3_bucket) - self.s3_client(s3_access_key, s3_secret_access_key, s3_region).bucket(s3_bucket) - end - - def self.s3_client(s3_access_key, s3_secret_access_key, s3_region) - Actions.verify_gem!('aws-sdk-s3') - require 'aws-sdk-s3' - - if s3_region - s3_client = Aws::S3::Resource.new( - access_key_id: s3_access_key, - secret_access_key: s3_secret_access_key, - region: s3_region - ) - else - s3_client = Aws::S3::Resource.new( - access_key_id: s3_access_key, - secret_access_key: s3_secret_access_key - ) + def self.base_url(environment) + case environment + when :production + "https://shuttle.polidea.com" + when :testing + "https://shuttle-testing.polidea.com" end - s3_client end + private_class_method :base_url - def self.upload_file(bucket, file_name, file_data, acl, treat_bucket_as_domain_name) - obj = bucket.put_object({ - key: file_name, - body: file_data, - acl: acl, - content_type: Mime.content_type_for_file(file_name) - }) - - # When you enable versioning on a S3 bucket, - # writing to an object will create an object version - # instead of replacing the existing object. - # http://docs.aws.amazon.com/AWSRubySDK/latest/AWS/S3/ObjectVersion.html - if obj.kind_of? Aws::S3::ObjectVersion - obj = obj.object - end - - if treat_bucket_as_domain_name - # Return public url - shorten_url(obj.public_url.to_s) - else - obj.public_url.to_s - end - end - - def self.upload_directory(bucket, directory_name, directory_path, acl) - files = files_at_path(directory_path) - - files.each do |file| - local_path = directory_path + file - s3_path = directory_name + file - - bucket.put_object({ - key: s3_path, - body: File.open(local_path), - acl: acl, - content_type: Mime.content_type_for_file(local_path) - }) - end - end - - def self.files_at_path(path) - files = Dir.glob(path + "/**/*") - to_remove = [] - files.each do |file| - if File.directory?(file) - to_remove.push file - else - file.slice! path - end - end - to_remove.each do |file| - files.delete file - end - return files - end - - # - # NOT a fan of this as this was taken straight from Shenzhen - # https://github.com/nomad/shenzhen/blob/986792db5d4d16a80c865a2748ee96ba63644821/lib/shenzhen/plugins/s3.rb#L32 - # - # Need to find a way to not use this copied method - # - # AGAIN, I am not happy about this right now. - # Using this for prototype reasons. - # - def self.expand_path_with_substitutions_from_ipa_plist(ipa, path) - substitutions = path.scan(/\{CFBundle[^}]+\}/) - return path if substitutions.empty? - info = FastlaneCore::IpaFileAnalyser.fetch_info_plist_file(ipa) or return path - - substitutions.uniq.each do |substitution| - key = substitution[1...-1] - value = info[key] - path.gsub!(Regexp.new(substitution), value) if value - end - - return path - end - - def self.upload_icon(icon_path, url_part, bucket, acl, treat_bucket_as_domain_name) - return unless icon_path - icon_file_basename = File.basename(icon_path) - icon_file = File.open(icon_path) - icon_file_name = "#{url_part}#{icon_file_basename}" - self.upload_file(bucket, icon_file_name, icon_file, acl, treat_bucket_as_domain_name) - end - - def self.shorten_url(url) - uri = URI.parse(url) - uri.scheme + ':/' + uri.path - end - def self.description - "Generates a plist file and uploads all to AWS S3" + "Uploads build to AWS S3" end def self.available_options [ FastlaneCore::ConfigItem.new(key: :ipa, @@ -468,89 +147,50 @@ FastlaneCore::ConfigItem.new(key: :mapping, env_name: "", description: "The path to the mapping.txt file", optional: true, default_value: Actions.lane_context[SharedValues::GRADLE_MAPPING_TXT_OUTPUT_PATH]), - FastlaneCore::ConfigItem.new(key: :icon, - env_name: "", - description: "app icon file to upload", - optional: true, - default_value: Actions.lane_context[SharedValues::ICON_OUTPUT_PATH]), - FastlaneCore::ConfigItem.new(key: :upload_metadata, - env_name: "", - description: "Upload relevant metadata for this build", - optional: true, - default_value: true, - is_string: false), - FastlaneCore::ConfigItem.new(key: :plist_template_path, - env_name: "", - description: "plist template path", - optional: true), - FastlaneCore::ConfigItem.new(key: :html_template_path, - env_name: "", - description: "html erb template path", - optional: true), - FastlaneCore::ConfigItem.new(key: :html_file_name, - env_name: "", - description: "uploaded html filename", - optional: true), - FastlaneCore::ConfigItem.new(key: :version_template_path, - env_name: "", - description: "version erb template path", - optional: true), - FastlaneCore::ConfigItem.new(key: :version_file_name, - env_name: "", - description: "uploaded version filename", - optional: true), - FastlaneCore::ConfigItem.new(key: :access_key, - env_name: "S3_ACCESS_KEY", - description: "AWS Access Key ID ", - optional: true, - default_value: ENV['AWS_ACCESS_KEY_ID']), - FastlaneCore::ConfigItem.new(key: :secret_access_key, - env_name: "S3_SECRET_ACCESS_KEY", - description: "AWS Secret Access Key ", - optional: true, - default_value: ENV['AWS_SECRET_ACCESS_KEY']), - FastlaneCore::ConfigItem.new(key: :bucket, - env_name: "S3_BUCKET", - description: "AWS bucket name", - optional: true, - default_value: ENV['AWS_BUCKET_NAME']), - FastlaneCore::ConfigItem.new(key: :region, - env_name: "S3_REGION", - description: "AWS region (for bucket creation) ", - optional: true, - default_value: ENV['AWS_REGION']), - FastlaneCore::ConfigItem.new(key: :acl, - env_name: "S3_ACL", - description: "Uploaded object permissions e.g public_read (default), private, public_read_write, authenticated_read ", - optional: true, - default_value: "public-read"), - FastlaneCore::ConfigItem.new(key: :release_notes, - env_name: "S3_RELEASE_NOTES", - description: "Release notes", - type: String, - optional: true, - default_value: Actions.lane_context[SharedValues::RELEASE_NOTES]), FastlaneCore::ConfigItem.new(key: :treat_bucket_as_domain_name, description: "If it's true, it transforms all urls from https://s3.amazonaws.com/BUCKET_NAME to https://BUCKET_NAME", is_string: false, optional: true, - default_value: true) + default_value: true), + FastlaneCore::ConfigItem.new(key: :environment, + description: "Select environment, defaults to :production", + type: Symbol, + default_value: :production, + optional: true), + FastlaneCore::ConfigItem.new(key: :api_token, + env_name: "SHUTTLE_API_TOKEN", + description: "API Token for Shuttle", + verify_block: proc do |api_token| + UI.user_error!("No API token for Shuttle given, pass using `api_token: 'token'`") unless api_token and !api_token.empty? + end), + FastlaneCore::ConfigItem.new(key: :app_identifier, + description: "App identifier, either bundle id or package name", + type: String, + default_value: Actions.lane_context[SharedValues::APP_IDENTIFIER]), + FastlaneCore::ConfigItem.new(key: :prefix_schema, + env_name: "SHUTTLE_PREFIX_SCHEMA", + description: "Prefix schema in uploaded app", + default_value: Actions.lane_context[SharedValues::PREFIX_SCHEMA], + optional: true), + FastlaneCore::ConfigItem.new(key: :build_number, + description: "Build number, eg. 1337", + is_string: false, + default_value: Actions.lane_context[SharedValues::BUILD_NUMBER], + verify_block: proc do |build_number| + UI.user_error!("No value found for 'build_number'") unless build_number and build_number.kind_of? Integer + end) ] end def self.output [ ['S3_IPA_OUTPUT_PATH', 'Direct HTTP link to the uploaded ipa file'], ['S3_DSYM_OUTPUT_PATH', 'Direct HTTP link to the uploaded dsym file'], - ['S3_PLIST_OUTPUT_PATH', 'Direct HTTP link to the uploaded plist file'], ['S3_APK_OUTPUT_PATH', 'Direct HTTP link to the uploaded apk file'], - ['S3_MAPPING_OUTPUT_PATH', 'Direct HTTP link to the uploaded mapping.txt file'], - ['S3_HTML_OUTPUT_PATH', 'Direct HTTP link to the uploaded HTML file'], - ['S3_VERSION_OUTPUT_PATH', 'Direct HTTP link to the uploaded Version file'], - ['S3_ICON_OUTPUT_PATH', 'Direct HTTP link to the uploaded icon file'] + ['S3_MAPPING_OUTPUT_PATH', 'Direct HTTP link to the uploaded mapping.txt file'] ] end def self.author "joshdholtz"