mysigner 0.1.7 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +25 -0
- data/.rubocop_todo.yml +6 -1
- data/CHANGELOG.md +92 -0
- data/Gemfile.lock +7 -7
- data/README.md +94 -1
- data/exe/mysigner +55 -1
- data/lib/mysigner/auth/asc_jwt_minter.rb +68 -0
- data/lib/mysigner/auth/google_oauth_minter.rb +89 -0
- data/lib/mysigner/cleanup/private_keys_purger.rb +0 -1
- data/lib/mysigner/cli/auth_commands.rb +355 -5
- data/lib/mysigner/cli/build_commands.rb +540 -267
- data/lib/mysigner/cli/concerns/helpers.rb +135 -0
- data/lib/mysigner/cli.rb +3 -2
- data/lib/mysigner/config.rb +40 -1
- data/lib/mysigner/credential_resolver.rb +1099 -0
- data/lib/mysigner/local_credentials.rb +281 -0
- data/lib/mysigner/signing/keystore_manager.rb +7 -10
- data/lib/mysigner/signing/validator.rb +20 -9
- data/lib/mysigner/upload/asc_rest_uploader.rb +252 -35
- data/lib/mysigner/upload/asc_submitter.rb +432 -0
- data/lib/mysigner/upload/play_store_uploader.rb +95 -3
- data/lib/mysigner/version.rb +1 -1
- data/lib/mysigner.rb +1 -0
- metadata +6 -5
- data/certificate_.cer +0 -0
- data/iOS_App_Store_Profile.mobileprovision +0 -1
- data/iOS_Distribution_Certificate.cer +0 -1
- data/profile_.mobileprovision +0 -0
|
@@ -73,6 +73,35 @@ module Mysigner
|
|
|
73
73
|
desc: 'Scheduled release date (ISO 8601, e.g., 2026-02-01T10:00:00Z)'
|
|
74
74
|
method_option :auto_submit, type: :boolean,
|
|
75
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.'
|
|
76
|
+
# mysigner-22 Phase 5 — local-only credential auto-discovery
|
|
77
|
+
# overrides. These layer on top of env vars / Keychain / disk scan;
|
|
78
|
+
# see Mysigner::CredentialResolver. They are no-ops in vault mode.
|
|
79
|
+
method_option :asc_key_path, type: :string, banner: 'PATH',
|
|
80
|
+
desc: 'Path to your App Store Connect .p8 key (local-only mode)'
|
|
81
|
+
method_option :asc_key_id, type: :string, banner: 'KEY_ID',
|
|
82
|
+
desc: 'App Store Connect Key ID (local-only mode)'
|
|
83
|
+
method_option :asc_issuer_id, type: :string, banner: 'UUID',
|
|
84
|
+
desc: 'App Store Connect Issuer ID (local-only mode)'
|
|
85
|
+
method_option :play_credentials, type: :string, banner: 'PATH',
|
|
86
|
+
desc: 'Path to Google Play service-account JSON (local-only mode)'
|
|
87
|
+
# mysigner-22 Phase 7 — Android keystore overrides for --local-only.
|
|
88
|
+
# In vault mode these are ignored; the cascade only fires when
|
|
89
|
+
# local_only? is true.
|
|
90
|
+
method_option :keystore_path, type: :string, banner: 'PATH',
|
|
91
|
+
desc: 'Path to Android signing keystore .jks/.keystore (local-only mode)'
|
|
92
|
+
method_option :keystore_password, type: :string, banner: 'PWD',
|
|
93
|
+
desc: 'Android keystore password (local-only mode)'
|
|
94
|
+
method_option :key_alias, type: :string, banner: 'ALIAS',
|
|
95
|
+
desc: 'Android key alias inside the keystore (local-only mode)'
|
|
96
|
+
method_option :key_password, type: :string, banner: 'PWD',
|
|
97
|
+
desc: 'Android key password (defaults to keystore password) (local-only mode)'
|
|
98
|
+
# mysigner-22 — explicit Apple-side app id override for local-only
|
|
99
|
+
# mode. When the bundleId lookup against Apple's /v1/apps returns
|
|
100
|
+
# zero or multiple matches, the user can short-circuit it by passing
|
|
101
|
+
# the exact appstoreconnect app id from the URL of their app page
|
|
102
|
+
# (e.g. https://appstoreconnect.apple.com/apps/<APPLE_APP_ID>/...).
|
|
103
|
+
method_option :apple_id, type: :string, banner: 'APPLE_APP_ID',
|
|
104
|
+
desc: 'App Store Connect app id (overrides bundleId lookup in --local-only mode)'
|
|
76
105
|
def ship(target)
|
|
77
106
|
ios_targets = %w[testflight appstore]
|
|
78
107
|
android_targets = %w[internal alpha beta production]
|
|
@@ -115,6 +144,12 @@ module Mysigner
|
|
|
115
144
|
|
|
116
145
|
is_appstore = (target == 'appstore')
|
|
117
146
|
|
|
147
|
+
# mysigner-42 — when the user opts into local-only for `ship`
|
|
148
|
+
# (testflight/appstore), surface the banner once so they know their
|
|
149
|
+
# .p8 won't traverse the MySigner server. The uploader itself
|
|
150
|
+
# enforces the contract below.
|
|
151
|
+
emit_local_only_banner if local_only?
|
|
152
|
+
|
|
118
153
|
config = load_config
|
|
119
154
|
client = create_client(config)
|
|
120
155
|
|
|
@@ -191,28 +226,38 @@ module Mysigner
|
|
|
191
226
|
project_team_id = parser.team_id(target_name, options[:configuration])
|
|
192
227
|
|
|
193
228
|
if !team_id_to_use && (project_team_id.nil? || project_team_id.empty?)
|
|
194
|
-
|
|
229
|
+
# mysigner-22 — local-only mode cannot read team-id from the
|
|
230
|
+
# MySigner org record (no client). Surface a useful hint
|
|
231
|
+
# instead of letting xcodebuild fail later with a generic
|
|
232
|
+
# signing error.
|
|
233
|
+
if local_only?
|
|
234
|
+
say '⚠️ No team set in project and --local-only mode (cannot fetch from MySigner).', :yellow
|
|
235
|
+
say ' Pass --team <TEAM_ID> or set DEVELOPMENT_TEAM in your Xcode project.', :yellow
|
|
236
|
+
else
|
|
237
|
+
say '🔍 No team set in project, fetching from My Signer...', :yellow
|
|
195
238
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
239
|
+
begin
|
|
240
|
+
org_response = client.get("/api/v1/organizations/#{config.current_organization_id}")
|
|
241
|
+
api_team_id = org_response.dig(:data,
|
|
242
|
+
'app_store_connect_team_id') || org_response['app_store_connect_team_id']
|
|
200
243
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
244
|
+
if api_team_id && !api_team_id.empty?
|
|
245
|
+
team_id_to_use = api_team_id
|
|
246
|
+
say "✓ Using team from My Signer: #{api_team_id}", :green
|
|
247
|
+
else
|
|
248
|
+
say '⚠️ No team ID configured in My Signer', :yellow
|
|
249
|
+
end
|
|
250
|
+
rescue StandardError => e
|
|
251
|
+
say "⚠️ Failed to fetch team from API: #{e.message}", :yellow
|
|
206
252
|
end
|
|
207
|
-
rescue StandardError => e
|
|
208
|
-
say "⚠️ Failed to fetch team from API: #{e.message}", :yellow
|
|
209
253
|
end
|
|
210
254
|
end
|
|
211
255
|
say ''
|
|
212
256
|
|
|
213
257
|
# Pre-build validation
|
|
214
258
|
say '🔍 Validating signing setup...', :cyan
|
|
215
|
-
validator = Signing::Validator.new(parser, target_name, options[:configuration],
|
|
259
|
+
validator = Signing::Validator.new(parser, target_name, options[:configuration],
|
|
260
|
+
team_id: team_id_to_use, local_only: local_only?)
|
|
216
261
|
validator.validate!
|
|
217
262
|
|
|
218
263
|
executor = Build::Executor.new(project_info, parser)
|
|
@@ -254,9 +299,14 @@ module Mysigner
|
|
|
254
299
|
say "📦 IPA: #{ipa_path}", :cyan
|
|
255
300
|
say ''
|
|
256
301
|
|
|
257
|
-
# STEP 2.5: Get current latest build (BEFORE upload) - App Store only
|
|
302
|
+
# STEP 2.5: Get current latest build (BEFORE upload) - App Store only.
|
|
303
|
+
# mysigner-22 — in local-only mode every probe here was a
|
|
304
|
+
# MySigner-server call. We skip the whole block: Apple itself is
|
|
305
|
+
# the source of truth for whether the upload succeeded
|
|
306
|
+
# (AscRestUploader polls /v1/buildUploads directly), so the
|
|
307
|
+
# "did a new build appear server-side" comparison is moot.
|
|
258
308
|
latest_build_before_upload = nil
|
|
259
|
-
if is_appstore
|
|
309
|
+
if is_appstore && !local_only?
|
|
260
310
|
say '=' * 80, :cyan
|
|
261
311
|
say 'Getting Current Latest Build', :cyan
|
|
262
312
|
say '=' * 80, :cyan
|
|
@@ -306,101 +356,113 @@ module Mysigner
|
|
|
306
356
|
|
|
307
357
|
upload_start = Time.now
|
|
308
358
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
359
|
+
# ASC REST Build Upload API. No .p8 ever leaves the server in
|
|
360
|
+
# vault mode; local-only mode delegates to altool via the
|
|
361
|
+
# uploader itself.
|
|
362
|
+
require_relative '../upload/asc_rest_uploader'
|
|
363
|
+
|
|
364
|
+
# In local-only mode, pre-resolve ASC creds via the cascade
|
|
365
|
+
# (flag → env → keychain → ~/.appstoreconnect → prompt) BEFORE
|
|
366
|
+
# the app-id lookup, because the lookup itself needs a JWT.
|
|
367
|
+
# The uploader will mint the JWT from these; no LocalCredentials
|
|
368
|
+
# round-trip happens inside it. In vault mode this is nil and
|
|
369
|
+
# the uploader takes the server-mediated path unchanged.
|
|
370
|
+
asc_creds_for_uploader = (resolve_local_asc_creds_or_exit if local_only?)
|
|
371
|
+
|
|
372
|
+
# Resolve apple_app_id.
|
|
373
|
+
# - vault mode: MySigner /apple_apps lookup (may have been
|
|
374
|
+
# pre-fetched in the appstore sync block above).
|
|
375
|
+
# - local-only: explicit --apple-id wins; otherwise hit Apple's
|
|
376
|
+
# /v1/apps?filter[bundleId]= directly.
|
|
377
|
+
apple_app_id =
|
|
378
|
+
if local_only?
|
|
379
|
+
resolve_apple_app_id_local!(
|
|
380
|
+
bundle_id: bundle_id,
|
|
381
|
+
apple_id_override: options[:apple_id],
|
|
382
|
+
asc_creds: asc_creds_for_uploader
|
|
383
|
+
)
|
|
384
|
+
else
|
|
385
|
+
if !defined?(app) || app.nil?
|
|
386
|
+
app_response = client.get("/api/v1/organizations/#{config.current_organization_id}/apple_apps",
|
|
387
|
+
params: { bundle_id: bundle_id })
|
|
388
|
+
app = Array(app_response.dig(:data, 'data', 'apps')).first
|
|
389
|
+
end
|
|
332
390
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
391
|
+
unless app && app['id']
|
|
392
|
+
say "✗ App not found in MySigner for bundle_id: #{bundle_id}", :red
|
|
393
|
+
say 'Run: mysigner sync ios', :yellow
|
|
394
|
+
exit 1
|
|
395
|
+
end
|
|
336
396
|
|
|
337
|
-
|
|
338
|
-
say '✗ Error: Invalid credentials received from API', :red
|
|
339
|
-
exit 1
|
|
397
|
+
app['id']
|
|
340
398
|
end
|
|
341
399
|
|
|
342
|
-
|
|
343
|
-
|
|
400
|
+
# Read version info from the built IPA
|
|
401
|
+
ipa_info = Upload::Uploader.extract_ipa_info(ipa_path)
|
|
402
|
+
cf_version = ipa_info[:cf_bundle_version] || '1'
|
|
403
|
+
cf_short = ipa_info[:cf_bundle_short_version_string] || '1.0'
|
|
344
404
|
|
|
345
|
-
|
|
346
|
-
uploader = Upload::Uploader.new(
|
|
347
|
-
ipa_path,
|
|
348
|
-
api_key: api_key,
|
|
349
|
-
api_issuer: api_issuer,
|
|
350
|
-
private_key: private_key
|
|
351
|
-
)
|
|
405
|
+
say "📤 Uploading via App Store Connect REST API (version #{cf_short} build #{cf_version})...", :cyan
|
|
352
406
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
|
407
|
+
rest = Mysigner::Upload::AscRestUploader.new(
|
|
408
|
+
client: client,
|
|
409
|
+
organization_id: local_only? ? nil : config.current_organization_id,
|
|
410
|
+
ipa_path: ipa_path,
|
|
411
|
+
apple_app_id: apple_app_id,
|
|
412
|
+
cf_bundle_version: cf_version,
|
|
413
|
+
cf_bundle_short_version_string: cf_short,
|
|
414
|
+
platform: 'IOS',
|
|
415
|
+
local_only: local_only?,
|
|
416
|
+
asc_creds: asc_creds_for_uploader
|
|
417
|
+
)
|
|
370
418
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
419
|
+
result = rest.call
|
|
420
|
+
case result[:final_state]
|
|
421
|
+
when 'COMPLETE'
|
|
422
|
+
say '✓ Upload complete — Apple accepted the build', :green
|
|
423
|
+
when 'FAILED', 'INVALIDATED'
|
|
424
|
+
say "✗ Apple rejected the upload: #{result[:final_state]}", :red
|
|
425
|
+
exit 1
|
|
426
|
+
when 'TIMEOUT'
|
|
427
|
+
say '⚠ Apple is still processing — check App Store Connect.', :yellow
|
|
428
|
+
end
|
|
375
429
|
|
|
376
|
-
|
|
430
|
+
timings[:upload] = Time.now - upload_start
|
|
377
431
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
432
|
+
# STEP 4: Submit for App Store Review (appstore only).
|
|
433
|
+
# mysigner-22 follow-up — local-only now drives the same
|
|
434
|
+
# 4-step Apple REST submission flow the vault path drives via
|
|
435
|
+
# MySigner, using the JWT minted from the user's local .p8.
|
|
436
|
+
# Gated by --auto-submit (default true, --no-auto-submit opts
|
|
437
|
+
# out) so users who want "upload only, finish in dashboard"
|
|
438
|
+
# still have that path.
|
|
439
|
+
if is_appstore && local_only?
|
|
440
|
+
auto_submit_default = true
|
|
441
|
+
auto_submit = options.key?(:auto_submit) ? options[:auto_submit] : auto_submit_default
|
|
387
442
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
say '
|
|
392
|
-
|
|
393
|
-
say
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
443
|
+
say ''
|
|
444
|
+
say '=' * 80, :cyan
|
|
445
|
+
if auto_submit
|
|
446
|
+
say '[4/4] Submit for App Store Review (--local-only)', :cyan
|
|
447
|
+
say '=' * 80, :cyan
|
|
448
|
+
say ''
|
|
449
|
+
submit_appstore_local!(
|
|
450
|
+
asc_creds: asc_creds_for_uploader,
|
|
451
|
+
apple_app_id: apple_app_id,
|
|
452
|
+
cf_bundle_version: cf_version,
|
|
453
|
+
cf_bundle_short_version_string: cf_short
|
|
454
|
+
)
|
|
455
|
+
else
|
|
456
|
+
say '[4/4] Submit for App Store Review (skipped: --no-auto-submit)', :cyan
|
|
457
|
+
say '=' * 80, :cyan
|
|
458
|
+
say ''
|
|
459
|
+
say '⚠️ Auto-submit disabled. The build is uploaded; finish the submission at:', :yellow
|
|
460
|
+
say ' https://appstoreconnect.apple.com/apps', :cyan
|
|
461
|
+
say ''
|
|
397
462
|
end
|
|
398
463
|
end
|
|
399
464
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
# STEP 4: Submit for App Store Review (appstore only)
|
|
403
|
-
if is_appstore
|
|
465
|
+
if is_appstore && !local_only?
|
|
404
466
|
say ''
|
|
405
467
|
say '=' * 80, :cyan
|
|
406
468
|
say '[4/5] Waiting for Apple to Process Build', :cyan
|
|
@@ -683,6 +745,14 @@ module Mysigner
|
|
|
683
745
|
say "✗ #{e.message}", :red
|
|
684
746
|
say ''
|
|
685
747
|
exit 1
|
|
748
|
+
rescue Mysigner::Upload::AscRestUploader::MissingLocalCredentialsError => e
|
|
749
|
+
# mysigner-42 — local-only requested but no credentials stored.
|
|
750
|
+
# Fail loud with a non-stack-trace message; the message already
|
|
751
|
+
# tells the user where to store the credentials.
|
|
752
|
+
say ''
|
|
753
|
+
say "✗ #{e.message}", :red
|
|
754
|
+
say ''
|
|
755
|
+
exit 1
|
|
686
756
|
rescue StandardError => e
|
|
687
757
|
say ''
|
|
688
758
|
say '=' * 80, :red
|
|
@@ -704,8 +774,138 @@ module Mysigner
|
|
|
704
774
|
end
|
|
705
775
|
|
|
706
776
|
no_commands do
|
|
777
|
+
# mysigner-22 — local-only equivalent of the MySigner `/apple_apps`
|
|
778
|
+
# lookup. Calls Apple's /v1/apps?filter[bundleId]= directly with
|
|
779
|
+
# the resolved ASC JWT and returns the matched `id` (string).
|
|
780
|
+
# Honors --apple-id as an unconditional override (skip the lookup
|
|
781
|
+
# entirely). Exits 1 with a clear pointer to --apple-id on zero or
|
|
782
|
+
# multiple matches so the user has a one-knob fix.
|
|
783
|
+
def resolve_apple_app_id_local!(bundle_id:, apple_id_override:, asc_creds:)
|
|
784
|
+
return apple_id_override.to_s if apple_id_override && !apple_id_override.empty?
|
|
785
|
+
|
|
786
|
+
require 'mysigner/auth/asc_jwt_minter'
|
|
787
|
+
require 'faraday'
|
|
788
|
+
require 'json'
|
|
789
|
+
require 'uri'
|
|
790
|
+
|
|
791
|
+
jwt = Mysigner::Auth::AscJwtMinter.new(
|
|
792
|
+
key_id: asc_creds.key_id,
|
|
793
|
+
issuer_id: asc_creds.issuer_id,
|
|
794
|
+
p8_pem: asc_creds.p8_pem
|
|
795
|
+
).mint
|
|
796
|
+
|
|
797
|
+
conn = Faraday.new(url: 'https://api.appstoreconnect.apple.com') do |f|
|
|
798
|
+
f.adapter Faraday.default_adapter
|
|
799
|
+
end
|
|
800
|
+
resp = conn.get('/v1/apps') do |req|
|
|
801
|
+
req.params['filter[bundleId]'] = bundle_id
|
|
802
|
+
req.headers['Authorization'] = "Bearer #{jwt}"
|
|
803
|
+
end
|
|
804
|
+
|
|
805
|
+
unless resp.status.between?(200, 299)
|
|
806
|
+
say "✗ Apple /v1/apps lookup failed for bundle_id #{bundle_id}: #{resp.status}", :red
|
|
807
|
+
say " #{resp.body}", :yellow if resp.body && !resp.body.empty?
|
|
808
|
+
say ' Override with: mysigner ship appstore --local-only --apple-id <APPLE_APP_ID>', :yellow
|
|
809
|
+
exit 1
|
|
810
|
+
end
|
|
811
|
+
|
|
812
|
+
data = Array(JSON.parse(resp.body)['data'])
|
|
813
|
+
case data.length
|
|
814
|
+
when 0
|
|
815
|
+
say "✗ No App Store Connect app found for bundle_id: #{bundle_id}", :red
|
|
816
|
+
say " Make sure the app exists in App Store Connect under this team's account,", :yellow
|
|
817
|
+
say ' or override with: --apple-id <APPLE_APP_ID>', :yellow
|
|
818
|
+
exit 1
|
|
819
|
+
when 1
|
|
820
|
+
data.first['id'].to_s
|
|
821
|
+
else
|
|
822
|
+
ids = data.map { |a| a['id'] }.join(', ')
|
|
823
|
+
say "✗ Apple returned multiple apps for bundle_id #{bundle_id} (ids: #{ids}).", :red
|
|
824
|
+
say ' Pick one and re-run with: --apple-id <APPLE_APP_ID>', :yellow
|
|
825
|
+
exit 1
|
|
826
|
+
end
|
|
827
|
+
end
|
|
828
|
+
|
|
829
|
+
# mysigner-22 follow-up — local-only submit-for-review.
|
|
830
|
+
# Mints a fresh JWT from the same local .p8 the uploader used,
|
|
831
|
+
# then drives Apple's REST submission flow via AscSubmitter.
|
|
832
|
+
# Translates each typed error into a one-line actionable message;
|
|
833
|
+
# exits 1 on failure so the caller's success-path doesn't run.
|
|
834
|
+
def submit_appstore_local!(asc_creds:, apple_app_id:, cf_bundle_version:,
|
|
835
|
+
cf_bundle_short_version_string:)
|
|
836
|
+
require 'mysigner/auth/asc_jwt_minter'
|
|
837
|
+
require_relative '../upload/asc_submitter'
|
|
838
|
+
|
|
839
|
+
say '⏳ Waiting for Apple to finish processing the build...', :yellow
|
|
840
|
+
|
|
841
|
+
jwt = Mysigner::Auth::AscJwtMinter.new(
|
|
842
|
+
key_id: asc_creds.key_id,
|
|
843
|
+
issuer_id: asc_creds.issuer_id,
|
|
844
|
+
p8_pem: asc_creds.p8_pem
|
|
845
|
+
).mint
|
|
846
|
+
|
|
847
|
+
submission_id = Mysigner::Upload::AscSubmitter.new(
|
|
848
|
+
jwt: jwt,
|
|
849
|
+
apple_app_id: apple_app_id,
|
|
850
|
+
cf_bundle_version: cf_bundle_version,
|
|
851
|
+
cf_bundle_short_version_string: cf_bundle_short_version_string
|
|
852
|
+
).submit!
|
|
853
|
+
|
|
854
|
+
say "✓ Submission created (id: #{submission_id})", :green
|
|
855
|
+
say ' Monitor review status at: https://appstoreconnect.apple.com/apps', :cyan
|
|
856
|
+
say ''
|
|
857
|
+
rescue Mysigner::Upload::AscSubmitter::BuildProcessingTimeoutError => e
|
|
858
|
+
say ''
|
|
859
|
+
say "✗ #{e.message}", :red
|
|
860
|
+
say ''
|
|
861
|
+
say ' Tip: this is not a CLI bug — Apple is still processing your build.', :yellow
|
|
862
|
+
say ' Re-run `mysigner ship appstore --local-only` once it shows', :yellow
|
|
863
|
+
say ' "Ready to Submit" in App Store Connect, or finish manually:', :yellow
|
|
864
|
+
say ' https://appstoreconnect.apple.com/apps', :cyan
|
|
865
|
+
say ''
|
|
866
|
+
exit 1
|
|
867
|
+
rescue Mysigner::Upload::AscSubmitter::VersionAlreadyReleasedError => e
|
|
868
|
+
say ''
|
|
869
|
+
say "✗ #{e.message}", :red
|
|
870
|
+
say ''
|
|
871
|
+
exit 1
|
|
872
|
+
rescue Mysigner::Upload::AscSubmitter::VersionInFlightError => e
|
|
873
|
+
# Apple has a version for this MARKETING_VERSION that's in an
|
|
874
|
+
# in-flight state (in review, rejected, etc). The error message
|
|
875
|
+
# names the state and the next user action verbatim.
|
|
876
|
+
say ''
|
|
877
|
+
say "✗ #{e.message}", :red
|
|
878
|
+
say ''
|
|
879
|
+
say ' Monitor or act in App Store Connect:', :yellow
|
|
880
|
+
say ' https://appstoreconnect.apple.com/apps', :cyan
|
|
881
|
+
say ''
|
|
882
|
+
exit 1
|
|
883
|
+
rescue Mysigner::Upload::AscSubmitter::SubmissionRejectedError => e
|
|
884
|
+
# Surface Apple's verbatim error body — usually missing metadata
|
|
885
|
+
# (description, what's new, screenshots). We don't pretend to
|
|
886
|
+
# auto-populate metadata; the user owns it in App Store Connect.
|
|
887
|
+
say ''
|
|
888
|
+
say "✗ Apple rejected the submission: #{e.message}", :red
|
|
889
|
+
say ''
|
|
890
|
+
say ' Missing required metadata is the most common cause.', :yellow
|
|
891
|
+
say ' Finish the listing at: https://appstoreconnect.apple.com/apps', :cyan
|
|
892
|
+
say ''
|
|
893
|
+
exit 1
|
|
894
|
+
rescue Mysigner::Upload::AscSubmitter::AppleApiError => e
|
|
895
|
+
say ''
|
|
896
|
+
say "✗ App Store Connect API error: #{e.message}", :red
|
|
897
|
+
say ''
|
|
898
|
+
exit 1
|
|
899
|
+
end
|
|
900
|
+
|
|
707
901
|
# Ship Android to Google Play
|
|
708
902
|
def ship_android(track)
|
|
903
|
+
# mysigner-43 — when the user opts into local-only for `ship
|
|
904
|
+
# android`, surface the banner once so they know their SA-JSON
|
|
905
|
+
# won't traverse the MySigner server. The uploader itself
|
|
906
|
+
# enforces the contract below.
|
|
907
|
+
emit_local_only_banner if local_only?
|
|
908
|
+
|
|
709
909
|
config = load_config
|
|
710
910
|
client = create_client(config)
|
|
711
911
|
|
|
@@ -763,12 +963,38 @@ module Mysigner
|
|
|
763
963
|
local_version_code = parser.version_code.to_i
|
|
764
964
|
version_name = parser.version_name
|
|
765
965
|
|
|
766
|
-
# Check highest version code from API and auto-increment if needed
|
|
767
|
-
|
|
966
|
+
# Check highest version code from API and auto-increment if needed.
|
|
967
|
+
# mysigner-22 follow-up — local-only mode now also runs the
|
|
968
|
+
# pre-check via Google Play directly (no MySigner round-trip):
|
|
969
|
+
# mint an OAuth2 token from local SA-JSON, list bundles on the
|
|
970
|
+
# app, take max(versionCode). Unlike vault mode we DO NOT
|
|
971
|
+
# auto-bump the AAB — that's the user's project state. We just
|
|
972
|
+
# warn so they can bump versionCode in their Gradle file and
|
|
973
|
+
# re-run rather than burn 3 minutes on a doomed upload.
|
|
974
|
+
#
|
|
975
|
+
# Best-effort: if the mint fails (network, mock, expired key)
|
|
976
|
+
# we skip the pre-check rather than fail the ship. Google will
|
|
977
|
+
# still reject at upload time with a clear message.
|
|
978
|
+
highest_version_code =
|
|
979
|
+
if local_only?
|
|
980
|
+
fetch_local_only_highest_version_code(package_name: package_name)
|
|
981
|
+
else
|
|
982
|
+
fetch_android_highest_version_code(client, config, package_name)
|
|
983
|
+
end
|
|
768
984
|
version_code = local_version_code
|
|
769
985
|
version_code_override = nil
|
|
770
986
|
|
|
771
|
-
if highest_version_code && local_version_code <= highest_version_code
|
|
987
|
+
if local_only? && highest_version_code && local_version_code <= highest_version_code
|
|
988
|
+
# Local-only: warn-only. Bumping the AAB's versionCode
|
|
989
|
+
# would silently mutate the user's project state, which
|
|
990
|
+
# the brief is explicit we should not do.
|
|
991
|
+
say ''
|
|
992
|
+
say "[mysigner] Your project's versionCode (#{local_version_code}) is ≤ Google Play's latest (#{highest_version_code}).", :red
|
|
993
|
+
say "Google will reject this upload. Bump versionCode to #{highest_version_code + 1} or higher in", :red
|
|
994
|
+
say 'android/app/build.gradle and re-run.', :red
|
|
995
|
+
say ''
|
|
996
|
+
exit 1
|
|
997
|
+
elsif highest_version_code && local_version_code <= highest_version_code
|
|
772
998
|
version_code = highest_version_code + 1
|
|
773
999
|
version_code_override = version_code
|
|
774
1000
|
say "📦 Package: #{package_name}", :cyan
|
|
@@ -800,53 +1026,85 @@ module Mysigner
|
|
|
800
1026
|
say '=' * 80, :cyan
|
|
801
1027
|
say ''
|
|
802
1028
|
|
|
803
|
-
#
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
1029
|
+
# mysigner-22 Phase 7 — local-only mode resolves the keystore
|
|
1030
|
+
# via the credential cascade (flag → env → keychain → project
|
|
1031
|
+
# sniff → prompt). The MySigner server is never contacted for
|
|
1032
|
+
# the .jks blob, passwords, or alias. In vault mode the old
|
|
1033
|
+
# KeystoreManager path runs unchanged.
|
|
1034
|
+
active_keystore = nil
|
|
1035
|
+
keystore_path = nil
|
|
1036
|
+
keystore_password = nil
|
|
1037
|
+
key_password = nil
|
|
1038
|
+
key_alias = nil
|
|
1039
|
+
|
|
1040
|
+
if local_only?
|
|
1041
|
+
say '🔐 Resolving Android keystore locally (--local-only)...', :yellow
|
|
1042
|
+
android_creds = resolve_local_android_keystore_or_exit
|
|
1043
|
+
keystore_path = android_creds.keystore_path
|
|
1044
|
+
keystore_password = android_creds.keystore_password
|
|
1045
|
+
key_password = android_creds.key_password || keystore_password
|
|
1046
|
+
key_alias = android_creds.key_alias
|
|
1047
|
+
# Hold the tmpfile (if any) on a local so GC can't unlink
|
|
1048
|
+
# the materialized .jks mid-build. The local goes out of
|
|
1049
|
+
# scope when ship_android returns, which is fine — the
|
|
1050
|
+
# build/upload have both consumed the file by then.
|
|
1051
|
+
_keystore_tmpfile_hold = android_creds.tmpfile
|
|
1052
|
+
say "✓ Keystore ready at: #{keystore_path} (source: #{android_creds.source})", :green
|
|
822
1053
|
say ''
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
say '
|
|
826
|
-
say ' 1. Upload a keystore: mysigner keystore upload', :green
|
|
827
|
-
say ' 2. Or configure in My Signer dashboard', :green
|
|
828
|
-
say ''
|
|
829
|
-
exit 1
|
|
830
|
-
end
|
|
1054
|
+
else
|
|
1055
|
+
# Fetch keystore from API (prefer app-specific, fallback to org-wide)
|
|
1056
|
+
say '🔐 Fetching keystore from My Signer...', :yellow
|
|
831
1057
|
|
|
832
|
-
|
|
1058
|
+
require_relative '../signing/keystore_manager'
|
|
1059
|
+
keystore_manager = Signing::KeystoreManager.new(client, config.current_organization_id)
|
|
833
1060
|
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
1061
|
+
# Try to find the app to get app-specific + org-wide keystores
|
|
1062
|
+
app_id = nil
|
|
1063
|
+
begin
|
|
1064
|
+
response = client.get("/api/v1/organizations/#{config.current_organization_id}/android_apps")
|
|
1065
|
+
apps = response[:data]['android_apps'] || []
|
|
1066
|
+
app = apps.find { |a| a['package_name'] == package_name }
|
|
1067
|
+
app_id = app['id'] if app
|
|
1068
|
+
rescue StandardError
|
|
1069
|
+
# Continue without app ID
|
|
1070
|
+
end
|
|
1071
|
+
|
|
1072
|
+
active_keystore = keystore_manager.active_keystore(android_app_id: app_id)
|
|
1073
|
+
unless active_keystore
|
|
1074
|
+
say ''
|
|
1075
|
+
say '✗ No active keystore found', :red
|
|
1076
|
+
say ''
|
|
1077
|
+
say 'Quick fix:', :cyan
|
|
1078
|
+
say ' 1. Upload a keystore: mysigner keystore upload', :green
|
|
1079
|
+
say ' 2. Or configure in My Signer dashboard', :green
|
|
1080
|
+
say ''
|
|
1081
|
+
exit 1
|
|
1082
|
+
end
|
|
838
1083
|
|
|
839
|
-
|
|
840
|
-
keystore_password = active_keystore['keystore_password'] || ENV.fetch('MYSIGNER_KEYSTORE_PASSWORD', nil)
|
|
841
|
-
key_password = active_keystore['key_password'] || ENV['MYSIGNER_KEY_PASSWORD'] || keystore_password
|
|
842
|
-
key_alias = active_keystore['key_alias']
|
|
1084
|
+
say "✓ Using keystore: #{active_keystore['name']}", :green
|
|
843
1085
|
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
1086
|
+
# Download keystore
|
|
1087
|
+
keystore_info = keystore_manager.get_or_download(active_keystore['id'])
|
|
1088
|
+
keystore_path = keystore_info[:path]
|
|
1089
|
+
say "✓ Keystore ready at: #{keystore_path}", :green
|
|
848
1090
|
say ''
|
|
849
|
-
|
|
1091
|
+
|
|
1092
|
+
# mysigner-49: passwords are never returned inline on the
|
|
1093
|
+
# active_keystore (list) payload. Fetch them through the
|
|
1094
|
+
# dedicated, audit-logged /secrets endpoint instead. ENV
|
|
1095
|
+
# vars remain a manual override for power users.
|
|
1096
|
+
secrets = keystore_manager.fetch_secrets(active_keystore['id'])
|
|
1097
|
+
keystore_password = secrets['keystore_password'] || ENV.fetch('MYSIGNER_KEYSTORE_PASSWORD', nil)
|
|
1098
|
+
key_password = secrets['key_password'] || ENV['MYSIGNER_KEY_PASSWORD'] || keystore_password
|
|
1099
|
+
key_alias = secrets['key_alias'] || active_keystore['key_alias']
|
|
1100
|
+
|
|
1101
|
+
unless keystore_password
|
|
1102
|
+
say '⚠️ Keystore password not found in My Signer', :yellow
|
|
1103
|
+
say ' Upload your keystore with password: mysigner keystore upload FILE', :yellow
|
|
1104
|
+
keystore_password = ask('Keystore password:', echo: false)
|
|
1105
|
+
say ''
|
|
1106
|
+
key_password ||= keystore_password
|
|
1107
|
+
end
|
|
850
1108
|
end
|
|
851
1109
|
|
|
852
1110
|
# Build AAB
|
|
@@ -855,7 +1113,7 @@ module Mysigner
|
|
|
855
1113
|
|
|
856
1114
|
aab_path = executor.build_aab!(
|
|
857
1115
|
variant: 'release',
|
|
858
|
-
keystore_path:
|
|
1116
|
+
keystore_path: keystore_path,
|
|
859
1117
|
keystore_password: keystore_password,
|
|
860
1118
|
key_alias: key_alias,
|
|
861
1119
|
key_password: key_password,
|
|
@@ -879,35 +1137,53 @@ module Mysigner
|
|
|
879
1137
|
|
|
880
1138
|
# Phase 0: mint short-lived OAuth2 access token server-side.
|
|
881
1139
|
# Service-account JSON never leaves the server.
|
|
882
|
-
|
|
1140
|
+
#
|
|
1141
|
+
# mysigner-43: in local-only mode the token is minted *inside*
|
|
1142
|
+
# PlayStoreUploader from Keychain-backed SA-JSON. Skip the
|
|
1143
|
+
# server round-trip entirely.
|
|
1144
|
+
access_token = nil
|
|
1145
|
+
if local_only?
|
|
1146
|
+
say '🔐 Local-only mode — will mint Google Play access token locally.', :yellow
|
|
1147
|
+
else
|
|
1148
|
+
say '🔐 Requesting Google Play access token...', :yellow
|
|
883
1149
|
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
1150
|
+
begin
|
|
1151
|
+
token_response = client.post(
|
|
1152
|
+
"/api/v1/organizations/#{config.current_organization_id}/credentials/google_play/access_token"
|
|
1153
|
+
)
|
|
1154
|
+
access_token = token_response[:data]['access_token']
|
|
1155
|
+
rescue Mysigner::NotFoundError, Mysigner::ValidationError
|
|
1156
|
+
say ''
|
|
1157
|
+
say '✗ Google Play credentials not configured', :red
|
|
1158
|
+
say ''
|
|
1159
|
+
say 'Quick fix:', :cyan
|
|
1160
|
+
say ' Configure Google Play credentials in My Signer dashboard', :green
|
|
1161
|
+
say ''
|
|
1162
|
+
exit 1
|
|
1163
|
+
end
|
|
898
1164
|
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
1165
|
+
if access_token.nil? || access_token.to_s.empty?
|
|
1166
|
+
say '✗ Error: Failed to mint Google Play access token', :red
|
|
1167
|
+
exit 1
|
|
1168
|
+
end
|
|
903
1169
|
|
|
904
|
-
|
|
1170
|
+
say '✓ Access token minted (valid ~1 hour)', :green
|
|
1171
|
+
end
|
|
905
1172
|
say ''
|
|
906
1173
|
|
|
907
1174
|
# Phase 0: fetch Android CLI Defaults from the dashboard
|
|
908
1175
|
# (android_apps.cli_defaults JSONB). Fields here act as base
|
|
909
1176
|
# values; explicit CLI flags below layer on top.
|
|
910
|
-
|
|
1177
|
+
# mysigner-22 Phase 7 — local-only mode bypasses dashboard
|
|
1178
|
+
# defaults; the user supplies everything via flags or accepts
|
|
1179
|
+
# the built-in defaults below.
|
|
1180
|
+
android_defaults =
|
|
1181
|
+
if local_only?
|
|
1182
|
+
warn '[mysigner] local-only: skipping fetch_android_release_defaults (server-only feature; using CLI flags + defaults)'
|
|
1183
|
+
nil
|
|
1184
|
+
else
|
|
1185
|
+
fetch_android_release_defaults(client, config, package_name)
|
|
1186
|
+
end
|
|
911
1187
|
if android_defaults
|
|
912
1188
|
say "✓ Loaded CLI Defaults for #{package_name}", :green
|
|
913
1189
|
say ''
|
|
@@ -938,10 +1214,18 @@ module Mysigner
|
|
|
938
1214
|
changes_not_sent_for_review = android_defaults&.key?('changes_not_sent_for_review') ? android_defaults['changes_not_sent_for_review'] : nil
|
|
939
1215
|
country_targeting = country_targeting.transform_keys(&:to_sym) if country_targeting.is_a?(Hash)
|
|
940
1216
|
|
|
1217
|
+
# mysigner-22 Phase 5 — pre-resolve Play creds via the
|
|
1218
|
+
# cascade (flag → env → keychain → project-sniff → prompt).
|
|
1219
|
+
# In vault mode this is nil and the existing access_token
|
|
1220
|
+
# round-trip is unchanged.
|
|
1221
|
+
play_creds_for_uploader = (resolve_local_play_creds_or_exit if local_only?)
|
|
1222
|
+
|
|
941
1223
|
uploader = Upload::PlayStoreUploader.new(
|
|
942
1224
|
aab_path: aab_path,
|
|
943
1225
|
access_token: access_token,
|
|
944
|
-
package_name: package_name
|
|
1226
|
+
package_name: package_name,
|
|
1227
|
+
local_only: local_only?,
|
|
1228
|
+
play_creds: play_creds_for_uploader
|
|
945
1229
|
)
|
|
946
1230
|
|
|
947
1231
|
uploader.upload!(
|
|
@@ -958,8 +1242,15 @@ module Mysigner
|
|
|
958
1242
|
timings[:upload] = Time.now - upload_start
|
|
959
1243
|
timings[:total] = Time.now - overall_start
|
|
960
1244
|
|
|
961
|
-
# Link keystore to app in MySigner (so dashboard shows it)
|
|
962
|
-
|
|
1245
|
+
# Link keystore to app in MySigner (so dashboard shows it).
|
|
1246
|
+
# mysigner-22 Phase 7 — local-only mode never has an
|
|
1247
|
+
# active_keystore record (the .jks came from the user's
|
|
1248
|
+
# machine, not MySigner), so the link step is unconditionally
|
|
1249
|
+
# skipped here. Surface it so users know the dashboard won't
|
|
1250
|
+
# auto-update.
|
|
1251
|
+
if local_only?
|
|
1252
|
+
warn '[mysigner] local-only: skipping android_keystores/:id/link_to_app (server-only feature)'
|
|
1253
|
+
elsif active_keystore && active_keystore['id']
|
|
963
1254
|
begin
|
|
964
1255
|
client.post(
|
|
965
1256
|
"/api/v1/organizations/#{config.current_organization_id}/android_keystores/#{active_keystore['id']}/link_to_app",
|
|
@@ -970,8 +1261,14 @@ module Mysigner
|
|
|
970
1261
|
end
|
|
971
1262
|
end
|
|
972
1263
|
|
|
973
|
-
# Save build record to MySigner (for version tracking)
|
|
974
|
-
|
|
1264
|
+
# Save build record to MySigner (for version tracking).
|
|
1265
|
+
# mysigner-22 Phase 7 — local-only mode has no MySigner
|
|
1266
|
+
# record to update; skip and warn.
|
|
1267
|
+
if local_only?
|
|
1268
|
+
warn '[mysigner] local-only: skipping save_android_build_record (server-only feature)'
|
|
1269
|
+
else
|
|
1270
|
+
save_android_build_record(client, config, package_name, version_code, version_name)
|
|
1271
|
+
end
|
|
975
1272
|
|
|
976
1273
|
# SUCCESS SUMMARY
|
|
977
1274
|
say ''
|
|
@@ -1033,6 +1330,14 @@ module Mysigner
|
|
|
1033
1330
|
say ' → For Flutter: check android/app/build.gradle exists', :yellow
|
|
1034
1331
|
say ''
|
|
1035
1332
|
exit 1
|
|
1333
|
+
rescue Upload::PlayStoreUploader::MissingLocalCredentialsError => e
|
|
1334
|
+
# mysigner-43 — local-only requested but no credentials stored.
|
|
1335
|
+
# Fail loud with a non-stack-trace message; the message already
|
|
1336
|
+
# tells the user where to store the credentials.
|
|
1337
|
+
say ''
|
|
1338
|
+
say "✗ #{e.message}", :red
|
|
1339
|
+
say ''
|
|
1340
|
+
exit 1
|
|
1036
1341
|
rescue Upload::PlayStoreUploader::PartialUploadError => e
|
|
1037
1342
|
# AAB was uploaded but track assignment/commit failed
|
|
1038
1343
|
# Save build record to prevent version conflicts on retry
|
|
@@ -1044,11 +1349,14 @@ module Mysigner
|
|
|
1044
1349
|
say "Error: #{e.message}", :red
|
|
1045
1350
|
say ''
|
|
1046
1351
|
|
|
1047
|
-
# Save build record even on partial failure (AAB is on Play Store)
|
|
1048
|
-
|
|
1352
|
+
# Save build record even on partial failure (AAB is on Play Store).
|
|
1353
|
+
# mysigner-22 Phase 7 — skip the server write in local-only mode.
|
|
1354
|
+
if e.version_code && !local_only?
|
|
1049
1355
|
save_android_build_record(client, config, package_name, e.version_code, version_name)
|
|
1050
1356
|
say "📝 Build v#{e.version_code} recorded (prevents version conflicts on retry)", :yellow
|
|
1051
1357
|
say ''
|
|
1358
|
+
elsif e.version_code && local_only?
|
|
1359
|
+
warn '[mysigner] local-only: skipping save_android_build_record on partial-upload retry path'
|
|
1052
1360
|
end
|
|
1053
1361
|
|
|
1054
1362
|
# Show track setup suggestions
|
|
@@ -1116,6 +1424,28 @@ module Mysigner
|
|
|
1116
1424
|
nil
|
|
1117
1425
|
end
|
|
1118
1426
|
|
|
1427
|
+
# mysigner-22 follow-up — local-only equivalent of
|
|
1428
|
+
# fetch_android_highest_version_code. Resolves Play creds via the
|
|
1429
|
+
# cascade, mints an OAuth2 token, and asks Google Play directly.
|
|
1430
|
+
# Best-effort: any failure (mint error, network, list error)
|
|
1431
|
+
# returns nil so the ship proceeds — Google will still reject at
|
|
1432
|
+
# upload time with a clear message, but a transient pre-check
|
|
1433
|
+
# failure shouldn't block the user.
|
|
1434
|
+
def fetch_local_only_highest_version_code(package_name:)
|
|
1435
|
+
require 'mysigner/auth/google_oauth_minter'
|
|
1436
|
+
|
|
1437
|
+
play_creds = resolve_local_play_creds_or_exit
|
|
1438
|
+
token = Mysigner::Auth::GoogleOauthMinter.new(play_creds.sa_json)
|
|
1439
|
+
.mint(scope: Upload::PlayStoreUploader::SCOPE)
|
|
1440
|
+
Upload::PlayStoreUploader.fetch_highest_version_code(
|
|
1441
|
+
package_name: package_name,
|
|
1442
|
+
access_token: token
|
|
1443
|
+
)
|
|
1444
|
+
rescue StandardError => e
|
|
1445
|
+
warn "[mysigner] local-only: skipping versionCode pre-check (#{e.class}: #{e.message})"
|
|
1446
|
+
nil
|
|
1447
|
+
end
|
|
1448
|
+
|
|
1119
1449
|
# Fetch highest version code from API
|
|
1120
1450
|
def fetch_android_highest_version_code(client, config, package_name)
|
|
1121
1451
|
response = client.get("/api/v1/organizations/#{config.current_organization_id}/android_apps")
|
|
@@ -1631,7 +1961,8 @@ module Mysigner
|
|
|
1631
1961
|
|
|
1632
1962
|
# Pre-build validation
|
|
1633
1963
|
say '🔍 Validating signing setup...', :cyan
|
|
1634
|
-
validator = Signing::Validator.new(parser, target_name, options[:configuration],
|
|
1964
|
+
validator = Signing::Validator.new(parser, target_name, options[:configuration],
|
|
1965
|
+
team_id: team_id_to_use, local_only: local_only?)
|
|
1635
1966
|
validator.validate!
|
|
1636
1967
|
|
|
1637
1968
|
# Build
|
|
@@ -1774,101 +2105,51 @@ module Mysigner
|
|
|
1774
2105
|
exit 1
|
|
1775
2106
|
end
|
|
1776
2107
|
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
say '⚠️ MYSIGNER_USE_LEGACY_ASC=1 — using legacy altool upload path (deprecated).', :yellow
|
|
1780
|
-
say '🔐 Fetching App Store Connect credentials...', :yellow
|
|
2108
|
+
# ASC REST Build Upload API.
|
|
2109
|
+
require_relative '../upload/asc_rest_uploader'
|
|
1781
2110
|
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
2111
|
+
ipa_info = Upload::Uploader.extract_ipa_info(ipa_path)
|
|
2112
|
+
bundle_id = ipa_info[:bundle_id]
|
|
2113
|
+
cf_version = ipa_info[:cf_bundle_version] || '1'
|
|
2114
|
+
cf_short = ipa_info[:cf_bundle_short_version_string] || '1.0'
|
|
1785
2115
|
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
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
|
|
1810
|
-
say ''
|
|
1811
|
-
rescue Mysigner::ClientError => e
|
|
1812
|
-
say ''
|
|
1813
|
-
say "✗ Error fetching credentials: #{e.message}", :red
|
|
1814
|
-
exit 1
|
|
1815
|
-
end
|
|
1816
|
-
|
|
1817
|
-
uploader = Upload::Uploader.new(
|
|
1818
|
-
ipa_path,
|
|
1819
|
-
api_key: api_key,
|
|
1820
|
-
api_issuer: api_issuer,
|
|
1821
|
-
private_key: private_key
|
|
1822
|
-
)
|
|
1823
|
-
|
|
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
|
|
1837
|
-
exit 1
|
|
1838
|
-
end
|
|
2116
|
+
if bundle_id.nil? || bundle_id.empty?
|
|
2117
|
+
say '✗ Could not extract bundle identifier from IPA.', :red
|
|
2118
|
+
say ' Ensure the file is a valid iOS .ipa with a Payload/*.app/Info.plist.', :yellow
|
|
2119
|
+
exit 1
|
|
2120
|
+
end
|
|
1839
2121
|
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
2122
|
+
app_response = client.get("/api/v1/organizations/#{config.current_organization_id}/apple_apps",
|
|
2123
|
+
params: { bundle_id: bundle_id })
|
|
2124
|
+
app = Array(app_response.dig(:data, 'data', 'apps')).first
|
|
1843
2125
|
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
2126
|
+
unless app && app['id']
|
|
2127
|
+
say "✗ App with bundle ID '#{bundle_id}' not found in MySigner.", :red
|
|
2128
|
+
say ' Run: mysigner sync ios', :yellow
|
|
2129
|
+
exit 1
|
|
2130
|
+
end
|
|
1849
2131
|
|
|
1850
|
-
|
|
2132
|
+
say "📤 Uploading #{File.basename(ipa_path)} via App Store Connect REST API (version #{cf_short} build #{cf_version})...", :cyan
|
|
1851
2133
|
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
2134
|
+
rest = Mysigner::Upload::AscRestUploader.new(
|
|
2135
|
+
client: client,
|
|
2136
|
+
organization_id: config.current_organization_id,
|
|
2137
|
+
ipa_path: ipa_path,
|
|
2138
|
+
apple_app_id: app['id'],
|
|
2139
|
+
cf_bundle_version: cf_version,
|
|
2140
|
+
cf_bundle_short_version_string: cf_short,
|
|
2141
|
+
platform: 'IOS'
|
|
2142
|
+
)
|
|
1861
2143
|
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
end
|
|
2144
|
+
result = rest.call
|
|
2145
|
+
case result[:final_state]
|
|
2146
|
+
when 'COMPLETE'
|
|
2147
|
+
say '✓ Upload complete — Apple accepted the build', :green
|
|
2148
|
+
when 'FAILED', 'INVALIDATED'
|
|
2149
|
+
say "✗ Apple rejected the upload: #{result[:final_state]}", :red
|
|
2150
|
+
exit 1
|
|
2151
|
+
when 'TIMEOUT'
|
|
2152
|
+
say '⚠ Apple is still processing — check App Store Connect.', :yellow
|
|
1872
2153
|
end
|
|
1873
2154
|
|
|
1874
2155
|
say '🎉 Upload complete!', :green
|
|
@@ -1878,14 +2159,6 @@ module Mysigner
|
|
|
1878
2159
|
say ' • Wait for processing (5-15 minutes)'
|
|
1879
2160
|
say ' • Distribute to TestFlight testers'
|
|
1880
2161
|
say ''
|
|
1881
|
-
rescue Upload::Uploader::TransporterNotFoundError => e
|
|
1882
|
-
say ''
|
|
1883
|
-
say "✗ Error: #{e.message}", :red
|
|
1884
|
-
exit 1
|
|
1885
|
-
rescue Upload::Uploader::UploadError => e
|
|
1886
|
-
say ''
|
|
1887
|
-
say "✗ Upload Error: #{e.message}", :red
|
|
1888
|
-
exit 1
|
|
1889
2162
|
rescue Mysigner::Upload::AscRestUploader::BuildVersionConflictError => e
|
|
1890
2163
|
say ''
|
|
1891
2164
|
say "✗ #{e.message}", :red
|