spaceship 0.3.2 → 0.4.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2f1c5ac6945f70388e4e1ad0b2f52a1f185c56fb
4
- data.tar.gz: a925727b880ed220b5f1d85dd7b523b7354149f7
3
+ metadata.gz: bee636c5de7df79ede26deda935649fb5a738e3f
4
+ data.tar.gz: 0e846d25300e1c0658e036f208991f841d55b5d7
5
5
  SHA512:
6
- metadata.gz: d50ba6dc463d4b920716eb792f9a00a01240e5c7d42a7980889f32b831818c4007a440fcef48cbe4793ac764e3849646bbd7bb435d2f4bf69eccf029a0bbe11d
7
- data.tar.gz: 1ec502faa18764220dd036e782bfafde2f9ea423469060e5b21f639454aebc0e53ffccfa7569f3cfd9f10c661726b712283377bffcb13233c0733bbeed62885a
6
+ metadata.gz: 5a81882cdb7b1169c78d456e4a9c531da3888632ad6d9ecd9cb36d3b546a1c95db5e98a88369d3996847a5a61b5f6b80663fb2db4b4950d612c417e84989d80b
7
+ data.tar.gz: c894e87a4e5819716c5959cfeba5b8de19b2b82c91ef46c4ab506225e1fabfedd51058a46b1ca545eaabd7ae48bf8bbad3499a276f453ca930759d0fe7cd9197
data/README.md CHANGED
@@ -66,17 +66,17 @@ Enough words, here is some code:
66
66
 
67
67
  ```ruby
68
68
  Spaceship.login
69
-
69
+
70
70
  # Create a new app
71
71
  app = Spaceship.app.create!(bundle_id: "com.krausefx.app", name: "Spaceship App")
72
-
72
+
73
73
  # Use an existing certificate
74
74
  cert = Spaceship.certificate.production.all.first
75
-
75
+
76
76
  # Create a new provisioning profile
77
77
  profile = Spaceship.provisioning_profile.app_store.create!(bundle_id: app.bundle_id,
78
78
  certificate: cert)
79
-
79
+
80
80
  # Print the name and download the new profile
81
81
  puts "Created Profile " + profile.name
82
82
  profile.download
@@ -84,7 +84,7 @@ profile.download
84
84
 
85
85
  ## Speed
86
86
 
87
- How fast are tools using `spaceship` compared to web scraping?
87
+ How fast are tools using `spaceship` compared to web scraping?
88
88
 
89
89
  ![assets/SpaceshipRecording.gif](assets/SpaceshipRecording.gif)
90
90
 
@@ -110,13 +110,15 @@ Most [fastlane tools](https://fastlane.tools) already use `spaceship`, like `sig
110
110
 
111
111
  The detailed documentation of all available classes is available on [RubyDoc](http://www.rubydoc.info/github/fastlane/spaceship/frames).
112
112
 
113
+ You can find the log file here `/tmp/spaceship[time].log`.
114
+
113
115
  # Technical Details
114
116
 
115
117
  ## HTTP Client
116
118
 
117
- Up until now all [fastlane tools](https://fastlane.tools) used web scraping to interact with Apple's web services. `spaceship` uses a simple HTTP client only, resulting in much less overhead and extremely improved speed.
119
+ Up until now all [fastlane tools](https://fastlane.tools) used web scraping to interact with Apple's web services. `spaceship` uses a simple HTTP client only, resulting in much less overhead and extremely improved speed.
118
120
 
119
- Advantages of `spaceship` (HTTP client) over web scraping:
121
+ Advantages of `spaceship` (HTTP client) over web scraping:
120
122
 
121
123
  - Blazing fast :rocket: 90% faster than previous methods
122
124
  - No more overhead by loading images, HTML, JS and CSS files on each page load
@@ -129,18 +131,19 @@ Advantages of `spaceship` (HTTP client) over web scraping:
129
131
  I won't go into too much technical details about the various API endpoints, but just to give you an idea:
130
132
 
131
133
  - `https://idmsa.apple.com`: Used to authenticate to get a valid session
132
- - `https://developerservices2.apple.com`:
134
+ - `https://developerservices2.apple.com`:
133
135
  - Get a detailed list of all available provisioning profiles
134
136
  - This API returns the devices, certificates and app for each of the profiles
135
137
  - Register new devices
136
- - `https://developer.apple.com`:
137
- - List all devices, certificates and apps
138
+ - `https://developer.apple.com`:
139
+ - List all devices, certificates, apps and app groups
138
140
  - Create new certificates, provisioning profiles and apps
141
+ - Disable/enable services on apps and assign them to app groups
139
142
  - Delete certificates and apps
140
143
  - Repair provisioning profiles
141
144
  - Download provisioning profiles
142
145
  - Team selection
143
- - `https://itunesconnect.apple.com`:
146
+ - `https://itunesconnect.apple.com`:
144
147
  - Managing apps
145
148
  - Managing beta testers
146
149
  - Submitting updates to review
@@ -150,11 +153,11 @@ I won't go into too much technical details about the various API endpoints, but
150
153
 
151
154
  ## Magic involved
152
155
 
153
- `spaceship` does a lot of magic to get everything working so neatly:
156
+ `spaceship` does a lot of magic to get everything working so neatly:
154
157
 
155
158
  - **Sensible Defaults**: You only have to provide the mandatory information (e.g. new provisioning profiles contain all devices by default)
156
159
  - **Local Validation**: When pushing changes back to the Apple Dev Portal `spaceship` will make sure only valid data is sent to Apple (e.g. automatic repairing of provisioning profiles)
157
- - **Various request/response types**: When working with the different API endpoints, `spaceship` has to deal with `JSON`, `XML`, `txt`, `plist` and sometimes even `HTML` responses and requests.
160
+ - **Various request/response types**: When working with the different API endpoints, `spaceship` has to deal with `JSON`, `XML`, `txt`, `plist` and sometimes even `HTML` responses and requests.
158
161
  - **Automatic Pagination**: Even if you have thousands of apps, profiles or certificates, `spaceship` **can** handle your scale. It was heavily tested by first using `spaceship` to create hundreds of profiles and then accessing them using `spaceship`.
159
162
  - **Session, Cookie and CSRF token**: All the security aspects are handled by `spaceship`.
160
163
  - **Profile Magic**: Create and upload code signing requests, all managed by `spaceship`
@@ -162,9 +165,9 @@ I won't go into too much technical details about the various API endpoints, but
162
165
 
163
166
  # Credits
164
167
 
165
- The initial release was sponsored by [ZeroPush](https://zeropush.com).
168
+ The initial release was sponsored by [ZeroPush](https://zeropush.com).
166
169
 
167
- `spaceship` was developed by
170
+ `spaceship` was developed by
168
171
  - [@KrauseFx](https://twitter.com/KrauseFx).
169
172
  - [@snatchev](https://twitter.com/snatchev/)
170
173
  - [@mathcarignani](https://twitter.com/mathcarignani/)
@@ -175,7 +175,6 @@ module Spaceship
175
175
  # This method can be used by subclasses to do additional initialisation
176
176
  # using the `raw_data`
177
177
  def setup
178
-
179
178
  end
180
179
 
181
180
  ##
@@ -5,7 +5,6 @@ require 'spaceship/ui'
5
5
  require 'spaceship/helper/plist_middleware'
6
6
  require 'spaceship/helper/net_http_generic_request'
7
7
 
8
-
9
8
  if ENV["DEBUG"]
10
9
  require 'openssl'
11
10
  # this has to be on top of this file, since the value can't be changed later
@@ -20,12 +19,15 @@ module Spaceship
20
19
  attr_accessor :cookie
21
20
 
22
21
  # The logger in which all requests are logged
23
- # /tmp/spaceship.log by default
22
+ # /tmp/spaceship[time].log by default
24
23
  attr_accessor :logger
25
24
 
26
25
  # Invalid user credentials were provided
27
26
  class InvalidUserCredentialsError < StandardError; end
28
27
 
28
+ # Raised when no user credentials were passed at all
29
+ class NoUserCredentialsError < StandardError; end
30
+
29
31
  class UnexpectedResponse < StandardError; end
30
32
 
31
33
  # Authenticates with Apple's web services. This method has to be called once
@@ -71,14 +73,14 @@ module Spaceship
71
73
  end
72
74
 
73
75
  # The logger in which all requests are logged
74
- # /tmp/spaceship.log by default
76
+ # /tmp/spaceship[time].log by default
75
77
  def logger
76
78
  unless @logger
77
79
  if $verbose || ENV["VERBOSE"]
78
80
  @logger = Logger.new(STDOUT)
79
81
  else
80
82
  # Log to file by default
81
- path = "/tmp/spaceship.log"
83
+ path = "/tmp/spaceship#{Time.now.to_i}.log"
82
84
  @logger = Logger.new(path)
83
85
  end
84
86
 
@@ -138,11 +140,11 @@ module Spaceship
138
140
  require 'credentials_manager'
139
141
  data = CredentialsManager::PasswordManager.shared_manager(user, false)
140
142
  user ||= data.username
141
- password ||= data.password
143
+ password = data.password
142
144
  end
143
145
 
144
- if user.to_s.empty? or password.to_s.empty?
145
- raise InvalidUserCredentialsError.new("No login data provided")
146
+ if user.to_s.strip.empty? or password.to_s.strip.empty?
147
+ raise NoUserCredentialsError.new("No login data provided")
146
148
  end
147
149
 
148
150
  send_login_request(user, password) # different in subclasses
@@ -153,94 +155,98 @@ module Spaceship
153
155
  !!@cookie
154
156
  end
155
157
 
158
+ def with_retry(tries = 5, &block)
159
+ return block.call
160
+ rescue Faraday::Error::TimeoutError => ex # New Faraday version: Faraday::TimeoutError => ex
161
+ unless (tries -= 1).zero?
162
+ sleep 3
163
+ retry
164
+ end
165
+
166
+ raise ex # re-raise the exception
167
+ end
168
+
156
169
  private
157
- # Is called from `parse_response` to store the latest csrf_token (if available)
158
- def store_csrf_tokens(response)
159
- if response and response.headers
160
- tokens = response.headers.select { |k, v| %w[csrf csrf_ts].include?(k) }
161
- if tokens and not tokens.empty?
162
- @csrf_tokens = tokens
163
- end
170
+
171
+ # Is called from `parse_response` to store the latest csrf_token (if available)
172
+ def store_csrf_tokens(response)
173
+ if response and response.headers
174
+ tokens = response.headers.select { |k, v| %w[csrf csrf_ts].include?(k) }
175
+ if tokens and not tokens.empty?
176
+ @csrf_tokens = tokens
164
177
  end
165
178
  end
179
+ end
166
180
 
167
181
  # memoize the last csrf tokens from responses
168
- def csrf_tokens
169
- @csrf_tokens || {}
170
- end
182
+ def csrf_tokens
183
+ @csrf_tokens || {}
184
+ end
171
185
 
172
- def request(method, url_or_path = nil, params = nil, headers = {}, &block)
173
- if session?
174
- headers.merge!({'Cookie' => cookie})
175
- headers.merge!(csrf_tokens)
176
- end
177
- headers.merge!({'User-Agent' => 'spaceship'})
186
+ def request(method, url_or_path = nil, params = nil, headers = {}, &block)
187
+ if session?
188
+ headers.merge!({'Cookie' => cookie})
189
+ headers.merge!(csrf_tokens)
190
+ end
191
+ headers.merge!({'User-Agent' => 'spaceship'})
178
192
 
179
- # Before encoding the parameters, log them
180
- log_request(method, url_or_path, params)
193
+ # Before encoding the parameters, log them
194
+ log_request(method, url_or_path, params)
181
195
 
182
- # form-encode the params only if there are params, and the block is not supplied.
183
- # this is so that certain requests can be made using the block for more control
184
- if method == :post && params && !block_given?
185
- params, headers = encode_params(params, headers)
186
- end
196
+ # form-encode the params only if there are params, and the block is not supplied.
197
+ # this is so that certain requests can be made using the block for more control
198
+ if method == :post && params && !block_given?
199
+ params, headers = encode_params(params, headers)
200
+ end
187
201
 
188
- response = send_request(method, url_or_path, params, headers, &block)
202
+ response = send_request(method, url_or_path, params, headers, &block)
189
203
 
190
- log_response(method, url_or_path, response)
204
+ log_response(method, url_or_path, response)
191
205
 
192
- return response
193
- end
206
+ return response
207
+ end
194
208
 
195
- def log_request(method, url, params)
196
- params_to_log = Hash(params).dup # to also work with nil
197
- params_to_log.delete(:accountPassword) # Dev Portal
198
- params_to_log.delete(:theAccountPW) # iTC
199
- params_to_log = params_to_log.collect do |key, value|
200
- "{#{key}: #{value}}"
201
- end
202
- logger.info("#{method.upcase}: #{url} #{params_to_log.join(', ')}")
209
+ def log_request(method, url, params)
210
+ params_to_log = Hash(params).dup # to also work with nil
211
+ params_to_log.delete(:accountPassword) # Dev Portal
212
+ params_to_log.delete(:theAccountPW) # iTC
213
+ params_to_log = params_to_log.collect do |key, value|
214
+ "{#{key}: #{value}}"
203
215
  end
216
+ logger.info("#{method.upcase}: #{url} #{params_to_log.join(', ')}")
217
+ end
204
218
 
205
- def log_response(method, url, response)
206
- logger.debug("#{method.upcase}: #{url}: #{response.body}")
207
- end
219
+ def log_response(method, url, response)
220
+ logger.debug("#{method.upcase}: #{url}: #{response.body}")
221
+ end
208
222
 
209
223
  # Actually sends the request to the remote server
210
224
  # Automatically retries the request up to 3 times if something goes wrong
211
- def send_request(method, url_or_path, params, headers, &block)
212
- tries ||= 5
213
-
214
- return @client.send(method, url_or_path, params, headers, &block)
215
-
216
- rescue Faraday::Error::TimeoutError => ex # New Faraday version: Faraday::TimeoutError => ex
217
- unless (tries -= 1).zero?
218
- sleep 3
219
- retry
220
- end
221
-
222
- raise ex # re-raise the exception
225
+ def send_request(method, url_or_path, params, headers, &block)
226
+ with_retry do
227
+ @client.send(method, url_or_path, params, headers, &block)
223
228
  end
229
+ end
224
230
 
225
- def parse_response(response, expected_key = nil)
226
- if expected_key
227
- content = response.body[expected_key]
228
- else
229
- content = response.body
230
- end
231
-
232
- if content == nil
233
- raise UnexpectedResponse.new(response.body)
234
- else
235
- store_csrf_tokens(response)
236
- content
237
- end
231
+ def parse_response(response, expected_key = nil)
232
+ if expected_key
233
+ content = response.body[expected_key]
234
+ else
235
+ content = response.body
238
236
  end
239
237
 
240
- def encode_params(params, headers)
241
- params = Faraday::Utils::ParamsHash[params].to_query
242
- headers = {'Content-Type' => 'application/x-www-form-urlencoded'}.merge(headers)
243
- return params, headers
238
+ if content == nil
239
+ raise UnexpectedResponse.new(response.body)
240
+ else
241
+ store_csrf_tokens(response)
242
+ content
244
243
  end
244
+ end
245
+
246
+ def encode_params(params, headers)
247
+ params = Faraday::Utils::ParamsHash[params].to_query
248
+ headers = {'Content-Type' => 'application/x-www-form-urlencoded'}.merge(headers)
249
+ return params, headers
250
+ end
245
251
  end
246
252
  end
@@ -9,4 +9,4 @@ class Net::HTTPGenericRequest
9
9
  def supply_default_content_type
10
10
  return if content_type
11
11
  end
12
- end
12
+ end
@@ -6,17 +6,17 @@ module Spaceship
6
6
  # spaceship. You can call `.new` without any parameters, but you'll have to call
7
7
  # `.login` at a later point. If you prefer, you can pass the login credentials
8
8
  # here already.
9
- #
9
+ #
10
10
  # Authenticates with Apple's web services. This method has to be called once
11
11
  # to generate a valid session. The session will automatically be used from then
12
12
  # on.
13
- #
13
+ #
14
14
  # This method will automatically use the username from the Appfile (if available)
15
15
  # and fetch the password from the Keychain (if available)
16
- #
16
+ #
17
17
  # @param user (String) (optional): The username (usually the email address)
18
18
  # @param password (String) (optional): The password
19
- #
19
+ #
20
20
  # @raise InvalidUserCredentialsError: raised if authentication failed
21
21
  def initialize(user = nil, password = nil)
22
22
  @client = PortalClient.new
@@ -33,30 +33,30 @@ module Spaceship
33
33
  # Authenticates with Apple's web services. This method has to be called once
34
34
  # to generate a valid session. The session will automatically be used from then
35
35
  # on.
36
- #
36
+ #
37
37
  # This method will automatically use the username from the Appfile (if available)
38
38
  # and fetch the password from the Keychain (if available)
39
- #
39
+ #
40
40
  # @param user (String) (optional): The username (usually the email address)
41
41
  # @param password (String) (optional): The password
42
- #
42
+ #
43
43
  # @raise InvalidUserCredentialsError: raised if authentication failed
44
- #
44
+ #
45
45
  # @return (Spaceship::Client) The client the login method was called for
46
- def login(user, password)
46
+ def login(user, password)
47
47
  @client.login(user, password)
48
48
  end
49
49
 
50
50
  # Open up the team selection for the user (if necessary).
51
- #
51
+ #
52
52
  # If the user is in multiple teams, a team selection is shown.
53
53
  # The user can then select a team by entering the number
54
- #
54
+ #
55
55
  # Additionally, the team ID is shown next to each team name
56
56
  # so that the user can use the environment variable `FASTLANE_TEAM_ID`
57
57
  # for future user.
58
- #
59
- # @return (String) The ID of the select team. You also get the value if
58
+ #
59
+ # @return (String) The ID of the select team. You also get the value if
60
60
  # the user is only in one team.
61
61
  def select_team
62
62
  @client.select_team
@@ -71,6 +71,11 @@ module Spaceship
71
71
  Spaceship::App.set_client(@client)
72
72
  end
73
73
 
74
+ # @return (Class) Access the app groups for this spaceship
75
+ def app_group
76
+ Spaceship::AppGroup.set_client(@client)
77
+ end
78
+
74
79
  # @return (Class) Access the devices for this spaceship
75
80
  def device
76
81
  Spaceship::Device.set_client(@client)
@@ -86,4 +91,4 @@ module Spaceship
86
91
  Spaceship::ProvisioningProfile.set_client(@client)
87
92
  end
88
93
  end
89
- end
94
+ end
@@ -4,7 +4,7 @@ module Spaceship
4
4
  class App < PortalBase
5
5
 
6
6
  # @return (String) The identifier of this app, provided by the Dev Portal
7
- # @example
7
+ # @example
8
8
  # "RGAWZGXSAA"
9
9
  attr_accessor :app_id
10
10
 
@@ -14,17 +14,17 @@ module Spaceship
14
14
  attr_accessor :name
15
15
 
16
16
  # @return (String) the supported platform of this app
17
- # @example
17
+ # @example
18
18
  # "ios"
19
19
  attr_accessor :platform
20
20
 
21
21
  # Prefix provided by the Dev Portal
22
- # @example
22
+ # @example
23
23
  # "5A997XSHK2"
24
24
  attr_accessor :prefix
25
25
 
26
26
  # @return (String) The bundle_id (app identifier) of your app
27
- # @example
27
+ # @example
28
28
  # "com.krausefx.app"
29
29
  attr_accessor :bundle_id
30
30
 
@@ -33,7 +33,7 @@ module Spaceship
33
33
 
34
34
  # @return (Hash) Feature details
35
35
  attr_accessor :features
36
-
36
+
37
37
  # @return (Array) List of enabled features
38
38
  attr_accessor :enabled_features
39
39
 
@@ -45,10 +45,10 @@ module Spaceship
45
45
 
46
46
  # @return (Fixnum) Number of associated app groups
47
47
  attr_accessor :app_groups_count
48
-
48
+
49
49
  # @return (Fixnum) Number of associated cloud containers
50
50
  attr_accessor :cloud_containers_count
51
-
51
+
52
52
  # @return (Fixnum) Number of associated identifiers
53
53
  attr_accessor :identifiers_count
54
54
 
@@ -81,7 +81,7 @@ module Spaceship
81
81
  end
82
82
 
83
83
  # Creates a new App ID on the Apple Dev Portal
84
- #
84
+ #
85
85
  # if bundle_id ends with '*' then it is a wildcard id otherwise, it is an explicit id
86
86
  # @param bundle_id [String] the bundle id (app_identifier) of the app associated with this provisioning profile
87
87
  # @param name [String] the name of the App
@@ -113,13 +113,27 @@ module Spaceship
113
113
  client.delete_app!(app_id)
114
114
  self
115
115
  end
116
-
116
+
117
117
  # Fetch a specific App ID details based on the bundle_id
118
118
  # @return (App) The app you're looking for. This is nil if the app can't be found.
119
119
  def details
120
120
  app = client.details_for_app(self)
121
121
  self.class.factory(app)
122
122
  end
123
+
124
+ # Associate specific groups with this app
125
+ # @return (App) The updated detailed app. This is nil if the app couldn't be found
126
+ def associate_groups(groups)
127
+ app = client.associate_groups_with_app(self, groups)
128
+ self.class.factory(app)
129
+ end
130
+
131
+ # Update a service for the app with given AppService object
132
+ # @return (App) The updated detailed app. This is nil if the app couldn't be found
133
+ def update_service(service)
134
+ app = client.update_service_for_app(self, service)
135
+ self.class.factory(app)
136
+ end
123
137
  end
124
138
  end
125
139
  end
@@ -0,0 +1,77 @@
1
+ module Spaceship
2
+ module Portal
3
+ # Represents an app group of the Apple Dev Portal
4
+ class AppGroup < PortalBase
5
+ # @return (String) The identifier assigned to this group
6
+ # @example
7
+ # "group.com.example.application"
8
+ attr_accessor :group_id
9
+
10
+ # @return (String) The prefix assigned to this group
11
+ # @example
12
+ # "9J57U9392R"
13
+ attr_accessor :prefix
14
+
15
+ # @return (String) The name of this group
16
+ # @example
17
+ # "App Group"
18
+ attr_accessor :name
19
+
20
+ # @return (String) Status of the group
21
+ # @example
22
+ # "current"
23
+ attr_accessor :status
24
+
25
+ # @return (String) The identifier of this app group, provided by the Dev Portal
26
+ # @example
27
+ # "2MAY7NPHAA"
28
+ attr_accessor :app_group_id
29
+
30
+ attr_mapping(
31
+ 'applicationGroup' => :app_group_id,
32
+ 'name' => :name,
33
+ 'prefix' => :prefix,
34
+ 'identifier' => :group_id,
35
+ 'status' => :status
36
+ )
37
+
38
+ class << self
39
+ # Create a new object based on a hash.
40
+ # This is used to create a new object based on the server response.
41
+ def factory(attrs)
42
+ self.new(attrs)
43
+ end
44
+
45
+ # @return (Array) Returns all app groups available for this account
46
+ def all
47
+ client.app_groups.map { |group| self.factory(group) }
48
+ end
49
+
50
+ # Creates a new App Group on the Apple Dev Portal
51
+ #
52
+ # @param group_id [String] the identifier to assign to this group
53
+ # @param name [String] the name of the group
54
+ # @return (AppGroup) The group you just created
55
+ def create!(group_id: nil, name: nil)
56
+ new_group = client.create_app_group!(name, group_id)
57
+ self.new(new_group)
58
+ end
59
+
60
+ # Find a specific App Group group_id
61
+ # @return (AppGroup) The app group you're looking for. This is nil if the app group can't be found.
62
+ def find(group_id)
63
+ all.find do |group|
64
+ group.group_id == group_id
65
+ end
66
+ end
67
+ end
68
+
69
+ # Delete this app group
70
+ # @return (AppGroup) The app group you just deletd
71
+ def delete!
72
+ client.delete_app_group!(app_group_id)
73
+ self
74
+ end
75
+ end
76
+ end
77
+ end