fastlane-plugin-polidea 3.0.1 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2496e6471bff3fc0eddd90de1a381e635ac1682b867a6512b1c72b5581cb7d99
4
- data.tar.gz: 6f8463f55cc6cc95938139fcbfb24bf830ba77ed59feb824a456f4db0c9efb62
3
+ metadata.gz: 16148ca5e32380ed55cb696df4722d1f17692425705a1a457ecee71fec18ae2d
4
+ data.tar.gz: 6538bb9941484012d8881a883188b2155c8f38bad3968070c75f5423225e0524
5
5
  SHA512:
6
- metadata.gz: 61427c87fc09b20e799aabfdee82e77ccba2081b1d4cb371d3d97b12fdf0c098ad909b5cd1a6f0e4ab2724f74888b40f3f6a198aebc9babb2e01d6ff9373a108
7
- data.tar.gz: 249482c6403cde339d2e8abcd1c182dee57d4e6e7a28aba34ede81339976c1f96393598b6b05dfadc98aa6c6426635c4283678f86a97d6ce65fa85cc4a70c4fe
6
+ metadata.gz: cf3e04301c39c543817ece108bdf4f5fe441a905dfb7c015f67c7c27eb609e406a648419d21ebd3e58c5c09c60b5e86a70205f5490fa298a13e228b5634db358
7
+ data.tar.gz: d94d95510d05f9713461f2198b8445b927cbd83e70d0f72e93433803aac7ba1d596dae12cf48f6a47c81cc1265a8dabe698c5de91e226c91f65495aa605aafb7
data/README.md CHANGED
@@ -17,8 +17,6 @@ fastlane add_plugin polidea
17
17
  Plugin contains following actions:
18
18
  - `extract_app_info`: Extracts application name, icon, version, binary size from .apk/.ipa
19
19
  - `release_notes`: Extracts release notes from git tag message
20
- - `fota_s3`: Custom version of s3 action with Polidea's installation page
21
- - `fota_mail`: Custom version of mailgun action with Polidea's mail template
22
20
  - `shuttle`: Upload new app to Shuttle
23
21
 
24
22
  See more details [here](./docs/Actions.md).
@@ -33,10 +31,7 @@ fastlane actions
33
31
  lane :deploy do
34
32
  (some actions that produces .ipa/.apk)
35
33
  extract_app_info
36
- fota_s3
37
- fota_mail(
38
- to: "hey@polidea.com"
39
- )
34
+ shuttle
40
35
  end
41
36
  ```
42
37
 
@@ -44,6 +39,10 @@ Check out the [example `Fastfile`](fastlane/Fastfile) to see how to use this plu
44
39
 
45
40
  **Note to author:** Please set up a sample project to make it easy for users to explore what your plugin does. Provide everything that is necessary to try out the plugin in this project (including a sample Xcode/Android project if necessary)
46
41
 
42
+ ## Migration guide
43
+
44
+ See [MIGRATION.md](./MIGRATION.md)
45
+
47
46
  ## Troubleshooting
48
47
 
49
48
  If you have trouble using plugins, check out the [Plugins Troubleshooting](https://github.com/fastlane/fastlane/blob/master/fastlane/docs/PluginsTroubleshooting.md) doc in the main `fastlane` repo.
@@ -10,9 +10,11 @@ module Fastlane
10
10
  platform = Actions.lane_context[Actions::SharedValues::PLATFORM_NAME].to_sym
11
11
  options = {
12
12
  api_token: params[:api_token],
13
- plist_url: params[:plist_url],
13
+ ipa: params[:ipa],
14
+ apk: params[:apk],
15
+ dsym: params[:dsym],
16
+ mapping: params[:mapping],
14
17
  prefix_schema: params[:prefix_schema],
15
- apk_url: params[:apk_url],
16
18
  app_identifier: params[:app_identifier],
17
19
  app_version: params[:app_version],
18
20
  build_number: params[:build_number],
@@ -23,7 +25,9 @@ module Fastlane
23
25
  }
24
26
  validate(platform, options)
25
27
  config = get_config(platform, options)
26
- notify(platform, config)
28
+ client = Shuttle::Client.new(base_url(params[:environment]), params[:api_token])
29
+ notify(client, platform, config)
30
+ upload(client, platform, config)
27
31
 
28
32
  UI.success("Successfully uploaded to #{params[:app_identifier]} #{params[:app_version]}.#{params[:build_number]} to Shuttle")
29
33
 
@@ -54,21 +58,31 @@ module Fastlane
54
58
  verify_block: proc do |api_token|
55
59
  UI.user_error!("No API token for Shuttle given, pass using `api_token: 'token'`") unless api_token and !api_token.empty?
56
60
  end),
57
- FastlaneCore::ConfigItem.new(key: :plist_url,
58
- env_name: "SHUTTLE_PLIST_URL",
59
- description: "Url to uploaded plist",
60
- default_value: Actions.lane_context[SharedValues::S3_PLIST_OUTPUT_PATH],
61
- optional: true),
61
+ FastlaneCore::ConfigItem.new(key: :ipa,
62
+ env_name: "",
63
+ description: ".ipa file for the build",
64
+ optional: true,
65
+ default_value: Actions.lane_context[SharedValues::IPA_OUTPUT_PATH]),
66
+ FastlaneCore::ConfigItem.new(key: :dsym,
67
+ env_name: "",
68
+ description: "zipped .dsym package for the build",
69
+ optional: true,
70
+ default_value: Actions.lane_context[SharedValues::DSYM_OUTPUT_PATH]),
71
+ FastlaneCore::ConfigItem.new(key: :apk,
72
+ env_name: "",
73
+ description: ".apk file for the build",
74
+ optional: true,
75
+ default_value: Actions.lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH]),
76
+ FastlaneCore::ConfigItem.new(key: :mapping,
77
+ env_name: "",
78
+ description: "The path to the mapping.txt file",
79
+ optional: true,
80
+ default_value: Actions.lane_context[SharedValues::GRADLE_MAPPING_TXT_OUTPUT_PATH]),
62
81
  FastlaneCore::ConfigItem.new(key: :prefix_schema,
63
82
  env_name: "SHUTTLE_PREFIX_SCHEMA",
64
83
  description: "Prefix schema in uploaded app",
65
84
  default_value: Actions.lane_context[SharedValues::PREFIX_SCHEMA],
66
85
  optional: true),
67
- FastlaneCore::ConfigItem.new(key: :apk_url,
68
- env_name: "SHUTTLE_APK_URL",
69
- description: "Url to uploaded apk",
70
- default_value: Actions.lane_context[SharedValues::S3_APK_OUTPUT_PATH],
71
- optional: true),
72
86
  FastlaneCore::ConfigItem.new(key: :app_identifier,
73
87
  description: "App identifier, either bundle id or package name",
74
88
  type: String,
@@ -114,14 +128,14 @@ module Fastlane
114
128
 
115
129
  def self.validate(platform, params)
116
130
  case platform
117
- when :android
118
- apk_url = params[:apk_url]
119
- UI.user_error!("No apk url given, pass using `apk_url: 'url'` or make sure s3 action succeded and exposed S3_APK_OUTPUT_PATH shared value") unless apk_url and !apk_url.empty?
120
131
  when :ios
121
- plist_url = params[:plist_url]
122
- UI.user_error!("No plist url given, pass using `plist_url: 'url'` or make sure s3 action succeded and exposed S3_PLIST_OUTPUT_PATH shared value") unless plist_url and !plist_url.empty?
123
132
  url_scheme = params[:prefix_schema]
124
133
  UI.user_error!("No prefix scheme given. Make sure `add_prefix_schema` action succeded before build action") if url_scheme.nil?
134
+ ipa = params[:ipa]
135
+ UI.user_error!("No .ipa file path given. Make sure `gym` action succeded") if ipa.nil?
136
+ when :android
137
+ apk = params[:apk]
138
+ UI.user_error!("No .apk file path given. Make sure `gradle` action succeded") if apk.nil?
125
139
  end
126
140
  end
127
141
 
@@ -135,11 +149,13 @@ module Fastlane
135
149
  releaser = params[:releaser] || commit_author
136
150
 
137
151
  case platform
152
+ when :android
153
+ apk = params[:apk]
154
+ mapping = params[:mapping]
138
155
  when :ios
139
- href = itms_href(params[:plist_url])
140
156
  prefix_schema = params[:prefix_schema]
141
- when :android
142
- href = params[:apk_url]
157
+ ipa = params[:ipa]
158
+ dsym = params[:dsym]
143
159
  end
144
160
 
145
161
  {
@@ -147,18 +163,20 @@ module Fastlane
147
163
  app_identifier: app_identifier,
148
164
  app_version: app_version,
149
165
  build_number: build_number,
150
- href: href,
151
166
  release_notes: release_notes,
152
167
  releaser: releaser,
153
168
  binary_size: binary_size,
154
169
  prefix_schema: prefix_schema,
155
- environment: params[:environment]
170
+ environment: params[:environment],
171
+ ipa: ipa,
172
+ apk: apk,
173
+ mapping: mapping,
174
+ dsym: dsym
156
175
  }
157
176
  end
158
177
 
159
- def self.notify(platform, params)
178
+ def self.notify(client, platform, params)
160
179
  build = {
161
- href: params[:href],
162
180
  version: params[:app_version],
163
181
  releaseNotes: params[:release_notes],
164
182
  bytes: params[:binary_size],
@@ -168,12 +186,40 @@ module Fastlane
168
186
  build[:prefixSchema] = params[:prefix_schema] if platform == :ios
169
187
  build[:versionCode] = params[:build_number] if platform == :android
170
188
 
171
- client = Shuttle::Client.new(base_url(params[:environment]), params[:api_token])
172
-
173
189
  client.create_build(platform, params[:app_identifier], build)
174
190
  end
175
191
  private_class_method :notify
176
192
 
193
+ def self.upload(client, platform, params)
194
+ case platform
195
+ when :ios
196
+ upload_urls = client.get_upload_urls(platform, params[:app_identifier], params[:prefix_schema])
197
+ upload_build_url = upload_urls['buildUrl']
198
+ self.upload_file(upload_build_url, params[:ipa])
199
+ UI.success("Successfully uploaded ipa file")
200
+ if params[:dsym]
201
+ upload_dsym_url = upload_urls['debugFileUrl']
202
+ self.upload_file(upload_dsym_url, params[:dsym])
203
+ UI.success("Successfully uploaded dsym file")
204
+ end
205
+ when :android
206
+ upload_urls = client.get_upload_urls(platform, params[:app_identifier], params[:build_number])
207
+ upload_build_url = upload_urls['buildUrl']
208
+ self.upload_file(upload_build_url, params[:apk])
209
+ UI.success("Successfully uploaded apk file")
210
+ if params[:mapping]
211
+ upload_mapping_file_url = upload_urls['debugFileUrl']
212
+ self.upload_file(upload_mapping_file_url, params[:mapping])
213
+ UI.success("Successfully uploaded mapping file")
214
+ end
215
+ end
216
+ end
217
+
218
+ def self.upload_file(url, build)
219
+ client = S3::Client.new
220
+ client.upload_file(url, build)
221
+ end
222
+
177
223
  def self.base_url(environment)
178
224
  case environment
179
225
  when :production
@@ -184,11 +230,6 @@ module Fastlane
184
230
  end
185
231
  private_class_method :base_url
186
232
 
187
- def self.itms_href(plist_url)
188
- "itms-services://?action=download-manifest&url=#{URI.encode_www_form_component(plist_url)}"
189
- end
190
- private_class_method :itms_href
191
-
192
233
  def self.commit_author
193
234
  sh("git --no-pager show -s --format='%ae'", print_command: false, print_command_output: false).strip
194
235
  end
@@ -0,0 +1,30 @@
1
+ require 'faraday'
2
+ require 'json'
3
+
4
+ module Fastlane
5
+ module S3
6
+ class Client
7
+ def upload_file(url, file)
8
+ retry_options = {
9
+ max: 2,
10
+ interval: 0.05,
11
+ interval_randomness: 0.5,
12
+ backoff_factor: 2
13
+ }
14
+ connection = Faraday.new(
15
+ url
16
+ ) do |conn|
17
+ conn.request :retry, retry_options
18
+ conn.request :multipart
19
+ conn.response :raise_error
20
+ conn.adapter :net_http
21
+ end
22
+
23
+ connection.put(
24
+ '',
25
+ File.read(file)
26
+ )
27
+ end
28
+ end
29
+ end
30
+ end
@@ -21,19 +21,25 @@ module Fastlane
21
21
  ) do |conn|
22
22
  conn.request :retry, retry_options
23
23
  conn.request :multipart
24
-
25
24
  conn.response :raise_error
26
-
25
+ conn.response :json
27
26
  conn.adapter :net_http
28
27
  end
29
28
  end
30
29
 
31
30
  def create_build(platform, app_identifier, build)
32
31
  @conn.post(
33
- "cd/apps/#{platform}/#{app_identifier}/builds",
32
+ "cd/apps/#{platform}/#{app_identifier}/builds/v2",
34
33
  { build: build }.to_json
35
34
  )
36
35
  end
36
+
37
+ def get_upload_urls(platform, app_id, build_identifier)
38
+ response = @conn.get(
39
+ "projects/apps/#{platform}/#{app_id}/builds/#{build_identifier}/upload-url/v2"
40
+ )
41
+ return response.body
42
+ end
37
43
  end
38
44
  end
39
45
  end
@@ -1,5 +1,5 @@
1
1
  module Fastlane
2
2
  module Polidea
3
- VERSION = "3.0.1"
3
+ VERSION = "4.0.0"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,29 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fastlane-plugin-polidea
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.1
4
+ version: 4.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Piotrek Dubiel
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-10-13 00:00:00.000000000 Z
11
+ date: 2020-12-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ">="
17
+ - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '0'
19
+ version: 0.17.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - ">="
24
+ - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '0'
26
+ version: 0.17.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: plist
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -230,16 +230,16 @@ dependencies:
230
230
  name: simplecov
231
231
  requirement: !ruby/object:Gem::Requirement
232
232
  requirements:
233
- - - ">="
233
+ - - '='
234
234
  - !ruby/object:Gem::Version
235
- version: '0'
235
+ version: '0.17'
236
236
  type: :development
237
237
  prerelease: false
238
238
  version_requirements: !ruby/object:Gem::Requirement
239
239
  requirements:
240
- - - ">="
240
+ - - '='
241
241
  - !ruby/object:Gem::Version
242
- version: '0'
242
+ version: '0.17'
243
243
  - !ruby/object:Gem::Dependency
244
244
  name: fastlane
245
245
  requirement: !ruby/object:Gem::Requirement
@@ -265,8 +265,6 @@ files:
265
265
  - lib/fastlane/plugin/polidea.rb
266
266
  - lib/fastlane/plugin/polidea/actions/add_prefix_schema.rb
267
267
  - lib/fastlane/plugin/polidea/actions/extract_app_info.rb
268
- - lib/fastlane/plugin/polidea/actions/fota_mail.rb
269
- - lib/fastlane/plugin/polidea/actions/fota_s3.rb
270
268
  - lib/fastlane/plugin/polidea/actions/import_provisioning.rb
271
269
  - lib/fastlane/plugin/polidea/actions/release_notes.rb
272
270
  - lib/fastlane/plugin/polidea/actions/release_notes_from_commits.rb
@@ -275,6 +273,7 @@ files:
275
273
  - lib/fastlane/plugin/polidea/helper/mime.rb
276
274
  - lib/fastlane/plugin/polidea/helper/page_generator.rb
277
275
  - lib/fastlane/plugin/polidea/helper/qr_generator.rb
276
+ - lib/fastlane/plugin/polidea/helper/s3.rb
278
277
  - lib/fastlane/plugin/polidea/helper/shuttle.rb
279
278
  - lib/fastlane/plugin/polidea/version.rb
280
279
  - templates/install.erb
@@ -1,187 +0,0 @@
1
- require 'redcarpet'
2
- require 'fastlane/erb_template_helper'
3
-
4
- module Fastlane
5
- module Actions
6
- class FotaMailAction < Action
7
- def self.is_supported?(platform)
8
- true
9
- end
10
-
11
- def self.run(options)
12
- Fastlane::Polidea.session.action_launched("fota_mail", options)
13
-
14
- Actions.verify_gem!('premailer')
15
- require 'premailer'
16
- inline_images(options)
17
- mailgunit(options)
18
-
19
- Fastlane::Polidea.session.action_completed("fota_mail")
20
- UI.success "Mail sent!"
21
- end
22
-
23
- def self.description
24
- "Send a success/error message to an email group"
25
- end
26
-
27
- def self.available_options
28
- [
29
- FastlaneCore::ConfigItem.new(key: :postmaster,
30
- env_name: "MAILGUN_SANDBOX_POSTMASTER",
31
- description: "Mailgun sandbox domain postmaster for your mail"),
32
- FastlaneCore::ConfigItem.new(key: :apikey,
33
- env_name: "MAILGUN_APIKEY",
34
- description: "Mailgun apikey for your mail"),
35
- FastlaneCore::ConfigItem.new(key: :to,
36
- env_name: "MAILGUN_TO",
37
- description: "Destination of your mail"),
38
- FastlaneCore::ConfigItem.new(key: :from,
39
- env_name: "MAILGUN_FROM",
40
- optional: true,
41
- description: "Mailgun sender name",
42
- default_value: "Polidea"),
43
- FastlaneCore::ConfigItem.new(key: :success,
44
- env_name: "MAILGUN_SUCCESS",
45
- description: "Was this build successful? (true/false)",
46
- optional: true,
47
- default_value: true,
48
- is_string: false),
49
- FastlaneCore::ConfigItem.new(key: :ci_build_link,
50
- env_name: "MAILGUN_CI_BUILD_LINK",
51
- description: "CI Build Link",
52
- optional: true,
53
- is_string: true),
54
- FastlaneCore::ConfigItem.new(key: :template_path,
55
- env_name: "MAILGUN_TEMPLATE_PATH",
56
- description: "Mail HTML template",
57
- optional: true,
58
- is_string: true),
59
- FastlaneCore::ConfigItem.new(key: :platform,
60
- env_name: "MAILGUN_PLATFORM",
61
- description: "Platform used in mail template",
62
- optional: true,
63
- default_value: Actions.lane_context[Actions::SharedValues::PLATFORM_NAME]),
64
- FastlaneCore::ConfigItem.new(key: :app_icon,
65
- env_name: "MAILGUN_APP_ICON",
66
- description: "Path to app icon file",
67
- is_string: true,
68
- optional: true,
69
- default_value: Actions.lane_context[SharedValues::ICON_OUTPUT_PATH]),
70
- FastlaneCore::ConfigItem.new(key: :app_name,
71
- env_name: "MAILGUN_APP_NAME",
72
- description: "Application name",
73
- is_string: true,
74
- default_value: Actions.lane_context[SharedValues::APP_NAME]),
75
- FastlaneCore::ConfigItem.new(key: :app_version,
76
- env_name: "MAILGUN_APP_VERSION",
77
- description: "Application version",
78
- is_string: true,
79
- default_value: Actions.lane_context[SharedValues::APP_VERSION]),
80
- FastlaneCore::ConfigItem.new(key: :build_number,
81
- env_name: "MAILGUN_BUILD_NUMBER",
82
- description: "Build number",
83
- default_value: Actions.lane_context[SharedValues::BUILD_NUMBER]),
84
- FastlaneCore::ConfigItem.new(key: :installation_link,
85
- env_name: "MAILGUN_INSTALLATION_LINK",
86
- description: "Link to installation page",
87
- is_string: true,
88
- default_value: Actions.lane_context[SharedValues::S3_HTML_OUTPUT_PATH]),
89
- FastlaneCore::ConfigItem.new(key: :release_notes,
90
- env_name: "MAILGUN_RELEASE_NOTES",
91
- description: "Release notes",
92
- type: String,
93
- optional: true,
94
- default_value: Actions.lane_context[SharedValues::RELEASE_NOTES]),
95
- FastlaneCore::ConfigItem.new(key: :binary_size,
96
- env_name: "MAILGUN_BINARY_SIZE",
97
- description: "Binary size",
98
- type: Integer,
99
- default_value: Actions.lane_context[SharedValues::BINARY_SIZE]),
100
- FastlaneCore::ConfigItem.new(key: :inline,
101
- type: Array,
102
- optional: true,
103
- default_value: [])
104
- ]
105
- end
106
-
107
- def self.author
108
- "thiagolioy"
109
- end
110
-
111
- def self.inline_images(options)
112
- options[:inline] = [
113
- qr_code(options[:installation_link])
114
- ]
115
- options[:inline] << File.new(options[:app_icon]) if options[:app_icon]
116
- end
117
-
118
- def self.qr_code(installation_link)
119
- qr_code_path = "/tmp/qr_code.png"
120
- QRGenerator.new(installation_link, ::ChunkyPNG::Color.rgb(21, 18, 126)).generate(qr_code_path)
121
- File.new(qr_code_path)
122
- end
123
-
124
- def self.mailgunit(options)
125
- sandbox_domain = options[:postmaster].split("@").last
126
- body = mail_template(options)
127
-
128
- sh """curl -s --user 'api:#{options[:apikey]}' \
129
- https://api.mailgun.net/v3/#{sandbox_domain}/messages \
130
- -F from='#{options[:from]} <#{options[:postmaster]}>' \
131
- -F to='#{options[:to]}' \
132
- -F subject='#{options[:app_name]} #{options[:app_version]} (#{options[:build_number]}) for #{human_platform(options[:platform])} is ready to install' \
133
- --form-string html=#{Shellwords.escape(body)} \ """ + options[:inline].map { |file| "-F inline=@#{file.path}" }.join(" "), print_command: false, print_command_output: false
134
-
135
- return body
136
- end
137
-
138
- def self.mail_template(options)
139
- hash = {
140
- author: Actions.git_author_email,
141
- last_commit: Actions.last_git_commit_message,
142
- is_android: options[:platform] == :android,
143
- app_icon: options[:app_icon] ? File.basename(options[:app_icon]) : nil,
144
- app_name: options[:app_name],
145
- app_version: options[:app_version],
146
- build_number: options[:build_number],
147
- installation_link: options[:installation_link],
148
- release_notes: options[:release_notes],
149
- platform: options[:platform],
150
- release_date: DateTime.now.strftime('%b %d, %Y'),
151
- binary_size: (options[:binary_size] / 1024.0 / 1024.0).round(2).to_s,
152
- qr_code: "qr_code.png"
153
- }
154
- hash[:success] = options[:success]
155
- hash[:ci_build_link] = options[:ci_build_link]
156
-
157
- # create html from template
158
- html_template_path = options[:template_path]
159
- if html_template_path && File.exist?(html_template_path)
160
- eth = Fastlane::ErbTemplateHelper
161
- html_template = eth.load_from_path(html_template_path)
162
- mail_html = eth.render(html_template, hash)
163
- else
164
- mail_html = PageGenerator.mail(hash)
165
- end
166
-
167
- premailer = Premailer.new(
168
- mail_html,
169
- { warn_level: Premailer::Warnings::SAFE, with_html_string: true }
170
- )
171
- premailer.to_inline_css
172
- end
173
-
174
- def self.human_platform(platform)
175
- case platform
176
- when :ios
177
- "iOS"
178
- when :android
179
- "Android"
180
- else
181
- platform.to_s
182
- end
183
- end
184
- end
185
- end
186
- end
187
- # rubocop:enable Metrics/MethodLength
@@ -1,568 +0,0 @@
1
- # rubocop:disable Metrics/AbcSize
2
- # rubocop:disable Metrics/ClassLength
3
- require 'fastlane/erb_template_helper'
4
- require 'ostruct'
5
- require 'securerandom'
6
-
7
- module Fastlane
8
- module Actions
9
- module SharedValues
10
- S3_IPA_OUTPUT_PATH = :S3_IPA_OUTPUT_PATH
11
- S3_DSYM_OUTPUT_PATH = :S3_DSYM_OUTPUT_PATH
12
- S3_PLIST_OUTPUT_PATH = :S3_PLIST_OUTPUT_PATH
13
- S3_APK_OUTPUT_PATH = :S3_APK_OUTPUT_PATH
14
- S3_MAPPING_OUTPUT_PATH = :S3_MAPPING_OUTPUT_PATH
15
- S3_HTML_OUTPUT_PATH = :S3_HTML_OUTPUT_PATH
16
- S3_VERSION_OUTPUT_PATH = :S3_VERSION_OUTPUT_PATH
17
- S3_ICON_OUTPUT_PATH = :S3_ICON_OUTPUT_PATH
18
- end
19
-
20
- class FotaS3Action < Action
21
- def self.run(config)
22
- Fastlane::Polidea.session.action_launched("fota_s3", config)
23
-
24
- platform = Actions.lane_context[Actions::SharedValues::PLATFORM_NAME].to_sym
25
-
26
- # Calling fetch on config so that default values will be used
27
- params = {}
28
- params[:ipa] = config[:ipa]
29
- params[:apk] = config[:apk]
30
- params[:icon] = config[:icon]
31
- params[:dsym] = config[:dsym]
32
- params[:mapping] = config[:mapping]
33
- params[:access_key] = config[:access_key]
34
- params[:secret_access_key] = config[:secret_access_key]
35
- params[:bucket] = config[:bucket]
36
- params[:region] = config[:region]
37
- params[:acl] = config[:acl]
38
- params[:upload_metadata] = config[:upload_metadata]
39
- params[:plist_template_path] = config[:plist_template_path]
40
- params[:html_template_path] = config[:html_template_path]
41
- params[:html_file_name] = config[:html_file_name]
42
- params[:version_template_path] = config[:version_template_path]
43
- params[:version_file_name] = config[:version_file_name]
44
- params[:acl] = config[:acl]
45
- params[:release_notes] = config[:release_notes]
46
- params[:treat_bucket_as_domain_name] = config[:treat_bucket_as_domain_name]
47
-
48
- case platform
49
- when :ios
50
- upload_ios(params)
51
- when :android
52
- upload_android(params)
53
- end
54
-
55
- Fastlane::Polidea.session.action_completed("fota_s3")
56
- return true
57
- end
58
-
59
- def self.upload_ios(params)
60
- # Pulling parameters for other uses
61
- s3_region = params[:region]
62
- s3_subdomain = params[:region] ? "s3-#{params[:region]}" : "s3"
63
- s3_access_key = params[:access_key]
64
- s3_secret_access_key = params[:secret_access_key]
65
- s3_bucket = params[:bucket]
66
- ipa_file = params[:ipa]
67
- icon_file = params[:icon]
68
- dsym_file = params[:dsym]
69
- acl = params[:acl]
70
- release_notes = params[:release_notes]
71
- treat_bucket_as_domain_name = params[:treat_bucket_as_domain_name]
72
-
73
- validate(params)
74
- UI.user_error!("No IPA file path given, pass using `ipa: 'ipa path'`") unless ipa_file.to_s.length > 0
75
-
76
- UI.message("Will transform S3 urls from https://s3.amazonaws.com/#{s3_bucket} to https://#{s3_bucket}") if treat_bucket_as_domain_name
77
-
78
- bucket = get_bucket(s3_access_key, s3_secret_access_key, s3_region, s3_bucket)
79
-
80
- # Gets info used for the plist
81
- info = FastlaneCore::IpaFileAnalyser.fetch_info_plist_file(ipa_file)
82
-
83
- build_number = info['CFBundleVersion']
84
- bundle_id = info['CFBundleIdentifier']
85
- bundle_version = info['CFBundleShortVersionString']
86
- app_name = info['CFBundleDisplayName'] || info['CFBundleName']
87
- full_version = "#{bundle_version}.#{build_number}"
88
- url_part = get_url_part(app_name, "ios", bundle_version, build_number)
89
-
90
- plist_template_path = params[:plist_template_path]
91
- html_file_name = params[:html_file_name]
92
- version_template_path = params[:version_template_path]
93
- version_file_name = params[:version_file_name]
94
-
95
- ipa_file_basename = File.basename(ipa_file)
96
- ipa_file_name = "#{url_part}#{ipa_file_basename}"
97
- ipa_file_data = File.open(ipa_file, 'rb')
98
-
99
- ipa_url = self.upload_file(bucket, ipa_file_name, ipa_file_data, acl, treat_bucket_as_domain_name)
100
-
101
- # Setting action and environment variables
102
- Actions.lane_context[SharedValues::S3_IPA_OUTPUT_PATH] = ipa_url
103
- ENV[SharedValues::S3_IPA_OUTPUT_PATH.to_s] = ipa_url
104
-
105
- if dsym_file
106
- dsym_file_basename = File.basename(dsym_file)
107
- dsym_file_name = "#{url_part}#{dsym_file_basename}"
108
- dsym_file_data = File.open(dsym_file, 'rb')
109
-
110
- dsym_url = self.upload_file(bucket, dsym_file_name, dsym_file_data, acl, treat_bucket_as_domain_name)
111
-
112
- dsym_file_data.close
113
- end
114
-
115
- if params[:upload_metadata] == false
116
- return true
117
- end
118
-
119
- #####################################
120
- #
121
- # html and plist building
122
- #
123
- #####################################
124
-
125
- # Creating plist and html names
126
- plist_file_name = "#{url_part}manifest.plist"
127
- plist_url = "https://#{s3_subdomain}.amazonaws.com/#{s3_bucket}/#{plist_file_name}"
128
-
129
- html_file_name ||= "#{url_part}index.html"
130
- html_resources_name = "#{url_part}installation-page"
131
-
132
- version_file_name ||= "#{url_part}version.json"
133
-
134
- # grabs module
135
- eth = Fastlane::ErbTemplateHelper
136
-
137
- # Creates plist from template
138
- if plist_template_path && File.exist?(plist_template_path)
139
- plist_template = eth.load_from_path(plist_template_path)
140
- else
141
- plist_template = eth.load("s3_plist_template")
142
- end
143
- plist_render = eth.render(plist_template, {
144
- url: ipa_url,
145
- ipa_url: ipa_url,
146
- bundle_id: bundle_id,
147
- build_number: build_number,
148
- bundle_version: bundle_version,
149
- title: app_name
150
- })
151
-
152
- # Gets icon from ipa and uploads it
153
- icon_url = self.upload_icon(icon_file, url_part, bucket, acl, treat_bucket_as_domain_name)
154
-
155
- # Creates html from template
156
- html_render = PageGenerator.installation_page({
157
- url: "itms-services://?action=download-manifest&url=#{URI.encode_www_form_component(plist_url)}",
158
- app_version: bundle_version,
159
- build_number: build_number,
160
- app_name: app_name,
161
- app_icon: icon_url,
162
- platform: "ios",
163
- release_notes: release_notes
164
- })
165
-
166
- # Creates version from template
167
- if version_template_path && File.exist?(version_template_path)
168
- version_template = eth.load_from_path(version_template_path)
169
- else
170
- version_template = eth.load("s3_version_template")
171
- end
172
- version_render = eth.render(version_template, {
173
- url: plist_url,
174
- plist_url: plist_url,
175
- ipa_url: ipa_url,
176
- build_number: build_number,
177
- bundle_version: bundle_version,
178
- full_version: full_version
179
- })
180
-
181
- #####################################
182
- #
183
- # html and plist uploading
184
- #
185
- #####################################
186
-
187
- plist_url = self.upload_file(bucket, plist_file_name, plist_render, acl, treat_bucket_as_domain_name)
188
- html_url = self.upload_file(bucket, html_file_name, html_render, acl, treat_bucket_as_domain_name)
189
- self.upload_directory(bucket, html_resources_name, "#{__dir__}/../templates/installation-page", acl)
190
- version_url = self.upload_file(bucket, version_file_name, version_render, acl, treat_bucket_as_domain_name)
191
-
192
- # Setting action and environment variables
193
- Actions.lane_context[SharedValues::S3_PLIST_OUTPUT_PATH] = plist_url
194
- ENV[SharedValues::S3_PLIST_OUTPUT_PATH.to_s] = plist_url
195
-
196
- Actions.lane_context[SharedValues::S3_HTML_OUTPUT_PATH] = html_url
197
- ENV[SharedValues::S3_HTML_OUTPUT_PATH.to_s] = html_url
198
-
199
- Actions.lane_context[SharedValues::S3_VERSION_OUTPUT_PATH] = version_url
200
- ENV[SharedValues::S3_VERSION_OUTPUT_PATH.to_s] = version_url
201
-
202
- UI.success("Successfully uploaded ipa file to '#{Actions.lane_context[SharedValues::S3_IPA_OUTPUT_PATH]}'")
203
- UI.success("Successfully uploaded plist file to '#{Actions.lane_context[SharedValues::S3_PLIST_OUTPUT_PATH]}'")
204
- UI.success("Successfully uploaded html file to '#{Actions.lane_context[SharedValues::S3_HTML_OUTPUT_PATH]}'")
205
- UI.success("Successfully uploaded version file to '#{Actions.lane_context[SharedValues::S3_VERSION_OUTPUT_PATH]}'")
206
-
207
- if icon_url
208
- Actions.lane_context[SharedValues::S3_ICON_OUTPUT_PATH] = icon_url
209
- ENV[SharedValues::S3_ICON_OUTPUT_PATH.to_s] = icon_url
210
- UI.success("Successfully uploaded icon file to '#{Actions.lane_context[SharedValues::S3_ICON_OUTPUT_PATH]}'")
211
- end
212
- if dsym_url
213
- Actions.lane_context[SharedValues::S3_DSYM_OUTPUT_PATH] = dsym_url
214
- ENV[SharedValues::S3_DSYM_OUTPUT_PATH.to_s] = dsym_url
215
- UI.success("Successfully uploaded dsym file to '#{Actions.lane_context[SharedValues::S3_DSYM_OUTPUT_PATH]}'")
216
- end
217
- end
218
-
219
- def self.upload_android(params)
220
- # Pulling parameters for other uses
221
- s3_region = params[:region]
222
- s3_access_key = params[:access_key]
223
- s3_secret_access_key = params[:secret_access_key]
224
- s3_bucket = params[:bucket]
225
- apk_file = params[:apk]
226
- icon_file = params[:icon]
227
- mapping_file = params[:mapping]
228
- acl = params[:acl]
229
- release_notes = params[:release_notes]
230
- treat_bucket_as_domain_name = params[:treat_bucket_as_domain_name]
231
-
232
- validate(params)
233
- UI.user_error!("No APK file path given, pass using `apk: 'apk path'`") unless apk_file.to_s.length > 0
234
-
235
- UI.message("Will transform S3 urls from https://s3.amazonaws.com/#{s3_bucket} to https://#{s3_bucket}") if treat_bucket_as_domain_name
236
-
237
- bucket = get_bucket(s3_access_key, s3_secret_access_key, s3_region, s3_bucket)
238
-
239
- # Gets info used from the apk manifest
240
- manifest = Android::Apk.new(apk_file).manifest
241
-
242
- app_name = manifest.label
243
- build_number = manifest.version_code
244
- app_version = manifest.version_name
245
- url_part = get_url_part(app_name, "android", app_version, build_number)
246
-
247
- html_file_name = params[:html_file_name]
248
-
249
- apk_file_basename = File.basename(apk_file)
250
- apk_file_name = "#{url_part}#{apk_file_basename}"
251
- apk_file_data = File.open(apk_file, 'rb')
252
-
253
- apk_url = self.upload_file(bucket, apk_file_name, apk_file_data, acl, treat_bucket_as_domain_name)
254
-
255
- # Setting action and environment variables
256
- Actions.lane_context[SharedValues::S3_APK_OUTPUT_PATH] = apk_url
257
- ENV[SharedValues::S3_APK_OUTPUT_PATH.to_s] = apk_url
258
-
259
- if mapping_file
260
- mapping_file_basename = File.basename(mapping_file)
261
- mapping_file_name = "#{url_part}#{mapping_file_basename}"
262
- mapping_file_data = File.open(mapping_file, 'rb')
263
-
264
- mapping_url = self.upload_file(bucket, mapping_file_name, mapping_file_data, acl, treat_bucket_as_domain_name)
265
-
266
- # Setting action and environment variables
267
- Actions.lane_context[SharedValues::S3_MAPPING_OUTPUT_PATH] = mapping_url
268
- ENV[SharedValues::S3_MAPPING_OUTPUT_PATH.to_s] = mapping_url
269
-
270
- mapping_file_data.close
271
- end
272
-
273
- #####################################
274
- #
275
- # html building
276
- #
277
- #####################################
278
-
279
- # Creating html names
280
-
281
- html_file_name ||= "#{url_part}index.html"
282
- html_resources_name = "#{url_part}installation-page"
283
-
284
- # Gets icon from ipa and uploads it
285
- icon_url = self.upload_icon(icon_file, url_part, bucket, acl, treat_bucket_as_domain_name)
286
-
287
- # Creates html from template
288
- html_render = PageGenerator.installation_page({
289
- url: apk_url,
290
- app_version: app_version,
291
- build_number: build_number,
292
- app_name: app_name,
293
- app_icon: icon_url,
294
- platform: "android",
295
- release_notes: release_notes
296
- })
297
-
298
- html_url = self.upload_file(bucket, html_file_name, html_render, acl, treat_bucket_as_domain_name)
299
- self.upload_directory(bucket, html_resources_name, "#{__dir__}/../templates/installation-page", acl)
300
-
301
- Actions.lane_context[SharedValues::S3_HTML_OUTPUT_PATH] = html_url
302
- ENV[SharedValues::S3_HTML_OUTPUT_PATH.to_s] = html_url
303
-
304
- UI.success("Successfully uploaded apk file to '#{Actions.lane_context[SharedValues::S3_APK_OUTPUT_PATH]}'")
305
- UI.success("Successfully uploaded html file to '#{Actions.lane_context[SharedValues::S3_HTML_OUTPUT_PATH]}'")
306
-
307
- if icon_url
308
- Actions.lane_context[SharedValues::S3_ICON_OUTPUT_PATH] = icon_url
309
- ENV[SharedValues::S3_ICON_OUTPUT_PATH.to_s] = icon_url
310
- UI.success("Successfully uploaded icon file to '#{Actions.lane_context[SharedValues::S3_ICON_OUTPUT_PATH]}'")
311
- end
312
- if mapping_url
313
- UI.success("Successfully uploaded mapping file to '#{Actions.lane_context[SharedValues::S3_MAPPING_OUTPUT_PATH]}'")
314
- end
315
- end
316
-
317
- def self.validate(params)
318
- s3_access_key = params[:access_key]
319
- s3_secret_access_key = params[:secret_access_key]
320
- s3_bucket = params[:bucket]
321
-
322
- UI.user_error!("No S3 access key given, pass using `access_key: 'key'`") unless s3_access_key.to_s.length > 0
323
- 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
324
- UI.user_error!("No S3 bucket given, pass using `bucket: 'bucket'`") unless s3_bucket.to_s.length > 0
325
- end
326
-
327
- def self.get_url_part(app_name, platform, app_version, build_number)
328
- random_part = SecureRandom.hex(10)
329
- "#{app_name}/#{platform}/#{app_version}_#{build_number}/#{random_part}/"
330
- end
331
-
332
- def self.get_bucket(s3_access_key, s3_secret_access_key, s3_region, s3_bucket)
333
- self.s3_client(s3_access_key, s3_secret_access_key, s3_region).bucket(s3_bucket)
334
- end
335
-
336
- def self.s3_client(s3_access_key, s3_secret_access_key, s3_region)
337
- Actions.verify_gem!('aws-sdk-s3')
338
- require 'aws-sdk-s3'
339
-
340
- if s3_region
341
- s3_client = Aws::S3::Resource.new(
342
- access_key_id: s3_access_key,
343
- secret_access_key: s3_secret_access_key,
344
- region: s3_region
345
- )
346
- else
347
- s3_client = Aws::S3::Resource.new(
348
- access_key_id: s3_access_key,
349
- secret_access_key: s3_secret_access_key
350
- )
351
- end
352
- s3_client
353
- end
354
-
355
- def self.upload_file(bucket, file_name, file_data, acl, treat_bucket_as_domain_name)
356
- obj = bucket.put_object({
357
- key: file_name,
358
- body: file_data,
359
- acl: acl,
360
- content_type: Mime.content_type_for_file(file_name)
361
- })
362
-
363
- # When you enable versioning on a S3 bucket,
364
- # writing to an object will create an object version
365
- # instead of replacing the existing object.
366
- # http://docs.aws.amazon.com/AWSRubySDK/latest/AWS/S3/ObjectVersion.html
367
- if obj.kind_of? Aws::S3::ObjectVersion
368
- obj = obj.object
369
- end
370
-
371
- if treat_bucket_as_domain_name
372
- # Return public url
373
- shorten_url(obj.public_url.to_s)
374
- else
375
- obj.public_url.to_s
376
- end
377
- end
378
-
379
- def self.upload_directory(bucket, directory_name, directory_path, acl)
380
- files = files_at_path(directory_path)
381
-
382
- files.each do |file|
383
- local_path = directory_path + file
384
- s3_path = directory_name + file
385
-
386
- bucket.put_object({
387
- key: s3_path,
388
- body: File.open(local_path),
389
- acl: acl,
390
- content_type: Mime.content_type_for_file(local_path)
391
- })
392
- end
393
- end
394
-
395
- def self.files_at_path(path)
396
- files = Dir.glob(path + "/**/*")
397
- to_remove = []
398
- files.each do |file|
399
- if File.directory?(file)
400
- to_remove.push file
401
- else
402
- file.slice! path
403
- end
404
- end
405
- to_remove.each do |file|
406
- files.delete file
407
- end
408
- return files
409
- end
410
-
411
- #
412
- # NOT a fan of this as this was taken straight from Shenzhen
413
- # https://github.com/nomad/shenzhen/blob/986792db5d4d16a80c865a2748ee96ba63644821/lib/shenzhen/plugins/s3.rb#L32
414
- #
415
- # Need to find a way to not use this copied method
416
- #
417
- # AGAIN, I am not happy about this right now.
418
- # Using this for prototype reasons.
419
- #
420
- def self.expand_path_with_substitutions_from_ipa_plist(ipa, path)
421
- substitutions = path.scan(/\{CFBundle[^}]+\}/)
422
- return path if substitutions.empty?
423
- info = FastlaneCore::IpaFileAnalyser.fetch_info_plist_file(ipa) or return path
424
-
425
- substitutions.uniq.each do |substitution|
426
- key = substitution[1...-1]
427
- value = info[key]
428
- path.gsub!(Regexp.new(substitution), value) if value
429
- end
430
-
431
- return path
432
- end
433
-
434
- def self.upload_icon(icon_path, url_part, bucket, acl, treat_bucket_as_domain_name)
435
- return unless icon_path
436
- icon_file_basename = File.basename(icon_path)
437
- icon_file = File.open(icon_path)
438
- icon_file_name = "#{url_part}#{icon_file_basename}"
439
- self.upload_file(bucket, icon_file_name, icon_file, acl, treat_bucket_as_domain_name)
440
- end
441
-
442
- def self.shorten_url(url)
443
- uri = URI.parse(url)
444
- uri.scheme + ':/' + uri.path
445
- end
446
-
447
- def self.description
448
- "Generates a plist file and uploads all to AWS S3"
449
- end
450
-
451
- def self.available_options
452
- [
453
- FastlaneCore::ConfigItem.new(key: :ipa,
454
- env_name: "",
455
- description: ".ipa file for the build",
456
- optional: true,
457
- default_value: Actions.lane_context[SharedValues::IPA_OUTPUT_PATH]),
458
- FastlaneCore::ConfigItem.new(key: :dsym,
459
- env_name: "",
460
- description: "zipped .dsym package for the build",
461
- optional: true,
462
- default_value: Actions.lane_context[SharedValues::DSYM_OUTPUT_PATH]),
463
- FastlaneCore::ConfigItem.new(key: :apk,
464
- env_name: "",
465
- description: ".apk file for the build",
466
- optional: true,
467
- default_value: Actions.lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH]),
468
- FastlaneCore::ConfigItem.new(key: :mapping,
469
- env_name: "",
470
- description: "The path to the mapping.txt file",
471
- optional: true,
472
- default_value: Actions.lane_context[SharedValues::GRADLE_MAPPING_TXT_OUTPUT_PATH]),
473
- FastlaneCore::ConfigItem.new(key: :icon,
474
- env_name: "",
475
- description: "app icon file to upload",
476
- optional: true,
477
- default_value: Actions.lane_context[SharedValues::ICON_OUTPUT_PATH]),
478
- FastlaneCore::ConfigItem.new(key: :upload_metadata,
479
- env_name: "",
480
- description: "Upload relevant metadata for this build",
481
- optional: true,
482
- default_value: true,
483
- is_string: false),
484
- FastlaneCore::ConfigItem.new(key: :plist_template_path,
485
- env_name: "",
486
- description: "plist template path",
487
- optional: true),
488
- FastlaneCore::ConfigItem.new(key: :html_template_path,
489
- env_name: "",
490
- description: "html erb template path",
491
- optional: true),
492
- FastlaneCore::ConfigItem.new(key: :html_file_name,
493
- env_name: "",
494
- description: "uploaded html filename",
495
- optional: true),
496
- FastlaneCore::ConfigItem.new(key: :version_template_path,
497
- env_name: "",
498
- description: "version erb template path",
499
- optional: true),
500
- FastlaneCore::ConfigItem.new(key: :version_file_name,
501
- env_name: "",
502
- description: "uploaded version filename",
503
- optional: true),
504
- FastlaneCore::ConfigItem.new(key: :access_key,
505
- env_name: "S3_ACCESS_KEY",
506
- description: "AWS Access Key ID ",
507
- optional: true,
508
- default_value: ENV['AWS_ACCESS_KEY_ID']),
509
- FastlaneCore::ConfigItem.new(key: :secret_access_key,
510
- env_name: "S3_SECRET_ACCESS_KEY",
511
- description: "AWS Secret Access Key ",
512
- optional: true,
513
- default_value: ENV['AWS_SECRET_ACCESS_KEY']),
514
- FastlaneCore::ConfigItem.new(key: :bucket,
515
- env_name: "S3_BUCKET",
516
- description: "AWS bucket name",
517
- optional: true,
518
- default_value: ENV['AWS_BUCKET_NAME']),
519
- FastlaneCore::ConfigItem.new(key: :region,
520
- env_name: "S3_REGION",
521
- description: "AWS region (for bucket creation) ",
522
- optional: true,
523
- default_value: ENV['AWS_REGION']),
524
- FastlaneCore::ConfigItem.new(key: :acl,
525
- env_name: "S3_ACL",
526
- description: "Uploaded object permissions e.g public_read (default), private, public_read_write, authenticated_read ",
527
- optional: true,
528
- default_value: "public-read"),
529
- FastlaneCore::ConfigItem.new(key: :release_notes,
530
- env_name: "S3_RELEASE_NOTES",
531
- description: "Release notes",
532
- type: String,
533
- optional: true,
534
- default_value: Actions.lane_context[SharedValues::RELEASE_NOTES]),
535
- FastlaneCore::ConfigItem.new(key: :treat_bucket_as_domain_name,
536
- description: "If it's true, it transforms all urls from https://s3.amazonaws.com/BUCKET_NAME to https://BUCKET_NAME",
537
- is_string: false,
538
- optional: true,
539
- default_value: true)
540
- ]
541
- end
542
-
543
- def self.output
544
- [
545
- ['S3_IPA_OUTPUT_PATH', 'Direct HTTP link to the uploaded ipa file'],
546
- ['S3_DSYM_OUTPUT_PATH', 'Direct HTTP link to the uploaded dsym file'],
547
- ['S3_PLIST_OUTPUT_PATH', 'Direct HTTP link to the uploaded plist file'],
548
- ['S3_APK_OUTPUT_PATH', 'Direct HTTP link to the uploaded apk file'],
549
- ['S3_MAPPING_OUTPUT_PATH', 'Direct HTTP link to the uploaded mapping.txt file'],
550
- ['S3_HTML_OUTPUT_PATH', 'Direct HTTP link to the uploaded HTML file'],
551
- ['S3_VERSION_OUTPUT_PATH', 'Direct HTTP link to the uploaded Version file'],
552
- ['S3_ICON_OUTPUT_PATH', 'Direct HTTP link to the uploaded icon file']
553
- ]
554
- end
555
-
556
- def self.author
557
- "joshdholtz"
558
- end
559
-
560
- def self.is_supported?(platform)
561
- [:ios, :android].include? platform
562
- end
563
- end
564
- end
565
- end
566
- # rubocop:enable Metrics/AbcSize
567
- # rubocop:enable Metrics/MethodLength
568
- # rubocop:enable Metrics/ClassLength