fastlane 2.28.5 → 2.28.6

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d176336ace01a0a0ecb188a978741ff33ffafe61
4
- data.tar.gz: 33a016a98b9967456b0e258388700f179867a7cd
3
+ metadata.gz: bf87006e4674fdd128d4cae3634a07ba4d506f1c
4
+ data.tar.gz: 2e806c93d18de7344cd44df0c92ca162749bf55b
5
5
  SHA512:
6
- metadata.gz: b7cd3daa86f90c7b2f4867305fd1e2fd38293f3d6dcbfbbd0eec6f3f4794fd6e559a001c9a8048db90a3125c247660084321c8597e4c5d4fc98b154a3a454aef
7
- data.tar.gz: 2ce89bb11864cf65c982b811cfef32ca6443e900d7a98849c090b5879d65a240cb6c08edabf3af3d1025ff83a3919ccfaf52ce115af43813db2ab6c39710f124
6
+ metadata.gz: 1d273bba01302f14006f1a308c605bd01ca3a20636c2f0b5e1ac7fa8c003df9f286e44ce4119409426c828f74827c0510c3a934abed1c0167155fb0f4aaadac2
7
+ data.tar.gz: 505e1862730380c1f0144969163e9de58deb212a5f6ab179f61e432572de39f137784c63c7714dec7ba9132ba4d51b2e8ad6f888f5367115ab42ff101e5c3b4b
@@ -37,7 +37,7 @@ module Deliver
37
37
  optional: true,
38
38
  env_name: "DELIVER_IPA_PATH",
39
39
  description: "Path to your ipa file",
40
- default_value: Dir["*.ipa"].first,
40
+ default_value: Dir["*.ipa"].sort_by { |x| File.mtime(x) }.last,
41
41
  verify_block: proc do |value|
42
42
  UI.user_error!("Could not find ipa file at path '#{File.expand_path(value)}'") unless File.exist?(value)
43
43
  UI.user_error!("'#{value}' doesn't seem to be an ipa file") unless value.end_with?(".ipa")
@@ -51,7 +51,7 @@ module Deliver
51
51
  optional: true,
52
52
  env_name: "DELIVER_PKG_PATH",
53
53
  description: "Path to your pkg file",
54
- default_value: Dir["*.pkg"].first,
54
+ default_value: Dir["*.pkg"].sort_by { |x| File.mtime(x) }.last,
55
55
  verify_block: proc do |value|
56
56
  UI.user_error!("Could not find pkg file at path '#{File.expand_path(value)}'") unless File.exist?(value)
57
57
  UI.user_error!("'#{value}' doesn't seem to be a pkg file") unless value.end_with?(".pkg")
@@ -65,7 +65,7 @@ module Fastlane
65
65
  platform = Actions.lane_context[Actions::SharedValues::PLATFORM_NAME]
66
66
 
67
67
  if platform == :ios or platform.nil?
68
- ipa_path_default = Dir["*.ipa"].last
68
+ ipa_path_default = Dir["*.ipa"].sort_by { |x| File.mtime(x) }.last
69
69
  end
70
70
 
71
71
  if platform == :android
@@ -1,4 +1,4 @@
1
1
  module Fastlane
2
- VERSION = '2.28.5'.freeze
2
+ VERSION = '2.28.6'.freeze
3
3
  DESCRIPTION = "The easiest way to automate beta deployments and releases for your iOS and Android apps".freeze
4
4
  end
@@ -1,36 +1,54 @@
1
1
  module FastlaneCore
2
2
  class BuildWatcher
3
- # @return The build we waited for. This method will always return a build
4
- def self.wait_for_build_processing_to_be_complete(app_id: nil, platform: nil)
5
- # First, find the train and build version we want to watch for
6
- processing_builds = Spaceship::TestFlight::Build.all_processing_builds(app_id: app_id, platform: platform)
3
+ class << self
4
+ # @return The build we waited for. This method will always return a build
5
+ def wait_for_build_processing_to_be_complete(app_id: nil, platform: nil)
6
+ # First, find the train and build version we want to watch for
7
+ watched_build = watching_build(app_id: app_id, platform: platform)
8
+ UI.crash!("Could not find a build for app: #{app_id} on platform: #{platform}") if watched_build.nil?
7
9
 
8
- watching_build = processing_builds.sort_by(&:upload_date).last # either it's still processing
9
- watching_build ||= Spaceship::TestFlight::Build.latest(app_id: app_id, platform: platform) # or we fallback to the most recent uplaod
10
+ loop do
11
+ matched_build = matching_build(watched_build: watched_build, app_id: app_id, platform: platform)
10
12
 
11
- loop do
12
- UI.message("Waiting for iTunes Connect to finish processing the new build (#{watching_build.train_version} - #{watching_build.build_version})")
13
+ report_status(build: matched_build)
13
14
 
15
+ if matched_build && matched_build.processed?
16
+ return matched_build
17
+ end
18
+
19
+ sleep 10
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def watching_build(app_id: nil, platform: nil)
26
+ processing_builds = Spaceship::TestFlight::Build.all_processing_builds(app_id: app_id, platform: platform)
27
+
28
+ watched_build = processing_builds.sort_by(&:upload_date).last
29
+ watched_build || Spaceship::TestFlight::Build.latest(app_id: app_id, platform: platform)
30
+ end
31
+
32
+ def matching_build(watched_build: nil, app_id: nil, platform: nil)
33
+ matched_builds = Spaceship::TestFlight::Build.builds_for_train(app_id: app_id, platform: platform, train_version: watched_build.train_version)
34
+ matched_builds.find { |build| build.build_version == watched_build.build_version }
35
+ end
36
+
37
+ def report_status(build: nil)
14
38
  # Due to iTunes Connect, builds disappear from the build list alltogether
15
39
  # after they finished processing. Before returning this build, we have to
16
40
  # wait for the build to appear in the build list again
17
41
  # As this method is very often used to wait for a build, and then do something
18
42
  # with it, we have to be sure that the build actually is ready
19
-
20
- matching_builds = Spaceship::TestFlight::Build.builds_for_train(app_id: app_id, platform: platform, train_version: watching_build.train_version)
21
- matching_build = matching_builds.find { |build| build.build_version == watching_build.build_version }
22
-
23
- if matching_build.nil?
43
+ if build.nil?
24
44
  UI.message("Build doesn't show up in the build list any more, waiting for it to appear again")
25
- elsif matching_build.active?
26
- UI.success("Build #{matching_build.train_version} - #{matching_build.build_version} is already being tested")
27
- return matching_build
28
- elsif matching_build.ready_to_submit? || matching_build.export_compliance_missing?
29
- UI.success("Successfully finished processing the build #{matching_build.train_version} - #{matching_build.build_version}")
30
- return matching_build
45
+ elsif build.active?
46
+ UI.success("Build #{build.train_version} - #{build.build_version} is already being tested")
47
+ elsif build.ready_to_submit? || build.export_compliance_missing?
48
+ UI.success("Successfully finished processing the build #{build.train_version} - #{build.build_version}")
49
+ else
50
+ UI.message("Waiting for iTunes Connect to finish processing the new build (#{build.train_version} - #{build.build_version})")
31
51
  end
32
-
33
- sleep 10
34
52
  end
35
53
  end
36
54
  end
@@ -118,11 +118,6 @@ module FastlaneCore
118
118
  FastlaneCore::Env.truthy?("TERM_PROGRAM_VERSION")
119
119
  end
120
120
 
121
- # Does the user use iTerm?
122
- def self.iterm?
123
- FastlaneCore::Env.truthy?("ITERM_SESSION_ID")
124
- end
125
-
126
121
  # Logs base directory
127
122
  def self.buildlog_path
128
123
  return ENV["FL_BUILDLOG_PATH"] || "~/Library/Logs"
@@ -37,15 +37,20 @@ module Pilot
37
37
  UI.message("If you want to skip waiting for the processing to be finished, use the `skip_waiting_for_build_processing` option")
38
38
  latest_build = FastlaneCore::BuildWatcher.wait_for_build_processing_to_be_complete(app_id: app.apple_id, platform: platform)
39
39
 
40
- distribute(options, latest_build)
40
+ distribute(options, build: latest_build)
41
41
  end
42
42
 
43
- def distribute(options, build)
43
+ def distribute(options, build: nil)
44
44
  start(options)
45
45
  if config[:apple_id].to_s.length == 0 and config[:app_identifier].to_s.length == 0
46
46
  config[:app_identifier] = UI.input("App Identifier: ")
47
47
  end
48
48
 
49
+ build ||= Spaceship::TestFlight::Build.latest(app_id: app.apple_id, platform: fetch_app_platform)
50
+ if build.nil?
51
+ UI.user_error!("No build to distribute!")
52
+ end
53
+
49
54
  if should_update_build_information(options)
50
55
  build.update_build_information!(whats_new: options[:changelog], description: options[:beta_app_description], feedback_email: options[:beta_app_feedback_email])
51
56
  UI.success "Successfully set the changelog and/or description for build"
@@ -32,7 +32,7 @@ module Pilot
32
32
  optional: true,
33
33
  env_name: "PILOT_IPA",
34
34
  description: "Path to the ipa file to upload",
35
- default_value: Dir["*.ipa"].first,
35
+ default_value: Dir["*.ipa"].sort_by { |x| File.mtime(x) }.last,
36
36
  verify_block: proc do |value|
37
37
  UI.user_error!("Could not find ipa file at path '#{value}'") unless File.exist? value
38
38
  UI.user_error!("'#{value}' doesn't seem to be an ipa file") unless value.end_with? ".ipa"
@@ -13,7 +13,7 @@ module Pilot
13
13
  tester = find_or_create_tester(email: config[:email], first_name: config[:first_name], last_name: config[:last_name])
14
14
 
15
15
  begin
16
- groups = add_tester_to_groups!(tester: tester, app: app, groups: config[:groups])
16
+ groups = Spaceship::TestFlight::Group.add_tester_to_groups!(tester: tester, app: app, groups: config[:groups])
17
17
  if tester.kind_of?(Spaceship::Tunes::Tester::Internal)
18
18
  UI.success("Successfully added tester to app #{app.name}")
19
19
  else
@@ -59,7 +59,7 @@ module Pilot
59
59
  test_flight_tester.remove_from_app!(app_id: app.apple_id)
60
60
  UI.success("Successfully removed tester, #{test_flight_tester.email}, from app: #{app.name}")
61
61
  else
62
- groups = remove_tester_from_groups!(tester: tester, app: app, groups: config[:groups])
62
+ groups = Spaceship::TestFlight::Group.remove_tester_from_groups!(tester: tester, app: app, groups: config[:groups])
63
63
  group_names = groups.map(&:name).join(", ")
64
64
  UI.success("Successfully removed tester #{tester.email} from app #{app.name} in group(s) #{group_names}")
65
65
  end
@@ -109,40 +109,6 @@ module Pilot
109
109
  raise ex
110
110
  end
111
111
 
112
- def perform_for_groups_in_app(app: nil, groups: nil, &block)
113
- if groups.nil?
114
- default_external_group = app.default_external_group
115
- if default_external_group.nil?
116
- UI.user_error!("The app #{app.name} does not have a default external group. Please make sure to pass group names to the `:groups` option.")
117
- end
118
- test_flight_groups = [default_external_group]
119
- else
120
- test_flight_groups = Spaceship::TestFlight::Group.filter_groups(app_id: app.apple_id) do |group|
121
- groups.include?(group.name)
122
- end
123
-
124
- UI.user_error!("There are no groups available matching the names passed to the `:groups` option.") if test_flight_groups.empty?
125
- end
126
-
127
- test_flight_groups.each(&block)
128
- end
129
-
130
- def add_tester_to_groups!(tester: nil, app: nil, groups: nil)
131
- if tester.kind_of?(Spaceship::Tunes::Tester::Internal)
132
- Spaceship::TestFlight::Group.internal_group(app_id: app.apple_id).add_tester!(tester)
133
- else
134
- perform_for_groups_in_app(app: app, groups: groups) { |group| group.add_tester!(tester) }
135
- end
136
- end
137
-
138
- def remove_tester_from_groups!(tester: nil, app: nil, groups: nil)
139
- if tester.kind_of?(Spaceship::Tunes::Tester::Internal)
140
- Spaceship::TestFlight::Group.internal_group(app_id: app.apple_id).remove_tester!(tester)
141
- else
142
- perform_for_groups_in_app(app: app, groups: groups) { |group| group.remove_tester!(tester) }
143
- end
144
- end
145
-
146
112
  def list_testers_by_app(app_filter)
147
113
  app = Spaceship::Application.find(app_filter)
148
114
  UI.user_error!("Couldn't find app with '#{app_filter}'") unless app
@@ -4,6 +4,18 @@ module Spaceship::TestFlight
4
4
  @client ||= Client.client_with_authorization_from(Spaceship::Tunes.client)
5
5
  end
6
6
 
7
+ ##
8
+ # Have subclasses inherit the client from their superclass
9
+ #
10
+ # Essentially, we are making a class-inheritable-accessor as described here:
11
+ # https://apidock.com/rails/v4.2.7/Class/class_attribute
12
+ def self.inherited(subclass)
13
+ this_class = self
14
+ subclass.define_singleton_method(:client) do
15
+ this_class.client
16
+ end
17
+ end
18
+
7
19
  def to_json
8
20
  raw_data.to_json
9
21
  end
@@ -67,16 +67,17 @@ module Spaceship::TestFlight
67
67
  BUILD_STATES = {
68
68
  processing: 'testflight.build.state.processing',
69
69
  active: 'testflight.build.state.testing.active',
70
- ready: 'testflight.build.state.submit.ready',
70
+ ready_to_submit: 'testflight.build.state.submit.ready',
71
+ ready_to_test: 'testflight.build.state.testing.ready',
71
72
  export_compliance_missing: 'testflight.build.state.export.compliance.missing'
72
73
  }
73
74
 
74
- # Find a Build by `build_id`. Returns `nil` if can't find it.
75
+ # Find a Build by `build_id`.
75
76
  #
76
77
  # @return (Spaceship::TestFlight::Build)
77
78
  def self.find(app_id: nil, build_id: nil)
78
79
  attrs = client.get_build(app_id: app_id, build_id: build_id)
79
- self.new(attrs) if attrs
80
+ self.new(attrs)
80
81
  end
81
82
 
82
83
  def self.all(app_id: nil, platform: nil)
@@ -104,13 +105,17 @@ module Spaceship::TestFlight
104
105
  #
105
106
  # Note: this will overwrite any non-saved changes to the object
106
107
  #
107
- # @return (Spaceceship::Base::DataHash) the raw_data of the build.
108
+ # @return (Spaceship::Base::DataHash) the raw_data of the build.
108
109
  def reload
109
110
  self.raw_data = self.class.find(app_id: app_id, build_id: id).raw_data
110
111
  end
111
112
 
112
113
  def ready_to_submit?
113
- external_state == BUILD_STATES[:ready]
114
+ external_state == BUILD_STATES[:ready_to_submit]
115
+ end
116
+
117
+ def ready_to_test?
118
+ external_state == BUILD_STATES[:ready_to_test]
114
119
  end
115
120
 
116
121
  def active?
@@ -125,6 +130,10 @@ module Spaceship::TestFlight
125
130
  external_state == BUILD_STATES[:export_compliance_missing]
126
131
  end
127
132
 
133
+ def self.processed?
134
+ active? || ready_to_submit? || export_compliance_missing?
135
+ end
136
+
128
137
  # Getting builds from BuildTrains only gets a partial Build object
129
138
  # We are then requesting the full build from iTC when we need to access
130
139
  # any of the variables below, because they are not inlcuded in the partial Build objects
@@ -159,13 +168,14 @@ module Spaceship::TestFlight
159
168
  end
160
169
 
161
170
  def update_build_information!(description: nil, feedback_email: nil, whats_new: nil)
162
- test_info.description = description
163
- test_info.feedback_email = feedback_email
164
- test_info.whats_new = whats_new
171
+ test_info.description = description if description
172
+ test_info.feedback_email = feedback_email if feedback_email
173
+ test_info.whats_new = whats_new if whats_new
165
174
  save!
166
175
  end
167
176
 
168
177
  def submit_for_testflight_review!
178
+ return if ready_to_test?
169
179
  client.post_for_testflight_review(app_id: app_id, build_id: id, build: self)
170
180
  end
171
181
 
@@ -1,20 +1,34 @@
1
1
  module Spaceship::TestFlight
2
2
  class Client < Spaceship::Client
3
+ ##
4
+ # Spaceship HTTP client for the testflight API.
5
+ #
6
+ # This client is solely responsible for the making HTTP requests and
7
+ # parsing their responses. Parameters should be either named parameters, or
8
+ # for large request data bodies, pass in anything that can resond to
9
+ # `to_json`.
10
+ #
11
+ # Each request method should validate the required parameters. A required parameter is one that would result in 400-range response if it is not supplied.
12
+ # Each request method should make only one request. For more high-level logic, put code in the data models.
13
+
3
14
  def self.hostname
4
15
  'https://itunesconnect.apple.com/testflight/v2/'
5
16
  end
6
17
 
18
+ ##
19
+ # @!group Build trains API
20
+ ##
21
+
7
22
  # Returns an array of all available build trains (not the builds they include)
8
- def get_build_trains(app_id: nil, platform: nil)
23
+ def get_build_trains(app_id: nil, platform: "ios")
9
24
  assert_required_params(__method__, binding)
10
- platform ||= "ios"
25
+
11
26
  response = request(:get, "providers/#{team_id}/apps/#{app_id}/platforms/#{platform}/trains")
12
27
  handle_response(response)
13
28
  end
14
29
 
15
- def get_builds_for_train(app_id: nil, platform: nil, train_version: nil)
30
+ def get_builds_for_train(app_id: nil, platform: "ios", train_version: nil)
16
31
  assert_required_params(__method__, binding)
17
- platform ||= "ios"
18
32
 
19
33
  response = request(:get, "providers/#{team_id}/apps/#{app_id}/platforms/#{platform}/trains/#{train_version}/builds")
20
34
  handle_response(response)
@@ -34,8 +48,72 @@ module Spaceship::TestFlight
34
48
  handle_response(response)
35
49
  end
36
50
 
51
+ ##
52
+ # @!group Builds API
53
+ ##
54
+
55
+ def get_build(app_id: nil, build_id: nil)
56
+ assert_required_params(__method__, binding)
57
+
58
+ response = request(:get, "providers/#{team_id}/apps/#{app_id}/builds/#{build_id}")
59
+ handle_response(response)
60
+ end
61
+
62
+ def put_build(app_id: nil, build_id: nil, build: nil)
63
+ assert_required_params(__method__, binding)
64
+
65
+ response = request(:put) do |req|
66
+ req.url "providers/#{team_id}/apps/#{app_id}/builds/#{build_id}"
67
+ req.body = build.to_json
68
+ req.headers['Content-Type'] = 'application/json'
69
+ end
70
+ handle_response(response)
71
+ end
72
+
73
+ def post_for_testflight_review(app_id: nil, build_id: nil, build: nil)
74
+ assert_required_params(__method__, binding)
75
+
76
+ response = request(:post) do |req|
77
+ req.url "providers/#{team_id}/apps/#{app_id}/builds/#{build_id}/review"
78
+ req.body = build.to_json
79
+ req.headers['Content-Type'] = 'application/json'
80
+ end
81
+ handle_response(response)
82
+ end
83
+
84
+ ##
85
+ # @!group Groups API
86
+ ##
87
+
88
+ def get_groups(app_id: nil)
89
+ assert_required_params(__method__, binding)
90
+
91
+ response = request(:get, "/testflight/v2/providers/#{team_id}/apps/#{app_id}/groups")
92
+ handle_response(response)
93
+ end
94
+
95
+ def add_group_to_build(app_id: nil, group_id: nil, build_id: nil)
96
+ assert_required_params(__method__, binding)
97
+
98
+ body = {
99
+ 'groupId' => group_id,
100
+ 'buildId' => build_id
101
+ }
102
+ response = request(:put) do |req|
103
+ req.url "providers/#{team_id}/apps/#{app_id}/groups/#{group_id}/builds/#{build_id}"
104
+ req.body = body.to_json
105
+ req.headers['Content-Type'] = 'application/json'
106
+ end
107
+ handle_response(response)
108
+ end
109
+
110
+ ##
111
+ # @!group Testers API
112
+ ##
113
+
37
114
  def post_tester(app_id: nil, tester: nil)
38
115
  assert_required_params(__method__, binding)
116
+
39
117
  url = "providers/#{team_id}/apps/#{app_id}/testers"
40
118
  response = request(:post) do |req|
41
119
  req.url url
@@ -51,6 +129,7 @@ module Spaceship::TestFlight
51
129
 
52
130
  def put_tester_to_group(app_id: nil, tester_id: nil, group_id: nil)
53
131
  assert_required_params(__method__, binding)
132
+
54
133
  # Then we can add the tester to the group that allows the app to test
55
134
  # This is easy enough, we already have all this data. We don't need any response from the previous request
56
135
  url = "providers/#{team_id}/apps/#{app_id}/groups/#{group_id}/testers/#{tester_id}"
@@ -67,6 +146,7 @@ module Spaceship::TestFlight
67
146
 
68
147
  def delete_tester_from_group(group_id: nil, tester_id: nil, app_id: nil)
69
148
  assert_required_params(__method__, binding)
149
+
70
150
  url = "providers/#{team_id}/apps/#{app_id}/groups/#{group_id}/testers/#{tester_id}"
71
151
  response = request(:delete) do |req|
72
152
  req.url url
@@ -75,53 +155,25 @@ module Spaceship::TestFlight
75
155
  handle_response(response)
76
156
  end
77
157
 
78
- def get_build(app_id: nil, build_id: nil)
79
- assert_required_params(__method__, binding)
80
- response = request(:get, "providers/#{team_id}/apps/#{app_id}/builds/#{build_id}")
81
- handle_response(response)
82
- end
158
+ ##
159
+ # @!group TestInfo
160
+ ##
83
161
 
84
- def put_build(app_id: nil, build_id: nil, build: nil)
162
+ def put_testinfo(app_id: nil, testinfo: nil)
85
163
  assert_required_params(__method__, binding)
86
- response = request(:put) do |req|
87
- req.url "providers/#{team_id}/apps/#{app_id}/builds/#{build_id}"
88
- req.body = build.to_json
89
- req.headers['Content-Type'] = 'application/json'
90
- end
91
- handle_response(response)
92
- end
93
164
 
94
- def post_for_testflight_review(app_id: nil, build_id: nil, build: nil)
95
- assert_required_params(__method__, binding)
96
- response = request(:post) do |req|
97
- req.url "providers/#{team_id}/apps/#{app_id}/builds/#{build_id}/review"
98
- req.body = build.to_json
99
- req.headers['Content-Type'] = 'application/json'
100
- end
101
- handle_response(response)
102
- end
103
-
104
- def get_groups(app_id: nil)
105
- assert_required_params(__method__, binding)
106
- response = request(:get, "/testflight/v2/providers/#{team_id}/apps/#{app_id}/groups")
107
- handle_response(response)
108
- end
109
-
110
- def add_group_to_build(app_id: nil, group_id: nil, build_id: nil)
111
- body = {
112
- 'groupId' => group_id,
113
- 'buildId' => build_id
114
- }
115
165
  response = request(:put) do |req|
116
- req.url "providers/#{team_id}/apps/#{app_id}/groups/#{group_id}/builds/#{build_id}"
117
- req.body = body.to_json
166
+ req.url "providers/#{team_id}/apps/#{app_id}/testInfo"
167
+ req.body = testinfo.to_json
118
168
  req.headers['Content-Type'] = 'application/json'
119
169
  end
120
170
  handle_response(response)
121
171
  end
122
172
 
173
+ protected
174
+
123
175
  def handle_response(response)
124
- if (200..300).cover?(response.status) && response.body.empty?
176
+ if (200...300).cover?(response.status) && (response.body.nil? || response.body.empty?)
125
177
  return
126
178
  end
127
179
 
@@ -11,16 +11,13 @@ module Spaceship::TestFlight
11
11
  'id' => :id,
12
12
  'name' => :name,
13
13
  'isInternalGroup' => :is_internal_group,
14
+ 'appAdamId' => :app_id,
14
15
  'isDefaultExternalGroup' => :is_default_external_group
15
16
  })
16
17
 
17
18
  def self.all(app_id: nil)
18
19
  groups = client.get_groups(app_id: app_id)
19
- groups.map do |g|
20
- current_element = self.new(g)
21
- current_element.app_id = app_id
22
- current_element
23
- end
20
+ groups.map { |g| self.new(g) }
24
21
  end
25
22
 
26
23
  def self.find(app_id: nil, group_name: nil)
@@ -59,6 +56,22 @@ module Spaceship::TestFlight
59
56
  client.delete_tester_from_group(group_id: self.id, tester_id: tester.tester_id, app_id: self.app_id)
60
57
  end
61
58
 
59
+ def self.add_tester_to_groups!(tester: nil, app: nil, groups: nil)
60
+ if tester.kind_of?(Spaceship::Tunes::Tester::Internal)
61
+ self.internal_group(app_id: app.apple_id).add_tester!(tester)
62
+ else
63
+ self.perform_for_groups_in_app(app: app, groups: groups) { |group| group.add_tester!(tester) }
64
+ end
65
+ end
66
+
67
+ def self.remove_tester_from_groups!(tester: nil, app: nil, groups: nil)
68
+ if tester.kind_of?(Spaceship::Tunes::Tester::Internal)
69
+ self.internal_group(app_id: app.apple_id).remove_tester!(tester)
70
+ else
71
+ self.perform_for_groups_in_app(app: app, groups: groups) { |group| group.remove_tester!(tester) }
72
+ end
73
+ end
74
+
62
75
  def default_external_group?
63
76
  is_default_external_group
64
77
  end
@@ -66,5 +79,23 @@ module Spaceship::TestFlight
66
79
  def internal_group?
67
80
  is_internal_group
68
81
  end
82
+
83
+ def self.perform_for_groups_in_app(app: nil, groups: nil, &block)
84
+ if groups.nil?
85
+ default_external_group = app.default_external_group
86
+ if default_external_group.nil?
87
+ UI.user_error!("The app #{app.name} does not have a default external group. Please make sure to pass group names to the `:groups` option.")
88
+ end
89
+ test_flight_groups = [default_external_group]
90
+ else
91
+ test_flight_groups = self.filter_groups(app_id: app.apple_id) do |group|
92
+ groups.include?(group.name)
93
+ end
94
+
95
+ UI.user_error!("There are no groups available matching the names passed to the `:groups` option.") if test_flight_groups.empty?
96
+ end
97
+
98
+ test_flight_groups.each(&block)
99
+ end
69
100
  end
70
101
  end
@@ -30,5 +30,9 @@ module Spaceship::TestFlight
30
30
  def whats_new=(value)
31
31
  raw_data.each { |locale| locale['whatsNew'] = value }
32
32
  end
33
+
34
+ def deep_copy
35
+ TestInfo.new(raw_data.map(&:dup))
36
+ end
33
37
  end
34
38
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fastlane
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.28.5
4
+ version: 2.28.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Felix Krause
@@ -15,7 +15,7 @@ authors:
15
15
  autorequire:
16
16
  bindir: bin
17
17
  cert_chain: []
18
- date: 2017-04-26 00:00:00.000000000 Z
18
+ date: 2017-04-28 00:00:00.000000000 Z
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
21
21
  name: slack-notifier
@@ -743,6 +743,20 @@ dependencies:
743
743
  - - "~>"
744
744
  - !ruby/object:Gem::Version
745
745
  version: 0.8.1
746
+ - !ruby/object:Gem::Dependency
747
+ name: sinatra
748
+ requirement: !ruby/object:Gem::Requirement
749
+ requirements:
750
+ - - "~>"
751
+ - !ruby/object:Gem::Version
752
+ version: 1.4.8
753
+ type: :development
754
+ prerelease: false
755
+ version_requirements: !ruby/object:Gem::Requirement
756
+ requirements:
757
+ - - "~>"
758
+ - !ruby/object:Gem::Version
759
+ version: 1.4.8
746
760
  description: The easiest way to automate beta deployments and releases for your iOS
747
761
  and Android apps
748
762
  email: