spaceship 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.
- 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
|