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