adsedare 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +35 -0
- data/Rakefile +4 -0
- data/adsedare.gemspec +36 -0
- data/exe/adsedare +3 -0
- data/lib/adsedare/capabilities.rb +138 -0
- data/lib/adsedare/version.rb +5 -0
- data/lib/adsedare.rb +518 -0
- data/lib/appstoreconnect.rb +86 -0
- data/lib/logging.rb +26 -0
- data/lib/starship/2fa_provider.rb +40 -0
- data/lib/starship/auth_helper.rb +450 -0
- data/lib/starship.rb +293 -0
- metadata +145 -0
data/lib/adsedare.rb
ADDED
@@ -0,0 +1,518 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "xcodeproj"
|
4
|
+
require "base64"
|
5
|
+
require "fileutils"
|
6
|
+
require "tempfile"
|
7
|
+
require "plist"
|
8
|
+
|
9
|
+
require_relative "adsedare/version"
|
10
|
+
require_relative "adsedare/capabilities"
|
11
|
+
require_relative "logging"
|
12
|
+
|
13
|
+
require_relative "starship"
|
14
|
+
require_relative "appstoreconnect"
|
15
|
+
|
16
|
+
module Adsedare
|
17
|
+
class Error < StandardError; end
|
18
|
+
|
19
|
+
class << self
|
20
|
+
include Logging
|
21
|
+
|
22
|
+
def renew_profiles(project_path = nil, certificate_id = nil, team_id = nil)
|
23
|
+
raise "Project path is not set" unless project_path
|
24
|
+
raise "Certificate ID is not set" unless certificate_id
|
25
|
+
|
26
|
+
project = Xcodeproj::Project.open(project_path)
|
27
|
+
project_dir = File.dirname(project_path)
|
28
|
+
|
29
|
+
bundle_entitlements = {}
|
30
|
+
|
31
|
+
project.targets.each do |target|
|
32
|
+
target.build_configurations.each do |config|
|
33
|
+
bundle_identifier = config.build_settings["PRODUCT_BUNDLE_IDENTIFIER"]
|
34
|
+
entitlements_path = config.build_settings["CODE_SIGN_ENTITLEMENTS"]
|
35
|
+
|
36
|
+
# If team_id is not set, use the first one from the project
|
37
|
+
team_id ||= config.build_settings["DEVELOPMENT_TEAM"]
|
38
|
+
|
39
|
+
if entitlements_path
|
40
|
+
full_entitlements_path = File.join(project_dir, entitlements_path)
|
41
|
+
bundle_entitlements[bundle_identifier] = full_entitlements_path
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
bundle_by_identifier = get_bundle_map(team_id)
|
47
|
+
profiles_by_bundle = get_profiles_map(team_id)
|
48
|
+
|
49
|
+
bundle_entitlements.each do |bundle_identifier, entitlements_path|
|
50
|
+
bundle_id = bundle_by_identifier[bundle_identifier]
|
51
|
+
unless bundle_id
|
52
|
+
logger.warn "Bundle '#{bundle_identifier}' is missing in Apple Developer portal. Will create."
|
53
|
+
bundle_id = Starship::Client.create_bundle(
|
54
|
+
bundle_identifier,
|
55
|
+
team_id,
|
56
|
+
# You cannot create bundle without this capability
|
57
|
+
[ SimpleCapability.new("IN_APP_PURCHASE").to_bundle_capability(nil, nil) ]
|
58
|
+
)["data"]["id"]
|
59
|
+
bundle_by_identifier[bundle_identifier] = bundle_id
|
60
|
+
logger.info "Bundle '#{bundle_identifier}' created with ID '#{bundle_id}'"
|
61
|
+
else
|
62
|
+
logger.info "Bundle '#{bundle_identifier}' resolved to Bundle ID '#{bundle_id}'"
|
63
|
+
end
|
64
|
+
|
65
|
+
renew_bundle_id(bundle_id, team_id, entitlements_path)
|
66
|
+
|
67
|
+
profile = profiles_by_bundle[bundle_id]
|
68
|
+
unless profile
|
69
|
+
logger.warn "Profile for Bundle ID '#{bundle_id}' is missing in Apple Developer portal. Will create."
|
70
|
+
devices = get_devices(team_id)
|
71
|
+
profile_id = Starship::Client.create_provisioning_profile(
|
72
|
+
team_id,
|
73
|
+
bundle_id,
|
74
|
+
bundle_identifier,
|
75
|
+
certificate_id,
|
76
|
+
devices
|
77
|
+
)["data"]["id"]
|
78
|
+
profiles_by_bundle[bundle_id] = Starship::Client.get_provisioning_profile(profile_id, team_id)
|
79
|
+
profile = profiles_by_bundle[bundle_id]
|
80
|
+
logger.info "Profile for Bundle ID '#{bundle_id}' created with ID '#{profile_id}'"
|
81
|
+
else
|
82
|
+
logger.info "Bundle ID '#{bundle_id}' resolved to Profile '#{profile["provisioningProfile"]["name"]}'"
|
83
|
+
end
|
84
|
+
|
85
|
+
renew_provisioning_profile(profile, team_id)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def install_profiles(project_path = nil)
|
90
|
+
raise "Project path is not set" unless project_path
|
91
|
+
|
92
|
+
project = Xcodeproj::Project.open(project_path)
|
93
|
+
|
94
|
+
project_bundles = project.targets.map do |target|
|
95
|
+
target.build_configurations.map do |config|
|
96
|
+
config.build_settings["PRODUCT_BUNDLE_IDENTIFIER"]
|
97
|
+
end
|
98
|
+
end.flatten.uniq
|
99
|
+
|
100
|
+
bundles_with_profiles = AppStoreConnect::Client.get_bundles_with_profiles(project_bundles)
|
101
|
+
bundle_by_identifier = {}
|
102
|
+
profiles_by_id = {}
|
103
|
+
|
104
|
+
bundles_with_profiles["data"].each do |bundle_id|
|
105
|
+
bundle_by_identifier[bundle_id["attributes"]["identifier"]] = bundle_id
|
106
|
+
end
|
107
|
+
|
108
|
+
bundles_with_profiles["included"].each do |profile|
|
109
|
+
profiles_by_id[profile["id"]] = profile
|
110
|
+
end
|
111
|
+
|
112
|
+
project_bundles.each do |bundle_identifier|
|
113
|
+
bundle_id = bundle_by_identifier[bundle_identifier]
|
114
|
+
unless bundle_id
|
115
|
+
logger.warn "Bundle '#{bundle_identifier}' is missing in App Store Connect. Skipping."
|
116
|
+
next
|
117
|
+
end
|
118
|
+
|
119
|
+
logger.info "Bundle '#{bundle_identifier}' resolved to Bundle ID '#{bundle_id['id']}'"
|
120
|
+
|
121
|
+
profiles = bundle_id["relationships"]["profiles"]["data"]
|
122
|
+
unless profiles
|
123
|
+
logger.warn "Profile for Bundle ID '#{bundle_id['id']}' is missing in App Store Connect. Skipping."
|
124
|
+
next
|
125
|
+
end
|
126
|
+
|
127
|
+
ad_hoc_profile = nil
|
128
|
+
profiles.each do |profile|
|
129
|
+
profile_id = profile["id"]
|
130
|
+
profile = profiles_by_id[profile_id]
|
131
|
+
|
132
|
+
if profile["attributes"]["profileType"] == "IOS_APP_ADHOC" && profile["attributes"]["profileState"] == "ACTIVE"
|
133
|
+
ad_hoc_profile = profile
|
134
|
+
break
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
unless ad_hoc_profile
|
139
|
+
logger.warn "Profile for Bundle ID '#{bundle_id['id']}' is missing in App Store Connect. Skipping."
|
140
|
+
next
|
141
|
+
end
|
142
|
+
|
143
|
+
logger.info "Profile for Bundle ID '#{bundle_id['id']}' resolved to Profile '#{ad_hoc_profile['attributes']['name']}'"
|
144
|
+
|
145
|
+
uuid = ad_hoc_profile["attributes"]["uuid"]
|
146
|
+
profile_content = Base64.decode64(ad_hoc_profile["attributes"]["profileContent"])
|
147
|
+
profile_path = "#{Dir.home}/Library/MobileDevice/Provisioning Profiles/#{uuid}.mobileprovision"
|
148
|
+
|
149
|
+
FileUtils.mkdir_p(File.dirname(profile_path))
|
150
|
+
File.write(profile_path, profile_content)
|
151
|
+
|
152
|
+
logger.info "Profile '#{ad_hoc_profile['attributes']['name']}' installed to '#{profile_path}'"
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def create_keychain(keychain_path = nil, keychain_password = nil, make_default = true)
|
157
|
+
raise "Keychain path is not set" unless keychain_path
|
158
|
+
raise "Keychain password is not set" unless keychain_password
|
159
|
+
|
160
|
+
keychain_path = File.expand_path(keychain_path)
|
161
|
+
|
162
|
+
logger.info "Creating keychain at '#{keychain_path}'"
|
163
|
+
|
164
|
+
FileUtils.mkdir_p(File.dirname(keychain_path))
|
165
|
+
status = system("security create-keychain -p #{keychain_password} #{keychain_path}")
|
166
|
+
unless status
|
167
|
+
logger.error "Failed to create keychain at '#{keychain_path}'"
|
168
|
+
return
|
169
|
+
end
|
170
|
+
|
171
|
+
apple_certs = [
|
172
|
+
"AppleWWDRCAG2.cer",
|
173
|
+
"AppleWWDRCAG3.cer",
|
174
|
+
"AppleWWDRCAG4.cer",
|
175
|
+
"AppleWWDRCAG5.cer",
|
176
|
+
"AppleWWDRCAG6.cer",
|
177
|
+
"AppleWWDRCAG7.cer",
|
178
|
+
"AppleWWDRCAG8.cer",
|
179
|
+
"DeveloperIDG2CA.cer"
|
180
|
+
]
|
181
|
+
|
182
|
+
apple_certs.each do |cert|
|
183
|
+
logger.info "Downloading certificate '#{cert}'"
|
184
|
+
|
185
|
+
response = Faraday.get(
|
186
|
+
"https://www.apple.com/certificateauthority/#{cert}"
|
187
|
+
)
|
188
|
+
unless response.status == 200
|
189
|
+
logger.error "Failed to download certificate '#{cert}'"
|
190
|
+
next
|
191
|
+
end
|
192
|
+
|
193
|
+
file = Tempfile.new(cert)
|
194
|
+
file.write(response.body)
|
195
|
+
file.close
|
196
|
+
|
197
|
+
install_certificate(file.path, keychain_path)
|
198
|
+
|
199
|
+
file.unlink
|
200
|
+
end
|
201
|
+
|
202
|
+
logger.info "Downloading certificate 'AppleWWDRCA.cer'"
|
203
|
+
response = Faraday.get(
|
204
|
+
"https://developer.apple.com/certificationauthority/AppleWWDRCA.cer"
|
205
|
+
)
|
206
|
+
unless response.status == 200
|
207
|
+
logger.error "Failed to download certificate 'AppleWWDRCA.cer'"
|
208
|
+
else
|
209
|
+
file = Tempfile.new("AppleWWDRCA.cer")
|
210
|
+
file.write(response.body)
|
211
|
+
file.close
|
212
|
+
|
213
|
+
install_certificate(file.path, keychain_path)
|
214
|
+
|
215
|
+
file.unlink
|
216
|
+
end
|
217
|
+
|
218
|
+
ad_hoc_certificate = ENV["AD_HOC_CERTIFICATE"]
|
219
|
+
ad_hoc_private_key = ENV["AD_HOC_PRIVATE_KEY"]
|
220
|
+
ad_hoc_key_password = ENV["AD_HOC_KEY_PASSWORD"]
|
221
|
+
|
222
|
+
unless ad_hoc_certificate || ad_hoc_private_key || ad_hoc_key_password
|
223
|
+
logger.warn "AD_HOC_CERTIFICATE, AD_HOC_PRIVATE_KEY, or AD_HOC_KEY_PASSWORD is not set"
|
224
|
+
return
|
225
|
+
end
|
226
|
+
|
227
|
+
install_certificate(ad_hoc_private_key, keychain_path, ad_hoc_key_password, "priv")
|
228
|
+
install_certificate(ad_hoc_certificate, keychain_path, "", "cert")
|
229
|
+
|
230
|
+
if make_default
|
231
|
+
status = system("security default-keychain -d user -s #{keychain_path}")
|
232
|
+
unless status
|
233
|
+
logger.warn "Failed to set default keychain"
|
234
|
+
return
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
status = system("security set-keychain-settings #{keychain_path}")
|
239
|
+
unless status
|
240
|
+
logger.error "Failed to set keychain settings"
|
241
|
+
return
|
242
|
+
end
|
243
|
+
|
244
|
+
status = system("security set-key-partition-list -S apple-tool:,apple: -k #{keychain_password} #{keychain_path}")
|
245
|
+
unless status
|
246
|
+
logger.error "Failed to set keychain partition list"
|
247
|
+
return
|
248
|
+
end
|
249
|
+
|
250
|
+
status = system("security unlock-keychain -p #{keychain_password} #{keychain_path}")
|
251
|
+
unless status
|
252
|
+
logger.error "Failed to unlock keychain"
|
253
|
+
return
|
254
|
+
end
|
255
|
+
|
256
|
+
logger.info "Keychain created at '#{keychain_path}'"
|
257
|
+
end
|
258
|
+
|
259
|
+
def make_export_options(project_path = nil, export_path = nil, team_id = nil, options = {})
|
260
|
+
raise "Project path is not set" unless project_path
|
261
|
+
raise "Export path is not set" unless export_path
|
262
|
+
|
263
|
+
project = Xcodeproj::Project.open(project_path)
|
264
|
+
export_options = {
|
265
|
+
"method" => "ad-hoc",
|
266
|
+
"destination" => "export",
|
267
|
+
"signingStyle" => "manual",
|
268
|
+
"provisioningProfiles" => {}
|
269
|
+
}.merge(options)
|
270
|
+
|
271
|
+
project_bundles = []
|
272
|
+
|
273
|
+
project.targets.each do |target|
|
274
|
+
target.build_configurations.each do |config|
|
275
|
+
team_id ||= config.build_settings["DEVELOPMENT_TEAM"]
|
276
|
+
project_bundles << config.build_settings["PRODUCT_BUNDLE_IDENTIFIER"]
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
export_options["teamID"] = team_id
|
281
|
+
|
282
|
+
logger.info "Fetching bundles with profiles for team ID '#{team_id}'"
|
283
|
+
|
284
|
+
bundles_with_profiles = AppStoreConnect::Client.get_bundles_with_profiles(project_bundles)
|
285
|
+
bundle_by_identifier = {}
|
286
|
+
profiles_by_id = {}
|
287
|
+
|
288
|
+
bundles_with_profiles["data"].each do |bundle_id|
|
289
|
+
bundle_by_identifier[bundle_id["attributes"]["identifier"]] = bundle_id
|
290
|
+
end
|
291
|
+
|
292
|
+
bundles_with_profiles["included"].each do |profile|
|
293
|
+
profiles_by_id[profile["id"]] = profile
|
294
|
+
end
|
295
|
+
|
296
|
+
project_bundles.each do |bundle_identifier|
|
297
|
+
bundle_id = bundle_by_identifier[bundle_identifier]
|
298
|
+
unless bundle_id
|
299
|
+
logger.warn "Bundle '#{bundle_identifier}' is missing in App Store Connect. Skipping."
|
300
|
+
next
|
301
|
+
end
|
302
|
+
|
303
|
+
logger.info "Bundle '#{bundle_identifier}' resolved to Bundle ID '#{bundle_id['id']}'"
|
304
|
+
|
305
|
+
profiles = bundle_id["relationships"]["profiles"]["data"]
|
306
|
+
unless profiles
|
307
|
+
logger.warn "Profile for Bundle ID '#{bundle_id['id']}' is missing in App Store Connect. Skipping."
|
308
|
+
next
|
309
|
+
end
|
310
|
+
|
311
|
+
ad_hoc_profile = nil
|
312
|
+
profiles.each do |profile|
|
313
|
+
profile_id = profile["id"]
|
314
|
+
profile = profiles_by_id[profile_id]
|
315
|
+
|
316
|
+
if profile["attributes"]["profileType"] == "IOS_APP_ADHOC" && profile["attributes"]["profileState"] == "ACTIVE"
|
317
|
+
ad_hoc_profile = profile
|
318
|
+
break
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
unless ad_hoc_profile
|
323
|
+
logger.warn "Profile for Bundle ID '#{bundle_id['id']}' is missing in App Store Connect. Skipping."
|
324
|
+
next
|
325
|
+
end
|
326
|
+
|
327
|
+
logger.info "Profile for Bundle ID '#{bundle_id['id']}' resolved to Profile '#{ad_hoc_profile['attributes']['name']}'"
|
328
|
+
|
329
|
+
profile_name = ad_hoc_profile["attributes"]["name"]
|
330
|
+
|
331
|
+
export_options["provisioningProfiles"][bundle_identifier] = profile_name
|
332
|
+
end
|
333
|
+
|
334
|
+
options_plist = Plist::Emit.dump(export_options)
|
335
|
+
File.write(export_path, options_plist)
|
336
|
+
|
337
|
+
return export_options
|
338
|
+
end
|
339
|
+
|
340
|
+
def patch_project(project_path, team_id = nil)
|
341
|
+
raise "Project path is not set" unless project_path
|
342
|
+
|
343
|
+
project = Xcodeproj::Project.open(project_path)
|
344
|
+
|
345
|
+
project_bundles = project.targets.map do |target|
|
346
|
+
target.build_configurations.map do |config|
|
347
|
+
config.build_settings["PRODUCT_BUNDLE_IDENTIFIER"]
|
348
|
+
end
|
349
|
+
end.flatten.uniq
|
350
|
+
|
351
|
+
bundles_with_profiles = AppStoreConnect::Client.get_bundles_with_profiles(project_bundles)
|
352
|
+
bundle_by_identifier = {}
|
353
|
+
profiles_by_id = {}
|
354
|
+
|
355
|
+
bundles_with_profiles["data"].each do |bundle_id|
|
356
|
+
bundle_by_identifier[bundle_id["attributes"]["identifier"]] = bundle_id
|
357
|
+
end
|
358
|
+
|
359
|
+
bundles_with_profiles["included"].each do |profile|
|
360
|
+
profiles_by_id[profile["id"]] = profile
|
361
|
+
end
|
362
|
+
|
363
|
+
project.targets.each do |target|
|
364
|
+
target.build_configurations.each do |config|
|
365
|
+
bundle_identifier = config.build_settings["PRODUCT_BUNDLE_IDENTIFIER"]
|
366
|
+
bundle_id = bundle_by_identifier[bundle_identifier]
|
367
|
+
unless bundle_id
|
368
|
+
logger.warn "Bundle '#{bundle_identifier}' is missing in App Store Connect. Skipping."
|
369
|
+
next
|
370
|
+
end
|
371
|
+
|
372
|
+
logger.info "Bundle '#{bundle_identifier}' resolved to Bundle ID '#{bundle_id['id']}'"
|
373
|
+
|
374
|
+
profiles = bundle_id["relationships"]["profiles"]["data"]
|
375
|
+
unless profiles
|
376
|
+
logger.warn "Profile for Bundle ID '#{bundle_id['id']}' is missing in App Store Connect. Skipping."
|
377
|
+
next
|
378
|
+
end
|
379
|
+
|
380
|
+
ad_hoc_profile = nil
|
381
|
+
profiles.each do |profile|
|
382
|
+
profile_id = profile["id"]
|
383
|
+
profile = profiles_by_id[profile_id]
|
384
|
+
|
385
|
+
if profile["attributes"]["profileType"] == "IOS_APP_ADHOC" && profile["attributes"]["profileState"] == "ACTIVE"
|
386
|
+
ad_hoc_profile = profile
|
387
|
+
break
|
388
|
+
end
|
389
|
+
end
|
390
|
+
|
391
|
+
unless ad_hoc_profile
|
392
|
+
logger.warn "Profile for Bundle ID '#{bundle_id['id']}' is missing in App Store Connect. Skipping."
|
393
|
+
next
|
394
|
+
end
|
395
|
+
|
396
|
+
config.build_settings["CODE_SIGN_IDENTITY"] = "iPhone Distribution"
|
397
|
+
config.build_settings["CODE_SIGN_STYLE"] = "Manual"
|
398
|
+
if team_id
|
399
|
+
config.build_settings["DEVELOPMENT_TEAM"] = team_id
|
400
|
+
end
|
401
|
+
config.build_settings["PROVISIONING_PROFILE_SPECIFIER"] = ad_hoc_profile["attributes"]["name"]
|
402
|
+
end
|
403
|
+
end
|
404
|
+
|
405
|
+
project.save
|
406
|
+
end
|
407
|
+
|
408
|
+
private
|
409
|
+
|
410
|
+
def install_certificate(certificate_path, keychain_path, certificate_password = "", certificate_type = "cert")
|
411
|
+
certificate_name = File.basename(certificate_path)
|
412
|
+
logger.info "Installing certificate '#{certificate_name}' to keychain '#{keychain_path}'"
|
413
|
+
|
414
|
+
status = system("security import #{certificate_path} -k #{keychain_path} -t #{certificate_type} -A -P #{certificate_password} -T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild")
|
415
|
+
unless status
|
416
|
+
logger.error "Failed to install certificate '#{certificate_name}' to keychain '#{keychain_path}'"
|
417
|
+
return
|
418
|
+
end
|
419
|
+
|
420
|
+
logger.info "Certificate '#{certificate_name}' installed to keychain '#{keychain_path}'"
|
421
|
+
end
|
422
|
+
|
423
|
+
def get_devices(team_id)
|
424
|
+
Starship::Client.get_devices(team_id)
|
425
|
+
end
|
426
|
+
|
427
|
+
def get_profiles_map(team_id)
|
428
|
+
logger.info "Fetching profiles for team ID '#{team_id}'"
|
429
|
+
|
430
|
+
registered_profiles = Starship::Client.get_profiles(team_id)
|
431
|
+
profiles = {}
|
432
|
+
|
433
|
+
registered_profiles.each do |profile|
|
434
|
+
provisioning_profile = Starship::Client::get_provisioning_profile(profile["id"], team_id)
|
435
|
+
app_id = provisioning_profile["provisioningProfile"]["appIdId"]
|
436
|
+
|
437
|
+
profiles[app_id] = provisioning_profile
|
438
|
+
end
|
439
|
+
|
440
|
+
return profiles
|
441
|
+
end
|
442
|
+
|
443
|
+
def get_bundle_map(team_id)
|
444
|
+
logger.info "Fetching bundle IDs for team ID '#{team_id}'"
|
445
|
+
|
446
|
+
registered_bundles = Starship::Client.get_bundle_ids(team_id)
|
447
|
+
bundle_ids = {}
|
448
|
+
|
449
|
+
registered_bundles.each do |bundle|
|
450
|
+
bundle_ids[bundle["attributes"]["identifier"]] = bundle["id"]
|
451
|
+
end
|
452
|
+
|
453
|
+
return bundle_ids
|
454
|
+
end
|
455
|
+
|
456
|
+
def renew_provisioning_profile(profile, team_id)
|
457
|
+
devices = get_devices(team_id)
|
458
|
+
deviceIds = devices.map { |device| device["id"] }
|
459
|
+
|
460
|
+
need_update = false
|
461
|
+
|
462
|
+
profile["provisioningProfile"]["devices"].each do |device|
|
463
|
+
if !deviceIds.include?(device["deviceId"])
|
464
|
+
need_update = true
|
465
|
+
break
|
466
|
+
end
|
467
|
+
end
|
468
|
+
|
469
|
+
logger.info "Profile '#{profile["provisioningProfile"]["name"]}' status: '#{profile["provisioningProfile"]["status"]}'"
|
470
|
+
|
471
|
+
if profile["provisioningProfile"]["status"] != "Active"
|
472
|
+
need_update = true
|
473
|
+
end
|
474
|
+
|
475
|
+
if need_update
|
476
|
+
logger.warn "Profile '#{profile["provisioningProfile"]["name"]}' is missing one or more devices."
|
477
|
+
|
478
|
+
Starship::Client.regen_provisioning_profile(profile, team_id, deviceIds)
|
479
|
+
|
480
|
+
logger.info "Profile '#{profile["provisioningProfile"]["name"]}' updated."
|
481
|
+
else
|
482
|
+
logger.info "Profile '#{profile["provisioningProfile"]["name"]}' is up to date."
|
483
|
+
end
|
484
|
+
end
|
485
|
+
|
486
|
+
def renew_bundle_id(bundle_id, team_id, entitlements_path)
|
487
|
+
bundle_info = Starship::Client.get_bundle_info(bundle_id, team_id)
|
488
|
+
bundle_identifier = bundle_info["data"]["attributes"]["identifier"]
|
489
|
+
|
490
|
+
logger.info "Checking capabilities for bundle '#{bundle_identifier}'"
|
491
|
+
|
492
|
+
capabilities = parse_entitlements(entitlements_path)
|
493
|
+
|
494
|
+
need_update = false
|
495
|
+
|
496
|
+
capabilities.each do |capability|
|
497
|
+
if capability.check?(bundle_info)
|
498
|
+
else
|
499
|
+
need_update = true
|
500
|
+
end
|
501
|
+
end
|
502
|
+
|
503
|
+
if need_update
|
504
|
+
logger.warn "Bundle '#{bundle_identifier}' is missing one or more capabilities."
|
505
|
+
new_capabilities = (
|
506
|
+
# You can't remove IN_APP_PURCHASE capability for some reason
|
507
|
+
capabilities + [ SimpleCapability.new("IN_APP_PURCHASE") ]
|
508
|
+
).map { |capability| capability.to_bundle_capability(bundle_info, team_id) }
|
509
|
+
|
510
|
+
Starship::Client.patch_bundle(bundle_info, team_id, new_capabilities)
|
511
|
+
|
512
|
+
logger.info "Bundle '#{bundle_identifier}' capabilities updated."
|
513
|
+
else
|
514
|
+
logger.info "Bundle '#{bundle_identifier}' capabilities are up to date."
|
515
|
+
end
|
516
|
+
end
|
517
|
+
end
|
518
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "faraday"
|
4
|
+
require "json"
|
5
|
+
require "jwt"
|
6
|
+
|
7
|
+
module AppStoreConnect
|
8
|
+
class Client
|
9
|
+
BASE_URL = "https://api.appstoreconnect.apple.com/v1"
|
10
|
+
|
11
|
+
class << self
|
12
|
+
def get_bundles_with_profiles(bundle_identifiers)
|
13
|
+
response = request(
|
14
|
+
BASE_URL + "/bundleIds",
|
15
|
+
method: :get,
|
16
|
+
params: {
|
17
|
+
"fields[bundleIds]" => "name,platform,identifier,profiles",
|
18
|
+
"filter[identifier]" => bundle_identifiers.join(","),
|
19
|
+
"fields[profiles]" => "name,profileType,profileState,profileContent,uuid",
|
20
|
+
"include" => "profiles",
|
21
|
+
}
|
22
|
+
)
|
23
|
+
|
24
|
+
if response.status == 200
|
25
|
+
JSON.parse(response.body)
|
26
|
+
else
|
27
|
+
puts response.body
|
28
|
+
raise "Failed to get bundle IDs: #{response.status}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def request(endpoint, method: :get, params: nil, body: nil, headers: nil)
|
35
|
+
default_headers = {
|
36
|
+
"Authorization" => "Bearer #{jwt_token}",
|
37
|
+
"Content-Type" => "application/json",
|
38
|
+
"Accept" => "application/json",
|
39
|
+
}
|
40
|
+
|
41
|
+
if headers
|
42
|
+
default_headers = default_headers.merge(headers)
|
43
|
+
end
|
44
|
+
|
45
|
+
response = case method
|
46
|
+
when :get
|
47
|
+
Faraday.get(endpoint, params, default_headers)
|
48
|
+
when :post
|
49
|
+
Faraday.post(endpoint, body, default_headers)
|
50
|
+
when :put
|
51
|
+
Faraday.put(endpoint, body, default_headers)
|
52
|
+
when :delete
|
53
|
+
Faraday.delete(endpoint, default_headers)
|
54
|
+
when :patch
|
55
|
+
Faraday.patch(endpoint, body, default_headers)
|
56
|
+
end
|
57
|
+
|
58
|
+
return response
|
59
|
+
end
|
60
|
+
|
61
|
+
def jwt_token
|
62
|
+
key_id = ENV["APPSTORE_CONNECT_KEY_ID"]
|
63
|
+
key = ENV["APPSTORE_CONNECT_KEY"]
|
64
|
+
issuer_id = ENV["APPSTORE_CONNECT_ISSUER_ID"]
|
65
|
+
|
66
|
+
private_key = OpenSSL::PKey.read(key)
|
67
|
+
token = JWT.encode(
|
68
|
+
{
|
69
|
+
iss: issuer_id,
|
70
|
+
exp: Time.now.to_i + 20 * 60,
|
71
|
+
aud: "appstoreconnect-v1",
|
72
|
+
},
|
73
|
+
private_key,
|
74
|
+
"ES256",
|
75
|
+
header_fields = {
|
76
|
+
alg: "ES256",
|
77
|
+
kid: key_id,
|
78
|
+
typ: "JWT"
|
79
|
+
}
|
80
|
+
)
|
81
|
+
|
82
|
+
return token
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
data/lib/logging.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "logger"
|
4
|
+
|
5
|
+
module Logging
|
6
|
+
def logger
|
7
|
+
@logger ||= Logging.logger_for(self.class.name)
|
8
|
+
end
|
9
|
+
|
10
|
+
@loggers = {}
|
11
|
+
|
12
|
+
class << self
|
13
|
+
def logger_for(classname)
|
14
|
+
@loggers[classname] ||= configure_logger_for(classname)
|
15
|
+
end
|
16
|
+
|
17
|
+
def configure_logger_for(classname)
|
18
|
+
logger = ::Logger.new(STDOUT)
|
19
|
+
logger.formatter = proc do |severity, datetime, progname, msg|
|
20
|
+
"[AdSedare.#{progname}] [#{severity.capitalize}]: #{msg}\n"
|
21
|
+
end
|
22
|
+
logger.progname = classname
|
23
|
+
logger
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../logging"
|
4
|
+
|
5
|
+
module Starship
|
6
|
+
class TwoFactorProvider
|
7
|
+
def initialize
|
8
|
+
end
|
9
|
+
|
10
|
+
# Get the 2FA code
|
11
|
+
# @param session_id [String] The session ID from Apple
|
12
|
+
# @param scnt [String] The scnt value from Apple
|
13
|
+
# @return [String] The 2FA code
|
14
|
+
def get_code(session_id, scnt)
|
15
|
+
raise NotImplementedError, "Subclasses must implement get_code"
|
16
|
+
end
|
17
|
+
|
18
|
+
# Verify if this provider can handle the given 2FA type
|
19
|
+
# @param type [String] The 2FA type (sms, voice, etc.)
|
20
|
+
# @return [Boolean] Whether this provider can handle the given type
|
21
|
+
def can_handle?(type)
|
22
|
+
raise NotImplementedError, "Subclasses must implement can_handle?"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Manual 2FA provider that prompts the user for a code
|
27
|
+
class ManualTwoFactorProvider < TwoFactorProvider
|
28
|
+
include Logging
|
29
|
+
|
30
|
+
def get_code(session_id, scnt)
|
31
|
+
logger.info "Please enter the code you received: "
|
32
|
+
code = gets.chomp.strip
|
33
|
+
code
|
34
|
+
end
|
35
|
+
|
36
|
+
def can_handle?(type)
|
37
|
+
true
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|