spaceship 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,12 @@
1
+ require 'net/http'
2
+
3
+ ## monkey-patch Net::HTTP
4
+ #
5
+ # Certain apple endpoints return 415 responses if a Content-Type is supplied.
6
+ # Net::HTTP will default a content-type if none is provided by faraday
7
+ # This monkey-patch allows us to leave out the content-type if we do not specify one.
8
+ class Net::HTTPGenericRequest
9
+ def supply_default_content_type
10
+ return if content_type()
11
+ end
12
+ end
@@ -0,0 +1,15 @@
1
+ require 'faraday_middleware/response_middleware'
2
+
3
+ module FaradayMiddleware
4
+ class PlistMiddleware < ResponseMiddleware
5
+ dependency do
6
+ require 'plist' unless defined?(::Plist)
7
+ end
8
+
9
+ define_parser do |body|
10
+ Plist::parse_xml(body)
11
+ end
12
+ end
13
+ end
14
+
15
+ Faraday::Response.register_middleware(:plist => FaradayMiddleware::PlistMiddleware)
@@ -0,0 +1,89 @@
1
+ module Spaceship
2
+ class Launcher
3
+ attr_accessor :client
4
+
5
+ # Launch a new spaceship, which can be used to maintain multiple instances of
6
+ # spaceship. You can call `.new` without any parameters, but you'll have to call
7
+ # `.login` at a later point. If you prefer, you can pass the login credentials
8
+ # here already.
9
+ #
10
+ # Authenticates with Apple's web services. This method has to be called once
11
+ # to generate a valid session. The session will automatically be used from then
12
+ # on.
13
+ #
14
+ # This method will automatically use the username from the Appfile (if available)
15
+ # and fetch the password from the Keychain (if available)
16
+ #
17
+ # @param user (String) (optional): The username (usually the email address)
18
+ # @param password (String) (optional): The password
19
+ #
20
+ # @raise InvalidUserCredentialsError: raised if authentication failed
21
+ def initialize(user = nil, password = nil)
22
+ @client = Client.new
23
+
24
+ if user or password
25
+ @client.login(user, password)
26
+ end
27
+ end
28
+
29
+ #####################################################
30
+ # @!group Login Helper
31
+ #####################################################
32
+
33
+ # Authenticates with Apple's web services. This method has to be called once
34
+ # to generate a valid session. The session will automatically be used from then
35
+ # on.
36
+ #
37
+ # This method will automatically use the username from the Appfile (if available)
38
+ # and fetch the password from the Keychain (if available)
39
+ #
40
+ # @param user (String) (optional): The username (usually the email address)
41
+ # @param password (String) (optional): The password
42
+ #
43
+ # @raise InvalidUserCredentialsError: raised if authentication failed
44
+ #
45
+ # @return (Spaceship::Client) The client the login method was called for
46
+ def login(user, password)
47
+ @client.login(user, password)
48
+ end
49
+
50
+ # Open up the team selection for the user (if necessary).
51
+ #
52
+ # If the user is in multiple teams, a team selection is shown.
53
+ # The user can then select a team by entering the number
54
+ #
55
+ # Additionally, the team ID is shown next to each team name
56
+ # so that the user can use the environment variable `FASTLANE_TEAM_ID`
57
+ # for future user.
58
+ #
59
+ # @return (String) The ID of the select team. You also get the value if
60
+ # the user is only in one team.
61
+ def select_team
62
+ @client.select_team
63
+ end
64
+
65
+ #####################################################
66
+ # @!group Helper methods for managing multiple instances of spaceship
67
+ #####################################################
68
+
69
+ # @return (Class) Access the apps for this spaceship
70
+ def app
71
+ Spaceship::App.set_client(@client)
72
+ end
73
+
74
+ # @return (Class) Access the devices for this spaceship
75
+ def device
76
+ Spaceship::Device.set_client(@client)
77
+ end
78
+
79
+ # @return (Class) Access the certificates for this spaceship
80
+ def certificate
81
+ Spaceship::Certificate.set_client(@client)
82
+ end
83
+
84
+ # @return (Class) Access the provisioning profiles for this spaceship
85
+ def provisioning_profile
86
+ Spaceship::ProvisioningProfile.set_client(@client)
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,39 @@
1
+ module Spaceship
2
+ # This class contains the codes used for the different types of profiles
3
+ class Client
4
+ class ProfileTypes
5
+ class SigningCertificate
6
+ def self.development
7
+ "5QPB9NHCEI"
8
+ end
9
+ def self.distribution
10
+ "R58UK2EWSO"
11
+ end
12
+ end
13
+
14
+ class Push
15
+ def self.development
16
+ "BKLRAVXMGM"
17
+ end
18
+ def self.production
19
+ "3BQKVH9I2X"
20
+ end
21
+ end
22
+
23
+ def self.all_profile_types
24
+ [
25
+ "5QPB9NHCEI", # Development Code Signing Identity
26
+ "R58UK2EWSO", # Distribution Code Signing Identity
27
+ "9RQEK7MSXA", # iOS Distribution certificate signing request
28
+ "LA30L5BJEU", # MDM CSR certificate signing request
29
+ "BKLRAVXMGM", # Development Push Certificates
30
+ "3BQKVH9I2X", # Production Push Certificates
31
+ "Y3B2F3TYSI", # Pass Type ID pass certificate request
32
+ "3T2ZP62QW8", # Website Push Id
33
+ "E5D663CMZW", # Website Push Id
34
+ "4APLUP237T" # Apple Pay
35
+ ]
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,352 @@
1
+ module Spaceship
2
+ # Represents a provisioning profile of the Apple Dev Portal
3
+ class ProvisioningProfile < Base
4
+ # @return (String) The ID generated by the Dev Portal
5
+ # You'll probably not really need this value
6
+ # @example
7
+ # "2MAY7NPHAA"
8
+ attr_accessor :id
9
+
10
+ # @return (String) The UDID of this provisioning profile
11
+ # This value is used for example for code signing
12
+ # It is also contained in the actual profile
13
+ # @example
14
+ # "23d7df3b-9767-4e85-a1ea-1df4d8f32fec"
15
+ attr_accessor :uuid
16
+
17
+ # @return (DateTime) The date and time of when the profile
18
+ # expires.
19
+ # @example
20
+ # #<DateTime: 2015-11-25T22:45:50+00:00 ((2457352j,81950s,0n),+0s,2299161j)>
21
+ attr_accessor :expires
22
+
23
+ # @return (String) The profile distribution type. You probably want to
24
+ # use the class type to detect the profile type instead of this string.
25
+ # @example AppStore Profile
26
+ # "store"
27
+ # @example AdHoc Profile
28
+ # "adhoc"
29
+ # @example Development Profile
30
+ # "limited"
31
+ attr_accessor :distribution_method
32
+
33
+ # @return (String) The name of this profile
34
+ # @example
35
+ # "com.krausefx.app AppStore"
36
+ attr_accessor :name
37
+
38
+ # @return (String) The status of this profile
39
+ # @example Active (profile is fine)
40
+ # "Active"
41
+ # @example Expired (time ran out)
42
+ # "Expired"
43
+ # @example Invalid (e.g. code signing identity not available any more)
44
+ # "Invalid"
45
+ attr_accessor :status
46
+
47
+ # @return (String) The type of the profile (development or distribution).
48
+ # You'll probably not need this value
49
+ # @example Distribution
50
+ # "iOS Distribution"
51
+ # @example Development
52
+ # "iOS Development"
53
+ attr_accessor :type
54
+
55
+ # @return (String) This will always be "2"
56
+ # @example
57
+ # "2"
58
+ attr_accessor :version
59
+
60
+ # @return (String) The supported platform for this profile
61
+ # @example
62
+ # "ios"
63
+ attr_accessor :platform
64
+
65
+ # No information about this attribute
66
+ attr_accessor :managing_app
67
+
68
+ # A reference to the app this profile is for.
69
+ # You can then easily access the value directly
70
+ # @return (App) The app this profile is for
71
+ #
72
+ # @example Example Value
73
+ # <Spaceship::App
74
+ # @app_id="2UMR2S6PAA"
75
+ # @name="App Name"
76
+ # @platform="ios"
77
+ # @prefix="5A997XSAAA"
78
+ # @bundle_id="com.krausefx.app"
79
+ # @is_wildcard=false
80
+ # @dev_push_enabled=false
81
+ # @prod_push_enabled=false>
82
+ #
83
+ # @example Usage
84
+ # profile.app.name
85
+ attr_accessor :app
86
+
87
+ # @return (Array) A list of certificates used for this profile
88
+ # @example Example Value
89
+ # [
90
+ # <Spaceship::Certificate::Production
91
+ # @status=nil
92
+ # @id="XC5PH8D4AA"
93
+ # @name="iOS Distribution"
94
+ # @created=nil
95
+ # @expires=#<DateTime: 2015-11-25T22:45:50+00:00 ((2457352j,81950s,0n),+0s,2299161j)>
96
+ # @owner_type="team"
97
+ # @owner_name=nil
98
+ # @owner_id=nil
99
+ # @type_display_id="R58UK2EWAA">]
100
+ # ]
101
+ #
102
+ # @example Usage
103
+ # profile.certificates.first.id
104
+ attr_accessor :certificates
105
+
106
+ # @return (Array) A list of devices this profile is enabled for.
107
+ # This will always be [] for AppStore profiles
108
+ #
109
+ # @example Example Value
110
+ # <Spaceship::Device
111
+ # @id="WXQ7V239BE"
112
+ # @name="Grahams iPhone 4s"
113
+ # @udid="ba0ac7d70f7a14c6fa02ef0e02f4fe9c5178e2f7"
114
+ # @platform="ios"
115
+ # @status="c">]
116
+ #
117
+ # @example Usage
118
+ # profile.devices.first.name
119
+ attr_accessor :devices
120
+
121
+ attr_mapping({
122
+ 'provisioningProfileId' => :id,
123
+ 'UUID' => :uuid,
124
+ 'dateExpire' => :expires,
125
+ 'distributionMethod' => :distribution_method,
126
+ 'name' => :name,
127
+ 'status' => :status,
128
+ 'type' => :type,
129
+ 'version' => :version,
130
+ 'proProPlatform' => :platform,
131
+ 'managingApp' => :managing_app,
132
+ 'appId' => :app
133
+ })
134
+
135
+ class << self
136
+ # @return (String) The profile type used for web requests to the Dev Portal
137
+ # @example
138
+ # "limited"
139
+ # "store"
140
+ # "adhoc"
141
+ # "inhouse"
142
+ def type
143
+ raise "You cannot create a ProvisioningProfile without a type. Use a subclass."
144
+ end
145
+
146
+ # Create a new object based on a hash.
147
+ # This is used to create a new object based on the server response.
148
+ def factory(attrs)
149
+ # Ad Hoc Profiles look exactly like App Store profiles, but usually include devices
150
+ attrs['distributionMethod'] = 'adhoc' if attrs['distributionMethod'] == 'store' && attrs['devices'].size > 0
151
+ # available values of `distributionMethod` at this point: ['adhoc', 'store', 'limited']
152
+
153
+ klass = case attrs['distributionMethod']
154
+ when 'limited'
155
+ Development
156
+ when 'store'
157
+ AppStore
158
+ when 'adhoc'
159
+ AdHoc
160
+ when 'inhouse'
161
+ InHouse
162
+ else
163
+ raise "Can't find class '#{attrs['distributionMethod']}'"
164
+ end
165
+
166
+ attrs['appId'] = App.factory(attrs['appId'])
167
+ attrs['devices'].map! { |device| Device.factory(device) }
168
+ attrs['certificates'].map! { |cert| Certificate.factory(cert) }
169
+
170
+ klass.client = @client
171
+ klass.new(attrs)
172
+ end
173
+
174
+ # @return (String) The human readable name of this profile type.
175
+ # @example
176
+ # "AppStore"
177
+ # "AdHoc"
178
+ # "Development"
179
+ # "InHouse"
180
+ def pretty_type
181
+ name.split('::').last
182
+ end
183
+
184
+ # Create a new provisioning profile
185
+ # @param name (String): The name of the provisioning profile on the Dev Portal
186
+ # @param bundle_id (String): The app identifier, this paramter is required
187
+ # @param certificate (Certificate): The certificate that should be used with this
188
+ # provisioning profile
189
+ # @param devices (Array) (optional): An array of Device objects that should be used in this profile.
190
+ # It is recommend to not pass devices as spaceship will automatically add all devices for AdHoc
191
+ # and Development profiles and add none for AppStore and Enterprise Profiles
192
+ # @return (ProvisioningProfile): The profile that was just created
193
+ def create!(name: nil, bundle_id: nil, certificate: nil, devices: [])
194
+ raise "Missing required parameter 'bundle_id'" if bundle_id.to_s.empty?
195
+ raise "Missing required parameter 'certificate'. e.g. use `Spaceship::Certificate::Production.all.first`" if certificate.to_s.empty?
196
+
197
+ app = Spaceship::App.find(bundle_id)
198
+ raise "Could not find app with bundle id '#{bundle_id}'" unless app
199
+
200
+ # Fill in sensible default values
201
+ name ||= [bundle_id, self.pretty_type].join(' ')
202
+
203
+ devices = [] if self == AppStore # App Store Profiles MUST NOT have devices
204
+
205
+ if devices.nil? or devices.count == 0
206
+ if self == Development or self == AdHoc
207
+ # For Development and AdHoc we usually want all devices by default
208
+ devices = Spaceship::Device.all
209
+ end
210
+ end
211
+
212
+ profile = client.create_provisioning_profile!(name,
213
+ self.type,
214
+ app.app_id,
215
+ [certificate.id],
216
+ devices.map {|d| d.id} )
217
+ self.new(profile)
218
+ end
219
+
220
+ # @return (Array) Returns all profiles registered for this account
221
+ # If you're calling this from a subclass (like AdHoc), this will
222
+ # only return the profiles that are of this type
223
+ def all
224
+ profiles = client.provisioning_profiles.map do |profile|
225
+ self.factory(profile)
226
+ end
227
+
228
+ # filter out the profiles managed by xcode
229
+ profiles.delete_if do |profile|
230
+ profile.managed_by_xcode?
231
+ end
232
+
233
+ return profiles if self == ProvisioningProfile
234
+
235
+ # only return the profiles that match the class
236
+ profiles.select do |profile|
237
+ profile.class == self
238
+ end
239
+ end
240
+
241
+ # @return (ProvisioningProfile) Find a provisioning based on the
242
+ # bundle_id (app identifier). This will return nil if it can't be
243
+ # found.
244
+ def find_by_bundle_id(bundle_id)
245
+ all.find_all do |profile|
246
+ profile.app.bundle_id == bundle_id
247
+ end
248
+ end
249
+
250
+ end
251
+
252
+ # Represents a Development profile from the Dev Portal
253
+ class Development < ProvisioningProfile
254
+ def self.type
255
+ 'limited'
256
+ end
257
+ end
258
+
259
+ # Represents an AppStore profile from the Dev Portal
260
+ class AppStore < ProvisioningProfile
261
+ def self.type
262
+ 'store'
263
+ end
264
+ end
265
+
266
+ # Represents an AdHoc profile from the Dev Portal
267
+ class AdHoc < ProvisioningProfile
268
+ def self.type
269
+ 'adhoc'
270
+ end
271
+ end
272
+
273
+ # Represents an Enterprise InHouse profile from the Dev Portal
274
+ class InHouse < ProvisioningProfile
275
+ def self.type
276
+ 'inhouse'
277
+ end
278
+ end
279
+
280
+ # Download the current provisioning profile. This will *not* store
281
+ # the provisioning profile on the file system. Instead this method
282
+ # will return the content of the profile.
283
+ # @return (String) The content of the provisioning profile
284
+ # You'll probably want to store it on the file system
285
+ # @example
286
+ # File.write("path.mobileprovision", profile.download)
287
+ def download
288
+ client.download_provisioning_profile(self.id)
289
+ end
290
+
291
+ # Delete the provisioning profile
292
+ def delete!
293
+ client.delete_provisioning_profile!(self.id)
294
+ end
295
+
296
+ # Repair an existing provisioning profile
297
+ # alias to update!
298
+ # @return (ProvisioningProfile) A new provisioning profile, as
299
+ # the repair method will generate a profile with a new ID
300
+ def repair!
301
+ update!
302
+ end
303
+
304
+ # Updates the provisioning profile from the local data
305
+ # e.g. after you added new devices to the profile
306
+ # This will also update the code signing identity if necessary
307
+ # @return (ProvisioningProfile) A new provisioning profile, as
308
+ # the repair method will generate a profile with a new ID
309
+ def update!
310
+ unless certificate_valid?
311
+ if self.kind_of?Development
312
+ self.certificates = [Spaceship::Certificate::Development.all.first]
313
+ else
314
+ self.certificates = [Spaceship::Certificate::Production.all.first]
315
+ end
316
+ end
317
+
318
+ client.repair_provisioning_profile!(
319
+ self.id,
320
+ self.name,
321
+ self.distribution_method,
322
+ self.app.app_id,
323
+ self.certificates.map { |c| c.id },
324
+ self.devices.map { |d| d.id }
325
+ )
326
+
327
+ # We need to fetch the provisioning profile again, as the ID changes
328
+ profile = Spaceship::ProvisioningProfile.all.find do |profile|
329
+ profile.name == self.name # we can use the name as it's valid
330
+ end
331
+
332
+ return profile
333
+ end
334
+
335
+ # Is the certificate of this profile available?
336
+ # @return (Bool) is the certificate valid?
337
+ def certificate_valid?
338
+ return false if (certificates || []).count == 0
339
+ certificates.each do |c|
340
+ if Spaceship::Certificate.all.collect { |s| s.id }.include?(c.id)
341
+ return true
342
+ end
343
+ end
344
+ return false
345
+ end
346
+
347
+ # @return (Bool) Is this profile managed by Xcode?
348
+ def managed_by_xcode?
349
+ managing_app == 'Xcode'
350
+ end
351
+ end
352
+ end