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