mysigner 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +4 -0
- data/.rubocop_todo.yml +23 -9
- data/Gemfile.lock +1 -1
- data/README.md +1 -1
- data/certificate_.cer +0 -0
- data/exe/mysigner +17 -1
- data/iOS_App_Store_Profile.mobileprovision +1 -0
- data/iOS_Distribution_Certificate.cer +1 -0
- data/lib/mysigner/build/android_executor.rb +37 -11
- data/lib/mysigner/build/executor.rb +69 -29
- data/lib/mysigner/build/parser.rb +2 -2
- data/lib/mysigner/cleanup/private_keys_purger.rb +41 -0
- data/lib/mysigner/cli/auth_commands.rb +42 -18
- data/lib/mysigner/cli/build_commands.rb +307 -117
- data/lib/mysigner/cli/concerns/helpers.rb +32 -0
- data/lib/mysigner/cli/diagnostic_commands.rb +8 -1
- data/lib/mysigner/cli/resource_commands.rb +304 -114
- data/lib/mysigner/cli.rb +8 -0
- data/lib/mysigner/config.rb +68 -4
- data/lib/mysigner/export/exporter.rb +6 -1
- data/lib/mysigner/signing/gradle_signing_injector.rb +67 -0
- data/lib/mysigner/signing/keystore_manager.rb +50 -25
- data/lib/mysigner/upload/app_store_automation.rb +46 -1
- data/lib/mysigner/upload/asc_rest_uploader.rb +119 -0
- data/lib/mysigner/upload/play_store_uploader.rb +77 -40
- data/lib/mysigner/upload/uploader.rb +41 -12
- data/lib/mysigner/version.rb +1 -1
- data/profile_.mobileprovision +0 -0
- metadata +9 -3
- data/.DS_Store +0 -0
|
@@ -6,6 +6,7 @@ require 'time'
|
|
|
6
6
|
require_relative '../upload/play_store_uploader'
|
|
7
7
|
require_relative '../upload/app_store_automation'
|
|
8
8
|
require_relative '../upload/app_store_submission'
|
|
9
|
+
require_relative '../upload/asc_rest_uploader'
|
|
9
10
|
|
|
10
11
|
module Mysigner
|
|
11
12
|
class CLI < Thor
|
|
@@ -63,12 +64,15 @@ module Mysigner
|
|
|
63
64
|
method_option :bundle_id, aliases: '-b', desc: 'Bundle ID (overrides project setting)'
|
|
64
65
|
method_option :platform, type: :string, desc: 'Platform: ios or android (auto-detect if not specified)'
|
|
65
66
|
method_option :package_name, type: :string, desc: 'Android package name (overrides project setting)'
|
|
66
|
-
method_option :release_notes, type: :string, desc: 'Release notes for Android
|
|
67
|
+
method_option :release_notes, type: :string, desc: 'Release notes for Play Store (Android) / App Store (iOS whatsNew)'
|
|
68
|
+
method_option :metadata_file, type: :string, desc: 'Path to metadata JSON file (iOS App Store submissions)'
|
|
67
69
|
method_option :version, type: :string, desc: 'Set version name for Android (e.g., 1.2.0)'
|
|
68
70
|
method_option :release_type, type: :string, enum: %w[AFTER_APPROVAL MANUAL SCHEDULED],
|
|
69
71
|
desc: 'Release type for App Store: AFTER_APPROVAL, MANUAL, or SCHEDULED'
|
|
70
72
|
method_option :scheduled_date, type: :string, banner: 'ISO8601',
|
|
71
73
|
desc: 'Scheduled release date (ISO 8601, e.g., 2026-02-01T10:00:00Z)'
|
|
74
|
+
method_option :auto_submit, type: :boolean,
|
|
75
|
+
desc: 'Submit for App Store review after upload. Defaults to dashboard CLI Defaults, else true for ship appstore. Use --no-auto-submit to skip.'
|
|
72
76
|
def ship(target)
|
|
73
77
|
ios_targets = %w[testflight appstore]
|
|
74
78
|
android_targets = %w[internal alpha beta production]
|
|
@@ -302,47 +306,96 @@ module Mysigner
|
|
|
302
306
|
|
|
303
307
|
upload_start = Time.now
|
|
304
308
|
|
|
305
|
-
|
|
306
|
-
|
|
309
|
+
if ENV['MYSIGNER_USE_LEGACY_ASC'] == '1'
|
|
310
|
+
# LEGACY PATH — altool with server-fetched .p8. Will be removed in next release.
|
|
311
|
+
say '⚠️ MYSIGNER_USE_LEGACY_ASC=1 — using legacy altool upload path (deprecated).', :yellow
|
|
307
312
|
|
|
308
|
-
|
|
309
|
-
|
|
313
|
+
# Fetch App Store Connect credentials
|
|
314
|
+
say '🔐 Fetching App Store Connect credentials...', :yellow
|
|
310
315
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
316
|
+
org_response = client.get("/api/v1/organizations/#{config.current_organization_id}")
|
|
317
|
+
org_data = org_response[:data]
|
|
318
|
+
|
|
319
|
+
unless org_data['app_store_connect_configured']
|
|
320
|
+
say ''
|
|
321
|
+
say '✗ App Store Connect credentials not configured', :red
|
|
322
|
+
say ''
|
|
323
|
+
say 'Quick fix:', :cyan
|
|
324
|
+
say ' mysigner doctor # Auto-configure now', :green
|
|
325
|
+
say ''
|
|
326
|
+
say 'Or manually:', :cyan
|
|
327
|
+
say ' 1. Run: mysigner onboard'
|
|
328
|
+
say ' 2. Follow Step 5 to add credentials'
|
|
329
|
+
say ''
|
|
330
|
+
exit 1
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
api_key = org_data['app_store_connect_key_id']
|
|
334
|
+
api_issuer = org_data['app_store_connect_issuer_id']
|
|
335
|
+
private_key = org_data['app_store_connect_private_key']
|
|
336
|
+
|
|
337
|
+
unless api_key && api_issuer && private_key
|
|
338
|
+
say '✗ Error: Invalid credentials received from API', :red
|
|
339
|
+
exit 1
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
say '✓ Credentials loaded', :green
|
|
321
343
|
say ''
|
|
322
|
-
exit 1
|
|
323
|
-
end
|
|
324
344
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
345
|
+
# Upload (Uploader writes .p8 to Dir.mktmpdir + cleans up in ensure)
|
|
346
|
+
uploader = Upload::Uploader.new(
|
|
347
|
+
ipa_path,
|
|
348
|
+
api_key: api_key,
|
|
349
|
+
api_issuer: api_issuer,
|
|
350
|
+
private_key: private_key
|
|
351
|
+
)
|
|
328
352
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
353
|
+
uploader.upload!(wait_for_processing: options[:wait])
|
|
354
|
+
else
|
|
355
|
+
# DEFAULT PATH — ASC REST Build Upload API. No .p8 ever leaves the server.
|
|
356
|
+
require_relative '../upload/asc_rest_uploader'
|
|
333
357
|
|
|
334
|
-
|
|
335
|
-
|
|
358
|
+
# Resolve apple_app_id from bundle_id (may already have been fetched for is_appstore)
|
|
359
|
+
if !defined?(app) || app.nil?
|
|
360
|
+
app_response = client.get("/api/v1/organizations/#{config.current_organization_id}/apple_apps",
|
|
361
|
+
params: { bundle_id: bundle_id })
|
|
362
|
+
app = Array(app_response.dig(:data, 'data', 'apps')).first
|
|
363
|
+
end
|
|
336
364
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
365
|
+
unless app && app['id']
|
|
366
|
+
say "✗ App not found in MySigner for bundle_id: #{bundle_id}", :red
|
|
367
|
+
say 'Run: mysigner sync ios', :yellow
|
|
368
|
+
exit 1
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
# Read version info from the built IPA
|
|
372
|
+
ipa_info = Upload::Uploader.extract_ipa_info(ipa_path)
|
|
373
|
+
cf_version = ipa_info[:cf_bundle_version] || '1'
|
|
374
|
+
cf_short = ipa_info[:cf_bundle_short_version_string] || '1.0'
|
|
344
375
|
|
|
345
|
-
|
|
376
|
+
say "📤 Uploading via App Store Connect REST API (version #{cf_short} build #{cf_version})...", :cyan
|
|
377
|
+
|
|
378
|
+
rest = Mysigner::Upload::AscRestUploader.new(
|
|
379
|
+
client: client,
|
|
380
|
+
organization_id: config.current_organization_id,
|
|
381
|
+
ipa_path: ipa_path,
|
|
382
|
+
apple_app_id: app['id'],
|
|
383
|
+
cf_bundle_version: cf_version,
|
|
384
|
+
cf_bundle_short_version_string: cf_short,
|
|
385
|
+
platform: 'IOS'
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
result = rest.call
|
|
389
|
+
case result[:final_state]
|
|
390
|
+
when 'COMPLETE'
|
|
391
|
+
say '✓ Upload complete — Apple accepted the build', :green
|
|
392
|
+
when 'FAILED', 'INVALIDATED'
|
|
393
|
+
say "✗ Apple rejected the upload: #{result[:final_state]}", :red
|
|
394
|
+
exit 1
|
|
395
|
+
when 'TIMEOUT'
|
|
396
|
+
say '⚠ Apple is still processing — check App Store Connect.', :yellow
|
|
397
|
+
end
|
|
398
|
+
end
|
|
346
399
|
|
|
347
400
|
timings[:upload] = Time.now - upload_start
|
|
348
401
|
|
|
@@ -427,14 +480,26 @@ module Mysigner
|
|
|
427
480
|
wait: true,
|
|
428
481
|
timeout: 1800,
|
|
429
482
|
poll_interval: 15,
|
|
430
|
-
no_submit: false
|
|
483
|
+
no_submit: false,
|
|
484
|
+
# ship appstore defaults to submitting for review, but
|
|
485
|
+
# dashboard `cli_defaults.auto_submit = false` now wins
|
|
486
|
+
# (the old hard-override that clobbered it was removed).
|
|
487
|
+
default_submit: true
|
|
431
488
|
}
|
|
432
489
|
)
|
|
433
490
|
|
|
434
491
|
# Submit the new build (use its specific build number)
|
|
435
|
-
# Build metadata overrides from CLI options
|
|
436
|
-
|
|
437
|
-
|
|
492
|
+
# Build metadata overrides from CLI options — start from any
|
|
493
|
+
# --metadata-file + --release-notes, then layer in ship-specific
|
|
494
|
+
# release_type/scheduled_date. auto_submit is NOT forced here;
|
|
495
|
+
# precedence is: --auto-submit flag > cli_defaults > command default.
|
|
496
|
+
ship_overrides, override_sources = build_metadata_overrides(options)
|
|
497
|
+
ship_override_keys = []
|
|
498
|
+
|
|
499
|
+
if options.key?(:auto_submit)
|
|
500
|
+
ship_overrides['auto_submit'] = options[:auto_submit]
|
|
501
|
+
ship_override_keys << 'auto_submit'
|
|
502
|
+
end
|
|
438
503
|
|
|
439
504
|
if options[:release_type]
|
|
440
505
|
# Validate release_type
|
|
@@ -477,6 +542,8 @@ module Mysigner
|
|
|
477
542
|
end
|
|
478
543
|
end
|
|
479
544
|
|
|
545
|
+
override_sources << { type: :inline, keys: ship_override_keys }
|
|
546
|
+
|
|
480
547
|
submission = Upload::AppStoreSubmission.new(
|
|
481
548
|
client,
|
|
482
549
|
config.current_organization_id,
|
|
@@ -485,7 +552,7 @@ module Mysigner
|
|
|
485
552
|
build_number: new_build['build_number'] # Use the specific build we found
|
|
486
553
|
},
|
|
487
554
|
metadata_overrides: ship_overrides,
|
|
488
|
-
override_sources:
|
|
555
|
+
override_sources: override_sources
|
|
489
556
|
)
|
|
490
557
|
|
|
491
558
|
submission_result = submission.submit_for_review!(automation: automation)
|
|
@@ -611,6 +678,11 @@ module Mysigner
|
|
|
611
678
|
ipa_path: ipa_path
|
|
612
679
|
})
|
|
613
680
|
exit 1
|
|
681
|
+
rescue Mysigner::Upload::AscRestUploader::BuildVersionConflictError => e
|
|
682
|
+
say ''
|
|
683
|
+
say "✗ #{e.message}", :red
|
|
684
|
+
say ''
|
|
685
|
+
exit 1
|
|
614
686
|
rescue StandardError => e
|
|
615
687
|
say ''
|
|
616
688
|
say '=' * 80, :red
|
|
@@ -805,49 +877,82 @@ module Mysigner
|
|
|
805
877
|
|
|
806
878
|
upload_start = Time.now
|
|
807
879
|
|
|
808
|
-
#
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
org_response = client.get("/api/v1/organizations/#{config.current_organization_id}")
|
|
812
|
-
org_data = org_response[:data]
|
|
880
|
+
# Phase 0: mint short-lived OAuth2 access token server-side.
|
|
881
|
+
# Service-account JSON never leaves the server.
|
|
882
|
+
say '🔐 Requesting Google Play access token...', :yellow
|
|
813
883
|
|
|
814
|
-
|
|
884
|
+
begin
|
|
885
|
+
token_response = client.post(
|
|
886
|
+
"/api/v1/organizations/#{config.current_organization_id}/credentials/google_play/access_token"
|
|
887
|
+
)
|
|
888
|
+
access_token = token_response[:data]['access_token']
|
|
889
|
+
rescue Mysigner::NotFoundError, Mysigner::ValidationError
|
|
815
890
|
say ''
|
|
816
891
|
say '✗ Google Play credentials not configured', :red
|
|
817
892
|
say ''
|
|
818
893
|
say 'Quick fix:', :cyan
|
|
819
894
|
say ' Configure Google Play credentials in My Signer dashboard', :green
|
|
820
895
|
say ''
|
|
821
|
-
say 'Or configure in My Signer dashboard', :yellow
|
|
822
|
-
say ''
|
|
823
896
|
exit 1
|
|
824
897
|
end
|
|
825
898
|
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
unless service_account_json
|
|
829
|
-
say '✗ Error: Service account JSON not found', :red
|
|
899
|
+
if access_token.nil? || access_token.to_s.empty?
|
|
900
|
+
say '✗ Error: Failed to mint Google Play access token', :red
|
|
830
901
|
exit 1
|
|
831
902
|
end
|
|
832
903
|
|
|
833
|
-
say '✓
|
|
904
|
+
say '✓ Access token minted (valid ~1 hour)', :green
|
|
834
905
|
say ''
|
|
835
906
|
|
|
836
|
-
#
|
|
907
|
+
# Phase 0: fetch Android CLI Defaults from the dashboard
|
|
908
|
+
# (android_apps.cli_defaults JSONB). Fields here act as base
|
|
909
|
+
# values; explicit CLI flags below layer on top.
|
|
910
|
+
android_defaults = fetch_android_release_defaults(client, config, package_name)
|
|
911
|
+
if android_defaults
|
|
912
|
+
say "✓ Loaded CLI Defaults for #{package_name}", :green
|
|
913
|
+
say ''
|
|
914
|
+
end
|
|
915
|
+
|
|
916
|
+
# Upload to Google Play with bare bearer token
|
|
837
917
|
require_relative '../upload/play_store_uploader'
|
|
838
918
|
|
|
919
|
+
# Merge release notes: flag > defaults.release_notes (Hash)
|
|
839
920
|
release_notes = nil
|
|
840
|
-
|
|
921
|
+
if options[:release_notes]
|
|
922
|
+
release_notes = { 'en-US' => options[:release_notes] }
|
|
923
|
+
elsif android_defaults && android_defaults['release_notes'].is_a?(Hash) && android_defaults['release_notes'].any?
|
|
924
|
+
release_notes = android_defaults['release_notes']
|
|
925
|
+
end
|
|
926
|
+
|
|
927
|
+
# Effective track: positional arg wins; defaults only kick in
|
|
928
|
+
# if the user left it implicit (which `ship android` currently
|
|
929
|
+
# doesn't allow — track is required — but kept here for
|
|
930
|
+
# symmetry with submit_android and future-proofing).
|
|
931
|
+
effective_track = track.presence || android_defaults&.dig('default_track') || 'internal'
|
|
932
|
+
|
|
933
|
+
status = android_defaults&.dig('default_status')
|
|
934
|
+
user_fraction = android_defaults&.dig('default_user_fraction')
|
|
935
|
+
in_app_update_priority = android_defaults&.dig('default_in_app_update_priority')
|
|
936
|
+
release_name = android_defaults&.dig('release_name')
|
|
937
|
+
country_targeting = android_defaults&.dig('country_targeting')
|
|
938
|
+
changes_not_sent_for_review = android_defaults&.key?('changes_not_sent_for_review') ? android_defaults['changes_not_sent_for_review'] : nil
|
|
939
|
+
country_targeting = country_targeting.transform_keys(&:to_sym) if country_targeting.is_a?(Hash)
|
|
841
940
|
|
|
842
941
|
uploader = Upload::PlayStoreUploader.new(
|
|
843
942
|
aab_path: aab_path,
|
|
844
|
-
|
|
943
|
+
access_token: access_token,
|
|
845
944
|
package_name: package_name
|
|
846
945
|
)
|
|
847
946
|
|
|
848
947
|
uploader.upload!(
|
|
849
|
-
track:
|
|
850
|
-
release_notes: release_notes
|
|
948
|
+
track: effective_track,
|
|
949
|
+
release_notes: release_notes,
|
|
950
|
+
user_fraction: user_fraction,
|
|
951
|
+
status: status,
|
|
952
|
+
in_app_update_priority: in_app_update_priority,
|
|
953
|
+
release_name: release_name,
|
|
954
|
+
country_targeting: country_targeting,
|
|
955
|
+
changes_not_sent_for_review: changes_not_sent_for_review
|
|
851
956
|
)
|
|
852
957
|
|
|
853
958
|
timings[:upload] = Time.now - upload_start
|
|
@@ -988,6 +1093,29 @@ module Mysigner
|
|
|
988
1093
|
end
|
|
989
1094
|
end
|
|
990
1095
|
|
|
1096
|
+
# Fetch Android CLI Defaults (android_apps.cli_defaults) for the
|
|
1097
|
+
# package_name. Returns nil if none configured or request fails —
|
|
1098
|
+
# ship proceeds with CLI-flag-only values in that case.
|
|
1099
|
+
def fetch_android_release_defaults(client, config, package_name)
|
|
1100
|
+
response = client.get(
|
|
1101
|
+
"/api/v1/organizations/#{config.current_organization_id}/android_releases",
|
|
1102
|
+
params: { package_name: package_name }
|
|
1103
|
+
)
|
|
1104
|
+
data = response[:data] if response[:success]
|
|
1105
|
+
return nil unless data.is_a?(Hash)
|
|
1106
|
+
|
|
1107
|
+
releases = data['android_releases']
|
|
1108
|
+
return nil unless releases.is_a?(Array) && releases.any?
|
|
1109
|
+
|
|
1110
|
+
releases.first
|
|
1111
|
+
rescue Mysigner::NotFoundError
|
|
1112
|
+
nil
|
|
1113
|
+
rescue StandardError => e
|
|
1114
|
+
# Non-fatal: log and proceed without defaults.
|
|
1115
|
+
puts "⚠️ Could not fetch Android CLI Defaults: #{e.message}" if ENV['MYSIGNER_VERBOSE'] == '1'
|
|
1116
|
+
nil
|
|
1117
|
+
end
|
|
1118
|
+
|
|
991
1119
|
# Fetch highest version code from API
|
|
992
1120
|
def fetch_android_highest_version_code(client, config, package_name)
|
|
993
1121
|
response = client.get("/api/v1/organizations/#{config.current_organization_id}/android_apps")
|
|
@@ -1091,13 +1219,15 @@ module Mysigner
|
|
|
1091
1219
|
say "🎯 Target Track: #{track_label}", :cyan
|
|
1092
1220
|
say ''
|
|
1093
1221
|
|
|
1094
|
-
#
|
|
1095
|
-
say '🔐
|
|
1096
|
-
|
|
1097
|
-
org_response = client.get("/api/v1/organizations/#{config.current_organization_id}")
|
|
1098
|
-
org_data = org_response[:data]
|
|
1222
|
+
# Phase 0: mint short-lived access token server-side; JSON stays on server
|
|
1223
|
+
say '🔐 Requesting Google Play access token...', :yellow
|
|
1099
1224
|
|
|
1100
|
-
|
|
1225
|
+
begin
|
|
1226
|
+
token_response = client.post(
|
|
1227
|
+
"/api/v1/organizations/#{config.current_organization_id}/credentials/google_play/access_token"
|
|
1228
|
+
)
|
|
1229
|
+
access_token = token_response[:data]['access_token']
|
|
1230
|
+
rescue Mysigner::NotFoundError, Mysigner::ValidationError
|
|
1101
1231
|
say ''
|
|
1102
1232
|
say '✗ Google Play credentials not configured', :red
|
|
1103
1233
|
say ''
|
|
@@ -1105,8 +1235,12 @@ module Mysigner
|
|
|
1105
1235
|
exit 1
|
|
1106
1236
|
end
|
|
1107
1237
|
|
|
1108
|
-
|
|
1109
|
-
|
|
1238
|
+
if access_token.nil? || access_token.to_s.empty?
|
|
1239
|
+
say '✗ Error: Failed to mint Google Play access token', :red
|
|
1240
|
+
exit 1
|
|
1241
|
+
end
|
|
1242
|
+
|
|
1243
|
+
say '✓ Access token minted', :green
|
|
1110
1244
|
say ''
|
|
1111
1245
|
|
|
1112
1246
|
# Get the latest build from the API
|
|
@@ -1147,19 +1281,11 @@ module Mysigner
|
|
|
1147
1281
|
release_notes = nil
|
|
1148
1282
|
release_notes = { 'en-US' => options[:release_notes] } if options[:release_notes]
|
|
1149
1283
|
|
|
1150
|
-
#
|
|
1151
|
-
# We need to use the Google API directly for this
|
|
1152
|
-
require 'googleauth'
|
|
1284
|
+
# Use the Google API directly with the bare bearer token for track assignment
|
|
1153
1285
|
require 'google/apis/androidpublisher_v3'
|
|
1154
|
-
require 'stringio'
|
|
1155
|
-
|
|
1156
|
-
auth = Google::Auth::ServiceAccountCredentials.make_creds(
|
|
1157
|
-
json_key_io: StringIO.new(service_account_json),
|
|
1158
|
-
scope: 'https://www.googleapis.com/auth/androidpublisher'
|
|
1159
|
-
)
|
|
1160
1286
|
|
|
1161
1287
|
service = Google::Apis::AndroidpublisherV3::AndroidPublisherService.new
|
|
1162
|
-
service.authorization =
|
|
1288
|
+
service.authorization = access_token
|
|
1163
1289
|
|
|
1164
1290
|
# Create edit
|
|
1165
1291
|
edit = service.insert_edit(package_name, Google::Apis::AndroidpublisherV3::AppEdit.new)
|
|
@@ -1648,56 +1774,102 @@ module Mysigner
|
|
|
1648
1774
|
exit 1
|
|
1649
1775
|
end
|
|
1650
1776
|
|
|
1651
|
-
|
|
1652
|
-
|
|
1777
|
+
if ENV['MYSIGNER_USE_LEGACY_ASC'] == '1'
|
|
1778
|
+
# LEGACY PATH — altool with server-fetched .p8. Will be removed in next release.
|
|
1779
|
+
say '⚠️ MYSIGNER_USE_LEGACY_ASC=1 — using legacy altool upload path (deprecated).', :yellow
|
|
1780
|
+
say '🔐 Fetching App Store Connect credentials...', :yellow
|
|
1653
1781
|
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1782
|
+
begin
|
|
1783
|
+
org_response = client.get("/api/v1/organizations/#{config.current_organization_id}")
|
|
1784
|
+
org_data = org_response[:data]
|
|
1657
1785
|
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1786
|
+
unless org_data['app_store_connect_configured']
|
|
1787
|
+
say ''
|
|
1788
|
+
say '✗ App Store Connect credentials not configured', :red
|
|
1789
|
+
say ''
|
|
1790
|
+
say 'Quick fix:', :cyan
|
|
1791
|
+
say ' mysigner doctor # Auto-configure now', :green
|
|
1792
|
+
say ''
|
|
1793
|
+
say 'Or manually:', :cyan
|
|
1794
|
+
say ' 1. Run: mysigner onboard'
|
|
1795
|
+
say ' 2. Follow Step 5 to add credentials'
|
|
1796
|
+
say ''
|
|
1797
|
+
exit 1
|
|
1798
|
+
end
|
|
1799
|
+
|
|
1800
|
+
api_key = org_data['app_store_connect_key_id']
|
|
1801
|
+
api_issuer = org_data['app_store_connect_issuer_id']
|
|
1802
|
+
private_key = org_data['app_store_connect_private_key']
|
|
1803
|
+
|
|
1804
|
+
unless api_key && api_issuer && private_key
|
|
1805
|
+
say '✗ Error: Invalid credentials received from API', :red
|
|
1806
|
+
exit 1
|
|
1807
|
+
end
|
|
1808
|
+
|
|
1809
|
+
say '✓ Credentials loaded', :green
|
|
1665
1810
|
say ''
|
|
1666
|
-
|
|
1667
|
-
say ' 1. Run: mysigner onboard'
|
|
1668
|
-
say ' 2. Follow Step 5 to add credentials'
|
|
1811
|
+
rescue Mysigner::ClientError => e
|
|
1669
1812
|
say ''
|
|
1813
|
+
say "✗ Error fetching credentials: #{e.message}", :red
|
|
1670
1814
|
exit 1
|
|
1671
1815
|
end
|
|
1672
1816
|
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1817
|
+
uploader = Upload::Uploader.new(
|
|
1818
|
+
ipa_path,
|
|
1819
|
+
api_key: api_key,
|
|
1820
|
+
api_issuer: api_issuer,
|
|
1821
|
+
private_key: private_key
|
|
1822
|
+
)
|
|
1677
1823
|
|
|
1678
|
-
|
|
1679
|
-
|
|
1824
|
+
uploader.upload!(wait_for_processing: options[:wait])
|
|
1825
|
+
else
|
|
1826
|
+
# DEFAULT PATH — ASC REST Build Upload API.
|
|
1827
|
+
require_relative '../upload/asc_rest_uploader'
|
|
1828
|
+
|
|
1829
|
+
ipa_info = Upload::Uploader.extract_ipa_info(ipa_path)
|
|
1830
|
+
bundle_id = ipa_info[:bundle_id]
|
|
1831
|
+
cf_version = ipa_info[:cf_bundle_version] || '1'
|
|
1832
|
+
cf_short = ipa_info[:cf_bundle_short_version_string] || '1.0'
|
|
1833
|
+
|
|
1834
|
+
if bundle_id.nil? || bundle_id.empty?
|
|
1835
|
+
say '✗ Could not extract bundle identifier from IPA.', :red
|
|
1836
|
+
say ' Ensure the file is a valid iOS .ipa with a Payload/*.app/Info.plist.', :yellow
|
|
1680
1837
|
exit 1
|
|
1681
1838
|
end
|
|
1682
1839
|
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
say ''
|
|
1687
|
-
say "✗ Error fetching credentials: #{e.message}", :red
|
|
1688
|
-
exit 1
|
|
1689
|
-
end
|
|
1840
|
+
app_response = client.get("/api/v1/organizations/#{config.current_organization_id}/apple_apps",
|
|
1841
|
+
params: { bundle_id: bundle_id })
|
|
1842
|
+
app = Array(app_response.dig(:data, 'data', 'apps')).first
|
|
1690
1843
|
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1844
|
+
unless app && app['id']
|
|
1845
|
+
say "✗ App with bundle ID '#{bundle_id}' not found in MySigner.", :red
|
|
1846
|
+
say ' Run: mysigner sync ios', :yellow
|
|
1847
|
+
exit 1
|
|
1848
|
+
end
|
|
1849
|
+
|
|
1850
|
+
say "📤 Uploading #{File.basename(ipa_path)} via App Store Connect REST API (version #{cf_short} build #{cf_version})...", :cyan
|
|
1851
|
+
|
|
1852
|
+
rest = Mysigner::Upload::AscRestUploader.new(
|
|
1853
|
+
client: client,
|
|
1854
|
+
organization_id: config.current_organization_id,
|
|
1855
|
+
ipa_path: ipa_path,
|
|
1856
|
+
apple_app_id: app['id'],
|
|
1857
|
+
cf_bundle_version: cf_version,
|
|
1858
|
+
cf_bundle_short_version_string: cf_short,
|
|
1859
|
+
platform: 'IOS'
|
|
1860
|
+
)
|
|
1698
1861
|
|
|
1699
|
-
|
|
1700
|
-
|
|
1862
|
+
result = rest.call
|
|
1863
|
+
case result[:final_state]
|
|
1864
|
+
when 'COMPLETE'
|
|
1865
|
+
say '✓ Upload complete — Apple accepted the build', :green
|
|
1866
|
+
when 'FAILED', 'INVALIDATED'
|
|
1867
|
+
say "✗ Apple rejected the upload: #{result[:final_state]}", :red
|
|
1868
|
+
exit 1
|
|
1869
|
+
when 'TIMEOUT'
|
|
1870
|
+
say '⚠ Apple is still processing — check App Store Connect.', :yellow
|
|
1871
|
+
end
|
|
1872
|
+
end
|
|
1701
1873
|
|
|
1702
1874
|
say '🎉 Upload complete!', :green
|
|
1703
1875
|
say ''
|
|
@@ -1714,6 +1886,11 @@ module Mysigner
|
|
|
1714
1886
|
say ''
|
|
1715
1887
|
say "✗ Upload Error: #{e.message}", :red
|
|
1716
1888
|
exit 1
|
|
1889
|
+
rescue Mysigner::Upload::AscRestUploader::BuildVersionConflictError => e
|
|
1890
|
+
say ''
|
|
1891
|
+
say "✗ #{e.message}", :red
|
|
1892
|
+
say ''
|
|
1893
|
+
exit 1
|
|
1717
1894
|
rescue StandardError => e
|
|
1718
1895
|
say ''
|
|
1719
1896
|
say "✗ Unexpected error: #{e.message}", :red
|
|
@@ -1769,6 +1946,8 @@ module Mysigner
|
|
|
1769
1946
|
method_option :package_name, type: :string, desc: 'Android package name'
|
|
1770
1947
|
method_option :version_code, type: :string, desc: 'Android version code to promote'
|
|
1771
1948
|
method_option :release_notes, type: :string, desc: 'Release notes for Android'
|
|
1949
|
+
method_option :auto_submit, type: :boolean,
|
|
1950
|
+
desc: 'Submit for review. Defaults to dashboard CLI Defaults, else true. Use --no-auto-submit to skip.'
|
|
1772
1951
|
def submit(track = nil)
|
|
1773
1952
|
config = load_config
|
|
1774
1953
|
client = create_client(config)
|
|
@@ -1829,7 +2008,11 @@ module Mysigner
|
|
|
1829
2008
|
organization_id: config.current_organization_id,
|
|
1830
2009
|
opts: {
|
|
1831
2010
|
wait: false, # No need to wait - only using already-processed builds
|
|
1832
|
-
no_submit: false
|
|
2011
|
+
no_submit: false,
|
|
2012
|
+
# `mysigner submit` without --auto-submit/--no-auto-submit
|
|
2013
|
+
# defaults to submitting; cli_defaults.auto_submit=false can
|
|
2014
|
+
# still suppress it.
|
|
2015
|
+
default_submit: true
|
|
1833
2016
|
}
|
|
1834
2017
|
)
|
|
1835
2018
|
|
|
@@ -1852,10 +2035,17 @@ module Mysigner
|
|
|
1852
2035
|
build_number: options[:build_number]
|
|
1853
2036
|
}
|
|
1854
2037
|
|
|
1855
|
-
#
|
|
1856
|
-
#
|
|
1857
|
-
|
|
1858
|
-
|
|
2038
|
+
# `mysigner submit` defaults to submitting (see AppStoreAutomation
|
|
2039
|
+
# opts[:default_submit]=true above) but no longer hard-clobbers
|
|
2040
|
+
# the user's dashboard `cli_defaults.auto_submit = false`.
|
|
2041
|
+
# Precedence: --auto-submit flag > cli_defaults > command default.
|
|
2042
|
+
metadata_overrides = {}
|
|
2043
|
+
override_keys = []
|
|
2044
|
+
|
|
2045
|
+
if options.key?(:auto_submit)
|
|
2046
|
+
metadata_overrides['auto_submit'] = options[:auto_submit]
|
|
2047
|
+
override_keys << 'auto_submit'
|
|
2048
|
+
end
|
|
1859
2049
|
|
|
1860
2050
|
if options[:whats_new]
|
|
1861
2051
|
metadata_overrides['whats_new'] = options[:whats_new]
|
|
@@ -36,6 +36,38 @@ module Mysigner
|
|
|
36
36
|
end
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
+
# Client-side UDID validity check for iOS devices. Matches the two
|
|
40
|
+
# formats Apple uses: 25-character alphanumeric (older devices pre-
|
|
41
|
+
# iPhone X) and 40-character hex, optionally with a single dash after
|
|
42
|
+
# the first 8 chars (newer). Also rejects obviously synthetic values
|
|
43
|
+
# (all zeros, single-character repeats) that Apple's dev-portal
|
|
44
|
+
# sandbox has been known to accept even though they can never match
|
|
45
|
+
# a real device.
|
|
46
|
+
def valid_ios_udid?(udid)
|
|
47
|
+
return false if udid.nil? || udid.strip.empty?
|
|
48
|
+
|
|
49
|
+
normalized = udid.strip.upcase
|
|
50
|
+
|
|
51
|
+
# 25-char legacy UDID: alphanumeric
|
|
52
|
+
legacy = normalized.match?(/\A[0-9A-F]{25}\z/)
|
|
53
|
+
|
|
54
|
+
# 40-char modern UDID: hex, optional dash after first 8 chars
|
|
55
|
+
modern_plain = normalized.match?(/\A[0-9A-F]{40}\z/)
|
|
56
|
+
modern_dashed = normalized.match?(/\A[0-9A-F]{8}-[0-9A-F]{16}\z/) # 8-16 form some tools emit (older spec)
|
|
57
|
+
modern_full_dashed = normalized.match?(/\A[0-9A-F]{8}-[0-9A-F]{32}\z/) # 8-32 (what xcrun outputs)
|
|
58
|
+
|
|
59
|
+
return false unless legacy || modern_plain || modern_dashed || modern_full_dashed
|
|
60
|
+
|
|
61
|
+
hex_only = normalized.delete('-')
|
|
62
|
+
# Reject trivially synthetic UDIDs. A real UDID has at least 4
|
|
63
|
+
# distinct hex characters among its 25/40 positions; "000…" or
|
|
64
|
+
# "AAAA…" or "012345…" style sequences flunk that.
|
|
65
|
+
distinct = hex_only.chars.uniq.size
|
|
66
|
+
return false if distinct < 4
|
|
67
|
+
|
|
68
|
+
true
|
|
69
|
+
end
|
|
70
|
+
|
|
39
71
|
def load_config
|
|
40
72
|
# CI/CD mode: prefer environment variables when set
|
|
41
73
|
return Config.from_env if Config.env_configured?
|
|
@@ -634,8 +634,15 @@ module Mysigner
|
|
|
634
634
|
end
|
|
635
635
|
|
|
636
636
|
no_commands do
|
|
637
|
-
# Helper method for yes/no prompts with Enter defaulting to yes
|
|
637
|
+
# Helper method for yes/no prompts with Enter defaulting to yes.
|
|
638
|
+
# When stdin is not a TTY (pipe, redirect, CI), default to NO so
|
|
639
|
+
# `mysigner doctor` never silently mutates user files (e.g. ~/.zshrc)
|
|
640
|
+
# without an interactive confirmation.
|
|
638
641
|
def yes_with_default?(statement, color = nil)
|
|
642
|
+
unless $stdin.tty?
|
|
643
|
+
say "#{statement} [Y/n] (non-interactive: assuming no)", color
|
|
644
|
+
return false
|
|
645
|
+
end
|
|
639
646
|
response = ask("#{statement} [Y/n]", color).to_s.strip.downcase
|
|
640
647
|
response.empty? || response == 'y' || response == 'yes'
|
|
641
648
|
end
|