fastlane-plugin-amazon_appstore 1.2.0 → 1.4.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: fc301533c2007325736e250da85e0efa77f6c24dc9e96575b660252dfe1fdfc1
4
- data.tar.gz: cc8fb420eefe04b69812b2cfa6b0a24b158f90713de4521183d24670022d6b8c
3
+ metadata.gz: 42700d0bc41d91c1a7cebc2f4d5583f703b2532ab72581f6a96e3c2e1a9508e8
4
+ data.tar.gz: 0b180a4dafa671749b7f3b95cac6c6991e602230d0aab9f0ce979fa281ecdd92
5
5
  SHA512:
6
- metadata.gz: f317b197eef47b6d805949fb5cb89d8e41dd4d10da35d5bd30cbb28d19f766320ad85ce594312b136832761ff83e16615b60ce436fda7b3820aa0c3f04b6ba2a
7
- data.tar.gz: 1ba515e18f5f67027b1843aa5c571bb90d3c9b24db1027d1d8627cbdc9ba5b78cc0650d79d2c5fd70218baa10fc7e7a15a99d8c0bfb16ee291bd41f783aadb05
6
+ metadata.gz: 2157e233420340003117f553e20187bff0ab2a66eda203188dd292529e0667f3c77e4ba80a62e3f30e7ba85684ddf787385d2fd92a9d7412f4ba7456d0b9616e
7
+ data.tar.gz: 2973cac2d292082f30358107c7017ea7e9efda6d477a320f16d4797022503b98c01acbb2b9b9a5f8d28abc7d07834bfffa1f03d333b5e7945571de05a07e8d4d
data/README.md CHANGED
@@ -15,8 +15,6 @@ fastlane add_plugin amazon_appstore
15
15
 
16
16
  Upload the apk to the Amazon Appstore using the [App Submission API](https://developer.amazon.com/docs/app-submission-api/overview.html).
17
17
 
18
- In the future, it would be nice to be able to use it to update store information like `upload_to_play_store`, but for now, it only supports replacing apk and submitting it for review.
19
-
20
18
  ## Usage
21
19
 
22
20
  Following the [guide](https://developer.amazon.com/docs/app-submission-api/auth.html), you will need to generate `client_id` and `client_secret` to access the console in advance.
@@ -35,20 +33,24 @@ upload_to_amazon_appstore(
35
33
  | Key | Description | Default |
36
34
  | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------- |
37
35
  | package_name | The package name of the application to use | * |
38
- | apk | Path to the APK file to upload | |
36
+ | apk | Path to the APK file to upload (optional if apk_paths is provided) | |
37
+ | apk_paths | An array of paths to APK files to upload (optional if apk is provided) | |
39
38
  | client_id | The client ID you saved | |
40
39
  | client_secret | The client secret you saved | |
41
40
  | skip_upload_changelogs | Whether to skip uploading changelogs | false |
42
41
  | metadata_path | Path to the directory containing the metadata files | ./fastlane/metadata/android |
43
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 |
44
43
  | overwrite_upload | Whether to allow overwriting an existing upload | false |
44
+ | 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
45
  | timeout | Timeout for read, open (in seconds) | 300 |
46
46
  * = default value is dependent on the user's system
47
47
 
48
48
  ### Changelogs
49
49
 
50
50
  You can update the release notes by adding a file under `changelogs/` in the same way as [supply](https://docs.fastlane.tools/actions/upload_to_play_store/).
51
- The filename should exactly match the version code of the APK that it represents. You can also provide default notes that will be used if no files match the version code by adding a default.txt file.
51
+ The filename should exactly match the version code of the APK that it represents. You can also provide default notes that will be used if no files match the version code by adding a default.txt file.
52
+
53
+ When uploading multiple APKs with different version codes, the plugin will use the changelog from the highest version code, following the same approach as Fastlane's `upload_to_play_store` action.
52
54
 
53
55
  ```
54
56
  └── fastlane
@@ -22,51 +22,79 @@ module Fastlane
22
22
  UI.abort_with_message!("Failed to get token") if token.nil?
23
23
 
24
24
  if params[:overwrite_upload]
25
- UI.message("Deleting existing edits if needed (overwrite_upload: true)...")
25
+ if params[:overwrite_upload_mode] == "new"
26
+ UI.message("Deleting existing edits if needed (overwrite_upload: true, overwrite_upload_mode: new)...")
27
+ begin
28
+ Helper::AmazonAppstoreHelper.delete_edits_if_exists(
29
+ app_id: params[:package_name],
30
+ token: token
31
+ )
32
+ rescue StandardError => e
33
+ UI.error(e.message)
34
+ UI.abort_with_message!("Failed to delete edits (overwrite_upload: true, overwrite_upload_mode: new)")
35
+ end
36
+ elsif params[:overwrite_upload_mode] == "reuse"
37
+ UI.message("Retrieving active edit (overwrite_upload: true, overwrite_upload_mode: reuse)...")
38
+ begin
39
+ edit_id, = Helper::AmazonAppstoreHelper.get_edits(
40
+ app_id: params[:package_name],
41
+ token: token
42
+ )
43
+ rescue StandardError => e
44
+ UI.error(e.message)
45
+ UI.abort_with_message!("Failed to get edit_id (overwrite_upload: true, overwrite_upload_mode: reuse)")
46
+ end
47
+ UI.message("No active edit") if edit_id.nil?
48
+ end
49
+ end
50
+
51
+ if edit_id.nil?
52
+ UI.message("Creating new edits...")
26
53
  begin
27
- Helper::AmazonAppstoreHelper.delete_edits_if_exists(
54
+ edit_id = Helper::AmazonAppstoreHelper.create_edits(
28
55
  app_id: params[:package_name],
29
56
  token: token
30
57
  )
31
58
  rescue StandardError => e
32
59
  UI.error(e.message)
33
- UI.abort_with_message!("Failed to delete edits (overwrite_upload: true)")
60
+ UI.abort_with_message!("Failed to create edits")
34
61
  end
62
+ UI.abort_with_message!("Failed to get edit_id") if edit_id.nil?
35
63
  end
36
64
 
37
- UI.message("Creating new edits...")
38
- begin
39
- edit_id = Helper::AmazonAppstoreHelper.create_edits(
40
- app_id: params[:package_name],
41
- token: token
42
- )
43
- rescue StandardError => e
44
- UI.error(e.message)
45
- UI.abort_with_message!("Failed to create edits")
65
+ apks = []
66
+ apks << params[:apk] if params[:apk]
67
+ apks += params[:apk_paths] if params[:apk_paths]
68
+
69
+ if apks.empty?
70
+ UI.abort_with_message!("No APK files provided. Please provide either 'apk' or 'apk_paths' parameter")
46
71
  end
47
- UI.abort_with_message!("Failed to get edit_id") if edit_id.nil?
48
72
 
49
- UI.message("Replacing apk...")
73
+ UI.message("Replacing APKs with #{apks.length} file(s)...")
50
74
  begin
51
- version_code = Helper::AmazonAppstoreHelper.replace_apk(
52
- local_apk_path: params[:apk],
75
+ apk_results = Helper::AmazonAppstoreHelper.replace_apks(
76
+ apk_paths: apks,
53
77
  app_id: params[:package_name],
54
78
  edit_id: edit_id,
55
79
  token: token
56
80
  )
57
81
  rescue StandardError => e
58
82
  UI.error(e.message)
59
- UI.abort_with_message!("Failed to replace apk")
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]}")
60
89
  end
61
- UI.abort_with_message!("Failed to get version_code") if version_code.nil?
62
90
 
63
91
  UI.message("Updating release notes...")
64
92
  begin
65
- Helper::AmazonAppstoreHelper.update_listings(
93
+ Helper::AmazonAppstoreHelper.update_listings_for_multiple_apks(
66
94
  app_id: params[:package_name],
67
95
  edit_id: edit_id,
68
96
  token: token,
69
- version_code: version_code,
97
+ version_codes: version_codes,
70
98
  skip_upload_changelogs: params[:skip_upload_changelogs],
71
99
  metadata_path: params[:metadata_path]
72
100
  )
@@ -132,8 +160,13 @@ module Fastlane
132
160
  FastlaneCore::ConfigItem.new(key: :apk,
133
161
  env_name: "AMAZON_APPSTORE_APK",
134
162
  description: "The path of the apk file",
135
- optional: false,
163
+ optional: true,
136
164
  type: String),
165
+ FastlaneCore::ConfigItem.new(key: :apk_paths,
166
+ env_name: "AMAZON_APPSTORE_APK_PATHS",
167
+ description: "An array of paths to APK files to upload",
168
+ optional: true,
169
+ type: Array),
137
170
  FastlaneCore::ConfigItem.new(key: :skip_upload_changelogs,
138
171
  env_name: "AMAZON_APPSTORE_SKIP_UPLOAD_CHANGELOGS",
139
172
  description: "Whether to skip uploading changelogs",
@@ -158,6 +191,13 @@ module Fastlane
158
191
  default_value: false,
159
192
  optional: true,
160
193
  type: Boolean),
194
+ FastlaneCore::ConfigItem.new(key: :overwrite_upload_mode,
195
+ env_name: "AMAZON_APPSTORE_OVERWRITE_UPLOAD_MODE",
196
+ description: "Upload strategy. Can be 'new' or 'reuse'",
197
+ default_value: 'new',
198
+ verify_block: proc do |value|
199
+ UI.user_error!("overwrite_upload can only be 'new' or 'reuse'") unless %w(new reuse).include?(value)
200
+ end),
161
201
  FastlaneCore::ConfigItem.new(key: :timeout,
162
202
  env_name: "AMAZON_APPSTORE_TIMEOUT",
163
203
  description: "Timeout for read, open (in seconds)",
@@ -47,6 +47,19 @@ module Fastlane
47
47
  end
48
48
 
49
49
  def self.delete_edits_if_exists(app_id:, token:)
50
+ edits_id, etag = self.get_edits(app_id: app_id, token: token)
51
+ return nil if edits_id.nil? || etag.nil? # Do nothing if edits do not exist
52
+
53
+ edits_path = "api/appstore/v1/applications/#{app_id}/edits"
54
+ delete_edits_response = api_client.delete("#{edits_path}/#{edits_id}") do |request|
55
+ request.headers['Authorization'] = "Bearer #{token}"
56
+ request.headers['If-Match'] = etag
57
+ end
58
+
59
+ raise StandardError, delete_edits_response.body unless delete_edits_response.success?
60
+ end
61
+
62
+ def self.get_edits(app_id:, token:)
50
63
  edits_path = "api/appstore/v1/applications/#{app_id}/edits"
51
64
  edits_response = api_client.get(edits_path) do |request|
52
65
  request.headers['Authorization'] = "Bearer #{token}"
@@ -55,27 +68,103 @@ module Fastlane
55
68
 
56
69
  edits_id = edits_response.body[:id]
57
70
  etag = edits_response.headers['Etag']
58
- return nil if edits_id.nil? || etag.nil? # Do nothing if edits do not exist
59
71
 
60
- delete_edits_response = api_client.delete("#{edits_path}/#{edits_id}") do |request|
72
+ return edits_id, etag
73
+ end
74
+
75
+ def self.upload_apk(local_apk_path:, app_id:, edit_id:, token:)
76
+ upload_apk_path = "api/appstore/v1/applications/#{app_id}/edits/#{edit_id}/apks/upload"
77
+ upload_apk_response = api_client.post(upload_apk_path) do |request|
78
+ request.body = Faraday::UploadIO.new(local_apk_path, 'application/vnd.android.package-archive')
79
+ request.headers['Content-Length'] = request.body.stat.size.to_s
80
+ request.headers['Content-Type'] = 'application/vnd.android.package-archive'
61
81
  request.headers['Authorization'] = "Bearer #{token}"
62
- request.headers['If-Match'] = etag
63
82
  end
83
+ raise StandardError, upload_apk_response.body unless upload_apk_response.success?
64
84
 
65
- raise StandardError, delete_edits_response.body unless delete_edits_response.success?
85
+ {
86
+ version_code: upload_apk_response.body[:versionCode],
87
+ apk_id: upload_apk_response.body[:id]
88
+ }
66
89
  end
67
90
 
68
- def self.replace_apk(local_apk_path:, app_id:, edit_id:, token:)
91
+ def self.replace_apks(apk_paths:, app_id:, edit_id:, token:)
92
+ # Get existing APKs in the edit
69
93
  get_apks_path = "api/appstore/v1/applications/#{app_id}/edits/#{edit_id}/apks"
70
94
  get_apks_response = api_client.get(get_apks_path) do |request|
71
95
  request.headers['Authorization'] = "Bearer #{token}"
72
96
  end
73
97
  raise StandardError, get_apks_response.body unless get_apks_response.success?
74
98
 
75
- first_apk = get_apks_response.body[0]
76
- apk_id = first_apk[:id]
77
- raise StandardError, 'apk_id is nil' if apk_id.nil?
99
+ existing_apks = get_apks_response.body
100
+ raise StandardError, 'No existing APKs found in edit' if existing_apks.empty?
101
+
102
+ version_codes = []
103
+ apk_results = []
104
+
105
+ apk_paths.each_with_index do |apk_path, index|
106
+ if index < existing_apks.length
107
+ # Replace existing APK at the specified index
108
+ apk_id = existing_apks[index][:id]
109
+ raise StandardError, "apk_id is nil for index #{index}" if apk_id.nil?
110
+
111
+ # Get ETag for the specific APK
112
+ get_etag_path = "api/appstore/v1/applications/#{app_id}/edits/#{edit_id}/apks/#{apk_id}"
113
+ etag_response = api_client.get(get_etag_path) do |request|
114
+ request.headers['Authorization'] = "Bearer #{token}"
115
+ end
116
+ raise StandardError, etag_response.body unless etag_response.success?
117
+
118
+ etag = etag_response.headers['Etag']
119
+
120
+ # Replace the APK
121
+ replace_apk_path = "api/appstore/v1/applications/#{app_id}/edits/#{edit_id}/apks/#{apk_id}/replace"
122
+ replace_apk_response = api_client.put(replace_apk_path) do |request|
123
+ request.body = Faraday::UploadIO.new(apk_path, 'application/vnd.android.package-archive')
124
+ request.headers['Content-Length'] = request.body.stat.size.to_s
125
+ request.headers['Content-Type'] = 'application/vnd.android.package-archive'
126
+ request.headers['Authorization'] = "Bearer #{token}"
127
+ request.headers['If-Match'] = etag
128
+ end
129
+ raise StandardError, replace_apk_response.body unless replace_apk_response.success?
130
+
131
+ version_code = replace_apk_response.body[:versionCode]
132
+ version_codes << version_code
133
+ apk_results << { version_code: version_code, apk_id: apk_id }
134
+ else
135
+ # Upload new APK if there are more APK paths than existing APKs
136
+ result = upload_apk(
137
+ local_apk_path: apk_path,
138
+ app_id: app_id,
139
+ edit_id: edit_id,
140
+ token: token
141
+ )
142
+ version_codes << result[:version_code]
143
+ apk_results << result
144
+ end
145
+ end
146
+
147
+ # Delete remaining APKs if there are more existing APKs than specified APK paths
148
+ if existing_apks.length > apk_paths.length
149
+ remaining_apks = existing_apks[apk_paths.length..]
150
+ UI.message("Deleting #{remaining_apks.length} remaining APK(s)...")
78
151
 
152
+ remaining_apks.each_with_index do |apk, index|
153
+ delete_apk(
154
+ app_id: app_id,
155
+ edit_id: edit_id,
156
+ apk_id: apk[:id],
157
+ token: token
158
+ )
159
+ UI.message("Deleted APK ID: #{apk[:id]} (position #{apk_paths.length + index + 1})")
160
+ end
161
+ end
162
+
163
+ apk_results
164
+ end
165
+
166
+ def self.delete_apk(app_id:, edit_id:, apk_id:, token:)
167
+ # Get ETag for the APK to be deleted
79
168
  get_etag_path = "api/appstore/v1/applications/#{app_id}/edits/#{edit_id}/apks/#{apk_id}"
80
169
  etag_response = api_client.get(get_etag_path) do |request|
81
170
  request.headers['Authorization'] = "Bearer #{token}"
@@ -84,17 +173,57 @@ module Fastlane
84
173
 
85
174
  etag = etag_response.headers['Etag']
86
175
 
87
- replace_apk_path = "api/appstore/v1/applications/#{app_id}/edits/#{edit_id}/apks/#{apk_id}/replace"
88
- replace_apk_response = api_client.put(replace_apk_path) do |request|
89
- request.body = Faraday::UploadIO.new(local_apk_path, 'application/vnd.android.package-archive')
90
- request.headers['Content-Length'] = request.body.stat.size.to_s
91
- request.headers['Content-Type'] = 'application/vnd.android.package-archive'
176
+ # Delete the APK
177
+ delete_apk_path = "api/appstore/v1/applications/#{app_id}/edits/#{edit_id}/apks/#{apk_id}"
178
+ delete_apk_response = api_client.delete(delete_apk_path) do |request|
92
179
  request.headers['Authorization'] = "Bearer #{token}"
93
180
  request.headers['If-Match'] = etag
94
181
  end
95
- raise StandardError, replace_apk_response.body unless replace_apk_response.success?
182
+ raise StandardError, delete_apk_response.body unless delete_apk_response.success?
183
+
184
+ UI.message("Successfully deleted APK #{apk_id}")
185
+ end
186
+
187
+ def self.update_listings_for_multiple_apks(app_id:, edit_id:, token:, version_codes:, skip_upload_changelogs:, metadata_path:)
188
+ return if skip_upload_changelogs
189
+
190
+ UI.message("Updating listings for #{version_codes.length} version codes: #{version_codes.join(', ')}")
191
+
192
+ # Get listings once with ETag
193
+ listings_path = "api/appstore/v1/applications/#{app_id}/edits/#{edit_id}/listings"
194
+ listings_response = api_client.get(listings_path) do |request|
195
+ request.headers['Authorization'] = "Bearer #{token}"
196
+ end
197
+ raise StandardError, listings_response.body unless listings_response.success?
198
+
199
+ # Process each language once
200
+ listings_response.body[:listings].each do |lang, listing|
201
+ # Get fresh ETag for each language update to avoid conflicts
202
+ etag_response = api_client.get(listings_path) do |request|
203
+ request.headers['Authorization'] = "Bearer #{token}"
204
+ end
205
+ raise StandardError, etag_response.body unless etag_response.success?
96
206
 
97
- replace_apk_response.body[:versionCode]
207
+ etag = etag_response.headers['Etag']
208
+
209
+ # Find the best changelog for multiple version codes
210
+ recent_changes = find_changelog_for_multiple_version_codes(
211
+ language: listing[:language],
212
+ version_codes: version_codes,
213
+ metadata_path: metadata_path
214
+ )
215
+ listing[:recentChanges] = recent_changes
216
+
217
+ # Update listings once per language
218
+ update_listings_path = "api/appstore/v1/applications/#{app_id}/edits/#{edit_id}/listings/#{lang}"
219
+ update_listings_response = api_client.put(update_listings_path) do |request|
220
+ request.body = listing.to_json
221
+ request.headers['Authorization'] = "Bearer #{token}"
222
+ request.headers['If-Match'] = etag
223
+ end
224
+ raise StandardError, update_listings_response.body unless update_listings_response.success?
225
+ end
226
+ nil
98
227
  end
99
228
 
100
229
  def self.update_listings(app_id:, edit_id:, token:, version_code:, skip_upload_changelogs:, metadata_path:)
@@ -173,6 +302,18 @@ module Fastlane
173
302
  end
174
303
  private_class_method :auth_client
175
304
 
305
+ def self.find_changelog_for_multiple_version_codes(language:, version_codes:, metadata_path:)
306
+ # Use the highest version code's changelog (same as Fastlane's approach)
307
+ max_version_code = version_codes.max
308
+ UI.message("Using changelog for highest version code: #{max_version_code}")
309
+ find_changelog(
310
+ language: language,
311
+ version_code: max_version_code,
312
+ skip_upload_changelogs: false,
313
+ metadata_path: metadata_path
314
+ )
315
+ end
316
+
176
317
  def self.find_changelog(language:, version_code:, skip_upload_changelogs:, metadata_path:)
177
318
  # The Amazon appstore requires you to enter changelogs before reviewing.
178
319
  # Therefore, if there is no metadata, hyphen text is returned.
@@ -1,5 +1,5 @@
1
1
  module Fastlane
2
2
  module AmazonAppstore
3
- VERSION = "1.2.0"
3
+ VERSION = "1.4.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.2.0
4
+ version: 1.4.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-07-19 00:00:00.000000000 Z
11
+ date: 2025-08-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -222,7 +222,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
222
222
  - !ruby/object:Gem::Version
223
223
  version: '0'
224
224
  requirements: []
225
- rubygems_version: 3.5.9
225
+ rubygems_version: 3.5.22
226
226
  signing_key:
227
227
  specification_version: 4
228
228
  summary: Upload apps to Amazon Appstore