mysigner 0.1.1 → 0.1.3

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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.githooks/pre-commit +15 -0
  3. data/.githooks/pre-push +21 -0
  4. data/.github/workflows/ci.yml +29 -0
  5. data/.gitignore +1 -0
  6. data/.rubocop.yml +55 -0
  7. data/.rubocop_todo.yml +112 -0
  8. data/CHANGELOG.md +96 -0
  9. data/Gemfile +5 -3
  10. data/Gemfile.lock +38 -8
  11. data/README.md +87 -17
  12. data/Rakefile +5 -3
  13. data/bin/console +4 -3
  14. data/bin/setup +3 -0
  15. data/exe/mysigner +2 -1
  16. data/lib/mysigner/build/android_executor.rb +46 -52
  17. data/lib/mysigner/build/android_parser.rb +33 -40
  18. data/lib/mysigner/build/configurator.rb +17 -16
  19. data/lib/mysigner/build/detector.rb +39 -50
  20. data/lib/mysigner/build/error_analyzer.rb +70 -68
  21. data/lib/mysigner/build/executor.rb +30 -37
  22. data/lib/mysigner/build/parser.rb +18 -18
  23. data/lib/mysigner/cli/auth_commands.rb +735 -752
  24. data/lib/mysigner/cli/build_commands.rb +697 -721
  25. data/lib/mysigner/cli/concerns/actionable_suggestions.rb +208 -154
  26. data/lib/mysigner/cli/concerns/api_helpers.rb +46 -54
  27. data/lib/mysigner/cli/concerns/error_handlers.rb +247 -237
  28. data/lib/mysigner/cli/concerns/helpers.rb +12 -1
  29. data/lib/mysigner/cli/diagnostic_commands.rb +659 -635
  30. data/lib/mysigner/cli/resource_commands.rb +1266 -822
  31. data/lib/mysigner/cli/validate_commands.rb +161 -0
  32. data/lib/mysigner/cli.rb +5 -1
  33. data/lib/mysigner/client.rb +27 -19
  34. data/lib/mysigner/config.rb +93 -56
  35. data/lib/mysigner/export/exporter.rb +32 -36
  36. data/lib/mysigner/signing/certificate_checker.rb +18 -23
  37. data/lib/mysigner/signing/keystore_manager.rb +34 -39
  38. data/lib/mysigner/signing/validator.rb +38 -40
  39. data/lib/mysigner/signing/wizard.rb +329 -342
  40. data/lib/mysigner/upload/app_store_automation.rb +51 -49
  41. data/lib/mysigner/upload/app_store_submission.rb +87 -92
  42. data/lib/mysigner/upload/play_store_uploader.rb +98 -115
  43. data/lib/mysigner/upload/uploader.rb +101 -109
  44. data/lib/mysigner/version.rb +3 -1
  45. data/lib/mysigner.rb +13 -11
  46. data/mysigner.gemspec +36 -33
  47. data/test_manual.rb +37 -36
  48. metadata +38 -16
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'json'
2
4
  require 'stringio'
3
5
 
@@ -7,12 +9,12 @@ module Mysigner
7
9
  class UploadError < Mysigner::Error; end
8
10
  class CredentialsError < UploadError; end
9
11
  class TrackError < UploadError; end
10
-
12
+
11
13
  # Special error for when AAB uploaded but track assignment failed
12
14
  # This carries the version_code so it can be saved to prevent conflicts
13
15
  class PartialUploadError < UploadError
14
16
  attr_reader :version_code
15
-
17
+
16
18
  def initialize(message, version_code:)
17
19
  super(message)
18
20
  @version_code = version_code
@@ -20,7 +22,7 @@ module Mysigner
20
22
  end
21
23
 
22
24
  VALID_TRACKS = %w[internal alpha beta production].freeze
23
- SCOPE = 'https://www.googleapis.com/auth/androidpublisher'.freeze
25
+ SCOPE = 'https://www.googleapis.com/auth/androidpublisher'
24
26
 
25
27
  def initialize(aab_path:, service_account_json:, package_name:)
26
28
  @aab_path = File.expand_path(aab_path)
@@ -37,11 +39,11 @@ module Mysigner
37
39
  # @param user_fraction [Float] Rollout percentage (0.0-1.0) for staged rollouts
38
40
  # @return [Hash] Upload result with version_code and track info
39
41
  def upload!(track: 'internal', release_notes: nil, user_fraction: nil)
40
- @current_track = track # Store for error messages
42
+ @current_track = track # Store for error messages
41
43
  say_uploading(track)
42
44
 
43
45
  version_code = nil
44
-
46
+
45
47
  begin
46
48
  # 1. Create an edit
47
49
  edit = create_edit
@@ -53,9 +55,7 @@ module Mysigner
53
55
  say_upload_success(version_code)
54
56
 
55
57
  # 3. Assign to track with release
56
- if track
57
- assign_to_track(edit.id, track, version_code, release_notes: release_notes, user_fraction: user_fraction)
58
- end
58
+ assign_to_track(edit.id, track, version_code, release_notes: release_notes, user_fraction: user_fraction) if track
59
59
 
60
60
  # 4. Commit the edit
61
61
  commit_edit(edit.id)
@@ -71,20 +71,16 @@ module Mysigner
71
71
  rescue Google::Apis::ClientError => e
72
72
  error_message = parse_google_error(e)
73
73
  # If AAB was uploaded, raise PartialUploadError so CLI can save the version
74
- if version_code
75
- raise PartialUploadError.new("Google Play API error: #{error_message}", version_code: version_code)
76
- else
77
- raise UploadError, "Google Play API error: #{error_message}"
78
- end
74
+ raise PartialUploadError.new("Google Play API error: #{error_message}", version_code: version_code) if version_code
75
+
76
+ raise UploadError, "Google Play API error: #{error_message}"
79
77
  rescue PartialUploadError
80
78
  # Re-raise as-is
81
79
  raise
82
- rescue => e
83
- if version_code
84
- raise PartialUploadError.new("Upload failed: #{e.message}", version_code: version_code)
85
- else
86
- raise UploadError, "Upload failed: #{e.message}"
87
- end
80
+ rescue StandardError => e
81
+ raise PartialUploadError.new("Upload failed: #{e.message}", version_code: version_code) if version_code
82
+
83
+ raise UploadError, "Upload failed: #{e.message}"
88
84
  end
89
85
  end
90
86
 
@@ -110,14 +106,14 @@ module Mysigner
110
106
  rescue Google::Apis::ClientError => e
111
107
  error_message = parse_google_error(e)
112
108
  raise UploadError, "Google Play API error: #{error_message}"
113
- rescue => e
109
+ rescue StandardError => e
114
110
  raise UploadError, "Upload failed: #{e.message}"
115
111
  end
116
112
  end
117
113
 
118
114
  # Assign an existing version code to a track
119
115
  def assign_existing_to_track!(version_code, track:, release_notes: nil, user_fraction: nil)
120
- @current_track = track # Store for error messages
116
+ @current_track = track # Store for error messages
121
117
  begin
122
118
  edit = create_edit
123
119
  assign_to_track(edit.id, track, version_code, release_notes: release_notes, user_fraction: user_fraction)
@@ -138,18 +134,14 @@ module Mysigner
138
134
  private
139
135
 
140
136
  def validate_aab!
141
- unless File.exist?(@aab_path)
142
- raise UploadError, "AAB file not found: #{@aab_path}"
143
- end
137
+ raise UploadError, "AAB file not found: #{@aab_path}" unless File.exist?(@aab_path)
144
138
 
145
- unless @aab_path.end_with?('.aab')
146
- raise UploadError, "Invalid file type: #{@aab_path} (must be .aab)"
147
- end
139
+ raise UploadError, "Invalid file type: #{@aab_path} (must be .aab)" unless @aab_path.end_with?('.aab')
148
140
 
149
141
  file_size = File.size(@aab_path)
150
- if file_size < 10_000
151
- raise UploadError, "AAB file seems too small: #{file_size} bytes (possible corruption)"
152
- end
142
+ return unless file_size < 10_000
143
+
144
+ raise UploadError, "AAB file seems too small: #{file_size} bytes (possible corruption)"
153
145
  end
154
146
 
155
147
  def setup_google_client!
@@ -173,20 +165,18 @@ module Mysigner
173
165
  @service = Google::Apis::AndroidpublisherV3::AndroidPublisherService.new
174
166
  @service.authorization = @auth
175
167
  @service.client_options.open_timeout_sec = 30
176
- @service.client_options.read_timeout_sec = 300 # Large file uploads need time
168
+ @service.client_options.read_timeout_sec = 300 # Large file uploads need time
177
169
  @service.request_options.retries = 3
178
-
179
- rescue LoadError => e
180
- raise CredentialsError, "Google API client not installed. Run: gem install google-api-client"
170
+ rescue LoadError
171
+ raise CredentialsError, 'Google API client not installed. Run: gem install google-api-client'
181
172
  end
182
173
 
183
174
  def create_edit
184
175
  edit = Google::Apis::AndroidpublisherV3::AppEdit.new
185
176
  @service.insert_edit(@package_name, edit)
186
177
  rescue Google::Apis::ClientError => e
187
- if e.message.include?("Package not found") || e.status_code == 404
188
- raise UploadError, first_upload_error_message
189
- end
178
+ raise UploadError, first_upload_error_message if e.message.include?('Package not found') || e.status_code == 404
179
+
190
180
  raise
191
181
  end
192
182
 
@@ -209,28 +199,25 @@ module Mysigner
209
199
 
210
200
  def upload_bundle(edit_id)
211
201
  puts "šŸ“¦ Uploading AAB (#{format_bytes(File.size(@aab_path))})..."
212
- puts ""
202
+ puts ''
213
203
 
214
204
  begin
215
- result = @service.upload_edit_bundle(
205
+ @service.upload_edit_bundle(
216
206
  @package_name,
217
207
  edit_id,
218
208
  upload_source: @aab_path,
219
209
  content_type: 'application/octet-stream'
220
210
  )
221
- result
222
211
  rescue Google::Apis::ClientError => e
223
212
  error_msg = parse_google_error(e)
224
213
  raise UploadError, "Bundle upload failed: #{error_msg}"
225
- rescue => e
214
+ rescue StandardError => e
226
215
  raise UploadError, "Bundle upload failed: #{e.message}"
227
216
  end
228
217
  end
229
218
 
230
219
  def assign_to_track(edit_id, track, version_code, release_notes: nil, user_fraction: nil)
231
- unless VALID_TRACKS.include?(track)
232
- raise TrackError, "Invalid track '#{track}'. Valid tracks: #{VALID_TRACKS.join(', ')}"
233
- end
220
+ raise TrackError, "Invalid track '#{track}'. Valid tracks: #{VALID_TRACKS.join(', ')}" unless VALID_TRACKS.include?(track)
234
221
 
235
222
  puts "šŸš‚ Assigning to #{track} track..."
236
223
 
@@ -241,7 +228,7 @@ module Mysigner
241
228
  )
242
229
 
243
230
  # Add release notes if provided
244
- if release_notes && release_notes.any?
231
+ if release_notes&.any?
245
232
  release.release_notes = release_notes.map do |lang, text|
246
233
  Google::Apis::AndroidpublisherV3::LocalizedText.new(
247
234
  language: lang,
@@ -251,9 +238,7 @@ module Mysigner
251
238
  end
252
239
 
253
240
  # Add user fraction for staged rollouts
254
- if user_fraction
255
- release.user_fraction = user_fraction
256
- end
241
+ release.user_fraction = user_fraction if user_fraction
257
242
 
258
243
  # Build track update
259
244
  track_obj = Google::Apis::AndroidpublisherV3::Track.new(
@@ -265,103 +250,101 @@ module Mysigner
265
250
  end
266
251
 
267
252
  def commit_edit(edit_id)
268
- puts "šŸ’¾ Committing changes..."
253
+ puts 'šŸ’¾ Committing changes...'
269
254
  begin
270
255
  # Try with changesNotSentForReview first (for apps without managed review)
271
256
  @service.commit_edit(@package_name, edit_id, changes_not_sent_for_review: true)
272
257
  rescue Google::Apis::ClientError => e
273
258
  error_text = e.message.to_s
274
259
  # Also check body if present
275
- if e.respond_to?(:body) && e.body
276
- error_text += " #{e.body}"
277
- end
278
-
279
- if error_text.include?('changesNotSentForReview')
280
- # App has managed review enabled, commit without the flag
281
- @service.commit_edit(@package_name, edit_id)
282
- else
283
- raise
284
- end
260
+ error_text += " #{e.body}" if e.respond_to?(:body) && e.body
261
+
262
+ raise unless error_text.include?('changesNotSentForReview')
263
+
264
+ # App has managed review enabled, commit without the flag
265
+ @service.commit_edit(@package_name, edit_id)
285
266
  end
286
267
  end
287
268
 
288
269
  def parse_google_error(error)
289
- begin
290
- body = nil
291
- if error.respond_to?(:body) && error.body
292
- body = JSON.parse(error.body) rescue nil
293
- end
294
-
295
- message = body&.dig('error', 'message') || error.message
296
- details = body&.dig('error', 'errors')&.map { |e| e['message'] }&.join('; ')
297
- full_message = details ? "#{message} (#{details})" : message
298
-
299
- # Provide helpful context for common errors
300
- case full_message.to_s.downcase
301
- when /package.*not found/i, /app not found/i
302
- "#{full_message}\n\nšŸ’” Make sure the package name '#{@package_name}' matches your app in Google Play Console."
303
- when /not authorized/i, /permission denied/i, /forbidden/i
304
- "#{full_message}\n\nšŸ’” Check that your service account has Editor or Admin access to the app in Google Play Console."
305
- when /version.*code.*already/i, /already.*used/i
306
- "#{full_message}\n\nšŸ’” Version code already exists. Increment versionCode in android/app/build.gradle and rebuild."
307
- when /precondition.*check.*failed/i, /precondition.*failed/i
308
- track_name = @current_track || "this track"
309
- "#{full_message}\n\n" \
310
- "šŸ’” Google Play Console requires setup before publishing to #{track_name}:\n\n" \
311
- " For PRODUCTION track:\n" \
312
- " • Complete store listing (description, screenshots, etc.)\n" \
313
- " • Set content rating\n" \
314
- " • Configure pricing & distribution\n\n" \
315
- " For BETA/ALPHA tracks:\n" \
316
- " • Create a closed/open testing track in Play Console\n" \
317
- " • Add at least one tester email\n\n" \
318
- " For INTERNAL track:\n" \
319
- " • Add internal testers in Play Console\n\n" \
320
- " āœ… Your AAB was uploaded successfully!\n" \
321
- " → Go to Play Console to complete track setup, then use:\n" \
322
- " mysigner submit #{track_name} --platform android --version-code VERSION"
323
- when /invalid request/i
324
- "#{full_message}\n\nšŸ’” Common causes:\n" \
325
- " • Version code not found on Google Play (must upload first)\n" \
326
- " • App not created in Google Play Console\n" \
327
- " • Service account missing permissions"
328
- when /signing/i, /signature/i
329
- "#{full_message}\n\nšŸ’” The AAB may not be signed with the correct key. Check your keystore matches what's registered in Play Console."
330
- else
331
- full_message
270
+ body = nil
271
+ if error.respond_to?(:body) && error.body
272
+ body = begin
273
+ JSON.parse(error.body)
274
+ rescue StandardError
275
+ nil
332
276
  end
333
- rescue => parse_error
334
- "#{error.message} (parsing error: #{parse_error.message})"
335
277
  end
278
+
279
+ message = body&.dig('error', 'message') || error.message
280
+ details = body&.dig('error', 'errors')&.map { |e| e['message'] }&.join('; ')
281
+ full_message = details ? "#{message} (#{details})" : message
282
+
283
+ # Provide helpful context for common errors
284
+ case full_message.to_s.downcase
285
+ when /package.*not found/i, /app not found/i
286
+ "#{full_message}\n\nšŸ’” Make sure the package name '#{@package_name}' matches your app in Google Play Console."
287
+ when /not authorized/i, /permission denied/i, /forbidden/i
288
+ "#{full_message}\n\nšŸ’” Check that your service account has Editor or Admin access to the app in Google Play Console."
289
+ when /version.*code.*already/i, /already.*used/i
290
+ "#{full_message}\n\nšŸ’” Version code already exists. Increment versionCode in android/app/build.gradle and rebuild."
291
+ when /precondition.*check.*failed/i, /precondition.*failed/i
292
+ track_name = @current_track || 'this track'
293
+ "#{full_message}\n\n" \
294
+ "šŸ’” Google Play Console requires setup before publishing to #{track_name}:\n\n " \
295
+ "For PRODUCTION track:\n " \
296
+ "• Complete store listing (description, screenshots, etc.)\n " \
297
+ "• Set content rating\n " \
298
+ "• Configure pricing & distribution\n\n " \
299
+ "For BETA/ALPHA tracks:\n " \
300
+ "• Create a closed/open testing track in Play Console\n " \
301
+ "• Add at least one tester email\n\n " \
302
+ "For INTERNAL track:\n " \
303
+ "• Add internal testers in Play Console\n\n " \
304
+ "āœ… Your AAB was uploaded successfully!\n " \
305
+ "→ Go to Play Console to complete track setup, then use:\n " \
306
+ "mysigner submit #{track_name} --platform android --version-code VERSION"
307
+ when /invalid request/i
308
+ "#{full_message}\n\nšŸ’” Common causes:\n " \
309
+ "• Version code not found on Google Play (must upload first)\n " \
310
+ "• App not created in Google Play Console\n " \
311
+ '• Service account missing permissions'
312
+ when /signing/i, /signature/i
313
+ "#{full_message}\n\nšŸ’” The AAB may not be signed with the correct key. Check your keystore matches what's registered in Play Console."
314
+ else
315
+ full_message
316
+ end
317
+ rescue StandardError => e
318
+ "#{error.message} (parsing error: #{e.message})"
336
319
  end
337
320
 
338
321
  def say_uploading(track)
339
- puts "ā˜ļø Uploading to Google Play#{track ? " (#{track} track)" : ''}..."
340
- puts ""
322
+ puts "ā˜ļø Uploading to Google Play#{" (#{track} track)" if track}..."
323
+ puts ''
341
324
  puts "AAB: #{File.basename(@aab_path)}"
342
325
  puts "Size: #{format_bytes(File.size(@aab_path))}"
343
326
  puts "Package: #{@package_name}"
344
327
  puts "Track: #{track || 'none (upload only)'}"
345
- puts ""
328
+ puts ''
346
329
  end
347
330
 
348
331
  def say_upload_success(version_code)
349
332
  puts "āœ“ Bundle uploaded successfully (version code: #{version_code})"
350
- puts ""
333
+ puts ''
351
334
  end
352
335
 
353
336
  def say_success(track, version_code)
354
- puts ""
355
- puts "=" * 80
356
- puts "āœ“ Upload complete!"
357
- puts "=" * 80
358
- puts ""
337
+ puts ''
338
+ puts '=' * 80
339
+ puts 'āœ“ Upload complete!'
340
+ puts '=' * 80
341
+ puts ''
359
342
  puts "šŸŽ‰ Your app is now on Google Play (#{track} track)"
360
- puts ""
343
+ puts ''
361
344
  puts "Version Code: #{version_code}"
362
345
  puts "Package: #{@package_name}"
363
346
  puts "Track: #{track}"
364
- puts ""
347
+ puts ''
365
348
  end
366
349
 
367
350
  def format_bytes(bytes)