mysigner 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.DS_Store +0 -0
- data/.gitignore +12 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/.travis.yml +7 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +137 -0
- data/LICENSE +201 -0
- data/MANUAL_TEST.md +341 -0
- data/README.md +493 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/mysigner +5 -0
- data/lib/mysigner/build/android_executor.rb +367 -0
- data/lib/mysigner/build/android_parser.rb +293 -0
- data/lib/mysigner/build/configurator.rb +126 -0
- data/lib/mysigner/build/detector.rb +388 -0
- data/lib/mysigner/build/error_analyzer.rb +193 -0
- data/lib/mysigner/build/executor.rb +176 -0
- data/lib/mysigner/build/parser.rb +206 -0
- data/lib/mysigner/cli/auth_commands.rb +1381 -0
- data/lib/mysigner/cli/build_commands.rb +2095 -0
- data/lib/mysigner/cli/concerns/actionable_suggestions.rb +500 -0
- data/lib/mysigner/cli/concerns/api_helpers.rb +131 -0
- data/lib/mysigner/cli/concerns/error_handlers.rb +446 -0
- data/lib/mysigner/cli/concerns/helpers.rb +63 -0
- data/lib/mysigner/cli/diagnostic_commands.rb +1034 -0
- data/lib/mysigner/cli/resource_commands.rb +2670 -0
- data/lib/mysigner/cli.rb +43 -0
- data/lib/mysigner/client.rb +189 -0
- data/lib/mysigner/config.rb +311 -0
- data/lib/mysigner/export/exporter.rb +150 -0
- data/lib/mysigner/signing/certificate_checker.rb +148 -0
- data/lib/mysigner/signing/keystore_manager.rb +239 -0
- data/lib/mysigner/signing/validator.rb +150 -0
- data/lib/mysigner/signing/wizard.rb +784 -0
- data/lib/mysigner/upload/app_store_automation.rb +402 -0
- data/lib/mysigner/upload/app_store_submission.rb +312 -0
- data/lib/mysigner/upload/play_store_uploader.rb +378 -0
- data/lib/mysigner/upload/uploader.rb +373 -0
- data/lib/mysigner/version.rb +3 -0
- data/lib/mysigner.rb +15 -0
- data/mysigner.gemspec +78 -0
- data/test_manual.rb +102 -0
- metadata +286 -0
|
@@ -0,0 +1,784 @@
|
|
|
1
|
+
require 'faraday'
|
|
2
|
+
require 'json'
|
|
3
|
+
|
|
4
|
+
module Mysigner
|
|
5
|
+
module Signing
|
|
6
|
+
class Wizard
|
|
7
|
+
class WizardError < StandardError; end
|
|
8
|
+
|
|
9
|
+
def initialize(parser, client, organization_id, options = {})
|
|
10
|
+
@parser = parser
|
|
11
|
+
@client = client
|
|
12
|
+
@organization_id = organization_id
|
|
13
|
+
@options = options
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Run the interactive wizard
|
|
17
|
+
def run!
|
|
18
|
+
puts ""
|
|
19
|
+
puts "🧙 Manual Signing Setup Wizard"
|
|
20
|
+
puts "=" * 80
|
|
21
|
+
puts ""
|
|
22
|
+
|
|
23
|
+
# Check if we're configuring all targets
|
|
24
|
+
if @options[:all_targets]
|
|
25
|
+
configure_all_targets
|
|
26
|
+
else
|
|
27
|
+
configure_single_target(@options[:target])
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def configure_all_targets
|
|
32
|
+
targets = @parser.signable_targets
|
|
33
|
+
|
|
34
|
+
if targets.empty?
|
|
35
|
+
error "No signable targets found in project"
|
|
36
|
+
return
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
puts "Found #{targets.count} signable target(s):"
|
|
40
|
+
targets.each do |info|
|
|
41
|
+
type_label = info[:type] == :app ? '📱 App' : '🧩 Extension'
|
|
42
|
+
puts " #{type_label}: #{info[:name]}"
|
|
43
|
+
end
|
|
44
|
+
puts ""
|
|
45
|
+
|
|
46
|
+
print "Configure all targets? (y/n): "
|
|
47
|
+
confirm = get_input.downcase
|
|
48
|
+
|
|
49
|
+
unless confirm == 'y' || confirm == 'yes'
|
|
50
|
+
puts "Cancelled"
|
|
51
|
+
return
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
puts ""
|
|
55
|
+
|
|
56
|
+
# Configure each target
|
|
57
|
+
successful = 0
|
|
58
|
+
failed = 0
|
|
59
|
+
|
|
60
|
+
targets.each_with_index do |info, index|
|
|
61
|
+
puts ""
|
|
62
|
+
puts "=" * 80
|
|
63
|
+
puts "Configuring #{index + 1}/#{targets.count}: #{info[:name]}"
|
|
64
|
+
puts "=" * 80
|
|
65
|
+
puts ""
|
|
66
|
+
|
|
67
|
+
if configure_single_target(info[:name], skip_header: true)
|
|
68
|
+
successful += 1
|
|
69
|
+
else
|
|
70
|
+
failed += 1
|
|
71
|
+
puts ""
|
|
72
|
+
print "Continue with remaining targets? (y/n): "
|
|
73
|
+
continue = get_input.downcase
|
|
74
|
+
unless continue == 'y' || continue == 'yes'
|
|
75
|
+
break
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
puts ""
|
|
81
|
+
puts "=" * 80
|
|
82
|
+
puts "✅ Completed: #{successful} successful, #{failed} failed"
|
|
83
|
+
puts "=" * 80
|
|
84
|
+
puts ""
|
|
85
|
+
puts "Next steps:"
|
|
86
|
+
puts " 1. Test build: mysigner build"
|
|
87
|
+
puts " 2. Or ship to TestFlight: mysigner ship testflight"
|
|
88
|
+
puts ""
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def configure_single_target(target_name = nil, skip_header: false)
|
|
92
|
+
unless skip_header
|
|
93
|
+
# No header needed, already printed in run!
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Step 1: Detect or validate target
|
|
97
|
+
target_name = detect_target(target_name)
|
|
98
|
+
return false unless target_name
|
|
99
|
+
|
|
100
|
+
@current_target = target_name
|
|
101
|
+
|
|
102
|
+
# Step 1.5: Check if project's team matches current org
|
|
103
|
+
check_org_team_match(target_name) if @options[:check_org_match] != false
|
|
104
|
+
|
|
105
|
+
# Step 2: Show current configuration
|
|
106
|
+
show_current_config(target_name)
|
|
107
|
+
|
|
108
|
+
# Step 3: Select team
|
|
109
|
+
team_id = select_team(target_name)
|
|
110
|
+
return false unless team_id
|
|
111
|
+
|
|
112
|
+
# Step 4: Select provisioning profile
|
|
113
|
+
profile = select_profile(target_name, team_id)
|
|
114
|
+
return false unless profile
|
|
115
|
+
|
|
116
|
+
# Step 5: Apply configuration
|
|
117
|
+
apply_configuration(target_name, team_id, profile)
|
|
118
|
+
|
|
119
|
+
# Step 6: Validate
|
|
120
|
+
validate_configuration(target_name, team_id)
|
|
121
|
+
|
|
122
|
+
unless skip_header
|
|
123
|
+
puts ""
|
|
124
|
+
puts "=" * 80
|
|
125
|
+
puts "✅ Signing configuration complete!"
|
|
126
|
+
puts "=" * 80
|
|
127
|
+
puts ""
|
|
128
|
+
puts "Next steps:"
|
|
129
|
+
puts " 1. Test build: mysigner build"
|
|
130
|
+
puts " 2. Or ship to TestFlight: mysigner ship testflight"
|
|
131
|
+
puts ""
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
true
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
private
|
|
138
|
+
|
|
139
|
+
def detect_target(target_name = nil)
|
|
140
|
+
# If target_name provided, validate it exists
|
|
141
|
+
if target_name
|
|
142
|
+
begin
|
|
143
|
+
@parser.find_target(target_name)
|
|
144
|
+
puts "📱 Target: #{target_name}"
|
|
145
|
+
puts ""
|
|
146
|
+
return target_name
|
|
147
|
+
rescue => e
|
|
148
|
+
error "Target '#{target_name}' not found: #{e.message}"
|
|
149
|
+
return nil
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# No target provided, auto-detect or let user choose
|
|
154
|
+
targets = @parser.app_targets
|
|
155
|
+
|
|
156
|
+
if targets.empty?
|
|
157
|
+
error "No app targets found in project"
|
|
158
|
+
return nil
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
if targets.count == 1
|
|
162
|
+
target = targets.first
|
|
163
|
+
puts "📱 Target: #{target.name}"
|
|
164
|
+
puts ""
|
|
165
|
+
return target.name
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Multiple targets - let user choose
|
|
169
|
+
puts "Multiple app targets found:"
|
|
170
|
+
targets.each_with_index do |target, i|
|
|
171
|
+
puts " #{i + 1}. #{target.name}"
|
|
172
|
+
end
|
|
173
|
+
puts ""
|
|
174
|
+
|
|
175
|
+
print "Select target (1-#{targets.count}): "
|
|
176
|
+
choice = get_input.to_i
|
|
177
|
+
|
|
178
|
+
if choice < 1 || choice > targets.count
|
|
179
|
+
error "Invalid selection"
|
|
180
|
+
return nil
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
targets[choice - 1].name
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def show_current_config(target_name)
|
|
187
|
+
puts "Current Configuration:"
|
|
188
|
+
puts "-" * 80
|
|
189
|
+
|
|
190
|
+
bundle_id = @parser.bundle_id(target_name)
|
|
191
|
+
puts " Bundle ID: #{bundle_id || 'Not set'}"
|
|
192
|
+
|
|
193
|
+
team_id = @parser.team_id(target_name)
|
|
194
|
+
puts " Team: #{team_id || 'Not set'}"
|
|
195
|
+
|
|
196
|
+
sign_style = @parser.code_sign_style(target_name)
|
|
197
|
+
puts " Signing: #{sign_style || 'Not set'}"
|
|
198
|
+
|
|
199
|
+
if @parser.signing_configured?(target_name)
|
|
200
|
+
profile_name = @parser.project.targets.find { |t| t.name == target_name }
|
|
201
|
+
&.build_configurations&.first
|
|
202
|
+
&.build_settings&.[]('PROVISIONING_PROFILE_SPECIFIER')
|
|
203
|
+
puts " Profile: #{profile_name || 'Auto'}"
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
puts ""
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def select_team(target_name)
|
|
210
|
+
puts "Step 1: Select Development Team"
|
|
211
|
+
puts "-" * 80
|
|
212
|
+
puts ""
|
|
213
|
+
|
|
214
|
+
current_team = @parser.team_id(target_name)
|
|
215
|
+
|
|
216
|
+
# Option 1: Keep current team
|
|
217
|
+
if current_team && !current_team.empty?
|
|
218
|
+
puts " 1. Keep current team: #{current_team}"
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Option 2: Fetch from API
|
|
222
|
+
puts " #{current_team ? '2' : '1'}. Fetch from My Signer API"
|
|
223
|
+
|
|
224
|
+
# Option 3: Enter manually
|
|
225
|
+
puts " #{current_team ? '3' : '2'}. Enter team ID manually"
|
|
226
|
+
|
|
227
|
+
puts ""
|
|
228
|
+
print "Select option: "
|
|
229
|
+
choice = get_input.to_i
|
|
230
|
+
|
|
231
|
+
case choice
|
|
232
|
+
when 1
|
|
233
|
+
if current_team
|
|
234
|
+
puts "✓ Using current team: #{current_team}"
|
|
235
|
+
puts ""
|
|
236
|
+
return current_team
|
|
237
|
+
else
|
|
238
|
+
# Fetch from API
|
|
239
|
+
fetch_team_from_api
|
|
240
|
+
end
|
|
241
|
+
when 2
|
|
242
|
+
if current_team
|
|
243
|
+
fetch_team_from_api
|
|
244
|
+
else
|
|
245
|
+
enter_team_manually
|
|
246
|
+
end
|
|
247
|
+
when 3
|
|
248
|
+
enter_team_manually
|
|
249
|
+
else
|
|
250
|
+
error "Invalid selection"
|
|
251
|
+
nil
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def fetch_team_from_api
|
|
256
|
+
puts ""
|
|
257
|
+
puts "Fetching team from My Signer..."
|
|
258
|
+
|
|
259
|
+
begin
|
|
260
|
+
response = @client.get("/api/v1/organizations/#{@organization_id}")
|
|
261
|
+
team_id = response.dig(:data, 'app_store_connect_team_id') || response['app_store_connect_team_id']
|
|
262
|
+
|
|
263
|
+
if team_id && !team_id.empty?
|
|
264
|
+
puts "✓ Found team: #{team_id}"
|
|
265
|
+
puts ""
|
|
266
|
+
return team_id
|
|
267
|
+
else
|
|
268
|
+
error "Team ID not saved in My Signer API (database)"
|
|
269
|
+
puts ""
|
|
270
|
+
current_team = @parser.team_id(@current_target)
|
|
271
|
+
puts "Note: Your Xcode project already has team: #{current_team}" if current_team
|
|
272
|
+
puts ""
|
|
273
|
+
puts "You can either:"
|
|
274
|
+
puts " 1. Keep your current Xcode team (go back and select option 1)"
|
|
275
|
+
puts " 2. Add it to My Signer web: https://mysigner.dev"
|
|
276
|
+
puts " → Open your organization → App Store Connect → Edit/Add credentials → Team ID field"
|
|
277
|
+
puts " 3. Enter it manually (go back and select option 3)"
|
|
278
|
+
puts ""
|
|
279
|
+
nil
|
|
280
|
+
end
|
|
281
|
+
rescue => e
|
|
282
|
+
error "Failed to fetch team: #{e.message}"
|
|
283
|
+
nil
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def enter_team_manually
|
|
288
|
+
puts ""
|
|
289
|
+
print "Enter Team ID (10 characters): "
|
|
290
|
+
team_id = get_input
|
|
291
|
+
|
|
292
|
+
if team_id =~ /^[A-Z0-9]{10}$/
|
|
293
|
+
puts "✓ Team ID: #{team_id}"
|
|
294
|
+
puts ""
|
|
295
|
+
return team_id
|
|
296
|
+
else
|
|
297
|
+
error "Invalid Team ID format (must be 10 alphanumeric characters)"
|
|
298
|
+
nil
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def select_profile(target_name, team_id)
|
|
303
|
+
puts "Step 2: Select Provisioning Profile"
|
|
304
|
+
puts "-" * 80
|
|
305
|
+
puts ""
|
|
306
|
+
|
|
307
|
+
# Fetch profiles from API
|
|
308
|
+
puts "Fetching provisioning profiles..."
|
|
309
|
+
|
|
310
|
+
begin
|
|
311
|
+
bundle_id = @parser.bundle_id(target_name)
|
|
312
|
+
|
|
313
|
+
# Get profiles for this bundle ID
|
|
314
|
+
response = @client.get("/api/v1/organizations/#{@organization_id}/profiles",
|
|
315
|
+
params: { bundle_id: bundle_id })
|
|
316
|
+
|
|
317
|
+
profiles = response[:data]['profiles'] || []
|
|
318
|
+
|
|
319
|
+
if profiles.empty?
|
|
320
|
+
puts "No provisioning profiles found for bundle ID: #{bundle_id}"
|
|
321
|
+
puts ""
|
|
322
|
+
puts "Options:"
|
|
323
|
+
puts " 1. Auto-create App Store profile (recommended)"
|
|
324
|
+
puts " 2. Auto-create Development profile"
|
|
325
|
+
puts " 3. Create manually and sync"
|
|
326
|
+
puts " 4. Skip"
|
|
327
|
+
puts ""
|
|
328
|
+
|
|
329
|
+
print "Select option (1-4): "
|
|
330
|
+
choice = get_input
|
|
331
|
+
puts ""
|
|
332
|
+
|
|
333
|
+
case choice
|
|
334
|
+
when "1"
|
|
335
|
+
profile = auto_create_profile(bundle_id, :appstore)
|
|
336
|
+
return profile if profile
|
|
337
|
+
return nil
|
|
338
|
+
when "2"
|
|
339
|
+
profile = auto_create_profile(bundle_id, :development)
|
|
340
|
+
return profile if profile
|
|
341
|
+
return nil
|
|
342
|
+
when "3"
|
|
343
|
+
puts "Create profile at: https://developer.apple.com/account/resources/profiles/add"
|
|
344
|
+
puts "Then sync from My Signer web dashboard"
|
|
345
|
+
puts ""
|
|
346
|
+
return nil
|
|
347
|
+
when "4"
|
|
348
|
+
puts "Skipped profile selection"
|
|
349
|
+
return nil
|
|
350
|
+
else
|
|
351
|
+
error "Invalid selection"
|
|
352
|
+
return nil
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# Filter profiles by type (development vs distribution)
|
|
357
|
+
dev_profiles = profiles.select { |p| p['profile_type']&.include?('DEVELOPMENT') }
|
|
358
|
+
dist_profiles = profiles.select do |p|
|
|
359
|
+
type = p['profile_type']
|
|
360
|
+
type&.include?('DISTRIBUTION') || type&.include?('APP_STORE') || type&.include?('ADHOC') || type&.include?('INHOUSE')
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
puts ""
|
|
364
|
+
puts "Available Profiles:"
|
|
365
|
+
puts ""
|
|
366
|
+
|
|
367
|
+
all_profiles = []
|
|
368
|
+
|
|
369
|
+
if dev_profiles.any?
|
|
370
|
+
puts " Development Profiles:"
|
|
371
|
+
dev_profiles.each_with_index do |profile, i|
|
|
372
|
+
all_profiles << profile
|
|
373
|
+
status = profile['status'] == 'ACTIVE' ? '✓' : '✗'
|
|
374
|
+
puts " #{all_profiles.count}. #{status} #{profile['name']}"
|
|
375
|
+
puts " Expires: #{profile['expires_at']&.split('T')&.first || 'Unknown'}"
|
|
376
|
+
end
|
|
377
|
+
puts ""
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
if dist_profiles.any?
|
|
381
|
+
puts " Distribution Profiles:"
|
|
382
|
+
dist_profiles.each_with_index do |profile, i|
|
|
383
|
+
all_profiles << profile
|
|
384
|
+
status = profile['status'] == 'ACTIVE' ? '✓' : '✗'
|
|
385
|
+
puts " #{all_profiles.count}. #{status} #{profile['name']}"
|
|
386
|
+
puts " Expires: #{profile['expires_at']&.split('T')&.first || 'Unknown'}"
|
|
387
|
+
end
|
|
388
|
+
puts ""
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
print "Select profile (1-#{all_profiles.count}): "
|
|
392
|
+
choice = get_input.to_i
|
|
393
|
+
|
|
394
|
+
if choice < 1 || choice > all_profiles.count
|
|
395
|
+
error "Invalid selection"
|
|
396
|
+
return nil
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
selected = all_profiles[choice - 1]
|
|
400
|
+
puts "✓ Selected: #{selected['name']}"
|
|
401
|
+
puts ""
|
|
402
|
+
|
|
403
|
+
# Download and install the profile
|
|
404
|
+
download_and_install_profile(selected)
|
|
405
|
+
|
|
406
|
+
selected
|
|
407
|
+
|
|
408
|
+
rescue => e
|
|
409
|
+
error "Failed to fetch profiles: #{e.message}"
|
|
410
|
+
nil
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
def download_and_install_profile(profile)
|
|
415
|
+
puts "Downloading and installing profile..."
|
|
416
|
+
|
|
417
|
+
begin
|
|
418
|
+
# Download profile using direct Faraday connection for binary data
|
|
419
|
+
# (the client's get method uses JSON middleware which corrupts binary data)
|
|
420
|
+
download_url = "/api/v1/organizations/#{@organization_id}/profiles/#{profile['id']}/download"
|
|
421
|
+
|
|
422
|
+
conn = Faraday.new(url: @client.api_url) do |f|
|
|
423
|
+
f.request :authorization, 'Bearer', @client.api_token
|
|
424
|
+
f.adapter Faraday.default_adapter
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
response = conn.get(download_url) do |req|
|
|
428
|
+
req.options.timeout = 30
|
|
429
|
+
req.options.open_timeout = 10
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
unless response.success?
|
|
433
|
+
if response.headers['content-type']&.include?('json')
|
|
434
|
+
begin
|
|
435
|
+
error_data = JSON.parse(response.body)
|
|
436
|
+
raise "Download failed: #{error_data['message'] || error_data['error']}"
|
|
437
|
+
rescue JSON::ParserError
|
|
438
|
+
raise "Download failed with status #{response.status}"
|
|
439
|
+
end
|
|
440
|
+
else
|
|
441
|
+
raise "Download failed with status #{response.status}"
|
|
442
|
+
end
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
profile_content = response.body
|
|
446
|
+
|
|
447
|
+
# Create profiles directory if it doesn't exist
|
|
448
|
+
profiles_dir = File.expand_path("~/Library/MobileDevice/Provisioning Profiles")
|
|
449
|
+
FileUtils.mkdir_p(profiles_dir) unless Dir.exist?(profiles_dir)
|
|
450
|
+
|
|
451
|
+
# Generate filename (use UUID if available, otherwise sanitized name)
|
|
452
|
+
uuid = profile['uuid'] || profile['id']
|
|
453
|
+
filename = "#{uuid}.mobileprovision"
|
|
454
|
+
output_path = File.join(profiles_dir, filename)
|
|
455
|
+
|
|
456
|
+
# Write binary profile to file
|
|
457
|
+
File.binwrite(output_path, profile_content)
|
|
458
|
+
|
|
459
|
+
puts "✓ Profile installed: #{output_path}"
|
|
460
|
+
puts ""
|
|
461
|
+
|
|
462
|
+
rescue => e
|
|
463
|
+
# Non-fatal error - profile might still work if already installed
|
|
464
|
+
puts "⚠️ Could not auto-install profile: #{e.message}"
|
|
465
|
+
puts " You may need to install it manually by double-clicking the .mobileprovision file"
|
|
466
|
+
puts ""
|
|
467
|
+
end
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
def apply_configuration(target_name, team_id, profile)
|
|
471
|
+
puts "Step 3: Applying Configuration"
|
|
472
|
+
puts "-" * 80
|
|
473
|
+
puts ""
|
|
474
|
+
|
|
475
|
+
puts "Setting up manual signing for target: #{target_name}"
|
|
476
|
+
puts " Team: #{team_id}"
|
|
477
|
+
puts " Profile: #{profile['name']}"
|
|
478
|
+
puts ""
|
|
479
|
+
|
|
480
|
+
target = @parser.project.targets.find { |t| t.name == target_name }
|
|
481
|
+
|
|
482
|
+
unless target
|
|
483
|
+
raise WizardError, "Target not found: #{target_name}"
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
# Update build configurations
|
|
487
|
+
target.build_configurations.each do |config|
|
|
488
|
+
puts " Configuring #{config.name}..."
|
|
489
|
+
|
|
490
|
+
# Set manual signing
|
|
491
|
+
config.build_settings['CODE_SIGN_STYLE'] = 'Manual'
|
|
492
|
+
|
|
493
|
+
# Set team
|
|
494
|
+
config.build_settings['DEVELOPMENT_TEAM'] = team_id
|
|
495
|
+
|
|
496
|
+
# Set provisioning profile
|
|
497
|
+
config.build_settings['PROVISIONING_PROFILE_SPECIFIER'] = profile['name']
|
|
498
|
+
|
|
499
|
+
# Set code sign identity
|
|
500
|
+
profile_type = profile['profile_type']
|
|
501
|
+
if profile_type&.include?('DEVELOPMENT')
|
|
502
|
+
config.build_settings['CODE_SIGN_IDENTITY'] = 'Apple Development'
|
|
503
|
+
else
|
|
504
|
+
config.build_settings['CODE_SIGN_IDENTITY'] = 'Apple Distribution'
|
|
505
|
+
end
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
# Save project
|
|
509
|
+
@parser.project.save
|
|
510
|
+
|
|
511
|
+
puts "✓ Configuration applied"
|
|
512
|
+
puts ""
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
def validate_configuration(target_name, team_id)
|
|
516
|
+
puts "Step 4: Validating Configuration"
|
|
517
|
+
puts "-" * 80
|
|
518
|
+
puts ""
|
|
519
|
+
|
|
520
|
+
validator = Validator.new(@parser, target_name, 'Release', team_id: team_id)
|
|
521
|
+
result = validator.validate
|
|
522
|
+
|
|
523
|
+
if result[:valid]
|
|
524
|
+
puts "✓ Configuration is valid"
|
|
525
|
+
|
|
526
|
+
if result[:warnings].any?
|
|
527
|
+
puts ""
|
|
528
|
+
puts "Warnings:"
|
|
529
|
+
result[:warnings].each do |warning|
|
|
530
|
+
puts " ⚠️ #{warning}"
|
|
531
|
+
end
|
|
532
|
+
end
|
|
533
|
+
else
|
|
534
|
+
puts "✗ Configuration has errors:"
|
|
535
|
+
puts ""
|
|
536
|
+
result[:errors].each do |error|
|
|
537
|
+
puts " • #{error}"
|
|
538
|
+
end
|
|
539
|
+
puts ""
|
|
540
|
+
raise WizardError, "Configuration validation failed"
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
puts ""
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
def check_org_team_match(target_name)
|
|
547
|
+
project_team = @parser.team_id(target_name)
|
|
548
|
+
return unless project_team # No team set in project yet
|
|
549
|
+
|
|
550
|
+
begin
|
|
551
|
+
# Fetch current org's team
|
|
552
|
+
response = @client.get("/api/v1/organizations/#{@organization_id}")
|
|
553
|
+
org_name = response.dig(:data, 'name') || response['name']
|
|
554
|
+
org_team = response.dig(:data, 'app_store_connect_team_id') || response['app_store_connect_team_id']
|
|
555
|
+
|
|
556
|
+
if org_team && org_team != project_team
|
|
557
|
+
puts ""
|
|
558
|
+
puts "⚠️ Warning: Organization / Team Mismatch", :yellow
|
|
559
|
+
puts "=" * 80
|
|
560
|
+
puts ""
|
|
561
|
+
puts "Your Xcode project uses team: #{project_team}"
|
|
562
|
+
puts "Current My Signer org: #{org_name} (Team: #{org_team})"
|
|
563
|
+
puts ""
|
|
564
|
+
puts "This means your project belongs to a different Apple Developer account"
|
|
565
|
+
puts "than the organization you're currently using in My Signer."
|
|
566
|
+
puts ""
|
|
567
|
+
puts "What this means:"
|
|
568
|
+
puts " • Profiles/certificates fetched will be for team #{org_team}"
|
|
569
|
+
puts " • But your project needs resources for team #{project_team}"
|
|
570
|
+
puts " • This will likely cause signing errors"
|
|
571
|
+
puts ""
|
|
572
|
+
puts "To fix this:"
|
|
573
|
+
puts " 1. Exit this wizard (Ctrl+C)"
|
|
574
|
+
puts " 2. Run: mysigner switch"
|
|
575
|
+
puts " 3. Select the organization that has team #{project_team}"
|
|
576
|
+
puts " 4. Run this wizard again"
|
|
577
|
+
puts ""
|
|
578
|
+
print "Continue anyway? (y/N): "
|
|
579
|
+
answer = get_input.downcase
|
|
580
|
+
|
|
581
|
+
unless answer == 'y' || answer == 'yes'
|
|
582
|
+
puts ""
|
|
583
|
+
puts "Wizard cancelled. Please switch organizations and try again."
|
|
584
|
+
exit 0
|
|
585
|
+
end
|
|
586
|
+
puts ""
|
|
587
|
+
elsif !org_team
|
|
588
|
+
# Current org has no team configured
|
|
589
|
+
puts ""
|
|
590
|
+
puts "ℹ️ Note: Current organization has no Team ID configured", :cyan
|
|
591
|
+
puts "=" * 80
|
|
592
|
+
puts ""
|
|
593
|
+
puts "Your Xcode project uses team: #{project_team}"
|
|
594
|
+
puts "Current My Signer org: #{org_name} (No team configured)"
|
|
595
|
+
puts ""
|
|
596
|
+
puts "You can continue, but the wizard won't be able to fetch the team from My Signer."
|
|
597
|
+
puts "Consider adding Team ID to this org at: https://mysigner.dev"
|
|
598
|
+
puts ""
|
|
599
|
+
print "Continue? (Y/n): "
|
|
600
|
+
answer = get_input.downcase
|
|
601
|
+
|
|
602
|
+
if answer == 'n' || answer == 'no'
|
|
603
|
+
puts ""
|
|
604
|
+
puts "Wizard cancelled."
|
|
605
|
+
exit 0
|
|
606
|
+
end
|
|
607
|
+
puts ""
|
|
608
|
+
end
|
|
609
|
+
rescue => e
|
|
610
|
+
# Ignore errors in org checking - don't block the wizard
|
|
611
|
+
puts "Warning: Could not verify organization match: #{e.message}" if ENV['DEBUG']
|
|
612
|
+
end
|
|
613
|
+
end
|
|
614
|
+
|
|
615
|
+
def auto_create_profile(bundle_id, type)
|
|
616
|
+
puts "Creating #{type} profile for #{bundle_id}..."
|
|
617
|
+
puts ""
|
|
618
|
+
|
|
619
|
+
profile_type = type == :appstore ? 'IOS_APP_STORE' : 'IOS_APP_DEVELOPMENT'
|
|
620
|
+
|
|
621
|
+
begin
|
|
622
|
+
# Sync first to ensure we have latest resources
|
|
623
|
+
puts " Syncing organization resources..."
|
|
624
|
+
@client.post("/api/v1/organizations/#{@organization_id}/sync_app_store_connect")
|
|
625
|
+
|
|
626
|
+
# Wait for sync
|
|
627
|
+
sleep 2
|
|
628
|
+
|
|
629
|
+
# Check sync status
|
|
630
|
+
max_wait = 15
|
|
631
|
+
waited = 0
|
|
632
|
+
|
|
633
|
+
while waited < max_wait
|
|
634
|
+
status_response = @client.get("/api/v1/organizations/#{@organization_id}/sync/status")
|
|
635
|
+
sync_data = status_response[:data]['sync']
|
|
636
|
+
|
|
637
|
+
if !sync_data['running']
|
|
638
|
+
break
|
|
639
|
+
end
|
|
640
|
+
|
|
641
|
+
sleep 1
|
|
642
|
+
waited += 1
|
|
643
|
+
end
|
|
644
|
+
|
|
645
|
+
puts " ✓ Sync complete"
|
|
646
|
+
puts ""
|
|
647
|
+
|
|
648
|
+
# Create profile
|
|
649
|
+
puts " Creating #{profile_type} profile..."
|
|
650
|
+
response = @client.post(
|
|
651
|
+
"/api/v1/organizations/#{@organization_id}/profiles/auto_create",
|
|
652
|
+
body: {
|
|
653
|
+
bundle_id: bundle_id,
|
|
654
|
+
profile_type: profile_type
|
|
655
|
+
}
|
|
656
|
+
)
|
|
657
|
+
|
|
658
|
+
profile = response[:data]['profile']
|
|
659
|
+
puts " ✓ Created profile: #{profile['name']}"
|
|
660
|
+
puts ""
|
|
661
|
+
|
|
662
|
+
# Download and install
|
|
663
|
+
download_and_install_profile(profile)
|
|
664
|
+
|
|
665
|
+
profile
|
|
666
|
+
rescue Mysigner::ClientError => e
|
|
667
|
+
error_msg = e.message
|
|
668
|
+
|
|
669
|
+
if error_msg.include?("bundle_id_not_found")
|
|
670
|
+
error "Bundle ID '#{bundle_id}' not found"
|
|
671
|
+
puts ""
|
|
672
|
+
puts "Register it at: https://developer.apple.com/account/resources/identifiers/add"
|
|
673
|
+
puts "Then sync in the web dashboard"
|
|
674
|
+
elsif error_msg.include?("certificates found") || error_msg.include?("no_certificates")
|
|
675
|
+
cert_name = type == :appstore ? "Apple Distribution" : "Apple Development"
|
|
676
|
+
|
|
677
|
+
error "No #{cert_name} certificates found"
|
|
678
|
+
puts ""
|
|
679
|
+
|
|
680
|
+
print "Generate CSR automatically? [Y/n] "
|
|
681
|
+
response = get_input.downcase
|
|
682
|
+
|
|
683
|
+
if response.empty? || response == 'y' || response == 'yes'
|
|
684
|
+
puts ""
|
|
685
|
+
csr_path = generate_csr_for_wizard
|
|
686
|
+
|
|
687
|
+
if csr_path
|
|
688
|
+
puts ""
|
|
689
|
+
puts " ✓ CSR ready: #{File.basename(csr_path)}"
|
|
690
|
+
puts ""
|
|
691
|
+
puts " 📋 Next steps:"
|
|
692
|
+
puts " 1. https://developer.apple.com/account/resources/certificates/add"
|
|
693
|
+
puts " 2. Select: '#{cert_name}' (or older 'iOS' variant if available)"
|
|
694
|
+
puts " 3. Upload: #{csr_path}"
|
|
695
|
+
puts " 4. Download .cer → Double-click → Sync → Try again"
|
|
696
|
+
puts ""
|
|
697
|
+
end
|
|
698
|
+
else
|
|
699
|
+
puts ""
|
|
700
|
+
puts "Quick fix:"
|
|
701
|
+
puts " 1. Open Keychain Access → Request Certificate (save CSR)"
|
|
702
|
+
puts " 2. https://developer.apple.com/account/resources/certificates/add"
|
|
703
|
+
puts " 3. Select '#{cert_name}' → Upload CSR → Download .cer"
|
|
704
|
+
puts " 4. Double-click .cer → Sync My Signer → Try again"
|
|
705
|
+
puts ""
|
|
706
|
+
end
|
|
707
|
+
elsif error_msg.include?("no_devices") || error_msg.include?("devices found")
|
|
708
|
+
error "No test devices registered"
|
|
709
|
+
puts ""
|
|
710
|
+
puts "Quick fix:"
|
|
711
|
+
puts " • Get UDID: Connect device → Finder → Click serial number"
|
|
712
|
+
puts " • Run: mysigner device add <UDID> <NAME>"
|
|
713
|
+
puts ""
|
|
714
|
+
else
|
|
715
|
+
error "Failed to create profile: #{error_msg}"
|
|
716
|
+
end
|
|
717
|
+
puts ""
|
|
718
|
+
nil
|
|
719
|
+
rescue => e
|
|
720
|
+
error "Unexpected error: #{e.message}"
|
|
721
|
+
puts ""
|
|
722
|
+
nil
|
|
723
|
+
end
|
|
724
|
+
end
|
|
725
|
+
|
|
726
|
+
def generate_csr_for_wizard
|
|
727
|
+
require 'openssl'
|
|
728
|
+
|
|
729
|
+
begin
|
|
730
|
+
# Save to Downloads (visible in file picker)
|
|
731
|
+
csr_dir = File.expand_path("~/Downloads")
|
|
732
|
+
FileUtils.mkdir_p(csr_dir)
|
|
733
|
+
|
|
734
|
+
# Generate RSA key pair
|
|
735
|
+
key = OpenSSL::PKey::RSA.new(2048)
|
|
736
|
+
|
|
737
|
+
# Create CSR
|
|
738
|
+
csr = OpenSSL::X509::Request.new
|
|
739
|
+
csr.version = 0
|
|
740
|
+
csr.subject = OpenSSL::X509::Name.new([
|
|
741
|
+
['CN', 'My Signer User'],
|
|
742
|
+
['emailAddress', 'user@example.com']
|
|
743
|
+
])
|
|
744
|
+
csr.public_key = key.public_key
|
|
745
|
+
csr.sign(key, OpenSSL::Digest::SHA256.new)
|
|
746
|
+
|
|
747
|
+
# Generate unique filename
|
|
748
|
+
timestamp = Time.now.strftime('%Y%m%d_%H%M%S')
|
|
749
|
+
csr_filename = "CertificateSigningRequest_#{timestamp}.certSigningRequest"
|
|
750
|
+
key_filename = "private_key_#{timestamp}.pem"
|
|
751
|
+
|
|
752
|
+
# Save CSR to Downloads (visible)
|
|
753
|
+
csr_path = File.join(csr_dir, csr_filename)
|
|
754
|
+
|
|
755
|
+
# Save private key to hidden location (secure)
|
|
756
|
+
key_dir = File.expand_path("~/.mysigner/keys")
|
|
757
|
+
FileUtils.mkdir_p(key_dir)
|
|
758
|
+
key_path = File.join(key_dir, key_filename)
|
|
759
|
+
|
|
760
|
+
# Save files
|
|
761
|
+
File.write(csr_path, csr.to_pem)
|
|
762
|
+
File.write(key_path, key.to_pem)
|
|
763
|
+
File.chmod(0600, key_path)
|
|
764
|
+
|
|
765
|
+
csr_path
|
|
766
|
+
rescue => e
|
|
767
|
+
puts " ✗ Failed to generate CSR: #{e.message}"
|
|
768
|
+
nil
|
|
769
|
+
end
|
|
770
|
+
end
|
|
771
|
+
|
|
772
|
+
# Safely get user input, returns empty string if STDIN is closed or nil
|
|
773
|
+
def get_input
|
|
774
|
+
input = STDIN.gets
|
|
775
|
+
input ? input.strip : ''
|
|
776
|
+
end
|
|
777
|
+
|
|
778
|
+
def error(message)
|
|
779
|
+
puts "✗ #{message}"
|
|
780
|
+
end
|
|
781
|
+
end
|
|
782
|
+
end
|
|
783
|
+
end
|
|
784
|
+
|