fastlane-plugin-amazon_appstore 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 42700d0bc41d91c1a7cebc2f4d5583f703b2532ab72581f6a96e3c2e1a9508e8
4
- data.tar.gz: 0b180a4dafa671749b7f3b95cac6c6991e602230d0aab9f0ce979fa281ecdd92
3
+ metadata.gz: 4329f021e36ea416d3dbe494fea656223f7681f01fdaf8c12c067850bd66d042
4
+ data.tar.gz: 5015e8ed07f219d28398b38d4c684db0ddae6342a84270a02cea9c001015041b
5
5
  SHA512:
6
- metadata.gz: 2157e233420340003117f553e20187bff0ab2a66eda203188dd292529e0667f3c77e4ba80a62e3f30e7ba85684ddf787385d2fd92a9d7412f4ba7456d0b9616e
7
- data.tar.gz: 2973cac2d292082f30358107c7017ea7e9efda6d477a320f16d4797022503b98c01acbb2b9b9a5f8d28abc7d07834bfffa1f03d333b5e7945571de05a07e8d4d
6
+ metadata.gz: 4b213a4b4855aa766bb8a5bd789a1c8cfae4ea1a6786d66986828442672b96030b45bc881bb30e8bcd513437396229f004ababe31c6b817b399f4cc23d9eb117
7
+ data.tar.gz: 5bee681d6820d171c9a09c33da5ac3306e3583156748128684c88596d69159a5b4e1858a9a2adf3f90c68a24264cb6f1c06eb56750aaac4fcda4972e8e490fd0
data/README.md CHANGED
@@ -30,19 +30,23 @@ upload_to_amazon_appstore(
30
30
  ```
31
31
 
32
32
  ### Parameters
33
- | Key | Description | Default |
34
- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------- |
35
- | package_name | The package name of the application to use | * |
33
+ | Key | Description | Default |
34
+ | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------- |
35
+ | package_name | The package name of the application to use | * |
36
36
  | apk | Path to the APK file to upload (optional if apk_paths is provided) | |
37
37
  | apk_paths | An array of paths to APK files to upload (optional if apk is provided) | |
38
- | client_id | The client ID you saved | |
39
- | client_secret | The client secret you saved | |
40
- | skip_upload_changelogs | Whether to skip uploading changelogs | false |
41
- | metadata_path | Path to the directory containing the metadata files | ./fastlane/metadata/android |
42
- | changes_not_sent_for_review | Indicates that the changes in this edit will not be reviewed until they are explicitly sent for review from the Amazon Appstore Console UI | false |
38
+ | client_id | The client ID you saved | |
39
+ | client_secret | The client secret you saved | |
40
+ | skip_upload_apk | Whether to skip uploading APK | false |
41
+ | skip_upload_metadata | Whether to skip uploading metadata (title, descriptions) | false |
42
+ | skip_upload_changelogs | Whether to skip uploading changelogs | false |
43
+ | skip_upload_images | Whether to skip uploading images (icons, promo images) | true |
44
+ | skip_upload_screenshots | Whether to skip uploading screenshots | true |
45
+ | metadata_path | Path to the directory containing the metadata files | ./fastlane/metadata/android |
46
+ | changes_not_sent_for_review | Indicates that the changes in this edit will not be reviewed until they are explicitly sent for review from the Amazon Appstore Console UI | false |
43
47
  | overwrite_upload | Whether to allow overwriting an existing upload | false |
44
48
  | overwrite_upload_mode | Upload strategy when overwrite_upload is true. Can be 'new' (delete existing edit and create new) or 'reuse' (reuse existing edit) | new |
45
- | timeout | Timeout for read, open (in seconds) | 300 |
49
+ | timeout | Timeout for read, open (in seconds) | 300 |
46
50
  * = default value is dependent on the user's system
47
51
 
48
52
  ### Changelogs
@@ -70,6 +74,74 @@ When uploading multiple APKs with different version codes, the plugin will use t
70
74
  One difference from Google Play is that the Amazon Appstore always requires release notes to be entered before review.
71
75
  For this reason, `-` will be entered by default if the corresponding changelogs file is not found, or if the `skip_upload_changelogs` parameter is used.
72
76
 
77
+ ### Metadata (Title and Descriptions)
78
+
79
+ You can update store listing metadata (title, short description, full description) by adding text files in the metadata directory. The plugin uses the same structure as Google Play's [supply](https://docs.fastlane.tools/actions/upload_to_play_store/).
80
+
81
+ ```
82
+ └── fastlane
83
+ └── metadata
84
+ └── android
85
+ ├── en-US
86
+ │ ├── title.txt
87
+ │ ├── short_description.txt
88
+ │ └── full_description.txt
89
+ └── ja-JP
90
+ ├── title.txt
91
+ ├── short_description.txt
92
+ └── full_description.txt
93
+ ```
94
+
95
+ ### Images and Screenshots
96
+
97
+ You can upload images (icons, promo images) and screenshots by placing them in the `images/` directory. The plugin supports the Google Play metadata structure and maps it to Amazon Appstore image types.
98
+
99
+ ```
100
+ └── fastlane
101
+ └── metadata
102
+ └── android
103
+ └── en-US
104
+ └── images
105
+ ├── icon.png → small-icons (114x114)
106
+ ├── large_icon.png → large-icons (512x512)
107
+ ├── featureGraphic.png → promo-images (1024x500)
108
+ ├── phoneScreenshots/ → screenshots
109
+ │ ├── 1.png
110
+ │ └── 2.png
111
+ ├── tvBanner.png → firetv-icons
112
+ ├── tvBackground.png → firetv-backgrounds
113
+ └── tvScreenshots/ → firetv-screenshots
114
+ ├── 1.png
115
+ └── 2.png
116
+ ```
117
+
118
+ To enable image uploads, set `skip_upload_images: false` and/or `skip_upload_screenshots: false`:
119
+
120
+ ```ruby
121
+ upload_to_amazon_appstore(
122
+ apk: "app/build/outputs/apk/release/app-release.apk",
123
+ client_id: <YOUR_CLIENT_ID>,
124
+ client_secret: <YOUR_CLIENT_SECRET>,
125
+ skip_upload_images: false,
126
+ skip_upload_screenshots: false
127
+ )
128
+ ```
129
+
130
+ ### Metadata-only Upload
131
+
132
+ You can upload only metadata (without APK) by using `skip_upload_apk: true`:
133
+
134
+ ```ruby
135
+ upload_to_amazon_appstore(
136
+ client_id: <YOUR_CLIENT_ID>,
137
+ client_secret: <YOUR_CLIENT_SECRET>,
138
+ skip_upload_apk: true,
139
+ skip_upload_metadata: false,
140
+ skip_upload_images: false,
141
+ skip_upload_screenshots: false
142
+ )
143
+ ```
144
+
73
145
  ## Run tests for this plugin
74
146
 
75
147
  To run both the tests, and code style validation, run
@@ -62,30 +62,33 @@ module Fastlane
62
62
  UI.abort_with_message!("Failed to get edit_id") if edit_id.nil?
63
63
  end
64
64
 
65
- apks = []
66
- apks << params[:apk] if params[:apk]
67
- apks += params[:apk_paths] if params[:apk_paths]
65
+ version_codes = []
68
66
 
69
- if apks.empty?
70
- UI.abort_with_message!("No APK files provided. Please provide either 'apk' or 'apk_paths' parameter")
71
- end
67
+ unless params[:skip_upload_apk]
68
+ apks = []
69
+ apks << params[:apk] if params[:apk]
70
+ apks += params[:apk_paths] if params[:apk_paths]
72
71
 
73
- UI.message("Replacing APKs with #{apks.length} file(s)...")
74
- begin
75
- apk_results = Helper::AmazonAppstoreHelper.replace_apks(
76
- apk_paths: apks,
77
- app_id: params[:package_name],
78
- edit_id: edit_id,
79
- token: token
80
- )
81
- rescue StandardError => e
82
- UI.error(e.message)
83
- UI.abort_with_message!("Failed to replace APKs")
84
- end
85
- # Extract version codes and display results
86
- version_codes = apk_results.map { |result| result[:version_code] }
87
- apk_results.each_with_index do |result, index|
88
- UI.message("Successfully processed APK #{index + 1} with version code: #{result[:version_code]}")
72
+ if apks.empty?
73
+ UI.abort_with_message!("No APK files provided. Please provide either 'apk' or 'apk_paths' parameter")
74
+ end
75
+
76
+ UI.message("Replacing APKs with #{apks.length} file(s)...")
77
+ begin
78
+ apk_results = Helper::AmazonAppstoreHelper.replace_apks(
79
+ apk_paths: apks,
80
+ app_id: params[:package_name],
81
+ edit_id: edit_id,
82
+ token: token
83
+ )
84
+ rescue StandardError => e
85
+ UI.error(e.message)
86
+ UI.abort_with_message!("Failed to replace APKs")
87
+ end
88
+ version_codes = apk_results.map { |result| result[:version_code] }
89
+ apk_results.each_with_index do |result, index|
90
+ UI.message("Successfully processed APK #{index + 1} with version code: #{result[:version_code]}")
91
+ end
89
92
  end
90
93
 
91
94
  UI.message("Updating release notes...")
@@ -103,6 +106,10 @@ module Fastlane
103
106
  UI.abort_with_message!("Failed to update listings")
104
107
  end
105
108
 
109
+ upload_metadata(params, edit_id, token) unless params[:skip_upload_metadata]
110
+ upload_images(params, edit_id, token) unless params[:skip_upload_images]
111
+ upload_screenshots(params, edit_id, token) unless params[:skip_upload_screenshots]
112
+
106
113
  if params[:changes_not_sent_for_review]
107
114
  UI.success('Successfully finished the upload to Amazon Appstore')
108
115
  return
@@ -123,6 +130,89 @@ module Fastlane
123
130
  UI.success('Successfully finished the upload to Amazon Appstore')
124
131
  end
125
132
 
133
+ def self.upload_metadata(params, edit_id, token)
134
+ UI.message("Uploading metadata...")
135
+ languages = available_languages(params[:metadata_path])
136
+ languages.each do |language|
137
+ metadata = Helper::AmazonAppstoreHelper.load_metadata_from_files(
138
+ metadata_path: params[:metadata_path],
139
+ language: language
140
+ )
141
+ next if metadata.values.all?(&:nil?)
142
+
143
+ begin
144
+ Helper::AmazonAppstoreHelper.update_listing_metadata(
145
+ app_id: params[:package_name],
146
+ edit_id: edit_id,
147
+ language: language,
148
+ listing_data: metadata,
149
+ token: token
150
+ )
151
+ UI.message("Updated metadata for #{language}")
152
+ rescue StandardError => e
153
+ UI.error("Failed to update metadata for #{language}: #{e.message}")
154
+ end
155
+ end
156
+ end
157
+
158
+ def self.upload_images(params, edit_id, token)
159
+ UI.message("Uploading images...")
160
+ upload_image_assets(params, edit_id, token, %w[small-icons large-icons promo-images firetv-icons firetv-backgrounds])
161
+ end
162
+
163
+ def self.upload_screenshots(params, edit_id, token)
164
+ UI.message("Uploading screenshots...")
165
+ upload_image_assets(params, edit_id, token, %w[screenshots firetv-screenshots])
166
+ end
167
+
168
+ def self.upload_image_assets(params, edit_id, token, image_types)
169
+ languages = available_languages(params[:metadata_path])
170
+
171
+ languages.each do |language|
172
+ image_types.each do |image_type|
173
+ images = Helper::AmazonAppstoreHelper.find_images_for_type(
174
+ metadata_path: params[:metadata_path],
175
+ language: language,
176
+ image_type: image_type
177
+ )
178
+ next if images.empty?
179
+
180
+ begin
181
+ Helper::AmazonAppstoreHelper.delete_all_images(
182
+ app_id: params[:package_name],
183
+ edit_id: edit_id,
184
+ language: language,
185
+ image_type: image_type,
186
+ token: token
187
+ )
188
+ rescue StandardError => e
189
+ UI.message("Failed to delete existing #{image_type} for #{language}: #{e.message}")
190
+ end
191
+
192
+ images.each do |image_path|
193
+ Helper::AmazonAppstoreHelper.upload_image(
194
+ app_id: params[:package_name],
195
+ edit_id: edit_id,
196
+ language: language,
197
+ image_type: image_type,
198
+ image_path: image_path,
199
+ token: token
200
+ )
201
+ UI.message("Uploaded #{image_type} for #{language}: #{File.basename(image_path)}")
202
+ rescue StandardError => e
203
+ UI.error("Failed to upload #{image_type} for #{language}: #{e.message}")
204
+ end
205
+ end
206
+ end
207
+ end
208
+
209
+ def self.available_languages(metadata_path)
210
+ return [] unless File.directory?(metadata_path)
211
+
212
+ Dir.entries(metadata_path)
213
+ .select { |entry| File.directory?(File.join(metadata_path, entry)) && !entry.start_with?('.') }
214
+ end
215
+
126
216
  def self.description
127
217
  "Upload apps to Amazon Appstore"
128
218
  end
@@ -167,12 +257,36 @@ module Fastlane
167
257
  description: "An array of paths to APK files to upload",
168
258
  optional: true,
169
259
  type: Array),
260
+ FastlaneCore::ConfigItem.new(key: :skip_upload_apk,
261
+ env_name: "AMAZON_APPSTORE_SKIP_UPLOAD_APK",
262
+ description: "Whether to skip uploading APK",
263
+ default_value: false,
264
+ optional: true,
265
+ type: Boolean),
266
+ FastlaneCore::ConfigItem.new(key: :skip_upload_metadata,
267
+ env_name: "AMAZON_APPSTORE_SKIP_UPLOAD_METADATA",
268
+ description: "Whether to skip uploading metadata (title, descriptions)",
269
+ default_value: false,
270
+ optional: true,
271
+ type: Boolean),
170
272
  FastlaneCore::ConfigItem.new(key: :skip_upload_changelogs,
171
273
  env_name: "AMAZON_APPSTORE_SKIP_UPLOAD_CHANGELOGS",
172
274
  description: "Whether to skip uploading changelogs",
173
275
  default_value: false,
174
276
  optional: true,
175
277
  type: Boolean),
278
+ FastlaneCore::ConfigItem.new(key: :skip_upload_images,
279
+ env_name: "AMAZON_APPSTORE_SKIP_UPLOAD_IMAGES",
280
+ description: "Whether to skip uploading images (icons, promo images)",
281
+ default_value: true,
282
+ optional: true,
283
+ type: Boolean),
284
+ FastlaneCore::ConfigItem.new(key: :skip_upload_screenshots,
285
+ env_name: "AMAZON_APPSTORE_SKIP_UPLOAD_SCREENSHOTS",
286
+ description: "Whether to skip uploading screenshots",
287
+ default_value: true,
288
+ optional: true,
289
+ type: Boolean),
176
290
  FastlaneCore::ConfigItem.new(key: :metadata_path,
177
291
  env_name: "AMAZON_APPSTORE_METADATA_PATH",
178
292
  description: "Path to the directory containing the metadata files",
@@ -7,7 +7,7 @@ module Fastlane
7
7
  UI = FastlaneCore::UI unless Fastlane.const_defined?(:UI)
8
8
 
9
9
  module Helper
10
- class AmazonAppstoreHelper
10
+ class AmazonAppstoreHelper # rubocop:disable Metrics/ClassLength
11
11
  BASE_URL = 'https://developer.amazon.com'
12
12
  AUTH_URL = 'https://api.amazon.com/auth/o2/token'
13
13
 
@@ -186,6 +186,7 @@ module Fastlane
186
186
 
187
187
  def self.update_listings_for_multiple_apks(app_id:, edit_id:, token:, version_codes:, skip_upload_changelogs:, metadata_path:)
188
188
  return if skip_upload_changelogs
189
+ return if version_codes.empty?
189
190
 
190
191
  UI.message("Updating listings for #{version_codes.length} version codes: #{version_codes.join(', ')}")
191
192
 
@@ -260,6 +261,142 @@ module Fastlane
260
261
  nil
261
262
  end
262
263
 
264
+ def self.upload_image(app_id:, edit_id:, language:, image_type:, image_path:, token:)
265
+ images_path = "api/appstore/v1/applications/#{app_id}/edits/#{edit_id}/listings/#{language}/#{image_type}"
266
+ etag_response = api_client.get(images_path) do |request|
267
+ request.headers['Authorization'] = "Bearer #{token}"
268
+ end
269
+ raise StandardError, etag_response.body unless etag_response.success?
270
+
271
+ etag = etag_response.headers['Etag']
272
+ upload_path = "#{images_path}/upload"
273
+ upload_response = api_client.post(upload_path) do |request|
274
+ request.body = File.binread(image_path)
275
+ request.headers['Content-Length'] = request.body.bytesize.to_s
276
+ request.headers['Content-Type'] = 'application/octet-stream'
277
+ request.headers['Authorization'] = "Bearer #{token}"
278
+ request.headers['If-Match'] = etag
279
+ end
280
+ raise StandardError, upload_response.body unless upload_response.success?
281
+
282
+ upload_response.body[:id]
283
+ end
284
+
285
+ def self.get_images(app_id:, edit_id:, language:, image_type:, token:)
286
+ images_path = "api/appstore/v1/applications/#{app_id}/edits/#{edit_id}/listings/#{language}/#{image_type}"
287
+ images_response = api_client.get(images_path) do |request|
288
+ request.headers['Authorization'] = "Bearer #{token}"
289
+ end
290
+ raise StandardError, images_response.body unless images_response.success?
291
+
292
+ images_response.body
293
+ end
294
+
295
+ def self.delete_all_images(app_id:, edit_id:, language:, image_type:, token:)
296
+ images_path = "api/appstore/v1/applications/#{app_id}/edits/#{edit_id}/listings/#{language}/#{image_type}"
297
+ etag_response = api_client.get(images_path) do |request|
298
+ request.headers['Authorization'] = "Bearer #{token}"
299
+ end
300
+ raise StandardError, etag_response.body unless etag_response.success?
301
+
302
+ etag = etag_response.headers['Etag']
303
+ delete_response = api_client.delete(images_path) do |request|
304
+ request.headers['Authorization'] = "Bearer #{token}"
305
+ request.headers['If-Match'] = etag
306
+ end
307
+ raise StandardError, delete_response.body unless delete_response.success?
308
+
309
+ nil
310
+ end
311
+
312
+ def self.upload_video(app_id:, edit_id:, language:, video_path:, token:)
313
+ videos_path = "api/appstore/v1/applications/#{app_id}/edits/#{edit_id}/listings/#{language}/videos"
314
+ etag_response = api_client.get(videos_path) do |request|
315
+ request.headers['Authorization'] = "Bearer #{token}"
316
+ end
317
+ raise StandardError, etag_response.body unless etag_response.success?
318
+
319
+ etag = etag_response.headers['Etag']
320
+ upload_response = api_client.post(videos_path) do |request|
321
+ request.body = File.binread(video_path)
322
+ request.headers['Content-Length'] = request.body.bytesize.to_s
323
+ request.headers['Content-Type'] = 'application/octet-stream'
324
+ request.headers['Authorization'] = "Bearer #{token}"
325
+ request.headers['If-Match'] = etag
326
+ end
327
+ raise StandardError, upload_response.body unless upload_response.success?
328
+
329
+ upload_response.body[:id]
330
+ end
331
+
332
+ def self.update_listing_metadata(app_id:, edit_id:, language:, listing_data:, token:)
333
+ listings_path = "api/appstore/v1/applications/#{app_id}/edits/#{edit_id}/listings/#{language}"
334
+ etag_response = api_client.get(listings_path) do |request|
335
+ request.headers['Authorization'] = "Bearer #{token}"
336
+ end
337
+ raise StandardError, etag_response.body unless etag_response.success?
338
+
339
+ etag = etag_response.headers['Etag']
340
+ existing_data = etag_response.body
341
+ merged_data = existing_data.merge(listing_data.compact)
342
+ merged_data[:recentChanges] = '-' if merged_data[:recentChanges].nil? || merged_data[:recentChanges].empty?
343
+
344
+ update_response = api_client.put(listings_path) do |request|
345
+ request.body = merged_data.to_json
346
+ request.headers['Content-Type'] = 'application/json'
347
+ request.headers['Authorization'] = "Bearer #{token}"
348
+ request.headers['If-Match'] = etag
349
+ end
350
+ raise StandardError, update_response.body unless update_response.success?
351
+
352
+ nil
353
+ end
354
+
355
+ def self.load_metadata_from_files(metadata_path:, language:)
356
+ lang_path = File.join(metadata_path, language)
357
+ {
358
+ title: read_metadata_file(lang_path, 'title.txt'),
359
+ shortDescription: read_metadata_file(lang_path, 'short_description.txt'),
360
+ fullDescription: read_metadata_file(lang_path, 'full_description.txt')
361
+ }
362
+ end
363
+
364
+ def self.read_metadata_file(lang_path, filename)
365
+ path = File.join(lang_path, filename)
366
+ return nil unless File.exist?(path)
367
+
368
+ File.read(path, encoding: 'UTF-8').strip
369
+ end
370
+ private_class_method :read_metadata_file
371
+
372
+ IMAGE_TYPE_MAPPING = {
373
+ 'screenshots' => 'phoneScreenshots',
374
+ 'small-icons' => 'icon.png',
375
+ 'large-icons' => 'large_icon.png',
376
+ 'promo-images' => 'featureGraphic.png',
377
+ 'firetv-icons' => 'tvBanner.png',
378
+ 'firetv-backgrounds' => 'tvBackground.png',
379
+ 'firetv-screenshots' => 'tvScreenshots',
380
+ 'firetv-featured-backgrounds' => 'tvFeaturedBackground.png',
381
+ 'firetv-featured-logos' => 'tvFeaturedLogo.png'
382
+ }.freeze
383
+
384
+ def self.find_images_for_type(metadata_path:, language:, image_type:)
385
+ images_path = File.join(metadata_path, language, 'images')
386
+ mapping = IMAGE_TYPE_MAPPING[image_type]
387
+ return [] if mapping.nil?
388
+
389
+ target_path = File.join(images_path, mapping)
390
+
391
+ if File.directory?(target_path)
392
+ Dir.glob(File.join(target_path, '*.{png,jpg,jpeg}')).sort
393
+ elsif File.exist?(target_path)
394
+ [target_path]
395
+ else
396
+ []
397
+ end
398
+ end
399
+
263
400
  def self.commit_edits(app_id:, edit_id:, token:)
264
401
  get_etag_path = "api/appstore/v1/applications/#{app_id}/edits/#{edit_id}"
265
402
  etag_response = api_client.get(get_etag_path) do |request|
@@ -1,5 +1,5 @@
1
1
  module Fastlane
2
2
  module AmazonAppstore
3
- VERSION = "1.4.0"
3
+ VERSION = "1.5.0"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fastlane-plugin-amazon_appstore
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.0
4
+ version: 1.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - ntsk
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-08-30 00:00:00.000000000 Z
11
+ date: 2026-03-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -215,7 +215,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
215
215
  requirements:
216
216
  - - ">="
217
217
  - !ruby/object:Gem::Version
218
- version: '2.6'
218
+ version: '2.7'
219
219
  required_rubygems_version: !ruby/object:Gem::Requirement
220
220
  requirements:
221
221
  - - ">="