mysigner 0.1.2 → 0.1.4
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/.githooks/pre-commit +15 -0
- data/.githooks/pre-push +21 -0
- data/.github/workflows/ci.yml +29 -0
- data/.gitignore +4 -0
- data/.rubocop.yml +55 -0
- data/.rubocop_todo.yml +126 -0
- data/CHANGELOG.md +96 -0
- data/Gemfile +5 -3
- data/Gemfile.lock +38 -8
- data/README.md +14 -16
- data/Rakefile +5 -3
- data/bin/console +4 -3
- data/bin/setup +3 -0
- data/certificate_.cer +0 -0
- data/exe/mysigner +19 -2
- data/iOS_App_Store_Profile.mobileprovision +1 -0
- data/iOS_Distribution_Certificate.cer +1 -0
- data/lib/mysigner/build/android_executor.rb +83 -63
- data/lib/mysigner/build/android_parser.rb +33 -40
- data/lib/mysigner/build/configurator.rb +17 -16
- data/lib/mysigner/build/detector.rb +39 -50
- data/lib/mysigner/build/error_analyzer.rb +70 -68
- data/lib/mysigner/build/executor.rb +30 -37
- data/lib/mysigner/build/parser.rb +18 -18
- data/lib/mysigner/cleanup/private_keys_purger.rb +41 -0
- data/lib/mysigner/cli/auth_commands.rb +771 -764
- data/lib/mysigner/cli/build_commands.rb +962 -796
- data/lib/mysigner/cli/concerns/actionable_suggestions.rb +208 -154
- data/lib/mysigner/cli/concerns/api_helpers.rb +46 -54
- data/lib/mysigner/cli/concerns/error_handlers.rb +247 -237
- data/lib/mysigner/cli/concerns/helpers.rb +44 -1
- data/lib/mysigner/cli/diagnostic_commands.rb +667 -636
- data/lib/mysigner/cli/resource_commands.rb +1153 -985
- data/lib/mysigner/cli/validate_commands.rb +25 -25
- data/lib/mysigner/cli.rb +11 -1
- data/lib/mysigner/client.rb +27 -19
- data/lib/mysigner/config.rb +161 -60
- data/lib/mysigner/export/exporter.rb +38 -37
- data/lib/mysigner/signing/certificate_checker.rb +18 -23
- data/lib/mysigner/signing/gradle_signing_injector.rb +67 -0
- data/lib/mysigner/signing/keystore_manager.rb +81 -61
- data/lib/mysigner/signing/validator.rb +38 -40
- data/lib/mysigner/signing/wizard.rb +329 -342
- data/lib/mysigner/upload/app_store_automation.rb +96 -49
- data/lib/mysigner/upload/app_store_submission.rb +87 -92
- data/lib/mysigner/upload/asc_rest_uploader.rb +119 -0
- data/lib/mysigner/upload/play_store_uploader.rb +164 -144
- data/lib/mysigner/upload/uploader.rb +136 -115
- data/lib/mysigner/version.rb +3 -1
- data/lib/mysigner.rb +13 -11
- data/mysigner.gemspec +36 -33
- data/profile_.mobileprovision +0 -0
- data/test_manual.rb +37 -36
- metadata +44 -17
- data/.DS_Store +0 -0
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Mysigner
|
|
2
4
|
module Upload
|
|
3
5
|
class AppStoreAutomation
|
|
@@ -11,23 +13,33 @@ module Mysigner
|
|
|
11
13
|
def initialize(client:, organization_id:, opts: {})
|
|
12
14
|
@client = client
|
|
13
15
|
@organization_id = organization_id
|
|
14
|
-
|
|
16
|
+
# Proper boolean coercion — `!x.nil?` evaluates to `true` for `false`,
|
|
17
|
+
# which previously caused `wait: false` / `no_submit: false` to be
|
|
18
|
+
# treated as `true`. Use `!!` to get the actual boolean semantics.
|
|
19
|
+
@wait_enabled = opts.key?(:wait) ? !opts[:wait].nil? : true
|
|
15
20
|
|
|
16
21
|
poll = opts[:poll_interval] || opts[:poll_seconds]
|
|
17
22
|
poll = poll.to_i if poll
|
|
18
|
-
@poll_interval = poll
|
|
23
|
+
@poll_interval = poll&.positive? ? poll : DEFAULT_POLL_INTERVAL
|
|
19
24
|
|
|
20
25
|
timeout = opts[:timeout] || opts[:timeout_seconds]
|
|
21
26
|
timeout = timeout.to_i if timeout
|
|
22
|
-
@timeout = timeout
|
|
27
|
+
@timeout = timeout&.positive? ? timeout : DEFAULT_WAIT_TIMEOUT
|
|
28
|
+
|
|
29
|
+
@no_submit = !opts[:no_submit].nil?
|
|
30
|
+
|
|
31
|
+
# Whether to submit by default when NEITHER cli_defaults nor CLI
|
|
32
|
+
# overrides specify `auto_submit`. `ship`/`submit` pass true (it's
|
|
33
|
+
# the user's explicit intent). Dashboard `cli_defaults.auto_submit`
|
|
34
|
+
# still wins if set — we only fall back to this when silent.
|
|
35
|
+
@default_submit = opts.key?(:default_submit) ? !opts[:default_submit].nil? : false
|
|
23
36
|
|
|
24
|
-
@no_submit = !!opts[:no_submit]
|
|
25
37
|
@now = opts[:now]
|
|
26
38
|
end
|
|
27
39
|
|
|
28
40
|
def perform!(metadata:, build_info:, metadata_overrides: {})
|
|
29
41
|
build_info = symbolize_keys(build_info)
|
|
30
|
-
metadata
|
|
42
|
+
metadata ||= {}
|
|
31
43
|
|
|
32
44
|
result = {
|
|
33
45
|
wait: {
|
|
@@ -43,9 +55,9 @@ module Mysigner
|
|
|
43
55
|
submission_source: nil
|
|
44
56
|
}
|
|
45
57
|
|
|
46
|
-
puts
|
|
47
|
-
puts
|
|
48
|
-
puts
|
|
58
|
+
puts ''
|
|
59
|
+
puts '🤖 App Store automation in progress...'
|
|
60
|
+
puts ''
|
|
49
61
|
|
|
50
62
|
app = ensure_app(build_info[:bundle_id])
|
|
51
63
|
raise AutomationError, "App with bundle ID #{build_info[:bundle_id]} not found" unless app
|
|
@@ -56,11 +68,13 @@ module Mysigner
|
|
|
56
68
|
unless build
|
|
57
69
|
version_info = [build_info[:version], build_info[:build_number]].compact.join(' / ')
|
|
58
70
|
version_info = "for #{version_info}" unless version_info.empty?
|
|
59
|
-
raise AutomationError,
|
|
71
|
+
raise AutomationError,
|
|
72
|
+
"No processed build found #{version_info}. Upload a build first with 'mysigner ship appstore --wait'"
|
|
60
73
|
end
|
|
61
74
|
|
|
62
75
|
unless build_processed?(build)
|
|
63
|
-
raise AutomationError,
|
|
76
|
+
raise AutomationError,
|
|
77
|
+
"Build #{build_info[:version]} (#{build_info[:build_number]}) is still processing. Wait for it or use --wait flag."
|
|
64
78
|
end
|
|
65
79
|
|
|
66
80
|
version = ensure_app_store_version(app_id: app['id'], metadata: metadata, overrides: metadata_overrides)
|
|
@@ -70,12 +84,12 @@ module Mysigner
|
|
|
70
84
|
|
|
71
85
|
if should_submit
|
|
72
86
|
submit_for_review(
|
|
73
|
-
version_id: version['id'],
|
|
87
|
+
version_id: version['id'],
|
|
74
88
|
version_string: version['version_string'],
|
|
75
|
-
metadata: metadata,
|
|
89
|
+
metadata: metadata,
|
|
76
90
|
overrides: metadata_overrides
|
|
77
91
|
)
|
|
78
|
-
puts
|
|
92
|
+
puts '✓ Submitted for App Store review'
|
|
79
93
|
result[:submitted] = true
|
|
80
94
|
result[:submission_source] = submit_source
|
|
81
95
|
else
|
|
@@ -83,8 +97,8 @@ module Mysigner
|
|
|
83
97
|
result[:skip_reason] = skip_reason
|
|
84
98
|
end
|
|
85
99
|
|
|
86
|
-
puts
|
|
87
|
-
puts
|
|
100
|
+
puts ''
|
|
101
|
+
puts '✅ App Store automation complete'
|
|
88
102
|
|
|
89
103
|
result
|
|
90
104
|
end
|
|
@@ -112,29 +126,29 @@ module Mysigner
|
|
|
112
126
|
|
|
113
127
|
return [build, status] unless @wait_enabled
|
|
114
128
|
|
|
115
|
-
puts
|
|
129
|
+
puts '⏳ Waiting for Apple to finish processing the build...'
|
|
116
130
|
puts " Polling every #{@poll_interval}s (timeout #{@timeout}s)"
|
|
117
|
-
print
|
|
131
|
+
print ''
|
|
118
132
|
|
|
119
133
|
start_time = current_time
|
|
120
|
-
|
|
134
|
+
|
|
121
135
|
loop do
|
|
122
136
|
build = latest_build(app_id, build_info)
|
|
123
137
|
status[:last_state] = build_state(build)
|
|
124
138
|
|
|
125
139
|
if build && build_processed?(build)
|
|
126
140
|
puts "\r✓ Build is processed and ready".ljust(70)
|
|
127
|
-
puts
|
|
141
|
+
puts ''
|
|
128
142
|
return [build, status]
|
|
129
143
|
end
|
|
130
|
-
|
|
144
|
+
|
|
131
145
|
elapsed = current_time - start_time
|
|
132
146
|
status[:elapsed_seconds] = elapsed
|
|
133
147
|
|
|
134
148
|
if elapsed >= @timeout
|
|
135
149
|
status[:timed_out] = true
|
|
136
150
|
puts "\r✗ Timed out after #{format_duration(elapsed)} (use --asc-timeout-seconds to extend)".ljust(90)
|
|
137
|
-
puts
|
|
151
|
+
puts ''
|
|
138
152
|
return [build, status]
|
|
139
153
|
end
|
|
140
154
|
|
|
@@ -152,32 +166,30 @@ module Mysigner
|
|
|
152
166
|
app_id: app_id,
|
|
153
167
|
processed_only: !@wait_enabled
|
|
154
168
|
}
|
|
155
|
-
|
|
169
|
+
|
|
156
170
|
# Only filter by version/build if NOT using latest
|
|
157
171
|
unless build_info[:use_latest]
|
|
158
172
|
params[:version] = build_info[:version] if build_info[:version]
|
|
159
173
|
params[:build_number] = build_info[:build_number] if build_info[:build_number]
|
|
160
174
|
end
|
|
161
|
-
|
|
175
|
+
|
|
162
176
|
# Apply min_build_number filter if set in release config
|
|
163
|
-
if build_info[:min_build_number]
|
|
164
|
-
|
|
165
|
-
end
|
|
166
|
-
|
|
177
|
+
params[:min_build_number] = build_info[:min_build_number] if build_info[:min_build_number]
|
|
178
|
+
|
|
167
179
|
response = @client.get(
|
|
168
180
|
"/api/v1/organizations/#{@organization_id}/builds",
|
|
169
181
|
params: params.compact
|
|
170
182
|
)
|
|
171
183
|
|
|
172
184
|
builds = Array(response[:data]['data']['builds'])
|
|
173
|
-
|
|
185
|
+
|
|
174
186
|
# Smart build selection: filter by min_build_number client-side if API doesn't support it
|
|
175
187
|
if build_info[:min_build_number] && builds.any?
|
|
176
188
|
min_bn = build_info[:min_build_number].to_i
|
|
177
189
|
builds = builds.select { |b| b['build_number'].to_i >= min_bn }
|
|
178
190
|
end
|
|
179
|
-
|
|
180
|
-
builds.first
|
|
191
|
+
|
|
192
|
+
builds.first # Already ordered by uploaded_date desc
|
|
181
193
|
rescue Mysigner::NotFoundError
|
|
182
194
|
nil
|
|
183
195
|
rescue StandardError => e
|
|
@@ -264,7 +276,7 @@ module Mysigner
|
|
|
264
276
|
def determine_earliest_release_date(metadata, overrides)
|
|
265
277
|
date = overrides['earliest_release_date'] || metadata['earliest_release_date']
|
|
266
278
|
return nil unless date
|
|
267
|
-
|
|
279
|
+
|
|
268
280
|
# Convert to ISO 8601 if not already
|
|
269
281
|
date.respond_to?(:iso8601) ? date.iso8601 : date.to_s
|
|
270
282
|
end
|
|
@@ -287,17 +299,17 @@ module Mysigner
|
|
|
287
299
|
|
|
288
300
|
def determine_release_type(metadata, overrides)
|
|
289
301
|
result = overrides['release_type'] || metadata['release_type']
|
|
290
|
-
|
|
302
|
+
|
|
291
303
|
# FIX v7: Changed default from MANUAL to AFTER_APPROVAL
|
|
292
304
|
# Log deprecation notice if no explicit release_type is set
|
|
293
305
|
if result.nil?
|
|
294
306
|
if ENV['MYSIGNER_DEBUG'] || @deprecation_warned.nil?
|
|
295
|
-
puts
|
|
307
|
+
puts ' Note: Using default release_type AFTER_APPROVAL (was MANUAL in older versions)'
|
|
296
308
|
@deprecation_warned = true
|
|
297
309
|
end
|
|
298
310
|
result = 'AFTER_APPROVAL'
|
|
299
311
|
end
|
|
300
|
-
|
|
312
|
+
|
|
301
313
|
result
|
|
302
314
|
end
|
|
303
315
|
|
|
@@ -307,7 +319,7 @@ module Mysigner
|
|
|
307
319
|
body: { build_id: build_id }
|
|
308
320
|
)
|
|
309
321
|
|
|
310
|
-
puts
|
|
322
|
+
puts '✓ Attached build to App Store version'
|
|
311
323
|
rescue StandardError => e
|
|
312
324
|
raise AutomationError, "Failed to attach build to version: #{e.message}"
|
|
313
325
|
end
|
|
@@ -315,49 +327,85 @@ module Mysigner
|
|
|
315
327
|
def should_submit_with_reason(metadata, overrides)
|
|
316
328
|
return [false, nil, '--no-auto-submit flag'] if @no_submit
|
|
317
329
|
|
|
330
|
+
# Precedence: explicit CLI flag > dashboard cli_defaults > command default.
|
|
331
|
+
# The command default is set by `ship appstore` / `submit` (true),
|
|
332
|
+
# preserving backward-compat when the user runs those commands without
|
|
333
|
+
# configuring anything. Dashboard `auto_submit: false` now correctly
|
|
334
|
+
# suppresses submission (previously hard-overridden at the call site).
|
|
318
335
|
if overrides.key?('auto_submit')
|
|
319
|
-
return overrides['auto_submit'] ? [true, 'CLI override',
|
|
336
|
+
return overrides['auto_submit'] ? [true, 'CLI override',
|
|
337
|
+
nil] : [false, nil, 'CLI override disabled auto_submit']
|
|
320
338
|
end
|
|
321
339
|
|
|
322
340
|
if metadata.key?('auto_submit')
|
|
323
|
-
return metadata['auto_submit'] ? [true, 'Dashboard configuration',
|
|
341
|
+
return metadata['auto_submit'] ? [true, 'Dashboard configuration',
|
|
342
|
+
nil] : [false, nil, 'Dashboard auto_submit disabled']
|
|
324
343
|
end
|
|
325
344
|
|
|
345
|
+
return [true, 'command default', nil] if @default_submit
|
|
346
|
+
|
|
326
347
|
[false, nil, 'No auto_submit configuration']
|
|
327
348
|
end
|
|
328
349
|
|
|
329
350
|
def submit_for_review(version_id:, version_string: nil, metadata: {}, overrides: {})
|
|
330
351
|
# Merge metadata and overrides
|
|
331
352
|
merged = metadata.merge(overrides)
|
|
332
|
-
|
|
353
|
+
|
|
333
354
|
# Get version string to check if first version
|
|
334
355
|
version_string ||= merged['version_string'] || merged['version'] || '1.0'
|
|
335
356
|
is_first_version = version_string.split('.').first.to_i <= 1
|
|
336
|
-
|
|
357
|
+
|
|
337
358
|
# Validate required fields for Apple submission
|
|
338
359
|
# Note: What's New is NOT required for version 1.0 (first release)
|
|
339
360
|
# Support URL may already be set in App Store Connect, so we only warn if missing
|
|
340
361
|
missing_fields = []
|
|
341
362
|
missing_fields << "What's New text" if !is_first_version && merged['whats_new'].to_s.strip.empty?
|
|
342
|
-
|
|
363
|
+
|
|
343
364
|
# Don't block on missing support_url - it may already be in App Store Connect
|
|
344
365
|
# Just warn about it
|
|
345
|
-
if merged['support_url'].to_s.strip.empty?
|
|
346
|
-
|
|
347
|
-
end
|
|
348
|
-
|
|
366
|
+
puts '⚠️ Note: No Support URL provided via CLI - using value from App Store Connect if available' if merged['support_url'].to_s.strip.empty?
|
|
367
|
+
|
|
349
368
|
unless missing_fields.empty?
|
|
350
|
-
raise AutomationError,
|
|
369
|
+
raise AutomationError,
|
|
370
|
+
"Cannot submit to Apple Store: missing required fields: #{missing_fields.join(', ')}. Please configure these in your My Signer dashboard or provide via --whats-new flag."
|
|
351
371
|
end
|
|
352
|
-
|
|
372
|
+
|
|
373
|
+
# Primary-locale fields (backward-compatible path): backend will
|
|
374
|
+
# upsert a single localization for the app's primary locale when
|
|
375
|
+
# these top-level keys are present.
|
|
353
376
|
payload = {
|
|
354
377
|
whats_new: merged['whats_new'],
|
|
355
378
|
keywords: merged['keywords'],
|
|
356
379
|
marketing_url: merged['marketing_url'],
|
|
357
380
|
promotional_text: merged['promotional_text'],
|
|
358
381
|
support_url: merged['support_url'],
|
|
382
|
+
description: merged['description'],
|
|
359
383
|
locale: merged['locale']
|
|
360
|
-
}.compact
|
|
384
|
+
}.compact
|
|
385
|
+
|
|
386
|
+
# Multi-locale path: if cli_defaults includes a `localizations` array,
|
|
387
|
+
# forward it so the backend can PATCH/POST each locale on ASC
|
|
388
|
+
# (appStoreVersionLocalizations) instead of only the primary.
|
|
389
|
+
if merged['localizations'].is_a?(Array) && merged['localizations'].any?
|
|
390
|
+
payload[:localizations] = merged['localizations'].map do |loc|
|
|
391
|
+
next nil unless loc.is_a?(Hash)
|
|
392
|
+
|
|
393
|
+
{
|
|
394
|
+
locale: loc['locale'] || loc[:locale],
|
|
395
|
+
whats_new: loc['whats_new'] || loc[:whats_new],
|
|
396
|
+
keywords: loc['keywords'] || loc[:keywords],
|
|
397
|
+
marketing_url: loc['marketing_url'] || loc[:marketing_url],
|
|
398
|
+
promotional_text: loc['promotional_text'] || loc[:promotional_text],
|
|
399
|
+
support_url: loc['support_url'] || loc[:support_url],
|
|
400
|
+
description: loc['description'] || loc[:description]
|
|
401
|
+
}.compact
|
|
402
|
+
end.compact
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
# Phased release: backend flips @version.phased_release_pending and
|
|
406
|
+
# enqueues PhasedReleaseActivationJob, which POSTs
|
|
407
|
+
# appStoreVersionPhasedReleases after Apple approves the submission.
|
|
408
|
+
payload[:phased_release] = true if merged['phased_release']
|
|
361
409
|
|
|
362
410
|
@client.post(
|
|
363
411
|
"/api/v1/organizations/#{@organization_id}/app_store_versions/#{version_id}/submit",
|
|
@@ -375,7 +423,6 @@ module Mysigner
|
|
|
375
423
|
end
|
|
376
424
|
end
|
|
377
425
|
|
|
378
|
-
|
|
379
426
|
def format_duration(seconds)
|
|
380
427
|
minutes = (seconds / 60).floor
|
|
381
428
|
seconds = (seconds % 60).round
|
|
@@ -383,7 +430,7 @@ module Mysigner
|
|
|
383
430
|
end
|
|
384
431
|
|
|
385
432
|
def current_time
|
|
386
|
-
|
|
433
|
+
@now&.call || Time.now
|
|
387
434
|
end
|
|
388
435
|
end
|
|
389
436
|
end
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Mysigner
|
|
4
4
|
module Upload
|
|
@@ -16,52 +16,47 @@ module Mysigner
|
|
|
16
16
|
|
|
17
17
|
# Submit build for App Store review
|
|
18
18
|
def submit_for_review!(automation: nil)
|
|
19
|
-
puts
|
|
20
|
-
puts
|
|
21
|
-
puts
|
|
22
|
-
|
|
19
|
+
puts ''
|
|
20
|
+
puts '📤 Preparing for App Store submission...'
|
|
21
|
+
puts ''
|
|
22
|
+
|
|
23
23
|
begin
|
|
24
24
|
# Step 1: Fetch release metadata from My Signer API
|
|
25
25
|
merged = merge_metadata(fetch_release_metadata)
|
|
26
26
|
metadata = merged[:merged]
|
|
27
|
-
|
|
28
|
-
if metadata
|
|
29
|
-
puts
|
|
30
|
-
puts
|
|
27
|
+
|
|
28
|
+
if metadata&.any?
|
|
29
|
+
puts '✓ Loaded release configuration from My Signer'
|
|
30
|
+
puts ''
|
|
31
31
|
display_metadata(metadata)
|
|
32
32
|
else
|
|
33
|
-
puts
|
|
33
|
+
puts '⚠️ No release configuration found in My Signer'
|
|
34
34
|
puts " Create one at: #{@client.api_url}/organizations/#{@organization_id}/app_store_releases"
|
|
35
|
-
puts
|
|
35
|
+
puts ''
|
|
36
36
|
end
|
|
37
|
-
|
|
37
|
+
|
|
38
38
|
# Enrich build_info with config values for smart build selection
|
|
39
39
|
enriched_build_info = symbolize_keys(@build_info)
|
|
40
40
|
if metadata
|
|
41
41
|
# min_build_number: skip builds below this number
|
|
42
|
-
if metadata['build_number'] && !enriched_build_info[:build_number]
|
|
43
|
-
enriched_build_info[:min_build_number] = metadata['build_number'].to_i
|
|
44
|
-
end
|
|
42
|
+
enriched_build_info[:min_build_number] = metadata['build_number'].to_i if metadata['build_number'] && !enriched_build_info[:build_number]
|
|
45
43
|
# Use version_string from config if not specified
|
|
46
|
-
if metadata['version_string'] && !enriched_build_info[:version]
|
|
47
|
-
enriched_build_info[:version] = metadata['version_string']
|
|
48
|
-
end
|
|
44
|
+
enriched_build_info[:version] = metadata['version_string'] if metadata['version_string'] && !enriched_build_info[:version]
|
|
49
45
|
end
|
|
50
|
-
|
|
46
|
+
|
|
51
47
|
automation_result = if automation
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
48
|
+
automation.perform!(
|
|
49
|
+
metadata: metadata,
|
|
50
|
+
build_info: enriched_build_info,
|
|
51
|
+
metadata_overrides: @metadata_overrides
|
|
52
|
+
)
|
|
53
|
+
else
|
|
54
|
+
guide_to_manual_submission(metadata)
|
|
55
|
+
nil
|
|
56
|
+
end
|
|
61
57
|
|
|
62
58
|
{ success: true, metadata: metadata, automation: automation_result }
|
|
63
|
-
|
|
64
|
-
rescue => e
|
|
59
|
+
rescue StandardError => e
|
|
65
60
|
raise SubmissionError, "Failed to prepare submission: #{e.message}"
|
|
66
61
|
end
|
|
67
62
|
end
|
|
@@ -70,40 +65,37 @@ module Mysigner
|
|
|
70
65
|
|
|
71
66
|
def fetch_release_metadata
|
|
72
67
|
# Fetch release metadata from My Signer API
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
else
|
|
88
|
-
data
|
|
89
|
-
end
|
|
68
|
+
|
|
69
|
+
response = @client.get(
|
|
70
|
+
"/api/v1/organizations/#{@organization_id}/app_store_releases",
|
|
71
|
+
params: { bundle_id: @build_info[:bundle_id] }
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
if response[:success]
|
|
75
|
+
data = response[:data]
|
|
76
|
+
# API returns { app_store_releases: [...] } - extract first release
|
|
77
|
+
if data.is_a?(Hash) && data['app_store_releases'].is_a?(Array)
|
|
78
|
+
data['app_store_releases'].first
|
|
79
|
+
elsif data.is_a?(Hash) && data['app_store_release']
|
|
80
|
+
# Single release format
|
|
81
|
+
data['app_store_release']
|
|
90
82
|
else
|
|
91
|
-
|
|
83
|
+
data
|
|
92
84
|
end
|
|
93
|
-
rescue Mysigner::NotFoundError
|
|
94
|
-
# No configuration found - that's okay
|
|
95
|
-
nil
|
|
96
|
-
rescue StandardError => e
|
|
97
|
-
puts "⚠️ Could not fetch release metadata: #{e.message}"
|
|
98
|
-
nil
|
|
99
85
|
end
|
|
86
|
+
rescue Mysigner::NotFoundError
|
|
87
|
+
# No configuration found - that's okay
|
|
88
|
+
nil
|
|
89
|
+
rescue StandardError => e
|
|
90
|
+
puts "⚠️ Could not fetch release metadata: #{e.message}"
|
|
91
|
+
nil
|
|
100
92
|
end
|
|
101
93
|
|
|
102
94
|
# Fetch release config and extract min_build_number for smart build selection
|
|
103
95
|
def fetch_release_config
|
|
104
96
|
metadata = fetch_release_metadata
|
|
105
97
|
return {} unless metadata
|
|
106
|
-
|
|
98
|
+
|
|
107
99
|
config = {}
|
|
108
100
|
config[:min_build_number] = metadata['build_number'].to_i if metadata['build_number']
|
|
109
101
|
config[:release_type] = metadata['release_type'] if metadata['release_type']
|
|
@@ -122,39 +114,40 @@ module Mysigner
|
|
|
122
114
|
end
|
|
123
115
|
|
|
124
116
|
def guide_to_manual_submission(metadata)
|
|
125
|
-
puts
|
|
126
|
-
puts
|
|
127
|
-
puts
|
|
128
|
-
puts
|
|
129
|
-
puts
|
|
130
|
-
puts
|
|
131
|
-
puts
|
|
132
|
-
puts
|
|
133
|
-
puts
|
|
117
|
+
puts '📋 Next Steps for App Store Submission'
|
|
118
|
+
puts '=' * 60
|
|
119
|
+
puts ''
|
|
120
|
+
puts 'Your build is uploaded to App Store Connect!'
|
|
121
|
+
puts ''
|
|
122
|
+
puts 'To submit for review:'
|
|
123
|
+
puts ' 1. Wait for Apple to process the build (5-15 minutes)'
|
|
124
|
+
puts ' 2. Open App Store Connect:'
|
|
125
|
+
puts ' https://appstoreconnect.apple.com'
|
|
134
126
|
puts " 3. Select your app and go to 'App Store' tab"
|
|
135
|
-
puts
|
|
127
|
+
puts ' 4. Create a new version or select existing one'
|
|
136
128
|
puts " 5. Select this build (#{@build_info[:version]} / #{@build_info[:build_number]})"
|
|
137
|
-
puts
|
|
129
|
+
puts ' 6. Add screenshots and metadata if not already present'
|
|
138
130
|
puts " 7. Click 'Submit for Review'"
|
|
139
|
-
puts
|
|
131
|
+
puts ''
|
|
140
132
|
|
|
141
|
-
if metadata && metadata['auto_submit']
|
|
142
|
-
puts "💡 Auto-submit enabled"
|
|
143
|
-
end
|
|
133
|
+
puts '💡 Auto-submit enabled' if metadata && metadata['auto_submit']
|
|
144
134
|
|
|
145
|
-
puts
|
|
146
|
-
puts
|
|
147
|
-
puts
|
|
148
|
-
puts
|
|
135
|
+
puts ''
|
|
136
|
+
puts 'Tip: rerun with --submit-for-review when ready'
|
|
137
|
+
puts ' Use --wait/--asc-timeout-seconds to control polling'
|
|
138
|
+
puts ''
|
|
149
139
|
end
|
|
150
140
|
|
|
151
141
|
def display_metadata(metadata)
|
|
152
|
-
puts
|
|
142
|
+
puts '📝 Release Configuration:'
|
|
153
143
|
print_metadata_line('Bundle ID', metadata['bundle_identifier'], 'bundle_identifier')
|
|
154
144
|
print_metadata_line('App Name', metadata['app_name'], 'app_name') if metadata['app_name']
|
|
155
|
-
|
|
145
|
+
|
|
156
146
|
# Version info from config
|
|
157
|
-
|
|
147
|
+
if metadata['version_string']
|
|
148
|
+
print_metadata_line('Version String', metadata['version_string'],
|
|
149
|
+
'version_string')
|
|
150
|
+
end
|
|
158
151
|
print_metadata_line('Min Build #', metadata['build_number'], 'build_number') if metadata['build_number']
|
|
159
152
|
|
|
160
153
|
if metadata['whats_new'] && !metadata['whats_new'].to_s.strip.empty?
|
|
@@ -162,7 +155,7 @@ module Mysigner
|
|
|
162
155
|
else
|
|
163
156
|
puts " What's New: —#{override_suffix('whats_new')}"
|
|
164
157
|
end
|
|
165
|
-
|
|
158
|
+
|
|
166
159
|
if metadata['promotional_text'] && !metadata['promotional_text'].to_s.strip.empty?
|
|
167
160
|
puts " Promo Text: #{truncate(metadata['promotional_text'])}#{override_suffix('promotional_text')}"
|
|
168
161
|
end
|
|
@@ -170,14 +163,14 @@ module Mysigner
|
|
|
170
163
|
print_metadata_line('Support URL', metadata['support_url'], 'support_url')
|
|
171
164
|
print_metadata_line('Marketing URL', metadata['marketing_url'], 'marketing_url')
|
|
172
165
|
print_metadata_line('Privacy URL', metadata['privacy_policy_url'], 'privacy_policy_url')
|
|
173
|
-
|
|
166
|
+
|
|
174
167
|
# Release settings
|
|
175
168
|
release_type_label = format_release_type(metadata['release_type'])
|
|
176
169
|
print_metadata_line('Release Type', release_type_label, 'release_type')
|
|
177
170
|
if metadata['release_type'] == 'SCHEDULED' && metadata['earliest_release_date']
|
|
178
171
|
print_metadata_line('Scheduled Date', metadata['earliest_release_date'], 'earliest_release_date')
|
|
179
172
|
end
|
|
180
|
-
|
|
173
|
+
|
|
181
174
|
print_metadata_toggle('Auto-submit', metadata['auto_submit'], 'auto_submit')
|
|
182
175
|
print_metadata_toggle('Phased Release', metadata['phased_release'], 'phased_release')
|
|
183
176
|
|
|
@@ -188,9 +181,9 @@ module Mysigner
|
|
|
188
181
|
end
|
|
189
182
|
|
|
190
183
|
warn_missing_submission_fields(metadata)
|
|
191
|
-
puts
|
|
184
|
+
puts ''
|
|
192
185
|
end
|
|
193
|
-
|
|
186
|
+
|
|
194
187
|
def format_release_type(release_type)
|
|
195
188
|
case release_type
|
|
196
189
|
when 'AFTER_APPROVAL' then 'After Approval (auto-release)'
|
|
@@ -205,10 +198,10 @@ module Mysigner
|
|
|
205
198
|
|
|
206
199
|
overrides.each do |key, value|
|
|
207
200
|
merged[key] = if merged[key].is_a?(Hash) && value.is_a?(Hash)
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
201
|
+
deep_merge(merged[key], value)
|
|
202
|
+
else
|
|
203
|
+
value
|
|
204
|
+
end
|
|
212
205
|
end
|
|
213
206
|
|
|
214
207
|
merged
|
|
@@ -253,7 +246,11 @@ module Mysigner
|
|
|
253
246
|
end
|
|
254
247
|
|
|
255
248
|
def print_metadata_toggle(label, value, key)
|
|
256
|
-
human = value.nil?
|
|
249
|
+
human = if value.nil?
|
|
250
|
+
'—'
|
|
251
|
+
else
|
|
252
|
+
(value ? 'Yes' : 'No')
|
|
253
|
+
end
|
|
257
254
|
puts " #{label}: #{human}#{override_suffix(key)}"
|
|
258
255
|
end
|
|
259
256
|
|
|
@@ -294,19 +291,17 @@ module Mysigner
|
|
|
294
291
|
warnings = []
|
|
295
292
|
# What's New only required for updates (version > 1.0)
|
|
296
293
|
warnings << "Missing What's New copy (required for version updates)" if !is_first_version && metadata['whats_new'].to_s.strip.empty?
|
|
297
|
-
|
|
294
|
+
|
|
298
295
|
# Support URL may already be in App Store Connect, so just note it
|
|
299
296
|
if metadata['support_url'].to_s.strip.empty?
|
|
300
|
-
warnings <<
|
|
297
|
+
warnings << 'Support URL not configured in My Signer (will use App Store Connect value if available)'
|
|
301
298
|
end
|
|
302
299
|
|
|
303
300
|
return if warnings.empty?
|
|
304
301
|
|
|
305
|
-
puts
|
|
302
|
+
puts ' ⚠️ Notes:' unless warnings.empty?
|
|
306
303
|
warnings.each { |msg| puts " - #{msg}" }
|
|
307
304
|
end
|
|
308
305
|
end
|
|
309
306
|
end
|
|
310
307
|
end
|
|
311
|
-
|
|
312
|
-
|