adsedare 0.0.1 → 0.0.2

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,11 @@ 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"
12
14
 
15
+ require_relative "logging"
13
16
  require_relative "starship"
14
17
  require_relative "appstoreconnect"
15
18
 
@@ -22,20 +25,20 @@ module Adsedare
22
25
  def renew_profiles(project_path = nil, certificate_id = nil, team_id = nil)
23
26
  raise "Project path is not set" unless project_path
24
27
  raise "Certificate ID is not set" unless certificate_id
25
-
28
+
26
29
  project = Xcodeproj::Project.open(project_path)
27
30
  project_dir = File.dirname(project_path)
28
-
31
+
29
32
  bundle_entitlements = {}
30
33
 
31
34
  project.targets.each do |target|
32
35
  target.build_configurations.each do |config|
33
36
  bundle_identifier = config.build_settings["PRODUCT_BUNDLE_IDENTIFIER"]
34
37
  entitlements_path = config.build_settings["CODE_SIGN_ENTITLEMENTS"]
35
-
38
+
36
39
  # If team_id is not set, use the first one from the project
37
40
  team_id ||= config.build_settings["DEVELOPMENT_TEAM"]
38
-
41
+
39
42
  if entitlements_path
40
43
  full_entitlements_path = File.join(project_dir, entitlements_path)
41
44
  bundle_entitlements[bundle_identifier] = full_entitlements_path
@@ -51,10 +54,10 @@ module Adsedare
51
54
  unless bundle_id
52
55
  logger.warn "Bundle '#{bundle_identifier}' is missing in Apple Developer portal. Will create."
53
56
  bundle_id = Starship::Client.create_bundle(
54
- bundle_identifier,
57
+ bundle_identifier,
55
58
  team_id,
56
59
  # You cannot create bundle without this capability
57
- [ SimpleCapability.new("IN_APP_PURCHASE").to_bundle_capability(nil, nil) ]
60
+ [SimpleCapability.new("IN_APP_PURCHASE").to_bundle_capability(nil, nil)]
58
61
  )["data"]["id"]
59
62
  bundle_by_identifier[bundle_identifier] = bundle_id
60
63
  logger.info "Bundle '#{bundle_identifier}' created with ID '#{bundle_id}'"
@@ -88,7 +91,7 @@ module Adsedare
88
91
 
89
92
  def install_profiles(project_path = nil)
90
93
  raise "Project path is not set" unless project_path
91
-
94
+
92
95
  project = Xcodeproj::Project.open(project_path)
93
96
 
94
97
  project_bundles = project.targets.map do |target|
@@ -116,11 +119,11 @@ module Adsedare
116
119
  next
117
120
  end
118
121
 
119
- logger.info "Bundle '#{bundle_identifier}' resolved to Bundle ID '#{bundle_id['id']}'"
122
+ logger.info "Bundle '#{bundle_identifier}' resolved to Bundle ID '#{bundle_id["id"]}'"
120
123
 
121
124
  profiles = bundle_id["relationships"]["profiles"]["data"]
122
125
  unless profiles
123
- logger.warn "Profile for Bundle ID '#{bundle_id['id']}' is missing in App Store Connect. Skipping."
126
+ logger.warn "Profile for Bundle ID '#{bundle_id["id"]}' is missing in App Store Connect. Skipping."
124
127
  next
125
128
  end
126
129
 
@@ -136,11 +139,11 @@ module Adsedare
136
139
  end
137
140
 
138
141
  unless ad_hoc_profile
139
- logger.warn "Profile for Bundle ID '#{bundle_id['id']}' is missing in App Store Connect. Skipping."
142
+ logger.warn "Profile for Bundle ID '#{bundle_id["id"]}' is missing in App Store Connect. Skipping."
140
143
  next
141
144
  end
142
145
 
143
- logger.info "Profile for Bundle ID '#{bundle_id['id']}' resolved to Profile '#{ad_hoc_profile['attributes']['name']}'"
146
+ logger.info "Profile for Bundle ID '#{bundle_id["id"]}' resolved to Profile '#{ad_hoc_profile["attributes"]["name"]}'"
144
147
 
145
148
  uuid = ad_hoc_profile["attributes"]["uuid"]
146
149
  profile_content = Base64.decode64(ad_hoc_profile["attributes"]["profileContent"])
@@ -149,275 +152,8 @@ module Adsedare
149
152
  FileUtils.mkdir_p(File.dirname(profile_path))
150
153
  File.write(profile_path, profile_content)
151
154
 
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
155
+ logger.info "Profile '#{ad_hoc_profile["attributes"]["name"]}' installed to '#{profile_path}'"
403
156
  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
157
  end
422
158
 
423
159
  def get_devices(team_id)
@@ -474,7 +210,7 @@ module Adsedare
474
210
 
475
211
  if need_update
476
212
  logger.warn "Profile '#{profile["provisioningProfile"]["name"]}' is missing one or more devices."
477
-
213
+
478
214
  Starship::Client.regen_provisioning_profile(profile, team_id, deviceIds)
479
215
 
480
216
  logger.info "Profile '#{profile["provisioningProfile"]["name"]}' updated."
@@ -490,7 +226,7 @@ module Adsedare
490
226
  logger.info "Checking capabilities for bundle '#{bundle_identifier}'"
491
227
 
492
228
  capabilities = parse_entitlements(entitlements_path)
493
-
229
+
494
230
  need_update = false
495
231
 
496
232
  capabilities.each do |capability|
@@ -502,11 +238,10 @@ module Adsedare
502
238
 
503
239
  if need_update
504
240
  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
-
241
+ # You can't remove IN_APP_PURCHASE capability for some reason
242
+ new_capabilities = capabilities + [SimpleCapability.new("IN_APP_PURCHASE")]
243
+ new_capabilities = new_capabilities.map { |capability| capability.to_bundle_capability(bundle_info, team_id) }
244
+
510
245
  Starship::Client.patch_bundle(bundle_info, team_id, new_capabilities)
511
246
 
512
247
  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