adsedare 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/adsedare.rb CHANGED
@@ -8,8 +8,12 @@ require "plist"
8
8
 
9
9
  require_relative "adsedare/version"
10
10
  require_relative "adsedare/capabilities"
11
- require_relative "logging"
11
+ require_relative "adsedare/keychain"
12
+ require_relative "adsedare/xcodeproj"
13
+ require_relative "adsedare/export_options"
14
+ require_relative "adsedare/install_profiles"
12
15
 
16
+ require_relative "logging"
13
17
  require_relative "starship"
14
18
  require_relative "appstoreconnect"
15
19
 
@@ -22,20 +26,20 @@ module Adsedare
22
26
  def renew_profiles(project_path = nil, certificate_id = nil, team_id = nil)
23
27
  raise "Project path is not set" unless project_path
24
28
  raise "Certificate ID is not set" unless certificate_id
25
-
29
+
26
30
  project = Xcodeproj::Project.open(project_path)
27
31
  project_dir = File.dirname(project_path)
28
-
32
+
29
33
  bundle_entitlements = {}
30
34
 
31
35
  project.targets.each do |target|
32
36
  target.build_configurations.each do |config|
33
37
  bundle_identifier = config.build_settings["PRODUCT_BUNDLE_IDENTIFIER"]
34
38
  entitlements_path = config.build_settings["CODE_SIGN_ENTITLEMENTS"]
35
-
39
+
36
40
  # If team_id is not set, use the first one from the project
37
41
  team_id ||= config.build_settings["DEVELOPMENT_TEAM"]
38
-
42
+
39
43
  if entitlements_path
40
44
  full_entitlements_path = File.join(project_dir, entitlements_path)
41
45
  bundle_entitlements[bundle_identifier] = full_entitlements_path
@@ -51,10 +55,10 @@ module Adsedare
51
55
  unless bundle_id
52
56
  logger.warn "Bundle '#{bundle_identifier}' is missing in Apple Developer portal. Will create."
53
57
  bundle_id = Starship::Client.create_bundle(
54
- bundle_identifier,
58
+ bundle_identifier,
55
59
  team_id,
56
60
  # You cannot create bundle without this capability
57
- [ SimpleCapability.new("IN_APP_PURCHASE").to_bundle_capability(nil, nil) ]
61
+ [SimpleCapability.new("IN_APP_PURCHASE").to_bundle_capability(nil, nil)]
58
62
  )["data"]["id"]
59
63
  bundle_by_identifier[bundle_identifier] = bundle_id
60
64
  logger.info "Bundle '#{bundle_identifier}' created with ID '#{bundle_id}'"
@@ -86,340 +90,6 @@ module Adsedare
86
90
  end
87
91
  end
88
92
 
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
93
  def get_devices(team_id)
424
94
  Starship::Client.get_devices(team_id)
425
95
  end
@@ -474,7 +144,7 @@ module Adsedare
474
144
 
475
145
  if need_update
476
146
  logger.warn "Profile '#{profile["provisioningProfile"]["name"]}' is missing one or more devices."
477
-
147
+
478
148
  Starship::Client.regen_provisioning_profile(profile, team_id, deviceIds)
479
149
 
480
150
  logger.info "Profile '#{profile["provisioningProfile"]["name"]}' updated."
@@ -490,7 +160,7 @@ module Adsedare
490
160
  logger.info "Checking capabilities for bundle '#{bundle_identifier}'"
491
161
 
492
162
  capabilities = parse_entitlements(entitlements_path)
493
-
163
+
494
164
  need_update = false
495
165
 
496
166
  capabilities.each do |capability|
@@ -502,11 +172,10 @@ module Adsedare
502
172
 
503
173
  if need_update
504
174
  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
-
175
+ # You can't remove IN_APP_PURCHASE capability for some reason
176
+ new_capabilities = capabilities + [SimpleCapability.new("IN_APP_PURCHASE")]
177
+ new_capabilities = new_capabilities.map { |capability| capability.to_bundle_capability(bundle_info, team_id) }
178
+
510
179
  Starship::Client.patch_bundle(bundle_info, team_id, new_capabilities)
511
180
 
512
181
  logger.info "Bundle '#{bundle_identifier}' capabilities updated."
@@ -7,80 +7,80 @@ require "jwt"
7
7
  module AppStoreConnect
8
8
  class Client
9
9
  BASE_URL = "https://api.appstoreconnect.apple.com/v1"
10
-
10
+
11
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
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
+ )
33
23
 
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
- }
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
40
31
 
41
- if headers
42
- default_headers = default_headers.merge(headers)
43
- end
32
+ private
44
33
 
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
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
+ }
57
40
 
58
- return response
41
+ if headers
42
+ default_headers = default_headers.merge(headers)
59
43
  end
60
44
 
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"]
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
65
57
 
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
- )
58
+ return response
59
+ end
81
60
 
82
- return token
83
- end
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
84
  end
85
85
  end
86
86
  end
@@ -6,7 +6,7 @@ module Starship
6
6
  class TwoFactorProvider
7
7
  def initialize
8
8
  end
9
-
9
+
10
10
  # Get the 2FA code
11
11
  # @param session_id [String] The session ID from Apple
12
12
  # @param scnt [String] The scnt value from Apple
@@ -14,7 +14,7 @@ module Starship
14
14
  def get_code(session_id, scnt)
15
15
  raise NotImplementedError, "Subclasses must implement get_code"
16
16
  end
17
-
17
+
18
18
  # Verify if this provider can handle the given 2FA type
19
19
  # @param type [String] The 2FA type (sms, voice, etc.)
20
20
  # @return [Boolean] Whether this provider can handle the given type
@@ -22,7 +22,7 @@ module Starship
22
22
  raise NotImplementedError, "Subclasses must implement can_handle?"
23
23
  end
24
24
  end
25
-
25
+
26
26
  # Manual 2FA provider that prompts the user for a code
27
27
  class ManualTwoFactorProvider < TwoFactorProvider
28
28
  include Logging
@@ -32,7 +32,7 @@ module Starship
32
32
  code = gets.chomp.strip
33
33
  code
34
34
  end
35
-
35
+
36
36
  def can_handle?(type)
37
37
  true
38
38
  end