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 'yaml'
3
5
  require 'time'
@@ -8,11 +10,12 @@ require_relative '../upload/app_store_submission'
8
10
  module Mysigner
9
11
  class CLI < Thor
10
12
  module BuildCommands
11
- MetadataFileError = Class.new(StandardError)
13
+ class MetadataFileError < StandardError
14
+ end
12
15
 
13
16
  def self.included(base)
14
17
  base.class_eval do
15
- desc "ship TARGET", "🚀 Build + upload (iOS: testflight/appstore, Android: internal/alpha/beta/production)"
18
+ desc 'ship TARGET', '🚀 Build + upload (iOS: testflight/appstore, Android: internal/alpha/beta/production)'
16
19
  long_desc <<~DESC
17
20
  Build your project, sign it, and upload in one go.
18
21
 
@@ -42,7 +45,7 @@ module Mysigner
42
45
  WORKFLOW
43
46
  For iOS TestFlight:
44
47
  mysigner ship testflight # Build → Upload → Done!
45
-
48
+ #{' '}
46
49
  For Android Internal Testing:
47
50
  mysigner ship internal --platform android # Build → Upload → Done!
48
51
 
@@ -62,18 +65,17 @@ module Mysigner
62
65
  method_option :package_name, type: :string, desc: 'Android package name (overrides project setting)'
63
66
  method_option :release_notes, type: :string, desc: 'Release notes for Android Play Store'
64
67
  method_option :version, type: :string, desc: 'Set version name for Android (e.g., 1.2.0)'
65
- method_option :release_type, type: :string, enum: %w[AFTER_APPROVAL MANUAL SCHEDULED],
66
- desc: 'Release type for App Store: AFTER_APPROVAL, MANUAL, or SCHEDULED'
67
- method_option :scheduled_date, type: :string, banner: 'ISO8601',
68
- desc: 'Scheduled release date (ISO 8601, e.g., 2026-02-01T10:00:00Z)'
68
+ method_option :release_type, type: :string, enum: %w[AFTER_APPROVAL MANUAL SCHEDULED],
69
+ desc: 'Release type for App Store: AFTER_APPROVAL, MANUAL, or SCHEDULED'
70
+ method_option :scheduled_date, type: :string, banner: 'ISO8601',
71
+ desc: 'Scheduled release date (ISO 8601, e.g., 2026-02-01T10:00:00Z)'
69
72
  def ship(target)
70
- ios_targets = ['testflight', 'appstore']
71
- android_targets = ['internal', 'alpha', 'beta', 'production']
72
- all_targets = ios_targets + android_targets
73
+ ios_targets = %w[testflight appstore]
74
+ android_targets = %w[internal alpha beta production]
73
75
 
74
76
  # Determine platform from option or target
75
77
  platform = options[:platform]&.to_sym
76
-
78
+
77
79
  if platform.nil?
78
80
  # Auto-detect from target
79
81
  if ios_targets.include?(target)
@@ -106,64 +108,63 @@ module Mysigner
106
108
  end
107
109
 
108
110
  # iOS flow continues below...
109
-
111
+
110
112
  is_appstore = (target == 'appstore')
111
113
 
112
114
  config = load_config
113
115
  client = create_client(config)
114
-
116
+
115
117
  overall_start = Time.now
116
118
  timings = {}
117
119
  archive_path = nil
118
120
  ipa_path = nil
119
- project_name = nil
121
+ nil
120
122
  bundle_id = nil
121
123
 
122
- target_label = is_appstore ? "App Store" : "TestFlight"
124
+ target_label = is_appstore ? 'App Store' : 'TestFlight'
123
125
  say "🚀 My Signer - Ship to #{target_label}", :cyan
124
- say "=" * 80, :cyan
125
- say ""
126
- say "This will:", :bold
127
- say " 1️⃣ Detect and build your project"
128
- say " 2️⃣ Export IPA for App Store"
126
+ say '=' * 80, :cyan
127
+ say ''
128
+ say 'This will:', :bold
129
+ say ' 1️⃣ Detect and build your project'
130
+ say ' 2️⃣ Export IPA for App Store'
129
131
  say " 3️⃣ Upload to #{target_label}"
130
132
  if is_appstore
131
- say " 4️⃣ Wait for Apple to process build"
132
- say " 5️⃣ Submit for App Store review"
133
+ say ' 4️⃣ Wait for Apple to process build'
134
+ say ' 5️⃣ Submit for App Store review'
133
135
  end
134
- say ""
136
+ say ''
135
137
  say "⏱️ Estimated time: #{is_appstore ? '15-30 minutes' : '3-7 minutes'}", :yellow
136
- say ""
138
+ say ''
137
139
 
138
140
  begin
139
-
140
141
  # STEP 1: BUILD
141
- say "=" * 80, :cyan
142
- say "[1/3] Building Archive", :cyan
143
- say "=" * 80, :cyan
144
- say ""
142
+ say '=' * 80, :cyan
143
+ say '[1/3] Building Archive', :cyan
144
+ say '=' * 80, :cyan
145
+ say ''
145
146
 
146
147
  build_start = Time.now
147
-
148
+
148
149
  # Detect project
149
150
  project_info = Build::Detector.detect
150
151
  project_name = File.basename(project_info[:path], '.*')
151
-
152
+
152
153
  framework_label = case project_info[:framework]
153
- when :capacitor then "Capacitor/Ionic"
154
- when :react_native then "React Native"
155
- when :flutter then "Flutter"
156
- else "Native iOS"
157
- end
158
-
154
+ when :capacitor then 'Capacitor/Ionic'
155
+ when :react_native then 'React Native'
156
+ when :flutter then 'Flutter'
157
+ else 'Native iOS'
158
+ end
159
+
159
160
  say "✓ Found: #{File.basename(project_info[:path])} (#{framework_label})", :green
160
- say ""
161
+ say ''
161
162
 
162
163
  # Parse and build
163
164
  parser = Build::Parser.new(project_info)
164
165
  target_name = options[:target] || parser.main_target.name
165
166
  bundle_id = options[:bundle_id] || parser.bundle_id(target_name, options[:configuration])
166
-
167
+
167
168
  # Validate bundle ID format if overridden
168
169
  if options[:bundle_id]
169
170
  if bundle_id =~ /\$\(|\$\{/
@@ -171,69 +172,70 @@ module Mysigner
171
172
  exit 1
172
173
  elsif bundle_id !~ /^[a-zA-Z0-9.-]+$/
173
174
  error "Invalid bundle ID format: #{bundle_id}"
174
- say "Bundle IDs must contain only letters, numbers, hyphens, and periods", :yellow
175
+ say 'Bundle IDs must contain only letters, numbers, hyphens, and periods', :yellow
175
176
  exit 1
176
177
  end
177
178
  end
178
-
179
+
179
180
  say "🎯 Target: #{target_name}", :cyan
180
- say "📦 Bundle ID: #{bundle_id}#{options[:bundle_id] ? ' (overridden)' : ''}", :cyan
181
- say "⏱️ Estimated: 2-5 minutes", :yellow
182
- say ""
183
-
181
+ say "📦 Bundle ID: #{bundle_id}#{' (overridden)' if options[:bundle_id]}", :cyan
182
+ say '⏱️ Estimated: 2-5 minutes', :yellow
183
+ say ''
184
+
184
185
  # Auto-fetch team ID from API if not provided and project missing it
185
186
  team_id_to_use = options[:team]
186
187
  project_team_id = parser.team_id(target_name, options[:configuration])
187
-
188
+
188
189
  if !team_id_to_use && (project_team_id.nil? || project_team_id.empty?)
189
- say "🔍 No team set in project, fetching from My Signer...", :yellow
190
-
190
+ say '🔍 No team set in project, fetching from My Signer...', :yellow
191
+
191
192
  begin
192
193
  org_response = client.get("/api/v1/organizations/#{config.current_organization_id}")
193
- api_team_id = org_response.dig(:data, 'app_store_connect_team_id') || org_response['app_store_connect_team_id']
194
-
194
+ api_team_id = org_response.dig(:data,
195
+ 'app_store_connect_team_id') || org_response['app_store_connect_team_id']
196
+
195
197
  if api_team_id && !api_team_id.empty?
196
198
  team_id_to_use = api_team_id
197
199
  say "✓ Using team from My Signer: #{api_team_id}", :green
198
200
  else
199
- say "⚠️ No team ID configured in My Signer", :yellow
201
+ say '⚠️ No team ID configured in My Signer', :yellow
200
202
  end
201
- rescue => e
203
+ rescue StandardError => e
202
204
  say "⚠️ Failed to fetch team from API: #{e.message}", :yellow
203
205
  end
204
206
  end
205
- say ""
206
-
207
+ say ''
208
+
207
209
  # Pre-build validation
208
- say "🔍 Validating signing setup...", :cyan
210
+ say '🔍 Validating signing setup...', :cyan
209
211
  validator = Signing::Validator.new(parser, target_name, options[:configuration], team_id: team_id_to_use)
210
212
  validator.validate!
211
-
213
+
212
214
  executor = Build::Executor.new(project_info, parser)
213
215
  archive_path = executor.build!(
214
- target_name,
215
- options[:configuration],
216
+ target_name,
217
+ options[:configuration],
216
218
  scheme: options[:scheme],
217
219
  signing_style: parser.code_sign_style(target_name, options[:configuration]),
218
220
  team_id: team_id_to_use
219
221
  )
220
222
 
221
223
  timings[:build] = Time.now - build_start
222
-
223
- say ""
224
+
225
+ say ''
224
226
  say "✓ Build complete in #{format_duration(timings[:build])}", :green
225
- say ""
227
+ say ''
226
228
 
227
229
  # STEP 2: EXPORT
228
- say "=" * 80, :cyan
229
- say "[2/3] Exporting IPA", :cyan
230
- say "=" * 80, :cyan
231
- say ""
232
- say "⏱️ Estimated: 30-90 seconds", :yellow
233
- say ""
230
+ say '=' * 80, :cyan
231
+ say '[2/3] Exporting IPA', :cyan
232
+ say '=' * 80, :cyan
233
+ say ''
234
+ say '⏱️ Estimated: 30-90 seconds', :yellow
235
+ say ''
234
236
 
235
237
  export_start = Time.now
236
-
238
+
237
239
  exporter = Export::Exporter.new(archive_path)
238
240
  ipa_path = exporter.export!(
239
241
  method: :appstore,
@@ -242,93 +244,95 @@ module Mysigner
242
244
  )
243
245
 
244
246
  timings[:export] = Time.now - export_start
245
-
246
- say ""
247
+
248
+ say ''
247
249
  say "✓ Export complete in #{format_duration(timings[:export])}", :green
248
250
  say "📦 IPA: #{ipa_path}", :cyan
249
- say ""
251
+ say ''
250
252
 
251
253
  # STEP 2.5: Get current latest build (BEFORE upload) - App Store only
252
254
  latest_build_before_upload = nil
253
255
  if is_appstore
254
- say "=" * 80, :cyan
255
- say "Getting Current Latest Build", :cyan
256
- say "=" * 80, :cyan
257
- say ""
258
-
259
- say "🔄 Syncing from App Store Connect...", :yellow
256
+ say '=' * 80, :cyan
257
+ say 'Getting Current Latest Build', :cyan
258
+ say '=' * 80, :cyan
259
+ say ''
260
+
261
+ say '🔄 Syncing from App Store Connect...', :yellow
260
262
  begin
261
263
  client.post("/api/v1/organizations/#{config.current_organization_id}/sync", body: { force: true })
262
264
  sleep 15
263
- say "✓ Sync complete", :green
264
- rescue => e
265
+ say '✓ Sync complete', :green
266
+ rescue StandardError => e
265
267
  say "⚠️ Sync failed: #{e.message}", :yellow
266
268
  end
267
- say ""
268
-
269
+ say ''
270
+
269
271
  begin
270
- app_response = client.get("/api/v1/organizations/#{config.current_organization_id}/apple_apps", params: { bundle_id: bundle_id })
272
+ app_response = client.get("/api/v1/organizations/#{config.current_organization_id}/apple_apps",
273
+ params: { bundle_id: bundle_id })
271
274
  app = Array(app_response.dig(:data, 'data', 'apps')).first
272
275
 
273
276
  if app
274
- builds_response = client.get("/api/v1/organizations/#{config.current_organization_id}/builds", params: { app_id: app['id'] })
277
+ builds_response = client.get("/api/v1/organizations/#{config.current_organization_id}/builds",
278
+ params: { app_id: app['id'] })
275
279
  latest = Array(builds_response.dig(:data, 'data', 'builds')).first
276
280
  if latest
277
281
  latest_build_before_upload = latest['build_number'].to_i
278
282
  say "✓ Current latest build: ##{latest_build_before_upload}", :green
279
283
  else
280
- say "✓ No builds yet", :green
284
+ say '✓ No builds yet', :green
281
285
  latest_build_before_upload = 0
282
286
  end
283
287
  end
284
- rescue => e
288
+ rescue StandardError => e
285
289
  say "⚠️ Could not fetch builds: #{e.message}", :yellow
286
290
  latest_build_before_upload = 0
287
291
  end
288
- say ""
292
+ say ''
289
293
  end
290
-
294
+
291
295
  # STEP 3: UPLOAD
292
- say "=" * 80, :cyan
296
+ say '=' * 80, :cyan
293
297
  say "[3/#{is_appstore ? '5' : '3'}] Uploading to #{target_label}", :cyan
294
- say "=" * 80, :cyan
295
- say ""
296
- say "⏱️ Estimated: 1-3 minutes", :yellow
297
- say ""
298
-
298
+ say '=' * 80, :cyan
299
+ say ''
300
+ say '⏱️ Estimated: 1-3 minutes', :yellow
301
+ say ''
302
+
299
303
  upload_start = Time.now
300
304
 
301
305
  # Fetch App Store Connect credentials
302
- say "🔐 Fetching App Store Connect credentials...", :yellow
303
-
306
+ say '🔐 Fetching App Store Connect credentials...', :yellow
307
+
304
308
  org_response = client.get("/api/v1/organizations/#{config.current_organization_id}")
305
309
  org_data = org_response[:data]
306
-
310
+
307
311
  unless org_data['app_store_connect_configured']
308
- say ""
309
- say "✗ App Store Connect credentials not configured", :red
310
- say ""
311
- say "Quick fix:", :cyan
312
- say " mysigner doctor # Auto-configure now", :green
313
- say ""
314
- say "Or manually:", :cyan
315
- say " 1. Run: mysigner onboard"
316
- say " 2. Follow Step 5 to add credentials"
317
- say ""
312
+ say ''
313
+ say '✗ App Store Connect credentials not configured', :red
314
+ say ''
315
+ say 'Quick fix:', :cyan
316
+ say ' mysigner doctor # Auto-configure now', :green
317
+ say ''
318
+ say 'Or manually:', :cyan
319
+ say ' 1. Run: mysigner onboard'
320
+ say ' 2. Follow Step 5 to add credentials'
321
+ say ''
318
322
  exit 1
319
323
  end
320
-
324
+
321
325
  api_key = org_data['app_store_connect_key_id']
322
326
  api_issuer = org_data['app_store_connect_issuer_id']
323
327
  private_key = org_data['app_store_connect_private_key']
324
-
328
+
325
329
  unless api_key && api_issuer && private_key
326
- say "✗ Error: Invalid credentials received from API", :red
330
+ say '✗ Error: Invalid credentials received from API', :red
327
331
  exit 1
328
332
  end
329
-
330
- say "✓ Credentials loaded", :green
331
- say ""
333
+
334
+ say '✓ Credentials loaded', :green
335
+ say ''
332
336
 
333
337
  # Upload
334
338
  uploader = Upload::Uploader.new(
@@ -337,52 +341,54 @@ module Mysigner
337
341
  api_issuer: api_issuer,
338
342
  private_key: private_key
339
343
  )
340
-
344
+
341
345
  uploader.upload!(wait_for_processing: options[:wait])
342
-
346
+
343
347
  timings[:upload] = Time.now - upload_start
344
-
348
+
345
349
  # STEP 4: Submit for App Store Review (appstore only)
346
350
  if is_appstore
347
- say ""
348
- say "=" * 80, :cyan
349
- say "[4/5] Waiting for Apple to Process Build", :cyan
350
- say "=" * 80, :cyan
351
- say ""
352
-
351
+ say ''
352
+ say '=' * 80, :cyan
353
+ say '[4/5] Waiting for Apple to Process Build', :cyan
354
+ say '=' * 80, :cyan
355
+ say ''
356
+
353
357
  submission_start = Time.now
354
-
358
+
355
359
  # Poll sync every 3 minutes until we find a newer build
356
360
  say "⏳ Waiting for build ##{latest_build_before_upload + 1} to sync (polls every 3min)...", :yellow
357
- timeout = 1800 # 30 minutes
358
- poll_interval = 180 # 3 minutes
361
+ timeout = 1800 # 30 minutes
362
+ poll_interval = 180 # 3 minutes
359
363
  start_time = Time.now
360
364
  new_build = nil
361
365
  poll_count = 0
362
-
366
+
363
367
  loop do
364
368
  poll_count += 1
365
369
  elapsed = Time.now - start_time
366
-
370
+
367
371
  # Run sync
368
372
  begin
369
373
  client.post("/api/v1/organizations/#{config.current_organization_id}/sync", body: { force: true })
370
374
  sleep 15
371
- rescue => e
375
+ rescue StandardError => e
372
376
  # Ignore
373
377
  end
374
-
378
+
375
379
  # Check for new build
376
380
  begin
377
- app_response = client.get("/api/v1/organizations/#{config.current_organization_id}/apple_apps", params: { bundle_id: bundle_id })
381
+ app_response = client.get("/api/v1/organizations/#{config.current_organization_id}/apple_apps",
382
+ params: { bundle_id: bundle_id })
378
383
  app = Array(app_response.dig(:data, 'data', 'apps')).first
379
384
 
380
385
  if app
381
- builds_response = client.get("/api/v1/organizations/#{config.current_organization_id}/builds", params: { app_id: app['id'] })
386
+ builds_response = client.get("/api/v1/organizations/#{config.current_organization_id}/builds",
387
+ params: { app_id: app['id'] })
382
388
  latest = Array(builds_response.dig(:data, 'data', 'builds')).first
383
-
389
+
384
390
  current_build_num = latest ? latest['build_number'].to_i : 0
385
-
391
+
386
392
  if current_build_num > latest_build_before_upload
387
393
  new_build = latest
388
394
  say "✅ Build ##{new_build['build_number']} synced! (#{new_build['processing_state']})", :green
@@ -393,27 +399,27 @@ module Mysigner
393
399
  $stdout.flush
394
400
  end
395
401
  end
396
- rescue => e
402
+ rescue StandardError => e
397
403
  say " ⚠️ Could not check builds: #{e.message}", :yellow
398
404
  end
399
-
405
+
400
406
  # Check timeout
401
407
  if elapsed >= timeout
402
- say ""
408
+ say ''
403
409
  say "✗ Timeout after #{(elapsed / 60).to_i} minutes", :red
404
410
  say " Latest build is still ##{latest_build_before_upload}", :yellow
405
411
  exit 1
406
412
  end
407
-
413
+
408
414
  # Wait before next poll
409
415
  sleep poll_interval unless new_build
410
416
  end
411
- say ""
412
-
417
+ say ''
418
+
413
419
  # Step 3: Now wait for the new build to be processed
414
420
  require_relative '../upload/app_store_submission'
415
421
  require_relative '../upload/app_store_automation'
416
-
422
+
417
423
  automation = Upload::AppStoreAutomation.new(
418
424
  client: client,
419
425
  organization_id: config.current_organization_id,
@@ -429,7 +435,7 @@ module Mysigner
429
435
  # Build metadata overrides from CLI options
430
436
  ship_overrides = { 'auto_submit' => true }
431
437
  ship_override_keys = ['auto_submit']
432
-
438
+
433
439
  if options[:release_type]
434
440
  # Validate release_type
435
441
  valid_types = %w[AFTER_APPROVAL MANUAL SCHEDULED]
@@ -441,20 +447,20 @@ module Mysigner
441
447
  end
442
448
  ship_overrides['release_type'] = rt
443
449
  ship_override_keys << 'release_type'
444
-
450
+
445
451
  # Validate scheduled_date is provided when SCHEDULED
446
452
  if rt == 'SCHEDULED' && !options[:scheduled_date]
447
- error "Scheduled release date is required when --release-type=SCHEDULED"
448
- say "Use: --scheduled-date 2026-02-01T10:00:00Z", :yellow
453
+ error 'Scheduled release date is required when --release-type=SCHEDULED'
454
+ say 'Use: --scheduled-date 2026-02-01T10:00:00Z', :yellow
449
455
  exit 1
450
456
  end
451
457
  end
452
-
458
+
453
459
  if options[:scheduled_date]
454
460
  begin
455
461
  parsed_date = Time.parse(options[:scheduled_date])
456
- if parsed_date < Time.now + 3600 # At least 1 hour in the future
457
- error "Scheduled date must be at least 1 hour in the future"
462
+ if parsed_date < Time.now + 3600 # At least 1 hour in the future
463
+ error 'Scheduled date must be at least 1 hour in the future'
458
464
  exit 1
459
465
  end
460
466
  ship_overrides['earliest_release_date'] = parsed_date.utc.iso8601
@@ -466,169 +472,159 @@ module Mysigner
466
472
  end
467
473
  rescue ArgumentError
468
474
  error "Invalid date format: #{options[:scheduled_date]}"
469
- say "Use ISO 8601 format, e.g., 2026-02-01T10:00:00Z", :yellow
475
+ say 'Use ISO 8601 format, e.g., 2026-02-01T10:00:00Z', :yellow
470
476
  exit 1
471
477
  end
472
478
  end
473
-
479
+
474
480
  submission = Upload::AppStoreSubmission.new(
475
481
  client,
476
482
  config.current_organization_id,
477
483
  {
478
484
  bundle_id: bundle_id,
479
- build_number: new_build['build_number'] # Use the specific build we found
485
+ build_number: new_build['build_number'] # Use the specific build we found
480
486
  },
481
487
  metadata_overrides: ship_overrides,
482
488
  override_sources: [{ type: :inline, keys: ship_override_keys }]
483
489
  )
484
-
490
+
485
491
  submission_result = submission.submit_for_review!(automation: automation)
486
492
  timings[:submission] = Time.now - submission_start
487
493
  end
488
-
494
+
489
495
  timings[:total] = Time.now - overall_start
490
496
 
491
497
  # SUCCESS SUMMARY!
492
- say ""
493
- say "=" * 80, :green
498
+ say ''
499
+ say '=' * 80, :green
494
500
  if is_appstore
495
501
  if submission_result && submission_result[:automation][:submitted]
496
- say "🎉 SUCCESS! Your app is submitted for App Store review!", :green
502
+ say '🎉 SUCCESS! Your app is submitted for App Store review!', :green
497
503
  else
498
- say "🎉 SUCCESS! Your app is uploaded to App Store Connect!", :green
504
+ say '🎉 SUCCESS! Your app is uploaded to App Store Connect!', :green
499
505
  end
500
506
  else
501
- say "🎉 SUCCESS! Your app is on TestFlight!", :green
507
+ say '🎉 SUCCESS! Your app is on TestFlight!', :green
502
508
  end
503
- say "=" * 80, :green
504
- say ""
505
-
509
+ say '=' * 80, :green
510
+ say ''
511
+
506
512
  # Summary table
507
- say "📊 Summary", :bold
508
- say ""
513
+ say '📊 Summary', :bold
514
+ say ''
509
515
  say " Project: #{project_name}"
510
516
  say " Bundle ID: #{bundle_id}"
511
517
  say " Target: #{target_name}"
512
518
  say " IPA Size: #{format_bytes(File.size(ipa_path))}"
513
- say ""
519
+ say ''
514
520
  if is_appstore && options[:submit_for_review]
515
521
  poll_msg = options[:wait] ? "every #{automation.poll_interval}s" : 'skipped (--no-wait)'
516
522
  say " ASC Polling: #{poll_msg}"
517
523
  say " ASC Timeout: #{format_duration(options[:asc_timeout_seconds])}" if options[:asc_timeout_seconds]
518
524
  end
519
-
525
+
520
526
  # Timing breakdown
521
- say "⏱️ Time Breakdown", :bold
522
- say ""
527
+ say '⏱️ Time Breakdown', :bold
528
+ say ''
523
529
  say " Build: #{format_duration(timings[:build])}"
524
530
  say " Export: #{format_duration(timings[:export])}"
525
531
  say " Upload: #{format_duration(timings[:upload])}"
526
- if timings[:submission]
527
- say " Submission: #{format_duration(timings[:submission])}"
528
- end
529
- say " " + "-" * 30
532
+ say " Submission: #{format_duration(timings[:submission])}" if timings[:submission]
533
+ say " #{'-' * 30}"
530
534
  say " Total: #{format_duration(timings[:total])}", :bold
531
- say ""
532
-
535
+ say ''
536
+
533
537
  # Files created
534
- say "📁 Files Created", :bold
535
- say ""
538
+ say '📁 Files Created', :bold
539
+ say ''
536
540
  say " Archive: #{archive_path}"
537
541
  say " IPA: #{ipa_path}"
538
- say ""
539
-
542
+ say ''
543
+
540
544
  # Next steps
541
- say "🔮 Next Steps", :bold
542
- say ""
545
+ say '🔮 Next Steps', :bold
546
+ say ''
543
547
  if is_appstore
544
548
  if submission_result && submission_result[:automation][:submitted]
545
- say " ✓ Your build is submitted for App Store review!", :green
546
- say ""
547
- say " Monitor review status:", :cyan
548
- say " https://appstoreconnect.apple.com/apps", :green
549
+ say ' ✓ Your build is submitted for App Store review!', :green
550
+ say ''
551
+ say ' Monitor review status:', :cyan
552
+ say ' https://appstoreconnect.apple.com/apps', :green
549
553
  else
550
- say " ⚠️ Submission completed but not submitted", :yellow
551
- say " (May need release config in My Signer dashboard)", :yellow
552
- say ""
553
- say " Or submit manually:", :cyan
554
- say " mysigner submit", :green
554
+ say ' ⚠️ Submission completed but not submitted', :yellow
555
+ say ' (May need release config in My Signer dashboard)', :yellow
556
+ say ''
557
+ say ' Or submit manually:', :cyan
558
+ say ' mysigner submit', :green
555
559
  end
556
560
  else
557
- say " 1. Wait 5-15 minutes for Apple to process your build"
558
- say " 2. Open App Store Connect:"
559
- say " https://appstoreconnect.apple.com/apps"
560
- say " 3. Add testers and distribute via TestFlight"
561
+ say ' 1. Wait 5-15 minutes for Apple to process your build'
562
+ say ' 2. Open App Store Connect:'
563
+ say ' https://appstoreconnect.apple.com/apps'
564
+ say ' 3. Add testers and distribute via TestFlight'
561
565
  end
562
- say ""
566
+ say ''
563
567
  rescue MetadataFileError => e
564
- say ""
565
- say "=" * 80, :red
566
- say "✗ Ship Failed", :red
567
- say "=" * 80, :red
568
- say ""
568
+ say ''
569
+ say '=' * 80, :red
570
+ say '✗ Ship Failed', :red
571
+ say '=' * 80, :red
572
+ say ''
569
573
  say "Error: #{e.message}", :red
570
- say ""
574
+ say ''
571
575
  exit 1
572
576
  rescue Build::Executor::BuildError => e
573
577
  # Analyze build errors and show helpful suggestions
574
- say ""
578
+ say ''
575
579
 
576
580
  if defined?(executor) && executor.respond_to?(:build_errors)
577
581
  require_relative '../build/error_analyzer'
578
582
  analyzer = Build::ErrorAnalyzer.new(executor.build_errors)
579
583
 
580
- if analyzer.any_issues?
581
- say analyzer.format_suggestions, :cyan
582
- end
584
+ say analyzer.format_suggestions, :cyan if analyzer.any_issues?
583
585
  end
584
586
 
585
- say "=" * 80, :red
586
- say "✗ Ship Failed", :red
587
- say "=" * 80, :red
588
- say ""
587
+ say '=' * 80, :red
588
+ say '✗ Ship Failed', :red
589
+ say '=' * 80, :red
590
+ say ''
589
591
  say "Error: #{e.message}", :red
590
- say ""
592
+ say ''
591
593
 
592
- if archive_path && File.exist?(archive_path)
593
- say "Archive saved at: #{archive_path}", :yellow
594
- end
594
+ say "Archive saved at: #{archive_path}", :yellow if archive_path && File.exist?(archive_path)
595
595
 
596
596
  exit 1
597
597
  rescue Upload::AppStoreAutomation::AutomationError => e
598
598
  # Use enhanced error handler for App Store automation errors
599
599
  handle_apple_api_error(e, context: {
600
- title: 'App Store Automation Failed',
601
- archive_path: archive_path,
602
- ipa_path: ipa_path,
603
- bundle_id: defined?(bundle_id) ? bundle_id : nil
604
- })
600
+ title: 'App Store Automation Failed',
601
+ archive_path: archive_path,
602
+ ipa_path: ipa_path,
603
+ bundle_id: defined?(bundle_id) ? bundle_id : nil
604
+ })
605
605
  exit 1
606
606
  rescue Mysigner::ClientError => e
607
607
  # Handle API client errors with actionable suggestions
608
608
  handle_apple_api_error(e, context: {
609
- title: 'API Error',
610
- archive_path: archive_path,
611
- ipa_path: ipa_path
612
- })
609
+ title: 'API Error',
610
+ archive_path: archive_path,
611
+ ipa_path: ipa_path
612
+ })
613
613
  exit 1
614
- rescue => e
615
- say ""
616
- say "=" * 80, :red
617
- say "✗ Ship Failed", :red
618
- say "=" * 80, :red
619
- say ""
614
+ rescue StandardError => e
615
+ say ''
616
+ say '=' * 80, :red
617
+ say '✗ Ship Failed', :red
618
+ say '=' * 80, :red
619
+ say ''
620
620
  say "Error: #{e.message}", :red
621
- say ""
621
+ say ''
622
622
 
623
623
  # Try to show actionable suggestions for unknown errors
624
624
  show_actionable_suggestions(e.message, platform: :ios)
625
625
 
626
- if archive_path && File.exist?(archive_path)
627
- say "Archive saved at: #{archive_path}", :yellow
628
- end
629
- if ipa_path && File.exist?(ipa_path)
630
- say "IPA saved at: #{ipa_path}", :yellow
631
- end
626
+ say "Archive saved at: #{archive_path}", :yellow if archive_path && File.exist?(archive_path)
627
+ say "IPA saved at: #{ipa_path}", :yellow if ipa_path && File.exist?(ipa_path)
632
628
 
633
629
  show_debug_info(e) if ENV['DEBUG']
634
630
  exit 1
@@ -640,7 +636,7 @@ module Mysigner
640
636
  def ship_android(track)
641
637
  config = load_config
642
638
  client = create_client(config)
643
-
639
+
644
640
  overall_start = Time.now
645
641
  timings = {}
646
642
  aab_path = nil
@@ -655,89 +651,89 @@ module Mysigner
655
651
  track_label = track_labels[track] || track.capitalize
656
652
 
657
653
  say "🤖 My Signer - Ship to Google Play (#{track_label})", :cyan
658
- say "=" * 80, :cyan
659
- say ""
660
- say "This will:", :bold
661
- say " 1️⃣ Detect and build your Android project"
662
- say " 2️⃣ Sign with your keystore"
654
+ say '=' * 80, :cyan
655
+ say ''
656
+ say 'This will:', :bold
657
+ say ' 1️⃣ Detect and build your Android project'
658
+ say ' 2️⃣ Sign with your keystore'
663
659
  say " 3️⃣ Upload to Google Play (#{track} track)"
664
- say ""
665
- say "⏱️ Estimated time: 3-10 minutes", :yellow
666
- say ""
660
+ say ''
661
+ say '⏱️ Estimated time: 3-10 minutes', :yellow
662
+ say ''
667
663
 
668
664
  begin
669
665
  # STEP 1: Detect and build
670
- say "=" * 80, :cyan
671
- say "[1/3] Building Android App Bundle (AAB)", :cyan
672
- say "=" * 80, :cyan
673
- say ""
666
+ say '=' * 80, :cyan
667
+ say '[1/3] Building Android App Bundle (AAB)', :cyan
668
+ say '=' * 80, :cyan
669
+ say ''
674
670
 
675
671
  build_start = Time.now
676
672
 
677
673
  # Detect Android project
678
674
  project_info = Build::Detector.detect_android
679
-
675
+
680
676
  framework_label = case project_info[:framework]
681
- when :capacitor then "Capacitor/Ionic"
682
- when :react_native then "React Native"
683
- when :flutter then "Flutter"
684
- else "Native Android"
685
- end
686
-
677
+ when :capacitor then 'Capacitor/Ionic'
678
+ when :react_native then 'React Native'
679
+ when :flutter then 'Flutter'
680
+ else 'Native Android'
681
+ end
682
+
687
683
  say "✓ Found: Android project (#{framework_label})", :green
688
- say ""
684
+ say ''
689
685
 
690
686
  # Parse project
691
687
  require_relative '../build/android_parser'
692
688
  parser = Build::AndroidParser.new(project_info)
693
-
689
+
694
690
  package_name = options[:package_name] || parser.application_id
695
691
  local_version_code = parser.version_code.to_i
696
692
  version_name = parser.version_name
697
-
693
+
698
694
  # Check highest version code from API and auto-increment if needed
699
695
  highest_version_code = fetch_android_highest_version_code(client, config, package_name)
700
696
  version_code = local_version_code
701
697
  version_code_override = nil
702
-
698
+
703
699
  if highest_version_code && local_version_code <= highest_version_code
704
700
  version_code = highest_version_code + 1
705
701
  version_code_override = version_code
706
702
  say "📦 Package: #{package_name}", :cyan
707
703
  say "🔢 Version: #{version_name} (#{local_version_code} → #{version_code})", :cyan
708
704
  say " ↳ Auto-incremented (#{highest_version_code} already on Play Store)", :yellow
709
-
705
+
710
706
  # For Expo projects, need to regenerate android folder with new versionCode
711
707
  # since versionCode is hardcoded by expo prebuild
712
708
  if expo_project?(Dir.pwd)
713
- say ""
709
+ say ''
714
710
  say "🔄 Regenerating android folder with version code #{version_code}...", :yellow
715
711
  regenerate_expo_android(Dir.pwd, version_code)
716
712
  # Re-detect after regeneration
717
713
  project_info = Build::Detector.detect_android
718
714
  parser = Build::AndroidParser.new(project_info)
719
- version_code_override = nil # No longer need override, it's baked in
720
- say "✓ Android folder regenerated", :green
715
+ version_code_override = nil # No longer need override, it's baked in
716
+ say '✓ Android folder regenerated', :green
721
717
  end
722
718
  else
723
719
  say "📦 Package: #{package_name}", :cyan
724
720
  say "🔢 Version: #{version_name} (#{version_code})", :cyan
725
721
  end
726
- say "⏱️ Estimated: 2-5 minutes", :yellow
727
- say ""
722
+ say '⏱️ Estimated: 2-5 minutes', :yellow
723
+ say ''
728
724
 
729
725
  # STEP 2: Get keystore and sign
730
- say "=" * 80, :cyan
731
- say "[2/3] Signing with Keystore", :cyan
732
- say "=" * 80, :cyan
733
- say ""
726
+ say '=' * 80, :cyan
727
+ say '[2/3] Signing with Keystore', :cyan
728
+ say '=' * 80, :cyan
729
+ say ''
734
730
 
735
731
  # Fetch keystore from API (prefer app-specific, fallback to org-wide)
736
- say "🔐 Fetching keystore from My Signer...", :yellow
737
-
732
+ say '🔐 Fetching keystore from My Signer...', :yellow
733
+
738
734
  require_relative '../signing/keystore_manager'
739
735
  keystore_manager = Signing::KeystoreManager.new(client, config.current_organization_id)
740
-
736
+
741
737
  # Try to find the app to get app-specific + org-wide keystores
742
738
  app_id = nil
743
739
  begin
@@ -745,46 +741,46 @@ module Mysigner
745
741
  apps = response[:data]['android_apps'] || []
746
742
  app = apps.find { |a| a['package_name'] == package_name }
747
743
  app_id = app['id'] if app
748
- rescue
744
+ rescue StandardError
749
745
  # Continue without app ID
750
746
  end
751
-
747
+
752
748
  active_keystore = keystore_manager.active_keystore(android_app_id: app_id, include_secrets: true)
753
749
  unless active_keystore
754
- say ""
755
- say "✗ No active keystore found", :red
756
- say ""
757
- say "Quick fix:", :cyan
758
- say " 1. Upload a keystore: mysigner keystore upload", :green
759
- say " 2. Or configure in My Signer dashboard", :green
760
- say ""
750
+ say ''
751
+ say '✗ No active keystore found', :red
752
+ say ''
753
+ say 'Quick fix:', :cyan
754
+ say ' 1. Upload a keystore: mysigner keystore upload', :green
755
+ say ' 2. Or configure in My Signer dashboard', :green
756
+ say ''
761
757
  exit 1
762
758
  end
763
759
 
764
760
  say "✓ Using keystore: #{active_keystore['name']}", :green
765
-
761
+
766
762
  # Download keystore
767
763
  keystore_info = keystore_manager.get_or_download(active_keystore['id'])
768
764
  say "✓ Keystore ready at: #{keystore_info[:path]}", :green
769
- say ""
765
+ say ''
770
766
 
771
767
  # Get keystore credentials from API response
772
- keystore_password = active_keystore['keystore_password'] || ENV['MYSIGNER_KEYSTORE_PASSWORD']
768
+ keystore_password = active_keystore['keystore_password'] || ENV.fetch('MYSIGNER_KEYSTORE_PASSWORD', nil)
773
769
  key_password = active_keystore['key_password'] || ENV['MYSIGNER_KEY_PASSWORD'] || keystore_password
774
770
  key_alias = active_keystore['key_alias']
775
771
 
776
772
  unless keystore_password
777
- say "⚠️ Keystore password not found in My Signer", :yellow
778
- say " Upload your keystore with password: mysigner keystore upload FILE", :yellow
779
- keystore_password = ask("Keystore password:", echo: false)
780
- say ""
773
+ say '⚠️ Keystore password not found in My Signer', :yellow
774
+ say ' Upload your keystore with password: mysigner keystore upload FILE', :yellow
775
+ keystore_password = ask('Keystore password:', echo: false)
776
+ say ''
781
777
  key_password ||= keystore_password
782
778
  end
783
779
 
784
780
  # Build AAB
785
781
  require_relative '../build/android_executor'
786
782
  executor = Build::AndroidExecutor.new(project_info, parser)
787
-
783
+
788
784
  aab_path = executor.build_aab!(
789
785
  variant: 'release',
790
786
  keystore_path: keystore_info[:path],
@@ -795,63 +791,61 @@ module Mysigner
795
791
  )
796
792
 
797
793
  timings[:build] = Time.now - build_start
798
-
799
- say ""
794
+
795
+ say ''
800
796
  say "✓ Build complete in #{format_duration(timings[:build])}", :green
801
797
  say "📦 AAB: #{aab_path}", :cyan
802
- say ""
798
+ say ''
803
799
 
804
800
  # STEP 3: Upload to Google Play
805
- say "=" * 80, :cyan
806
- say "[3/3] Uploading to Google Play", :cyan
807
- say "=" * 80, :cyan
808
- say ""
801
+ say '=' * 80, :cyan
802
+ say '[3/3] Uploading to Google Play', :cyan
803
+ say '=' * 80, :cyan
804
+ say ''
809
805
 
810
806
  upload_start = Time.now
811
807
 
812
808
  # Fetch Google Play credentials from API
813
- say "🔐 Fetching Google Play credentials...", :yellow
814
-
809
+ say '🔐 Fetching Google Play credentials...', :yellow
810
+
815
811
  org_response = client.get("/api/v1/organizations/#{config.current_organization_id}")
816
812
  org_data = org_response[:data]
817
-
813
+
818
814
  unless org_data['google_play_configured']
819
- say ""
820
- say "✗ Google Play credentials not configured", :red
821
- say ""
822
- say "Quick fix:", :cyan
823
- say " Configure Google Play credentials in My Signer dashboard", :green
824
- say ""
825
- say "Or configure in My Signer dashboard", :yellow
826
- say ""
815
+ say ''
816
+ say '✗ Google Play credentials not configured', :red
817
+ say ''
818
+ say 'Quick fix:', :cyan
819
+ say ' Configure Google Play credentials in My Signer dashboard', :green
820
+ say ''
821
+ say 'Or configure in My Signer dashboard', :yellow
822
+ say ''
827
823
  exit 1
828
824
  end
829
825
 
830
826
  service_account_json = org_data['google_play_service_account']
831
-
827
+
832
828
  unless service_account_json
833
- say "✗ Error: Service account JSON not found", :red
829
+ say '✗ Error: Service account JSON not found', :red
834
830
  exit 1
835
831
  end
836
-
837
- say "✓ Credentials loaded", :green
838
- say ""
832
+
833
+ say '✓ Credentials loaded', :green
834
+ say ''
839
835
 
840
836
  # Upload to Google Play
841
837
  require_relative '../upload/play_store_uploader'
842
-
838
+
843
839
  release_notes = nil
844
- if options[:release_notes]
845
- release_notes = { 'en-US' => options[:release_notes] }
846
- end
840
+ release_notes = { 'en-US' => options[:release_notes] } if options[:release_notes]
847
841
 
848
842
  uploader = Upload::PlayStoreUploader.new(
849
843
  aab_path: aab_path,
850
844
  service_account_json: service_account_json,
851
845
  package_name: package_name
852
846
  )
853
-
854
- result = uploader.upload!(
847
+
848
+ uploader.upload!(
855
849
  track: track,
856
850
  release_notes: release_notes
857
851
  )
@@ -866,7 +860,7 @@ module Mysigner
866
860
  "/api/v1/organizations/#{config.current_organization_id}/android_keystores/#{active_keystore['id']}/link_to_app",
867
861
  body: { package_name: package_name }
868
862
  )
869
- rescue => e
863
+ rescue StandardError => e
870
864
  # Non-fatal, continue
871
865
  end
872
866
  end
@@ -875,125 +869,120 @@ module Mysigner
875
869
  save_android_build_record(client, config, package_name, version_code, version_name)
876
870
 
877
871
  # SUCCESS SUMMARY
878
- say ""
879
- say "=" * 80, :green
872
+ say ''
873
+ say '=' * 80, :green
880
874
  say "🎉 SUCCESS! Your app is on Google Play (#{track} track)!", :green
881
- say "=" * 80, :green
882
- say ""
875
+ say '=' * 80, :green
876
+ say ''
883
877
 
884
- say "📊 Summary", :bold
885
- say ""
878
+ say '📊 Summary', :bold
879
+ say ''
886
880
  say " Package: #{package_name}"
887
881
  say " Version: #{version_name} (#{version_code})"
888
882
  say " Track: #{track}"
889
883
  say " AAB Size: #{format_bytes(File.size(aab_path))}"
890
- say ""
884
+ say ''
891
885
 
892
- say "⏱️ Time Breakdown", :bold
893
- say ""
886
+ say '⏱️ Time Breakdown', :bold
887
+ say ''
894
888
  say " Build: #{format_duration(timings[:build])}"
895
889
  say " Upload: #{format_duration(timings[:upload])}"
896
- say " " + "-" * 30
890
+ say " #{'-' * 30}"
897
891
  say " Total: #{format_duration(timings[:total])}", :bold
898
- say ""
892
+ say ''
899
893
 
900
- say "📁 Files Created", :bold
901
- say ""
894
+ say '📁 Files Created', :bold
895
+ say ''
902
896
  say " AAB: #{aab_path}"
903
- say ""
897
+ say ''
904
898
 
905
- say "🔮 Next Steps", :bold
906
- say ""
899
+ say '🔮 Next Steps', :bold
900
+ say ''
907
901
  case track
908
902
  when 'internal'
909
- say " 1. Add internal testers in Google Play Console"
910
- say " 2. Testers will receive the build automatically"
903
+ say ' 1. Add internal testers in Google Play Console'
904
+ say ' 2. Testers will receive the build automatically'
911
905
  when 'alpha', 'beta'
912
- say " 1. Review the build in Google Play Console"
913
- say " 2. Promote to #{track == 'alpha' ? 'beta or ' : ''}production when ready"
906
+ say ' 1. Review the build in Google Play Console'
907
+ say " 2. Promote to #{'beta or ' if track == 'alpha'}production when ready"
914
908
  when 'production'
915
- say " 1. Review is pending in Google Play Console"
916
- say " 2. Once approved, users will receive the update"
909
+ say ' 1. Review is pending in Google Play Console'
910
+ say ' 2. Once approved, users will receive the update'
917
911
  end
918
- say ""
919
- say " Google Play Console: https://play.google.com/console", :green
920
- say ""
921
-
912
+ say ''
913
+ say ' Google Play Console: https://play.google.com/console', :green
914
+ say ''
922
915
  rescue Build::Detector::NoProjectError => e
923
- say ""
924
- say "=" * 80, :red
925
- say "✗ Ship Failed", :red
926
- say "=" * 80, :red
927
- say ""
916
+ say ''
917
+ say '=' * 80, :red
918
+ say '✗ Ship Failed', :red
919
+ say '=' * 80, :red
920
+ say ''
928
921
  say "Error: #{e.message}", :red
929
- say ""
930
- say "💡 No Android Project Found: How to fix", :cyan
931
- say ""
922
+ say ''
923
+ say '💡 No Android Project Found: How to fix', :cyan
924
+ say ''
932
925
  say " → Make sure you're in an Android project directory", :yellow
933
- say " → Check for build.gradle or build.gradle.kts file", :yellow
934
- say " → For React Native: cd android && check build.gradle exists", :yellow
935
- say " → For Flutter: check android/app/build.gradle exists", :yellow
936
- say ""
926
+ say ' → Check for build.gradle or build.gradle.kts file', :yellow
927
+ say ' → For React Native: cd android && check build.gradle exists', :yellow
928
+ say ' → For Flutter: check android/app/build.gradle exists', :yellow
929
+ say ''
937
930
  exit 1
938
931
  rescue Upload::PlayStoreUploader::PartialUploadError => e
939
932
  # AAB was uploaded but track assignment/commit failed
940
933
  # Save build record to prevent version conflicts on retry
941
- say ""
942
- say "=" * 80, :red
943
- say "✗ Partial Upload - Track Assignment Failed", :red
944
- say "=" * 80, :red
945
- say ""
934
+ say ''
935
+ say '=' * 80, :red
936
+ say '✗ Partial Upload - Track Assignment Failed', :red
937
+ say '=' * 80, :red
938
+ say ''
946
939
  say "Error: #{e.message}", :red
947
- say ""
948
-
940
+ say ''
941
+
949
942
  # Save build record even on partial failure (AAB is on Play Store)
950
943
  if e.version_code
951
944
  save_android_build_record(client, config, package_name, e.version_code, version_name)
952
945
  say "📝 Build v#{e.version_code} recorded (prevents version conflicts on retry)", :yellow
953
- say ""
946
+ say ''
954
947
  end
955
-
948
+
956
949
  # Show track setup suggestions
957
950
  show_track_not_setup_suggestions(track)
958
-
959
- if aab_path && File.exist?(aab_path)
960
- say "📦 AAB saved at: #{aab_path}", :yellow
961
- end
962
- say ""
951
+
952
+ say "📦 AAB saved at: #{aab_path}", :yellow if aab_path && File.exist?(aab_path)
953
+ say ''
963
954
  exit 1
964
955
  rescue Upload::PlayStoreUploader::UploadError => e
965
956
  # Use enhanced error handler for Google Play errors
966
957
  handle_android_api_error(e, context: {
967
- title: 'Google Play Upload Failed',
968
- aab_path: aab_path,
969
- package_name: package_name,
970
- track: track
971
- })
958
+ title: 'Google Play Upload Failed',
959
+ aab_path: aab_path,
960
+ package_name: package_name,
961
+ track: track
962
+ })
972
963
  exit 1
973
964
  rescue Mysigner::ClientError => e
974
965
  # Handle My Signer API client errors
975
966
  handle_android_api_error(e, context: {
976
- title: 'API Error',
977
- aab_path: aab_path,
978
- package_name: package_name
979
- })
967
+ title: 'API Error',
968
+ aab_path: aab_path,
969
+ package_name: package_name
970
+ })
980
971
  exit 1
981
- rescue => e
982
- say ""
983
- say "=" * 80, :red
984
- say "✗ Ship Failed", :red
985
- say "=" * 80, :red
986
- say ""
972
+ rescue StandardError => e
973
+ say ''
974
+ say '=' * 80, :red
975
+ say '✗ Ship Failed', :red
976
+ say '=' * 80, :red
977
+ say ''
987
978
  say "Error: #{e.message}", :red
988
- say ""
989
-
979
+ say ''
980
+
990
981
  # Try to show actionable suggestions for unknown errors
991
982
  show_actionable_suggestions(e.message, platform: :android)
992
-
993
- if aab_path && File.exist?(aab_path)
994
- say "📦 AAB saved at: #{aab_path}", :yellow
995
- end
996
-
983
+
984
+ say "📦 AAB saved at: #{aab_path}", :yellow if aab_path && File.exist?(aab_path)
985
+
997
986
  show_debug_info(e) if ENV['DEBUG']
998
987
  exit 1
999
988
  end
@@ -1004,10 +993,11 @@ module Mysigner
1004
993
  response = client.get("/api/v1/organizations/#{config.current_organization_id}/android_apps")
1005
994
  apps = response[:data]['android_apps'] || []
1006
995
  app = apps.find { |a| a['package_name'] == package_name }
1007
-
996
+
1008
997
  return app['highest_version_code'].to_i if app && app['highest_version_code']
998
+
1009
999
  nil
1010
- rescue => e
1000
+ rescue StandardError
1011
1001
  # Silently fail - we'll use local version
1012
1002
  nil
1013
1003
  end
@@ -1018,7 +1008,7 @@ module Mysigner
1018
1008
  response = client.get("/api/v1/organizations/#{config.current_organization_id}/android_apps")
1019
1009
  apps = response[:data]['android_apps'] || []
1020
1010
  app = apps.find { |a| a['package_name'] == package_name }
1021
-
1011
+
1022
1012
  unless app
1023
1013
  # App doesn't exist in MySigner yet - create it with a friendly name
1024
1014
  friendly_name = generate_app_name_from_package(package_name)
@@ -1034,7 +1024,7 @@ module Mysigner
1034
1024
  "/api/v1/organizations/#{config.current_organization_id}/android_apps/#{app['id']}/android_builds",
1035
1025
  body: { android_build: { version_code: version_code, version_name: version_name, status: 'completed' } }
1036
1026
  )
1037
- rescue => e
1027
+ rescue StandardError => e
1038
1028
  # Non-fatal - just log for debugging
1039
1029
  say "⚠️ Could not save build record: #{e.message}", :yellow if options[:verbose]
1040
1030
  end
@@ -1057,7 +1047,7 @@ module Mysigner
1057
1047
  config = load_config
1058
1048
  client = create_client(config)
1059
1049
 
1060
- valid_tracks = ['internal', 'alpha', 'beta', 'production']
1050
+ valid_tracks = %w[internal alpha beta production]
1061
1051
  unless valid_tracks.include?(track)
1062
1052
  error "Invalid Android track: #{track}"
1063
1053
  say "Valid tracks: #{valid_tracks.join(', ')}", :yellow
@@ -1073,13 +1063,13 @@ module Mysigner
1073
1063
  track_label = track_labels[track]
1074
1064
 
1075
1065
  say "📤 Promote to Google Play #{track_label}", :cyan
1076
- say "=" * 80, :cyan
1077
- say ""
1066
+ say '=' * 80, :cyan
1067
+ say ''
1078
1068
 
1079
1069
  begin
1080
1070
  # Get package name
1081
1071
  package_name = options[:package_name]
1082
-
1072
+
1083
1073
  unless package_name
1084
1074
  begin
1085
1075
  project_info = Build::Detector.detect_android
@@ -1087,41 +1077,41 @@ module Mysigner
1087
1077
  parser = Build::AndroidParser.new(project_info)
1088
1078
  package_name = parser.application_id
1089
1079
  say "✓ Detected package: #{package_name}", :green
1090
- rescue
1091
- error "Could not detect package name from project"
1092
- say ""
1093
- say "Please specify manually:", :yellow
1080
+ rescue StandardError
1081
+ error 'Could not detect package name from project'
1082
+ say ''
1083
+ say 'Please specify manually:', :yellow
1094
1084
  say " mysigner submit #{track} --platform android --package-name com.your.app", :cyan
1095
1085
  exit 1
1096
1086
  end
1097
1087
  end
1098
1088
 
1099
- say ""
1089
+ say ''
1100
1090
  say "📦 Package: #{package_name}", :cyan
1101
1091
  say "🎯 Target Track: #{track_label}", :cyan
1102
- say ""
1092
+ say ''
1103
1093
 
1104
1094
  # Fetch Google Play credentials
1105
- say "🔐 Fetching Google Play credentials...", :yellow
1106
-
1095
+ say '🔐 Fetching Google Play credentials...', :yellow
1096
+
1107
1097
  org_response = client.get("/api/v1/organizations/#{config.current_organization_id}")
1108
1098
  org_data = org_response[:data]
1109
-
1099
+
1110
1100
  unless org_data['google_play_configured']
1111
- say ""
1112
- say "✗ Google Play credentials not configured", :red
1113
- say ""
1114
- say "Configure Google Play credentials in My Signer dashboard", :cyan
1101
+ say ''
1102
+ say '✗ Google Play credentials not configured', :red
1103
+ say ''
1104
+ say 'Configure Google Play credentials in My Signer dashboard', :cyan
1115
1105
  exit 1
1116
1106
  end
1117
1107
 
1118
1108
  service_account_json = org_data['google_play_service_account']
1119
- say "✓ Credentials loaded", :green
1120
- say ""
1109
+ say '✓ Credentials loaded', :green
1110
+ say ''
1121
1111
 
1122
1112
  # Get the latest build from the API
1123
- say "🔍 Finding builds in My Signer...", :yellow
1124
-
1113
+ say '🔍 Finding builds in My Signer...', :yellow
1114
+
1125
1115
  apps_response = client.get(
1126
1116
  "/api/v1/organizations/#{config.current_organization_id}/android_apps",
1127
1117
  params: { q: package_name }
@@ -1130,34 +1120,32 @@ module Mysigner
1130
1120
  app = apps.find { |a| a['package_name'] == package_name }
1131
1121
 
1132
1122
  unless app
1133
- say ""
1134
- say "⚠️ App not found in My Signer", :yellow
1135
- say ""
1136
- say "The app may not be synced yet. Try:", :cyan
1137
- say " mysigner sync android", :green
1138
- say ""
1123
+ say ''
1124
+ say '⚠️ App not found in My Signer', :yellow
1125
+ say ''
1126
+ say 'The app may not be synced yet. Try:', :cyan
1127
+ say ' mysigner sync android', :green
1128
+ say ''
1139
1129
  end
1140
1130
 
1141
1131
  # Use version code from option or prompt for it
1142
1132
  version_code = options[:version_code]
1143
-
1133
+
1144
1134
  unless version_code
1145
- say ""
1135
+ say ''
1146
1136
  say "Enter the version code to promote to #{track}:", :yellow
1147
- version_code = ask("Version code:")
1137
+ version_code = ask('Version code:')
1148
1138
  end
1149
1139
 
1150
- say ""
1140
+ say ''
1151
1141
  say "📤 Promoting version #{version_code} to #{track} track...", :cyan
1152
- say ""
1142
+ say ''
1153
1143
 
1154
1144
  # Use PlayStoreUploader to assign to track
1155
1145
  require_relative '../upload/play_store_uploader'
1156
-
1146
+
1157
1147
  release_notes = nil
1158
- if options[:release_notes]
1159
- release_notes = { 'en-US' => options[:release_notes] }
1160
- end
1148
+ release_notes = { 'en-US' => options[:release_notes] } if options[:release_notes]
1161
1149
 
1162
1150
  # Create a minimal uploader just for track assignment
1163
1151
  # We need to use the Google API directly for this
@@ -1198,33 +1186,32 @@ module Mysigner
1198
1186
  # Commit
1199
1187
  service.commit_edit(package_name, edit.id, changes_not_sent_for_review: true)
1200
1188
 
1201
- say ""
1202
- say "=" * 80, :green
1189
+ say ''
1190
+ say '=' * 80, :green
1203
1191
  say "✓ Successfully promoted to #{track} track!", :green
1204
- say "=" * 80, :green
1205
- say ""
1192
+ say '=' * 80, :green
1193
+ say ''
1206
1194
  say "📦 Package: #{package_name}"
1207
1195
  say "🔢 Version Code: #{version_code}"
1208
1196
  say "🎯 Track: #{track_label}"
1209
- say ""
1210
- say "View in Google Play Console:", :cyan
1211
- say " https://play.google.com/console", :green
1212
- say ""
1213
-
1197
+ say ''
1198
+ say 'View in Google Play Console:', :cyan
1199
+ say ' https://play.google.com/console', :green
1200
+ say ''
1214
1201
  rescue Google::Apis::ClientError => e
1215
- say ""
1216
- say "=" * 80, :red
1217
- say "✗ Promotion Failed", :red
1218
- say "=" * 80, :red
1219
- say ""
1202
+ say ''
1203
+ say '=' * 80, :red
1204
+ say '✗ Promotion Failed', :red
1205
+ say '=' * 80, :red
1206
+ say ''
1220
1207
  say "Google Play API error: #{e.message}", :red
1221
1208
  exit 1
1222
- rescue => e
1223
- say ""
1224
- say "=" * 80, :red
1225
- say "✗ Promotion Failed", :red
1226
- say "=" * 80, :red
1227
- say ""
1209
+ rescue StandardError => e
1210
+ say ''
1211
+ say '=' * 80, :red
1212
+ say '✗ Promotion Failed', :red
1213
+ say '=' * 80, :red
1214
+ say ''
1228
1215
  say "Error: #{e.message}", :red
1229
1216
  exit 1
1230
1217
  end
@@ -1236,7 +1223,7 @@ module Mysigner
1236
1223
 
1237
1224
  if opts[:metadata_file]
1238
1225
  file_overrides = load_metadata_file(opts[:metadata_file])
1239
- overrides = deep_merge_hashes(overrides, file_overrides)
1226
+ overrides = deep_merge_hashes(overrides, file_overrides)
1240
1227
  sources << {
1241
1228
  type: :file,
1242
1229
  path: File.expand_path(opts[:metadata_file]),
@@ -1258,16 +1245,12 @@ module Mysigner
1258
1245
  def load_metadata_file(path)
1259
1246
  expanded = File.expand_path(path)
1260
1247
 
1261
- unless File.exist?(expanded) && File.file?(expanded)
1262
- raise MetadataFileError, "Metadata file not found: #{expanded}"
1263
- end
1248
+ raise MetadataFileError, "Metadata file not found: #{expanded}" unless File.exist?(expanded) && File.file?(expanded)
1264
1249
 
1265
1250
  content = File.read(expanded)
1266
1251
  parsed = parse_metadata_content(content, expanded)
1267
1252
 
1268
- unless parsed.is_a?(Hash)
1269
- raise MetadataFileError, 'Metadata file must contain a JSON/YAML object at the top level'
1270
- end
1253
+ raise MetadataFileError, 'Metadata file must contain a JSON/YAML object at the top level' unless parsed.is_a?(Hash)
1271
1254
 
1272
1255
  stringify_keys(parsed)
1273
1256
  rescue Errno::EACCES => e
@@ -1278,11 +1261,9 @@ module Mysigner
1278
1261
  stripped = content.lstrip
1279
1262
 
1280
1263
  begin
1281
- if stripped.start_with?('---') || stripped.start_with?('- ')
1282
- return YAML.safe_load(content, aliases: true) || {}
1283
- end
1264
+ return YAML.safe_load(content, aliases: true) || {} if stripped.start_with?('---') || stripped.start_with?('- ')
1284
1265
 
1285
- return JSON.parse(content)
1266
+ JSON.parse(content)
1286
1267
  rescue JSON::ParserError
1287
1268
  begin
1288
1269
  YAML.safe_load(content, aliases: true) || {}
@@ -1301,10 +1282,10 @@ module Mysigner
1301
1282
  merged = base.dup
1302
1283
  overrides.each do |key, value|
1303
1284
  merged[key] = if merged[key].is_a?(Hash) && value.is_a?(Hash)
1304
- deep_merge_hashes(merged[key], value)
1305
- else
1306
- value
1307
- end
1285
+ deep_merge_hashes(merged[key], value)
1286
+ else
1287
+ value
1288
+ end
1308
1289
  end
1309
1290
  merged
1310
1291
  end
@@ -1348,7 +1329,7 @@ module Mysigner
1348
1329
  say " ASC Polling: completed in #{format_duration(wait[:elapsed_seconds])}"
1349
1330
  end
1350
1331
  else
1351
- say " ASC Polling: skipped (--no-wait)", :yellow
1332
+ say ' ASC Polling: skipped (--no-wait)', :yellow
1352
1333
  end
1353
1334
 
1354
1335
  if result[:submitted]
@@ -1361,81 +1342,84 @@ module Mysigner
1361
1342
  keys = Array(source[:keys]).join(', ')
1362
1343
  case source[:type]
1363
1344
  when :inline
1364
- say " Overrides: CLI flag#{keys.empty? ? '' : " (#{keys})"}"
1345
+ say " Overrides: CLI flag#{" (#{keys})" unless keys.empty?}"
1365
1346
  when :file
1366
- say " Overrides: #{File.basename(source[:path])}#{keys.empty? ? '' : " (#{keys})"}"
1347
+ say " Overrides: #{File.basename(source[:path])}#{" (#{keys})" unless keys.empty?}"
1367
1348
  end
1368
1349
  end
1369
1350
  end
1370
1351
  end
1371
1352
 
1372
- desc "build", "Build .xcarchive only (advanced - most users should use 'ship')"
1373
- method_option :configuration, aliases: '-c', default: 'Release', desc: 'Build configuration (Debug, Release, etc.)'
1353
+ desc 'build', "Build .xcarchive only (advanced - most users should use 'ship')"
1354
+ method_option :configuration, aliases: '-c', default: 'Release',
1355
+ desc: 'Build configuration (Debug, Release, etc.)'
1374
1356
  method_option :target, aliases: '-t', desc: 'Target to build (auto-detect if not specified)'
1375
1357
  method_option :scheme, aliases: '-s', desc: 'Scheme to build (defaults to target name)'
1376
1358
  method_option :type, default: 'appstore', desc: 'Build type: development, adhoc, appstore, enterprise'
1377
1359
  method_option :team, desc: 'Development team ID (overrides project setting)'
1378
1360
  method_option :bundle_id, aliases: '-b', desc: 'Bundle ID (overrides project setting)'
1379
- method_option :skip_extensions, type: :boolean, default: false, desc: 'Skip extension targets (useful when extensions are not configured)'
1361
+ method_option :skip_extensions, type: :boolean, default: false,
1362
+ desc: 'Skip extension targets (useful when extensions are not configured)'
1380
1363
  def build
1381
1364
  config = load_config
1382
1365
  client = create_client(config)
1383
1366
 
1384
- say "🔍 Detecting project...", :cyan
1385
- say ""
1367
+ say '🔍 Detecting project...', :cyan
1368
+ say ''
1386
1369
 
1387
1370
  begin
1388
1371
  # Detect project
1389
1372
  project_info = Build::Detector.detect
1390
-
1373
+
1391
1374
  framework_label = case project_info[:framework]
1392
- when :capacitor then "Capacitor/Ionic"
1393
- when :react_native then "React Native"
1394
- when :flutter then "Flutter"
1395
- else "Native iOS"
1396
- end
1397
-
1375
+ when :capacitor then 'Capacitor/Ionic'
1376
+ when :react_native then 'React Native'
1377
+ when :flutter then 'Flutter'
1378
+ else 'Native iOS'
1379
+ end
1380
+
1398
1381
  say "✓ Found: #{File.basename(project_info[:path])} (#{framework_label})", :green
1399
- say ""
1382
+ say ''
1400
1383
 
1401
1384
  # Parse project
1402
1385
  parser = Build::Parser.new(project_info)
1403
-
1386
+
1404
1387
  # Check if this is a buildable app (not framework/library)
1405
1388
  main_product_type = parser.product_type
1406
- unless [:app, :unknown].include?(main_product_type)
1389
+ unless %i[app unknown].include?(main_product_type)
1407
1390
  error "Cannot build #{main_product_type} projects for TestFlight"
1408
- say ""
1409
- say "My Signer builds iOS/macOS/tvOS apps for distribution.", :yellow
1391
+ say ''
1392
+ say 'My Signer builds iOS/macOS/tvOS apps for distribution.', :yellow
1410
1393
  say "This project builds a #{main_product_type}, not an app.", :yellow
1411
- say ""
1394
+ say ''
1412
1395
  exit 1
1413
1396
  end
1414
-
1397
+
1415
1398
  # Check for multiple apps and prompt user if needed
1416
1399
  if parser.has_multiple_apps? && !options[:target]
1417
1400
  app_targets = parser.app_targets
1418
- say "Multiple apps found in project:", :yellow
1401
+ say 'Multiple apps found in project:', :yellow
1419
1402
  app_targets.each_with_index do |target, i|
1420
1403
  say " #{i + 1}. #{target.name}", :cyan
1421
1404
  end
1422
- say ""
1423
-
1424
- choice = ask("Select app to build (1-#{app_targets.count}):", limited_to: (1..app_targets.count).map(&:to_s))
1405
+ say ''
1406
+
1407
+ choice = ask("Select app to build (1-#{app_targets.count}):",
1408
+ limited_to: (1..app_targets.count).map(&:to_s))
1425
1409
  target_name = app_targets[choice.to_i - 1].name
1426
1410
  else
1427
1411
  target_name = options[:target] || parser.main_target.name
1428
1412
  end
1429
-
1413
+
1430
1414
  say "🎯 Target: #{target_name}", :cyan
1431
-
1415
+
1432
1416
  # Show platform if not iOS
1433
1417
  platform = parser.target_platform(target_name)
1434
1418
  unless platform == :ios
1435
1419
  platform_label = platform.to_s.upcase
1436
1420
  say "📱 Platform: #{platform_label}", :cyan
1437
1421
  end
1438
-
1422
+
1439
1423
  # Show extensions if any
1440
1424
  if parser.has_extensions?
1441
1425
  ext_count = parser.extension_targets.count
@@ -1445,9 +1429,9 @@ module Mysigner
1445
1429
  say "🧩 Extensions: #{ext_count} (will be included in build)", :cyan
1446
1430
  end
1447
1431
  end
1448
-
1432
+
1449
1433
  bundle_id = options[:bundle_id] || parser.bundle_id(target_name, options[:configuration])
1450
-
1434
+
1451
1435
  # Validate bundle ID format if overridden
1452
1436
  if options[:bundle_id]
1453
1437
  if bundle_id =~ /\$\(|\$\{/
@@ -1455,74 +1439,72 @@ module Mysigner
1455
1439
  exit 1
1456
1440
  elsif bundle_id !~ /^[a-zA-Z0-9.-]+$/
1457
1441
  error "Invalid bundle ID format: #{bundle_id}"
1458
- say "Bundle IDs must contain only letters, numbers, hyphens, and periods", :yellow
1442
+ say 'Bundle IDs must contain only letters, numbers, hyphens, and periods', :yellow
1459
1443
  exit 1
1460
1444
  end
1461
1445
  end
1462
-
1463
- say "📦 Bundle ID: #{bundle_id}#{options[:bundle_id] ? ' (overridden)' : ''}", :cyan
1446
+
1447
+ say "📦 Bundle ID: #{bundle_id}#{' (overridden)' if options[:bundle_id]}", :cyan
1464
1448
  say "⚙️ Configuration: #{options[:configuration]}", :cyan
1465
-
1449
+
1466
1450
  # Check signing style
1467
1451
  sign_style = parser.code_sign_style(target_name, options[:configuration])
1468
1452
  say "🔐 Signing: #{sign_style || 'Not Set'}", :cyan
1469
-
1453
+
1470
1454
  # Auto-fetch team ID from API if not provided and project missing it
1471
1455
  team_id_to_use = options[:team]
1472
1456
  project_team_id = parser.team_id(target_name, options[:configuration])
1473
-
1457
+
1474
1458
  if !team_id_to_use && (project_team_id.nil? || project_team_id.empty?)
1475
- say "🔍 No team set in project, fetching from My Signer...", :yellow
1476
-
1459
+ say '🔍 No team set in project, fetching from My Signer...', :yellow
1460
+
1477
1461
  begin
1478
1462
  org_response = client.get("/api/v1/organizations/#{config.current_organization_id}")
1479
- api_team_id = org_response.dig(:data, 'app_store_connect_team_id') || org_response['app_store_connect_team_id']
1480
-
1463
+ api_team_id = org_response.dig(:data,
1464
+ 'app_store_connect_team_id') || org_response['app_store_connect_team_id']
1465
+
1481
1466
  if api_team_id && !api_team_id.empty?
1482
1467
  team_id_to_use = api_team_id
1483
1468
  say "✓ Using team from My Signer: #{api_team_id}", :green
1484
1469
  else
1485
- say "⚠️ No team ID configured in My Signer", :yellow
1470
+ say '⚠️ No team ID configured in My Signer', :yellow
1486
1471
  end
1487
- rescue => e
1472
+ rescue StandardError => e
1488
1473
  say "⚠️ Failed to fetch team from API: #{e.message}", :yellow
1489
1474
  end
1490
1475
  end
1491
-
1492
- say ""
1476
+
1477
+ say ''
1493
1478
 
1494
1479
  # Handle signing based on style
1495
1480
  if sign_style == 'Automatic'
1496
- say "ℹ️ Using Automatic signing (Xcode will manage profiles)", :yellow
1497
- say ""
1481
+ say 'ℹ️ Using Automatic signing (Xcode will manage profiles)', :yellow
1498
1482
  elsif sign_style == 'Manual'
1499
1483
  # Check if manual signing is already configured
1500
1484
  if parser.signing_configured?(target_name, options[:configuration])
1501
- say "ℹ️ Manual signing already configured, using existing settings", :yellow
1502
- say ""
1485
+ say 'ℹ️ Manual signing already configured, using existing settings', :yellow
1503
1486
  else
1504
- say "⚠️ Manual signing enabled but not configured", :yellow
1505
- say "🔐 Configuring manual signing via My Signer API...", :cyan
1506
-
1487
+ say '⚠️ Manual signing enabled but not configured', :yellow
1488
+ say '🔐 Configuring manual signing via My Signer API...', :cyan
1489
+
1507
1490
  configurator = Build::Configurator.new(parser, client, config.current_organization_id)
1508
1491
  build_type = options[:type].to_sym
1509
-
1492
+
1510
1493
  profile = configurator.configure!(target_name, options[:configuration], build_type: build_type)
1511
-
1494
+
1512
1495
  say "✓ Configured with profile: #{profile['name']}", :green
1513
- say ""
1514
1496
  end
1515
1497
  else
1516
1498
  # No signing style set, default to automatic signing for simplicity
1517
- say "ℹ️ No signing style set, defaulting to Automatic signing", :yellow
1518
- say "ℹ️ Xcode will manage profiles automatically", :yellow
1519
- say ""
1520
- say "💡 To use Manual signing instead, run: mysigner signing configure", :cyan
1521
- say ""
1499
+ say 'ℹ️ No signing style set, defaulting to Automatic signing', :yellow
1500
+ say 'ℹ️ Xcode will manage profiles automatically', :yellow
1501
+ say ''
1502
+ say '💡 To use Manual signing instead, run: mysigner signing configure', :cyan
1522
1503
  end
1504
+ say ''
1523
1505
 
1524
1506
  # Pre-build validation
1525
- say "🔍 Validating signing setup...", :cyan
1507
+ say '🔍 Validating signing setup...', :cyan
1526
1508
  validator = Signing::Validator.new(parser, target_name, options[:configuration], team_id: team_id_to_use)
1527
1509
  validator.validate!
1528
1510
 
@@ -1538,81 +1520,79 @@ module Mysigner
1538
1520
  skip_extensions: options[:skip_extensions]
1539
1521
  )
1540
1522
 
1541
- say ""
1542
- say "=" * 80, :green
1543
- say "✓ Build succeeded!", :green
1544
- say "=" * 80, :green
1545
- say ""
1523
+ say ''
1524
+ say '=' * 80, :green
1525
+ say '✓ Build succeeded!', :green
1526
+ say '=' * 80, :green
1527
+ say ''
1546
1528
  say "📦 Archive: #{archive_path}", :cyan
1547
- say ""
1548
- say "Next steps:", :bold
1529
+ say ''
1530
+ say 'Next steps:', :bold
1549
1531
  say " mysigner export #{archive_path}"
1550
- say " mysigner ship testflight"
1551
- say ""
1552
-
1532
+ say ' mysigner ship testflight'
1533
+ say ''
1553
1534
  rescue Build::Detector::NoProjectError => e
1554
1535
  error e.message
1555
- say ""
1556
- say "Supported project types:", :yellow
1557
- say " - Native iOS (.xcodeproj, .xcworkspace)"
1558
- say " - Capacitor/Ionic (ionic project with ios/ folder)"
1559
- say " - React Native (RN project with ios/ folder)"
1560
- say " - Flutter (flutter project with ios/ folder)"
1536
+ say ''
1537
+ say 'Supported project types:', :yellow
1538
+ say ' - Native iOS (.xcodeproj, .xcworkspace)'
1539
+ say ' - Capacitor/Ionic (ionic project with ios/ folder)'
1540
+ say ' - React Native (RN project with ios/ folder)'
1541
+ say ' - Flutter (flutter project with ios/ folder)'
1561
1542
  exit 1
1562
1543
  rescue Build::Configurator::ProfileNotFoundError => e
1563
1544
  error e.message
1564
- say ""
1565
- say "Try:", :yellow
1566
- say " mysigner profiles # List available profiles"
1567
- say " mysigner profile create # Create a new profile"
1545
+ say ''
1546
+ say 'Try:', :yellow
1547
+ say ' mysigner profiles # List available profiles'
1548
+ say ' mysigner doctor # Auto-create or repair profiles'
1568
1549
  exit 1
1569
1550
  rescue Build::Executor::BuildError => e
1570
1551
  # Analyze build errors and show helpful suggestions
1571
- say ""
1552
+ say ''
1572
1553
 
1573
- if executor && executor.respond_to?(:build_errors)
1554
+ if executor.respond_to?(:build_errors)
1574
1555
  require_relative '../build/error_analyzer'
1575
1556
  analyzer = Build::ErrorAnalyzer.new(executor.build_errors)
1576
1557
 
1577
- if analyzer.any_issues?
1578
- say analyzer.format_suggestions, :cyan
1579
- end
1558
+ say analyzer.format_suggestions, :cyan if analyzer.any_issues?
1580
1559
  end
1581
1560
 
1582
1561
  error e.message
1583
1562
  exit 1
1584
- rescue => e
1563
+ rescue StandardError => e
1585
1564
  error "Build failed: #{e.message}"
1586
- say ""
1587
- say "Full error:", :yellow
1565
+ say ''
1566
+ say 'Full error:', :yellow
1588
1567
  say e.full_message
1589
1568
  exit 1
1590
1569
  end
1591
1570
  end
1592
1571
 
1593
- desc "export ARCHIVE_PATH", "Export .xcarchive to .ipa (advanced - most users should use 'ship')"
1594
- method_option :method, type: :string, default: 'appstore', desc: 'Export method (appstore, adhoc, enterprise, development)'
1572
+ desc 'export ARCHIVE_PATH', "Export .xcarchive to .ipa (advanced - most users should use 'ship')"
1573
+ method_option :method, type: :string, default: 'appstore',
1574
+ desc: 'Export method (appstore, adhoc, enterprise, development)'
1595
1575
  method_option :output, type: :string, desc: 'Output directory for .ipa file'
1596
1576
  def export(archive_path)
1597
- config = load_config
1598
-
1577
+ load_config
1578
+
1599
1579
  begin
1600
- say "📦 My Signer - Export", :cyan
1601
- say "=" * 80, :cyan
1602
- say ""
1603
-
1580
+ say '📦 My Signer - Export', :cyan
1581
+ say '=' * 80, :cyan
1582
+ say ''
1583
+
1604
1584
  # Validate archive path
1605
1585
  unless File.exist?(archive_path)
1606
1586
  say "✗ Error: Archive not found: #{archive_path}", :red
1607
1587
  exit 1
1608
1588
  end
1609
-
1589
+
1610
1590
  # Create exporter
1611
1591
  exporter = Export::Exporter.new(
1612
1592
  archive_path,
1613
1593
  output_dir: options[:output]
1614
1594
  )
1615
-
1595
+
1616
1596
  # Export
1617
1597
  method = options[:method].to_sym
1618
1598
  ipa_path = exporter.export!(
@@ -1620,94 +1600,94 @@ module Mysigner
1620
1600
  team_id: nil,
1621
1601
  signing_style: 'automatic'
1622
1602
  )
1623
-
1624
- say ""
1625
- say "=" * 80, :green
1626
- say "✓ Export succeeded!", :green
1627
- say "=" * 80, :green
1628
- say ""
1603
+
1604
+ say ''
1605
+ say '=' * 80, :green
1606
+ say '✓ Export succeeded!', :green
1607
+ say '=' * 80, :green
1608
+ say ''
1629
1609
  say "📦 IPA: #{ipa_path}", :green
1630
- say ""
1631
- say "Next steps:", :cyan
1610
+ say ''
1611
+ say 'Next steps:', :cyan
1632
1612
  say " mysigner upload testflight #{ipa_path}", :cyan
1633
- say " mysigner ship testflight", :cyan
1634
- say ""
1635
-
1613
+ say ' mysigner ship testflight', :cyan
1614
+ say ''
1636
1615
  rescue Export::Exporter::ExportError => e
1637
- say ""
1616
+ say ''
1638
1617
  say "✗ Error: #{e.message}", :red
1639
1618
  exit 1
1640
1619
  rescue StandardError => e
1641
- say ""
1620
+ say ''
1642
1621
  say "✗ Unexpected error: #{e.message}", :red
1643
1622
  say e.backtrace.first(5).join("\n"), :red if ENV['DEBUG']
1644
1623
  exit 1
1645
1624
  end
1646
1625
  end
1647
1626
 
1648
- desc "upload testflight IPA_PATH", "Upload existing .ipa to TestFlight (advanced - most users should use 'ship')"
1627
+ desc 'upload testflight IPA_PATH',
1628
+ "Upload existing .ipa to TestFlight (advanced - most users should use 'ship')"
1649
1629
  method_option :wait, type: :boolean, default: false, desc: 'Wait for processing to complete'
1650
1630
  def upload(target, ipa_path)
1651
1631
  unless target == 'testflight'
1652
1632
  error "Only 'testflight' target is supported currently"
1653
- say "Usage: mysigner upload testflight IPA_PATH", :yellow
1633
+ say 'Usage: mysigner upload testflight IPA_PATH', :yellow
1654
1634
  exit 1
1655
1635
  end
1656
1636
 
1657
1637
  config = load_config
1658
1638
  client = create_client(config)
1659
-
1639
+
1660
1640
  begin
1661
- say "☁️ My Signer - Upload to TestFlight", :cyan
1662
- say "=" * 80, :cyan
1663
- say ""
1664
-
1641
+ say '☁️ My Signer - Upload to TestFlight', :cyan
1642
+ say '=' * 80, :cyan
1643
+ say ''
1644
+
1665
1645
  # Validate IPA path
1666
1646
  unless File.exist?(ipa_path)
1667
1647
  say "✗ Error: IPA file not found: #{ipa_path}", :red
1668
1648
  exit 1
1669
1649
  end
1670
-
1650
+
1671
1651
  # Fetch App Store Connect credentials from API
1672
- say "🔐 Fetching App Store Connect credentials...", :yellow
1673
-
1652
+ say '🔐 Fetching App Store Connect credentials...', :yellow
1653
+
1674
1654
  begin
1675
1655
  org_response = client.get("/api/v1/organizations/#{config.current_organization_id}")
1676
1656
  org_data = org_response[:data]
1677
-
1657
+
1678
1658
  # Check if credentials are configured
1679
1659
  unless org_data['app_store_connect_configured']
1680
- say ""
1681
- say "✗ App Store Connect credentials not configured", :red
1682
- say ""
1683
- say "Quick fix:", :cyan
1684
- say " mysigner doctor # Auto-configure now", :green
1685
- say ""
1686
- say "Or manually:", :cyan
1687
- say " 1. Run: mysigner onboard"
1688
- say " 2. Follow Step 5 to add credentials"
1689
- say ""
1660
+ say ''
1661
+ say '✗ App Store Connect credentials not configured', :red
1662
+ say ''
1663
+ say 'Quick fix:', :cyan
1664
+ say ' mysigner doctor # Auto-configure now', :green
1665
+ say ''
1666
+ say 'Or manually:', :cyan
1667
+ say ' 1. Run: mysigner onboard'
1668
+ say ' 2. Follow Step 5 to add credentials'
1669
+ say ''
1690
1670
  exit 1
1691
1671
  end
1692
-
1672
+
1693
1673
  # Get credentials (API will return the decrypted values)
1694
1674
  api_key = org_data['app_store_connect_key_id']
1695
1675
  api_issuer = org_data['app_store_connect_issuer_id']
1696
1676
  private_key = org_data['app_store_connect_private_key']
1697
-
1677
+
1698
1678
  unless api_key && api_issuer && private_key
1699
- say "✗ Error: Invalid credentials received from API", :red
1679
+ say '✗ Error: Invalid credentials received from API', :red
1700
1680
  exit 1
1701
1681
  end
1702
-
1703
- say "✓ Credentials loaded", :green
1704
- say ""
1682
+
1683
+ say '✓ Credentials loaded', :green
1684
+ say ''
1705
1685
  rescue Mysigner::ClientError => e
1706
- say ""
1686
+ say ''
1707
1687
  say "✗ Error fetching credentials: #{e.message}", :red
1708
1688
  exit 1
1709
1689
  end
1710
-
1690
+
1711
1691
  # Create uploader
1712
1692
  uploader = Upload::Uploader.new(
1713
1693
  ipa_path,
@@ -1715,35 +1695,34 @@ module Mysigner
1715
1695
  api_issuer: api_issuer,
1716
1696
  private_key: private_key
1717
1697
  )
1718
-
1698
+
1719
1699
  # Upload
1720
- result = uploader.upload!(wait_for_processing: options[:wait])
1721
-
1722
- say "🎉 Upload complete!", :green
1723
- say ""
1724
- say "Next steps:", :cyan
1725
- say " • Open App Store Connect to see your build"
1726
- say " • Wait for processing (5-15 minutes)"
1727
- say " • Distribute to TestFlight testers"
1728
- say ""
1729
-
1700
+ uploader.upload!(wait_for_processing: options[:wait])
1701
+
1702
+ say '🎉 Upload complete!', :green
1703
+ say ''
1704
+ say 'Next steps:', :cyan
1705
+ say ' • Open App Store Connect to see your build'
1706
+ say ' • Wait for processing (5-15 minutes)'
1707
+ say ' • Distribute to TestFlight testers'
1708
+ say ''
1730
1709
  rescue Upload::Uploader::TransporterNotFoundError => e
1731
- say ""
1710
+ say ''
1732
1711
  say "✗ Error: #{e.message}", :red
1733
1712
  exit 1
1734
1713
  rescue Upload::Uploader::UploadError => e
1735
- say ""
1714
+ say ''
1736
1715
  say "✗ Upload Error: #{e.message}", :red
1737
1716
  exit 1
1738
1717
  rescue StandardError => e
1739
- say ""
1718
+ say ''
1740
1719
  say "✗ Unexpected error: #{e.message}", :red
1741
1720
  say e.backtrace.first(5).join("\n"), :red if ENV['DEBUG']
1742
1721
  exit 1
1743
1722
  end
1744
1723
  end
1745
-
1746
- desc "submit [TRACK]", "📤 Submit existing build for store review (no upload)"
1724
+
1725
+ desc 'submit [TRACK]', '📤 Submit existing build for store review (no upload)'
1747
1726
  long_desc <<~DESC
1748
1727
  Submit an existing build for review without building/uploading.
1749
1728
 
@@ -1782,10 +1761,10 @@ module Mysigner
1782
1761
  method_option :support_url, type: :string, banner: 'URL', desc: 'Support URL (required for submission)'
1783
1762
  method_option :marketing_url, type: :string, banner: 'URL', desc: 'Marketing URL (optional)'
1784
1763
  method_option :privacy_url, type: :string, banner: 'URL', desc: 'Privacy Policy URL (optional)'
1785
- method_option :release_type, type: :string, enum: %w[AFTER_APPROVAL MANUAL SCHEDULED],
1786
- desc: 'Release type: AFTER_APPROVAL (auto-release), MANUAL (hold for manual release), or SCHEDULED'
1787
- method_option :scheduled_date, type: :string, banner: 'ISO8601',
1788
- desc: 'Scheduled release date (ISO 8601 format, e.g., 2026-02-01T10:00:00Z). Required when --release-type=SCHEDULED'
1764
+ method_option :release_type, type: :string, enum: %w[AFTER_APPROVAL MANUAL SCHEDULED],
1765
+ desc: 'Release type: AFTER_APPROVAL (auto-release), MANUAL (hold for manual release), or SCHEDULED'
1766
+ method_option :scheduled_date, type: :string, banner: 'ISO8601',
1767
+ desc: 'Scheduled release date (ISO 8601 format, e.g., 2026-02-01T10:00:00Z). Required when --release-type=SCHEDULED'
1789
1768
  method_option :platform, type: :string, desc: 'Platform: ios or android'
1790
1769
  method_option :package_name, type: :string, desc: 'Android package name'
1791
1770
  method_option :version_code, type: :string, desc: 'Android version code to promote'
@@ -1795,16 +1774,16 @@ module Mysigner
1795
1774
  client = create_client(config)
1796
1775
 
1797
1776
  # Determine platform
1798
- android_tracks = ['internal', 'alpha', 'beta', 'production']
1777
+ android_tracks = %w[internal alpha beta production]
1799
1778
  platform = options[:platform]&.to_sym
1800
1779
 
1801
1780
  # Auto-detect platform from track argument or option
1802
1781
  if platform.nil?
1803
- if track && android_tracks.include?(track)
1804
- platform = :android
1805
- else
1806
- platform = :ios
1807
- end
1782
+ platform = if track && android_tracks.include?(track)
1783
+ :android
1784
+ else
1785
+ :ios
1786
+ end
1808
1787
  end
1809
1788
 
1810
1789
  # Route to Android submit if needed
@@ -1814,13 +1793,13 @@ module Mysigner
1814
1793
  end
1815
1794
 
1816
1795
  # iOS submit flow continues below...
1817
- say "📤 Submit for App Store Review", :cyan
1818
- say "=" * 80, :cyan
1819
- say ""
1820
-
1796
+ say '📤 Submit for App Store Review', :cyan
1797
+ say '=' * 80, :cyan
1798
+ say ''
1799
+
1821
1800
  # Get bundle ID from project or option
1822
1801
  bundle_id = options[:bundle_id]
1823
-
1802
+
1824
1803
  unless bundle_id
1825
1804
  begin
1826
1805
  project_info = Build::Detector.detect
@@ -1828,32 +1807,32 @@ module Mysigner
1828
1807
  target_name = parser.main_target.name
1829
1808
  bundle_id = parser.bundle_id(target_name, 'Release')
1830
1809
  say "✓ Detected bundle ID from project: #{bundle_id}", :green
1831
- rescue
1832
- error "Could not detect bundle ID from project"
1833
- say ""
1834
- say "Please specify manually:", :yellow
1835
- say " mysigner submit --bundle-id com.your.app.id", :cyan
1810
+ rescue StandardError
1811
+ error 'Could not detect bundle ID from project'
1812
+ say ''
1813
+ say 'Please specify manually:', :yellow
1814
+ say ' mysigner submit --bundle-id com.your.app.id', :cyan
1836
1815
  exit 1
1837
1816
  end
1838
1817
  end
1839
-
1840
- say ""
1818
+
1819
+ say ''
1841
1820
  say "📱 Bundle ID: #{bundle_id}", :cyan
1842
- say ""
1843
-
1821
+ say ''
1822
+
1844
1823
  begin
1845
1824
  require_relative '../upload/app_store_submission'
1846
1825
  require_relative '../upload/app_store_automation'
1847
-
1826
+
1848
1827
  automation = Upload::AppStoreAutomation.new(
1849
1828
  client: client,
1850
1829
  organization_id: config.current_organization_id,
1851
1830
  opts: {
1852
- wait: false, # No need to wait - only using already-processed builds
1831
+ wait: false, # No need to wait - only using already-processed builds
1853
1832
  no_submit: false
1854
1833
  }
1855
1834
  )
1856
-
1835
+
1857
1836
  # Get version from project or option
1858
1837
  version_string = options[:version]
1859
1838
  unless version_string
@@ -1862,42 +1841,42 @@ module Mysigner
1862
1841
  parser ||= Build::Parser.new(project_info)
1863
1842
  target_name ||= parser.main_target.name
1864
1843
  version_string = parser.build_settings(target_name, 'Release')['MARKETING_VERSION']
1865
- rescue
1844
+ rescue StandardError
1866
1845
  version_string = nil
1867
1846
  end
1868
1847
  end
1869
-
1848
+
1870
1849
  build_info = {
1871
1850
  bundle_id: bundle_id,
1872
1851
  version: version_string || '1.0',
1873
1852
  build_number: options[:build_number]
1874
1853
  }
1875
-
1854
+
1876
1855
  # Force submission when running 'mysigner submit' explicitly
1877
1856
  # Build metadata overrides from CLI options
1878
1857
  metadata_overrides = { 'auto_submit' => true }
1879
1858
  override_keys = ['auto_submit']
1880
-
1859
+
1881
1860
  if options[:whats_new]
1882
1861
  metadata_overrides['whats_new'] = options[:whats_new]
1883
1862
  override_keys << 'whats_new'
1884
1863
  end
1885
-
1864
+
1886
1865
  if options[:support_url]
1887
1866
  metadata_overrides['support_url'] = options[:support_url]
1888
1867
  override_keys << 'support_url'
1889
1868
  end
1890
-
1869
+
1891
1870
  if options[:marketing_url]
1892
1871
  metadata_overrides['marketing_url'] = options[:marketing_url]
1893
1872
  override_keys << 'marketing_url'
1894
1873
  end
1895
-
1874
+
1896
1875
  if options[:privacy_url]
1897
1876
  metadata_overrides['privacy_policy_url'] = options[:privacy_url]
1898
1877
  override_keys << 'privacy_policy_url'
1899
1878
  end
1900
-
1879
+
1901
1880
  if options[:release_type]
1902
1881
  # Validate release_type
1903
1882
  valid_types = %w[AFTER_APPROVAL MANUAL SCHEDULED]
@@ -1909,25 +1888,25 @@ module Mysigner
1909
1888
  end
1910
1889
  metadata_overrides['release_type'] = rt
1911
1890
  override_keys << 'release_type'
1912
-
1891
+
1913
1892
  # Validate scheduled_date is provided when SCHEDULED
1914
1893
  if rt == 'SCHEDULED' && !options[:scheduled_date]
1915
- error "Scheduled release date is required when --release-type=SCHEDULED"
1916
- say "Use: --scheduled-date 2026-02-01T10:00:00Z", :yellow
1894
+ error 'Scheduled release date is required when --release-type=SCHEDULED'
1895
+ say 'Use: --scheduled-date 2026-02-01T10:00:00Z', :yellow
1917
1896
  exit 1
1918
1897
  end
1919
1898
  end
1920
-
1899
+
1921
1900
  if options[:scheduled_date]
1922
1901
  begin
1923
1902
  parsed_date = Time.parse(options[:scheduled_date])
1924
- if parsed_date < Time.now + 3600 # At least 1 hour in the future
1925
- error "Scheduled date must be at least 1 hour in the future"
1903
+ if parsed_date < Time.now + 3600 # At least 1 hour in the future
1904
+ error 'Scheduled date must be at least 1 hour in the future'
1926
1905
  exit 1
1927
1906
  end
1928
1907
  metadata_overrides['earliest_release_date'] = parsed_date.utc.iso8601
1929
1908
  override_keys << 'earliest_release_date'
1930
-
1909
+
1931
1910
  # Auto-set release_type to SCHEDULED if not already set
1932
1911
  unless metadata_overrides['release_type']
1933
1912
  metadata_overrides['release_type'] = 'SCHEDULED'
@@ -1935,11 +1914,11 @@ module Mysigner
1935
1914
  end
1936
1915
  rescue ArgumentError => e
1937
1916
  error "Invalid date format: #{options[:scheduled_date]}"
1938
- say "Use ISO 8601 format, e.g., 2026-02-01T10:00:00Z", :yellow
1917
+ say 'Use ISO 8601 format, e.g., 2026-02-01T10:00:00Z', :yellow
1939
1918
  exit 1
1940
1919
  end
1941
1920
  end
1942
-
1921
+
1943
1922
  submission = Upload::AppStoreSubmission.new(
1944
1923
  client,
1945
1924
  config.current_organization_id,
@@ -1947,71 +1926,70 @@ module Mysigner
1947
1926
  metadata_overrides: metadata_overrides,
1948
1927
  override_sources: [{ type: :inline, keys: override_keys }]
1949
1928
  )
1950
-
1929
+
1951
1930
  result = submission.submit_for_review!(automation: automation)
1952
-
1953
- say ""
1954
- say "=" * 80, :green
1955
- say "✓ Submission Complete!", :green
1956
- say "=" * 80, :green
1957
- say ""
1958
-
1931
+
1932
+ say ''
1933
+ say '=' * 80, :green
1934
+ say '✓ Submission Complete!', :green
1935
+ say '=' * 80, :green
1936
+ say ''
1937
+
1959
1938
  if result[:automation][:submitted]
1960
- say "🎉 Your app is submitted for App Store review!", :green
1961
- say ""
1962
- say "Monitor status:", :cyan
1963
- say " https://appstoreconnect.apple.com/apps", :green
1939
+ say '🎉 Your app is submitted for App Store review!', :green
1940
+ say ''
1941
+ say 'Monitor status:', :cyan
1942
+ say ' https://appstoreconnect.apple.com/apps', :green
1964
1943
  else
1965
1944
  say "⚠️ Submission skipped: #{result[:automation][:skip_reason]}", :yellow
1966
1945
  end
1967
- say ""
1968
-
1946
+ say ''
1969
1947
  rescue Upload::AppStoreAutomation::AutomationError => e
1970
1948
  # Use enhanced error handler for App Store automation errors
1971
1949
  handle_apple_api_error(e, context: {
1972
- title: 'Submission Failed',
1973
- bundle_id: options[:bundle_id]
1974
- })
1950
+ title: 'Submission Failed',
1951
+ bundle_id: options[:bundle_id]
1952
+ })
1975
1953
  exit 1
1976
1954
  rescue Mysigner::ClientError => e
1977
1955
  # Handle API client errors with actionable suggestions
1978
1956
  handle_apple_api_error(e, context: {
1979
- title: 'API Error'
1980
- })
1957
+ title: 'API Error'
1958
+ })
1981
1959
  exit 1
1982
- rescue => e
1983
- say ""
1984
- say "=" * 80, :red
1985
- say "✗ Submission Failed", :red
1986
- say "=" * 80, :red
1987
- say ""
1960
+ rescue StandardError => e
1961
+ say ''
1962
+ say '=' * 80, :red
1963
+ say '✗ Submission Failed', :red
1964
+ say '=' * 80, :red
1965
+ say ''
1988
1966
  say "Error: #{e.message}", :red
1989
- say ""
1990
-
1967
+ say ''
1968
+
1991
1969
  # Try to show actionable suggestions for unknown errors
1992
1970
  show_actionable_suggestions(e.message, platform: :ios)
1993
-
1971
+
1994
1972
  show_debug_info(e) if ENV['DEBUG']
1995
1973
  exit 1
1996
1974
  end
1997
1975
  end
1998
1976
 
1999
- desc "signing configure", "🧙 Wizard: Configure manual code signing in your Xcode project"
1977
+ desc 'signing configure', '🧙 Wizard: Configure manual code signing in your Xcode project'
2000
1978
  long_desc <<~DESC
2001
1979
  Guides you through setting up manual code signing for your project:
2002
-
1980
+
2003
1981
  1. Detects your project and targets
2004
1982
  2. Shows current signing configuration
2005
1983
  3. Helps you select team ID and provisioning profile
2006
1984
  4. Applies configuration to your Xcode project
2007
1985
  5. Validates the setup
2008
-
1986
+
2009
1987
  This is useful when automatic signing doesn't work or you need specific profiles.
2010
-
1988
+
2011
1989
  OPTIONS:
2012
1990
  --target NAME Configure specific target only
2013
1991
  --all-targets Configure all app and extension targets
2014
-
1992
+
2015
1993
  EXAMPLES:
2016
1994
  mysigner signing configure # Configure main app (auto-detect)
2017
1995
  mysigner signing configure --target MyWidget # Configure specific target
@@ -2022,50 +2000,50 @@ module Mysigner
2022
2000
  def signing(action)
2023
2001
  unless action == 'configure'
2024
2002
  error "Unknown action: #{action}"
2025
- say "Usage: mysigner signing configure", :yellow
2003
+ say 'Usage: mysigner signing configure', :yellow
2026
2004
  exit 1
2027
2005
  end
2028
-
2006
+
2029
2007
  config = load_config
2030
-
2008
+
2031
2009
  unless config.api_token
2032
2010
  error "Not logged in. Please run 'mysigner login' or 'mysigner onboard' first."
2033
2011
  exit 1
2034
2012
  end
2035
-
2013
+
2036
2014
  client = create_client(config)
2037
-
2015
+
2038
2016
  begin
2039
2017
  # Detect project
2040
2018
  project_info = Build::Detector.detect
2041
2019
  parser = Build::Parser.new(project_info)
2042
-
2020
+
2043
2021
  # Validate options
2044
2022
  if options[:target] && options[:all_targets]
2045
- error "Cannot use both --target and --all-targets"
2023
+ error 'Cannot use both --target and --all-targets'
2046
2024
  exit 1
2047
2025
  end
2048
-
2026
+
2049
2027
  # Check current signing style
2050
2028
  target_name = options[:target] || parser.main_target.name
2051
2029
  signing_style = parser.code_sign_style(target_name)
2052
-
2030
+
2053
2031
  if signing_style == 'Automatic'
2054
- say "⚠️ Project uses Automatic signing", :yellow
2055
- say ""
2056
- say "Your project is configured for Automatic signing, which means:", :cyan
2057
- say " • Xcode manages profiles automatically"
2058
- say " • No manual profile configuration needed"
2059
- say " • Team ID is all you need"
2060
- say ""
2032
+ say '⚠️ Project uses Automatic signing', :yellow
2033
+ say ''
2034
+ say 'Your project is configured for Automatic signing, which means:', :cyan
2035
+ say ' • Xcode manages profiles automatically'
2036
+ say ' • No manual profile configuration needed'
2037
+ say ' • Team ID is all you need'
2038
+ say ''
2061
2039
  say "Current Team ID: #{parser.team_id(target_name) || '(not set)'}", :green
2062
- say ""
2063
- say "You can build with: mysigner build"
2064
- say ""
2065
- say "💡 To convert to Manual signing, use: mysigner signing configure --force-manual"
2040
+ say ''
2041
+ say 'You can build with: mysigner build'
2042
+ say ''
2043
+ say '💡 To convert to Manual signing, use: mysigner signing configure --force-manual'
2066
2044
  return
2067
2045
  end
2068
-
2046
+
2069
2047
  # Run wizard for Manual signing
2070
2048
  require_relative '../signing/wizard'
2071
2049
  wizard_options = {
@@ -2074,7 +2052,6 @@ module Mysigner
2074
2052
  }
2075
2053
  wizard = Signing::Wizard.new(parser, client, config.current_organization_id, wizard_options)
2076
2054
  wizard.run!
2077
-
2078
2055
  rescue Build::Detector::NoProjectError => e
2079
2056
  error e.message
2080
2057
  exit 1
@@ -2087,7 +2064,6 @@ module Mysigner
2087
2064
  exit 1
2088
2065
  end
2089
2066
  end
2090
-
2091
2067
  end
2092
2068
  end
2093
2069
  end