spaceship 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/LICENSE +21 -0
- data/README.md +390 -17
- data/lib/spaceship.rb +69 -3
- data/lib/spaceship/app.rb +96 -0
- data/lib/spaceship/base.rb +86 -0
- data/lib/spaceship/certificate.rb +270 -0
- data/lib/spaceship/client.rb +399 -0
- data/lib/spaceship/device.rb +100 -0
- data/lib/spaceship/helper/net_http_generic_request.rb +12 -0
- data/lib/spaceship/helper/plist_middleware.rb +15 -0
- data/lib/spaceship/launcher.rb +89 -0
- data/lib/spaceship/profile_types.rb +39 -0
- data/lib/spaceship/provisioning_profile.rb +352 -0
- data/lib/spaceship/ui.rb +28 -0
- data/lib/spaceship/ui/select_team.rb +55 -0
- data/lib/spaceship/version.rb +2 -2
- metadata +179 -33
- data/.gitignore +0 -14
- data/Gemfile +0 -4
- data/LICENSE.txt +0 -22
- data/Rakefile +0 -3
- data/lib/spaceship/ship.rb +0 -7
- data/spaceship.gemspec +0 -24
- data/spec/ship_spec.rb +0 -16
- data/spec/spec_helper.rb +0 -1
- data/tasks/rspec.rake +0 -3
@@ -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
|