fastlane 2.241.1 → 2.241.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/README.md +38 -38
- data/fastlane/lib/fastlane/commands_generator.rb +7 -1
- data/fastlane/lib/fastlane/server/socket_server.rb +57 -7
- data/fastlane/lib/fastlane/swift_lane_manager.rb +32 -11
- data/fastlane/lib/fastlane/version.rb +1 -1
- data/fastlane/swift/ArgumentProcessor.swift +10 -2
- data/fastlane/swift/Deliverfile.swift +1 -1
- data/fastlane/swift/DeliverfileProtocol.swift +1 -1
- data/fastlane/swift/Fastlane.swift +2 -2
- data/fastlane/swift/Gymfile.swift +1 -1
- data/fastlane/swift/GymfileProtocol.swift +1 -1
- data/fastlane/swift/MainProcess.swift +27 -0
- data/fastlane/swift/Matchfile.swift +1 -1
- data/fastlane/swift/MatchfileProtocol.swift +1 -1
- data/fastlane/swift/Precheckfile.swift +1 -1
- data/fastlane/swift/PrecheckfileProtocol.swift +1 -1
- data/fastlane/swift/RubyCommand.swift +1 -9
- data/fastlane/swift/RubyCommandable.swift +3 -2
- data/fastlane/swift/Scanfile.swift +1 -1
- data/fastlane/swift/ScanfileProtocol.swift +1 -1
- data/fastlane/swift/Screengrabfile.swift +1 -1
- data/fastlane/swift/ScreengrabfileProtocol.swift +1 -1
- data/fastlane/swift/Snapshotfile.swift +1 -1
- data/fastlane/swift/SnapshotfileProtocol.swift +1 -1
- data/fastlane/swift/main.swift +1 -1
- data/fastlane_core/lib/fastlane_core/itunes_transporter.rb +19 -5
- data/match/lib/match/runner.rb +2 -0
- data/match/lib/match/storage/gitlab/secure_file.rb +2 -1
- data/match/lib/match/storage/google_cloud_storage.rb +1 -1
- data/match/lib/match/storage/interface.rb +10 -0
- data/match/lib/match/storage/s3_storage.rb +1 -1
- data/sigh/lib/sigh/manager.rb +5 -1
- data/spaceship/lib/spaceship/client.rb +7 -1
- metadata +18 -18
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: de93b89428a3de2726024a5c924723c7fcb3f3096402884e4139749e8d22cb39
|
|
4
|
+
data.tar.gz: 25aa9615e76bb0f0e503ebf6cad2170178938f249027994b8b463ce45bd94403
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a3ae511471c7729e7854ac3aa91ea06d2ceb306e87b7e968aab9be77ce05d0ba6e3b127e3e720598380084686158c2b40b2e587cf78d402b5ec1a6aebcf4e42c
|
|
7
|
+
data.tar.gz: 84016a1687602afdde1ab12f6c6dacb1f22550c6ab3c3724e79638435dea908cdcf58735519523ba670074190caddb6bb4b0fb792c359b56b181751acb0347eb
|
data/README.md
CHANGED
|
@@ -30,12 +30,6 @@ If the above doesn't help, please [submit an issue](https://github.com/fastlane/
|
|
|
30
30
|
<!-- team:start -->
|
|
31
31
|
<table id='team'>
|
|
32
32
|
<tr>
|
|
33
|
-
<td id='stefan-natchev'>
|
|
34
|
-
<a href='https://github.com/snatchev'>
|
|
35
|
-
<img src='https://github.com/snatchev.png?size=200' width='140'>
|
|
36
|
-
</a>
|
|
37
|
-
<h4 align='center'><a href='https://twitter.com/snatchev'>Stefan Natchev</a></h4>
|
|
38
|
-
</td>
|
|
39
33
|
<td id='jan-piotrowski'>
|
|
40
34
|
<a href='https://github.com/janpio'>
|
|
41
35
|
<img src='https://github.com/janpio.png?size=200' width='140'>
|
|
@@ -48,43 +42,49 @@ If the above doesn't help, please [submit an issue](https://github.com/fastlane/
|
|
|
48
42
|
</a>
|
|
49
43
|
<h4 align='center'><a href='https://twitter.com/ibotpeaches'>Connor Tumbleson</a></h4>
|
|
50
44
|
</td>
|
|
45
|
+
<td id='olivier-halligon'>
|
|
46
|
+
<a href='https://github.com/AliSoftware'>
|
|
47
|
+
<img src='https://github.com/AliSoftware.png?size=200' width='140'>
|
|
48
|
+
</a>
|
|
49
|
+
<h4 align='center'><a href='https://twitter.com/aligatr'>Olivier Halligon</a></h4>
|
|
50
|
+
</td>
|
|
51
51
|
<td id='felix-krause'>
|
|
52
52
|
<a href='https://github.com/KrauseFx'>
|
|
53
53
|
<img src='https://github.com/KrauseFx.png?size=200' width='140'>
|
|
54
54
|
</a>
|
|
55
55
|
<h4 align='center'><a href='https://twitter.com/KrauseFx'>Felix Krause</a></h4>
|
|
56
56
|
</td>
|
|
57
|
-
<td id='
|
|
58
|
-
<a href='https://github.com/
|
|
59
|
-
<img src='https://github.com/
|
|
57
|
+
<td id='josh-holtz'>
|
|
58
|
+
<a href='https://github.com/joshdholtz'>
|
|
59
|
+
<img src='https://github.com/joshdholtz.png?size=200' width='140'>
|
|
60
60
|
</a>
|
|
61
|
-
<h4 align='center'><a href='https://twitter.com/
|
|
61
|
+
<h4 align='center'><a href='https://twitter.com/joshdholtz'>Josh Holtz</a></h4>
|
|
62
62
|
</td>
|
|
63
63
|
</tr>
|
|
64
64
|
<tr>
|
|
65
|
-
<td id='roger-oba'>
|
|
66
|
-
<a href='https://github.com/rogerluan'>
|
|
67
|
-
<img src='https://github.com/rogerluan.png?size=200' width='140'>
|
|
68
|
-
</a>
|
|
69
|
-
<h4 align='center'><a href='https://twitter.com/rogerluan_'>Roger Oba</a></h4>
|
|
70
|
-
</td>
|
|
71
65
|
<td id='joshua-liebowitz'>
|
|
72
66
|
<a href='https://github.com/taquitos'>
|
|
73
67
|
<img src='https://github.com/taquitos.png?size=200' width='140'>
|
|
74
68
|
</a>
|
|
75
69
|
<h4 align='center'><a href='https://twitter.com/taquitos'>Joshua Liebowitz</a></h4>
|
|
76
70
|
</td>
|
|
77
|
-
<td id='
|
|
78
|
-
<a href='https://github.com/
|
|
79
|
-
<img src='https://github.com/
|
|
71
|
+
<td id='jérôme-lacoste'>
|
|
72
|
+
<a href='https://github.com/lacostej'>
|
|
73
|
+
<img src='https://github.com/lacostej.png?size=200' width='140'>
|
|
80
74
|
</a>
|
|
81
|
-
<h4 align='center'><a href='https://twitter.com/
|
|
75
|
+
<h4 align='center'><a href='https://twitter.com/lacostej'>Jérôme Lacoste</a></h4>
|
|
82
76
|
</td>
|
|
83
|
-
<td id='
|
|
84
|
-
<a href='https://github.com/
|
|
85
|
-
<img src='https://github.com/
|
|
77
|
+
<td id='stefan-natchev'>
|
|
78
|
+
<a href='https://github.com/snatchev'>
|
|
79
|
+
<img src='https://github.com/snatchev.png?size=200' width='140'>
|
|
86
80
|
</a>
|
|
87
|
-
<h4 align='center'><a href='https://twitter.com/
|
|
81
|
+
<h4 align='center'><a href='https://twitter.com/snatchev'>Stefan Natchev</a></h4>
|
|
82
|
+
</td>
|
|
83
|
+
<td id='roger-oba'>
|
|
84
|
+
<a href='https://github.com/rogerluan'>
|
|
85
|
+
<img src='https://github.com/rogerluan.png?size=200' width='140'>
|
|
86
|
+
</a>
|
|
87
|
+
<h4 align='center'><a href='https://twitter.com/rogerluan_'>Roger Oba</a></h4>
|
|
88
88
|
</td>
|
|
89
89
|
</tr>
|
|
90
90
|
</table>
|
|
@@ -94,24 +94,24 @@ If the above doesn't help, please [submit an issue](https://github.com/fastlane/
|
|
|
94
94
|
_fastlane_ wouldn't be what it is today without the countless hours these former team members have dedicated to the project ❤️
|
|
95
95
|
|
|
96
96
|
<p id='alumni'>
|
|
97
|
-
<a href='https://github.com/
|
|
98
|
-
<a href='https://github.com/minuscorp'>Jorge Revuelta H</a>,
|
|
99
|
-
<a href='https://github.com/milch'>Manu Wallner</a>,
|
|
100
|
-
<a href='https://github.com/nafu'>Fumiya Nakamura</a>,
|
|
101
|
-
<a href='https://github.com/revolter'>Iulian Onofrei</a>,
|
|
102
|
-
<a href='https://github.com/ainame'>Satoshi Namai</a>,
|
|
103
|
-
<a href='https://github.com/getaaron'>Aaron Brager</a>,
|
|
104
|
-
<a href='https://github.com/hjanuschka'>Helmut Januschka</a>,
|
|
97
|
+
<a href='https://github.com/endocrimes'>Danielle Tomlinson</a>,
|
|
105
98
|
<a href='https://github.com/max-ott'>Max Ott</a>,
|
|
99
|
+
<a href='https://github.com/crazymanish'>Manish Rathi</a>,
|
|
100
|
+
<a href='https://github.com/lucgrabowski'>Łukasz Grabowski</a>,
|
|
106
101
|
<a href='https://github.com/mollyIV'>Daniel Jankowski</a>,
|
|
107
|
-
<a href='https://github.com/
|
|
102
|
+
<a href='https://github.com/ainame'>Satoshi Namai</a>,
|
|
103
|
+
<a href='https://github.com/getaaron'>Aaron Brager</a>,
|
|
104
|
+
<a href='https://github.com/jdee'>Jimmy Dee</a>,
|
|
105
|
+
<a href='https://github.com/revolter'>Iulian Onofrei</a>,
|
|
108
106
|
<a href='https://github.com/mgrebenets'>Maksym Grebenets</a>,
|
|
109
107
|
<a href='https://github.com/armcburney'>Andrew McBurney</a>,
|
|
110
|
-
<a href='https://github.com/
|
|
111
|
-
<a href='https://github.com/
|
|
112
|
-
<a href='https://github.com/
|
|
113
|
-
<a href='https://github.com/
|
|
114
|
-
<a href='https://github.com/
|
|
108
|
+
<a href='https://github.com/lmirosevic'>Luka Mirosevic</a>,
|
|
109
|
+
<a href='https://github.com/milch'>Manu Wallner</a>,
|
|
110
|
+
<a href='https://github.com/minuscorp'>Jorge Revuelta H</a>,
|
|
111
|
+
<a href='https://github.com/matthewellis'>Matthew Ellis</a>,
|
|
112
|
+
<a href='https://github.com/hjanuschka'>Helmut Januschka</a>,
|
|
113
|
+
<a href='https://github.com/nafu'>Fumiya Nakamura</a>,
|
|
114
|
+
<a href='https://github.com/giginet'>Kohki Miki</a>
|
|
115
115
|
</p>
|
|
116
116
|
<!-- team:end -->
|
|
117
117
|
|
|
@@ -144,11 +144,16 @@ module Fastlane
|
|
|
144
144
|
c.option('-s', '--stay_alive', 'Keeps socket server up even after error or disconnects, requires CTRL-C to kill.')
|
|
145
145
|
c.option('-c seconds', '--connection_timeout', 'Sets connection established timeout')
|
|
146
146
|
c.option('-p port', '--port', "Sets the port on localhost for the socket connection")
|
|
147
|
+
c.option('--token_file path', "File holding the token every command must carry (default: FASTLANE_SOCKET_SERVER_TOKEN_FILE)")
|
|
147
148
|
c.action do |args, options|
|
|
148
149
|
default_connection_timeout = 5
|
|
149
150
|
stay_alive = options.stay_alive || false
|
|
150
151
|
connection_timeout = options.connection_timeout || default_connection_timeout
|
|
151
152
|
port = options.port || 2000
|
|
153
|
+
token_file = options.token_file || ENV["FASTLANE_SOCKET_SERVER_TOKEN_FILE"]
|
|
154
|
+
token = File.read(token_file).strip if token_file
|
|
155
|
+
UI.user_error!("The token file #{token_file} is empty") if token_file && token.empty?
|
|
156
|
+
UI.important("No --token_file given: any local process that connects first can run actions") unless token
|
|
152
157
|
|
|
153
158
|
if stay_alive && options.connection_timeout.nil?
|
|
154
159
|
UI.important("stay_alive is set, but the connection timeout is not, this will give you #{default_connection_timeout} seconds to (re)connect")
|
|
@@ -162,7 +167,8 @@ module Fastlane
|
|
|
162
167
|
command_executor: command_executor,
|
|
163
168
|
connection_timeout: connection_timeout,
|
|
164
169
|
stay_alive: stay_alive,
|
|
165
|
-
port: port
|
|
170
|
+
port: port,
|
|
171
|
+
token: token
|
|
166
172
|
)
|
|
167
173
|
result = server.start
|
|
168
174
|
UI.success("Result: #{result}") if result
|
|
@@ -3,6 +3,7 @@ require 'fastlane/server/command_parser.rb'
|
|
|
3
3
|
require 'fastlane/server/json_return_value_processor.rb'
|
|
4
4
|
require 'socket'
|
|
5
5
|
require 'json'
|
|
6
|
+
require 'openssl'
|
|
6
7
|
|
|
7
8
|
module Fastlane
|
|
8
9
|
class SocketServer
|
|
@@ -20,7 +21,8 @@ module Fastlane
|
|
|
20
21
|
return_value_processor: nil,
|
|
21
22
|
connection_timeout: 5,
|
|
22
23
|
stay_alive: false,
|
|
23
|
-
port: 2000
|
|
24
|
+
port: 2000,
|
|
25
|
+
token: nil
|
|
24
26
|
)
|
|
25
27
|
if return_value_processor.nil?
|
|
26
28
|
return_value_processor = JSONReturnValueProcessor.new
|
|
@@ -31,6 +33,8 @@ module Fastlane
|
|
|
31
33
|
@connection_timeout = connection_timeout.to_i
|
|
32
34
|
@stay_alive = stay_alive
|
|
33
35
|
@port = port.to_i
|
|
36
|
+
# When set, every command must carry it: the runner reads it from a file only this user can read
|
|
37
|
+
@token = token
|
|
34
38
|
end
|
|
35
39
|
|
|
36
40
|
# this is the public API, don't call anything else
|
|
@@ -78,6 +82,11 @@ module Fastlane
|
|
|
78
82
|
end
|
|
79
83
|
|
|
80
84
|
def parse_and_execute_command(command_string: nil)
|
|
85
|
+
unless token_matches?(command_string)
|
|
86
|
+
UI.error("Refusing a command without this run's token: it does not come from the FastlaneRunner fastlane started")
|
|
87
|
+
return COMMAND_EXECUTION_STATE[:error]
|
|
88
|
+
end
|
|
89
|
+
|
|
81
90
|
command = CommandParser.parse(json: command_string)
|
|
82
91
|
case command
|
|
83
92
|
when ControlCommand
|
|
@@ -137,15 +146,17 @@ module Fastlane
|
|
|
137
146
|
end
|
|
138
147
|
|
|
139
148
|
def listen
|
|
140
|
-
@
|
|
141
|
-
|
|
149
|
+
@servers = listening_sockets
|
|
150
|
+
@port = @servers.first.local_address.ip_port
|
|
151
|
+
UI.verbose("Waiting for #{@connection_timeout} seconds for a connection from FastlaneRunner on port #{@port}")
|
|
142
152
|
|
|
143
|
-
# set thread
|
|
153
|
+
# set thread locals so the caller can tell the runner where to connect
|
|
154
|
+
Thread.current[:port] = @port
|
|
144
155
|
Thread.current[:ready] = true
|
|
145
156
|
@client = nil
|
|
146
157
|
begin
|
|
147
158
|
Timeout.timeout(@connection_timeout) do
|
|
148
|
-
@client =
|
|
159
|
+
@client = accept_first_connection
|
|
149
160
|
end
|
|
150
161
|
rescue Timeout::Error
|
|
151
162
|
UI.user_error!("fastlane failed to receive a connection from the FastlaneRunner binary after #{@connection_timeout} seconds, shutting down")
|
|
@@ -166,8 +177,47 @@ module Fastlane
|
|
|
166
177
|
@client.close
|
|
167
178
|
@client = nil
|
|
168
179
|
|
|
169
|
-
|
|
170
|
-
|
|
180
|
+
close_servers
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# A socket on every address localhost resolves to. The runner connects to
|
|
184
|
+
# `localhost` and tries those addresses in turn, so another process listening
|
|
185
|
+
# on one this server did not take would receive its commands.
|
|
186
|
+
def listening_sockets
|
|
187
|
+
# Port 0: a free port, the same on every address
|
|
188
|
+
return Socket.tcp_server_sockets('localhost', 0) if @port.zero?
|
|
189
|
+
|
|
190
|
+
sockets = []
|
|
191
|
+
Addrinfo.getaddrinfo('localhost', @port, nil, :STREAM).uniq(&:ip_address).each do |address|
|
|
192
|
+
sockets << address.listen
|
|
193
|
+
rescue SystemCallError => e
|
|
194
|
+
sockets.each(&:close)
|
|
195
|
+
UI.user_error!("fastlane could not listen on #{address.inspect_sockaddr} for the FastlaneRunner binary: #{e.message}")
|
|
196
|
+
end
|
|
197
|
+
sockets
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# The first connection on any of the sockets. They are closed once it is
|
|
201
|
+
# accepted, so nothing else can connect while the runner is talking to us.
|
|
202
|
+
def accept_first_connection
|
|
203
|
+
ready, = IO.select(@servers)
|
|
204
|
+
ready.first.accept.first
|
|
205
|
+
ensure
|
|
206
|
+
close_servers
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def token_matches?(command_string)
|
|
210
|
+
return true if @token.nil?
|
|
211
|
+
|
|
212
|
+
token = JSON.parse(command_string)['token']
|
|
213
|
+
token.kind_of?(String) && OpenSSL.secure_compare(token, @token)
|
|
214
|
+
rescue JSON::ParserError, TypeError
|
|
215
|
+
false
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def close_servers
|
|
219
|
+
(@servers || []).each { |server| server.close unless server.closed? }
|
|
220
|
+
@servers = nil
|
|
171
221
|
end
|
|
172
222
|
|
|
173
223
|
# record fastlane action command and then execute it
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
require_relative 'lane_manager_base.rb'
|
|
2
2
|
require_relative 'swift_runner_upgrader.rb'
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
require 'tmpdir'
|
|
3
5
|
|
|
4
6
|
module Fastlane
|
|
5
7
|
class SwiftLaneManager < LaneManagerBase
|
|
6
8
|
# The runner protocol version this fastlane ships, see main.swift
|
|
7
|
-
RUNNER_PROTOCOL_VERSION =
|
|
9
|
+
RUNNER_PROTOCOL_VERSION = 2
|
|
8
10
|
RUNNER_PROTOCOL_VERSION_REGEX = /FastlaneRunnerProtocolVersion \[(\d+)\]/
|
|
9
11
|
|
|
10
12
|
# @param lane_name The name of the lane to execute
|
|
@@ -31,15 +33,25 @@ module Fastlane
|
|
|
31
33
|
|
|
32
34
|
self.ensure_runner_built!
|
|
33
35
|
self.warn_if_runner_outdated
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
# A secret only this user can read, which the runner sends back with every command
|
|
37
|
+
token_dir = Dir.mktmpdir('fastlane-runner')
|
|
38
|
+
token_file = File.join(token_dir, 'token')
|
|
39
|
+
token = SecureRandom.hex(32)
|
|
40
|
+
File.write(token_file, token, perm: 0o600)
|
|
41
|
+
token = nil unless self.runner_sends_token?
|
|
42
|
+
# Port 0 lets the server pick a free port, which no other process can take beforehand
|
|
43
|
+
socket_thread = self.start_socket_thread(port: swift_server_port || 0, token: token)
|
|
44
|
+
sleep(0.250) while socket_thread[:ready].nil? && socket_thread.alive?
|
|
45
|
+
# The thread ended without listening: join raises its error rather than waiting forever
|
|
46
|
+
socket_thread.join unless socket_thread[:ready]
|
|
37
47
|
# wait on socket_thread to be in ready state, then start the runner thread
|
|
38
|
-
self.cruise_swift_lane_in_thread(lane, parameters,
|
|
48
|
+
self.cruise_swift_lane_in_thread(lane, parameters, socket_thread[:port], token_file)
|
|
39
49
|
|
|
40
50
|
socket_thread.value
|
|
41
51
|
rescue Exception => ex # rubocop:disable Lint/RescueException
|
|
42
52
|
e = ex
|
|
53
|
+
ensure
|
|
54
|
+
FileUtils.remove_entry(token_dir) if token_dir
|
|
43
55
|
end
|
|
44
56
|
# If we have a thread exception, drop that in the exception
|
|
45
57
|
# won't ever have a situation where e is non-nil, and socket_thread[:exception] is also non-nil
|
|
@@ -84,14 +96,14 @@ module Fastlane
|
|
|
84
96
|
end
|
|
85
97
|
end
|
|
86
98
|
|
|
87
|
-
def self.cruise_swift_lane_in_thread(lane, parameters = nil, swift_server_port)
|
|
99
|
+
def self.cruise_swift_lane_in_thread(lane, parameters = nil, swift_server_port, token_file)
|
|
88
100
|
if parameters.nil?
|
|
89
101
|
parameters = {}
|
|
90
102
|
end
|
|
91
103
|
|
|
92
104
|
parameter_string = ""
|
|
93
105
|
parameters.each do |key, value|
|
|
94
|
-
parameter_string += " #{key} #{value}"
|
|
106
|
+
parameter_string += " #{key.to_s.shellescape} #{value.to_s.shellescape}"
|
|
95
107
|
end
|
|
96
108
|
|
|
97
109
|
if FastlaneCore::Globals.verbose?
|
|
@@ -99,13 +111,16 @@ module Fastlane
|
|
|
99
111
|
end
|
|
100
112
|
|
|
101
113
|
parameter_string += " swiftServerPort #{swift_server_port}"
|
|
114
|
+
parameter_string += " swiftServerTokenFile #{token_file.shellescape}"
|
|
115
|
+
|
|
116
|
+
command = "#{FastlaneCore::FastlaneFolder.swift_runner_path.shellescape} lane #{lane.to_s.shellescape}#{parameter_string}"
|
|
102
117
|
|
|
103
118
|
return Thread.new do
|
|
104
119
|
if FastlaneCore::Globals.verbose?
|
|
105
|
-
return_value = Actions.sh(
|
|
120
|
+
return_value = Actions.sh(command)
|
|
106
121
|
UI.message("runner output: ".yellow + return_value)
|
|
107
122
|
else
|
|
108
|
-
Actions.sh(
|
|
123
|
+
Actions.sh("#{command} > /dev/null")
|
|
109
124
|
end
|
|
110
125
|
end
|
|
111
126
|
end
|
|
@@ -223,13 +238,13 @@ module Fastlane
|
|
|
223
238
|
return project_modified
|
|
224
239
|
end
|
|
225
240
|
|
|
226
|
-
def self.start_socket_thread(port: nil)
|
|
241
|
+
def self.start_socket_thread(port: nil, token: nil)
|
|
227
242
|
require 'fastlane/server/socket_server'
|
|
228
243
|
require 'fastlane/server/socket_server_action_command_executor'
|
|
229
244
|
|
|
230
245
|
return Thread.new do
|
|
231
246
|
command_executor = SocketServerActionCommandExecutor.new
|
|
232
|
-
server = Fastlane::SocketServer.new(command_executor: command_executor, port: port)
|
|
247
|
+
server = Fastlane::SocketServer.new(command_executor: command_executor, port: port, token: token)
|
|
233
248
|
server.start
|
|
234
249
|
end
|
|
235
250
|
end
|
|
@@ -245,6 +260,12 @@ module Fastlane
|
|
|
245
260
|
UI.important("Your FastlaneRunner is outdated for this version of fastlane. Run a lane outside CI, without `disable_runner_upgrades`, to update it, then commit the changes.")
|
|
246
261
|
end
|
|
247
262
|
|
|
263
|
+
# Runners from protocol version 2 send the run's token with every command; older ones get warn_if_runner_outdated.
|
|
264
|
+
# Version 1 was released before the token existed.
|
|
265
|
+
def self.runner_sends_token?
|
|
266
|
+
runner_protocol_version >= 2
|
|
267
|
+
end
|
|
268
|
+
|
|
248
269
|
def self.ensure_runner_built!
|
|
249
270
|
UI.verbose("Checking for new user-provided tool configuration files")
|
|
250
271
|
# if self.link_user_configs_to_project returns true, that means we need to rebuild the runner
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
module Fastlane
|
|
2
|
-
VERSION = '2.241.
|
|
2
|
+
VERSION = '2.241.2'.freeze
|
|
3
3
|
SUMMARY = "The easiest way to build and release mobile apps.".freeze
|
|
4
4
|
DESCRIPTION = "The easiest way to automate beta deployments and releases for your iOS and Android apps".freeze
|
|
5
5
|
MINIMUM_XCODE_RELEASE = "7.0".freeze
|
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
import Foundation
|
|
12
12
|
|
|
13
13
|
struct ArgumentProcessor {
|
|
14
|
+
/// The secret fastlane wrote to a file only this user can read: sent with every command so that
|
|
15
|
+
/// fastlane can tell this runner from another process that connected to its port
|
|
16
|
+
static var serverToken: String?
|
|
17
|
+
|
|
14
18
|
let args: [RunnerArgument]
|
|
15
19
|
let currentLane: String
|
|
16
20
|
let commandTimeout: Int
|
|
@@ -33,6 +37,10 @@ struct ArgumentProcessor {
|
|
|
33
37
|
|
|
34
38
|
port = UInt32(fastlaneArgsMinusLanes.first(where: { $0.name == "swiftServerPort" })?.value ?? "") ?? 2000
|
|
35
39
|
|
|
40
|
+
if let tokenFile = fastlaneArgsMinusLanes.first(where: { $0.name == "swiftServerTokenFile" })?.value {
|
|
41
|
+
ArgumentProcessor.serverToken = (try? String(contentsOfFile: tokenFile, encoding: .utf8))?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
42
|
+
}
|
|
43
|
+
|
|
36
44
|
// Configure logMode since we might need to use it before we finish parsing
|
|
37
45
|
if let logModeArg = potentialLogMode.first {
|
|
38
46
|
let logModeString = logModeArg.value
|
|
@@ -74,7 +82,7 @@ struct ArgumentProcessor {
|
|
|
74
82
|
func laneParameters() -> [String: String] {
|
|
75
83
|
let laneParametersArgs = args.filter { arg in
|
|
76
84
|
let lowercasedName = arg.name.lowercased()
|
|
77
|
-
return lowercasedName != "timeoutseconds" && lowercasedName != "lane" && lowercasedName != "logmode"
|
|
85
|
+
return lowercasedName != "timeoutseconds" && lowercasedName != "lane" && lowercasedName != "logmode" && lowercasedName != "swiftservertokenfile"
|
|
78
86
|
}
|
|
79
87
|
var laneParameters = [String: String]()
|
|
80
88
|
for arg in laneParametersArgs {
|
|
@@ -86,4 +94,4 @@ struct ArgumentProcessor {
|
|
|
86
94
|
|
|
87
95
|
// Please don't remove the lines below
|
|
88
96
|
// They are used to detect outdated files
|
|
89
|
-
// FastlaneRunnerAPIVersion [0.9.
|
|
97
|
+
// FastlaneRunnerAPIVersion [0.9.3]
|
|
@@ -12605,7 +12605,7 @@ public func updateProjectProvisioning(xcodeproj: OptionalConfigValue<String?> =
|
|
|
12605
12605
|
targetFilter: OptionalConfigValue<String?> = .fastlaneDefault(nil),
|
|
12606
12606
|
buildConfigurationFilter: OptionalConfigValue<String?> = .fastlaneDefault(nil),
|
|
12607
12607
|
buildConfiguration: OptionalConfigValue<String?> = .fastlaneDefault(nil),
|
|
12608
|
-
certificate: String = "/var/folders/
|
|
12608
|
+
certificate: String = "/var/folders/n6/hn37q6pj7sj_9dmqqc2113800000gn/T/AppleIncRootCertificate.cer",
|
|
12609
12609
|
codeSigningIdentity: OptionalConfigValue<String?> = .fastlaneDefault(nil))
|
|
12610
12610
|
{
|
|
12611
12611
|
let xcodeprojArg = xcodeproj.asRubyArgument(name: "xcodeproj", type: nil)
|
|
@@ -14245,4 +14245,4 @@ public let snapshotfile: Snapshotfile = .init()
|
|
|
14245
14245
|
|
|
14246
14246
|
// Please don't remove the lines below
|
|
14247
14247
|
// They are used to detect outdated files
|
|
14248
|
-
// FastlaneRunnerAPIVersion [0.9.
|
|
14248
|
+
// FastlaneRunnerAPIVersion [0.9.211]
|
|
@@ -42,12 +42,21 @@ class MainProcess {
|
|
|
42
42
|
killExistingSocketServerProcesses(port: argumentProcessor.port)
|
|
43
43
|
abortIfPortIsInUse(argumentProcessor.port)
|
|
44
44
|
|
|
45
|
+
let serverToken = createServerToken()
|
|
46
|
+
ArgumentProcessor.serverToken = serverToken.token
|
|
47
|
+
|
|
45
48
|
rubySocketCommand = Process()
|
|
46
49
|
rubySocketCommand.launchPath = "/usr/bin/env"
|
|
47
50
|
rubySocketCommand.arguments = fastlaneLaunchArguments() + ["socket_server", "-c", String(timeout), "-p", String(argumentProcessor.port)]
|
|
51
|
+
// An environment variable rather than --token_file, so that an older fastlane still starts
|
|
52
|
+
var environment = ProcessInfo.processInfo.environment
|
|
53
|
+
environment["FASTLANE_SOCKET_SERVER_TOKEN_FILE"] = serverToken.file
|
|
54
|
+
rubySocketCommand.environment = environment
|
|
48
55
|
rubySocketCommand.launch()
|
|
49
56
|
|
|
50
57
|
waitUntilSocketServerIsListening(port: argumentProcessor.port, serverProcess: rubySocketCommand)
|
|
58
|
+
// socket_server read the token when it started
|
|
59
|
+
try? FileManager.default.removeItem(atPath: serverToken.directory)
|
|
51
60
|
thread.start()
|
|
52
61
|
#endif
|
|
53
62
|
}
|
|
@@ -78,6 +87,24 @@ class MainProcess {
|
|
|
78
87
|
return ["bundle", "exec", "fastlane"]
|
|
79
88
|
}
|
|
80
89
|
|
|
90
|
+
/// A random token in a file only this user can read, which socket_server requires on every command and the runner sends
|
|
91
|
+
func createServerToken() -> (token: String, directory: String, file: String) {
|
|
92
|
+
var template = Array((NSTemporaryDirectory() as NSString).appendingPathComponent("fastlane-runner.XXXXXX").utf8CString)
|
|
93
|
+
guard let created = mkdtemp(&template), let random = FileHandle(forReadingAtPath: "/dev/urandom") else {
|
|
94
|
+
log(message: "Could not create the token for fastlane socket_server")
|
|
95
|
+
exit(1)
|
|
96
|
+
}
|
|
97
|
+
let directory = String(cString: created)
|
|
98
|
+
let token = random.readData(ofLength: 32).map { String(format: "%02x", $0) }.joined()
|
|
99
|
+
random.closeFile()
|
|
100
|
+
let file = directory + "/token"
|
|
101
|
+
guard token.count == 64, FileManager.default.createFile(atPath: file, contents: token.data(using: .utf8), attributes: [.posixPermissions: 0o600]) else {
|
|
102
|
+
log(message: "Could not create the token for fastlane socket_server")
|
|
103
|
+
exit(1)
|
|
104
|
+
}
|
|
105
|
+
return (token, directory, file)
|
|
106
|
+
}
|
|
107
|
+
|
|
81
108
|
/// The directory of the Gemfile Bundler would use: BUNDLE_GEMFILE, else the
|
|
82
109
|
/// nearest Gemfile or gems.rb from the working directory up.
|
|
83
110
|
func bundleRootDirectory() -> String? {
|
|
@@ -81,15 +81,7 @@ struct RubyCommand: RubyCommandable {
|
|
|
81
81
|
|
|
82
82
|
var closure: ((String) -> Void)? {
|
|
83
83
|
let callbacks = args.filter { ($0.type != nil) && $0.type == .stringClosure }
|
|
84
|
-
guard let callback = callbacks.first else {
|
|
85
|
-
return nil
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
guard let callbackArgValue = callback.value else {
|
|
89
|
-
return nil
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
guard let callbackClosure = callbackArgValue as? ((String) -> Void) else {
|
|
84
|
+
guard let callback = callbacks.first, let callbackArgValue = callback.value, let callbackClosure = callbackArgValue as? ((String) -> Void) else {
|
|
93
85
|
return nil
|
|
94
86
|
}
|
|
95
87
|
return callbackClosure
|
|
@@ -32,12 +32,13 @@ protocol RubyCommandable {
|
|
|
32
32
|
|
|
33
33
|
extension RubyCommandable {
|
|
34
34
|
var json: String {
|
|
35
|
+
let token = ArgumentProcessor.serverToken.map { ", \"token\": \"\($0)\"" } ?? ""
|
|
35
36
|
return """
|
|
36
|
-
{ "commandType": "\(type.token)", "command": \(commandJson) }
|
|
37
|
+
{ "commandType": "\(type.token)", "command": \(commandJson)\(token) }
|
|
37
38
|
"""
|
|
38
39
|
}
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
// Please don't remove the lines below
|
|
42
43
|
// They are used to detect outdated files
|
|
43
|
-
// FastlaneRunnerAPIVersion [0.9.
|
|
44
|
+
// FastlaneRunnerAPIVersion [0.9.3]
|
data/fastlane/swift/main.swift
CHANGED
|
@@ -12,7 +12,7 @@ import Foundation
|
|
|
12
12
|
|
|
13
13
|
let argumentProcessor = ArgumentProcessor(args: CommandLine.arguments)
|
|
14
14
|
/// fastlane reads this from the built runner, to tell whether it is outdated. Increase it when the runner changes how it talks to fastlane.
|
|
15
|
-
let runnerProtocolVersion = "FastlaneRunnerProtocolVersion [
|
|
15
|
+
let runnerProtocolVersion = "FastlaneRunnerProtocolVersion [2]"
|
|
16
16
|
verbose(message: runnerProtocolVersion)
|
|
17
17
|
let timeout = argumentProcessor.commandTimeout
|
|
18
18
|
|
|
@@ -58,30 +58,44 @@ module FastlaneCore
|
|
|
58
58
|
# Runs preparations before executing any command from the executor.
|
|
59
59
|
#
|
|
60
60
|
# @param original_api_key [Hash] api key containing the issuer id and private key
|
|
61
|
-
# @return [Hash] copy of `api_key` which includes an extra `key_dir` with the location of the .p8 file on disk
|
|
61
|
+
# @return [Hash] copy of `api_key` which includes an extra `key_dir` with the location of the .p8 file on disk,
|
|
62
|
+
# and `cleanup_path`: what #cleanup removes afterwards
|
|
62
63
|
def prepare(original_api_key:)
|
|
63
64
|
return if original_api_key.nil?
|
|
64
65
|
# Create .p8 file from api_key and provide api key info which contains .p8 file path
|
|
65
66
|
api_key = original_api_key.dup
|
|
67
|
+
p8_name = "AuthKey_#{api_key[:key_id]}.p8"
|
|
66
68
|
if self.kind_of?(ShellScriptTransporterExecutor)
|
|
67
69
|
# as of Transporter v3.3.0, the app is unable to detect the private keys under the 'private_keys' folder in current directory
|
|
68
70
|
# so we must rely on the other search paths in the Home dir:
|
|
69
71
|
# https://help.apple.com/itc/transporteruserguide/en.lproj/static.html#itc803b7be80
|
|
70
72
|
private_keys_dir = File.join(Dir.home, ".appstoreconnect/private_keys")
|
|
71
73
|
unless Dir.exist?(private_keys_dir)
|
|
72
|
-
FileUtils.mkdir_p(private_keys_dir)
|
|
74
|
+
FileUtils.mkdir_p(private_keys_dir, mode: 0o700)
|
|
73
75
|
end
|
|
74
76
|
api_key[:key_dir] = private_keys_dir
|
|
77
|
+
# Keys already there belong to the user: only remove the one written below
|
|
78
|
+
p8_path = File.join(private_keys_dir, p8_name)
|
|
79
|
+
api_key[:cleanup_path] = p8_path unless File.exist?(p8_path)
|
|
75
80
|
else
|
|
76
81
|
api_key[:key_dir] = Dir.mktmpdir("deliver-")
|
|
82
|
+
api_key[:cleanup_path] = api_key[:key_dir]
|
|
77
83
|
end
|
|
78
84
|
# Specified p8 needs to be generated to call altool or iTMSTransporter
|
|
79
|
-
File.
|
|
85
|
+
p8_path = File.join(api_key[:key_dir], p8_name)
|
|
86
|
+
File.open(p8_path, "wb", 0o600) do |p8|
|
|
80
87
|
p8.write(api_key[:key])
|
|
81
88
|
end
|
|
89
|
+
File.chmod(0o600, p8_path)
|
|
82
90
|
api_key
|
|
83
91
|
end
|
|
84
92
|
|
|
93
|
+
# Removes what #prepare wrote: its temporary directory, or the key file it added to ~/.appstoreconnect/private_keys
|
|
94
|
+
def cleanup(api_key)
|
|
95
|
+
return if api_key.nil? || api_key[:cleanup_path].nil?
|
|
96
|
+
FileUtils.rm_rf(api_key[:cleanup_path])
|
|
97
|
+
end
|
|
98
|
+
|
|
85
99
|
def execute(command, hide_output)
|
|
86
100
|
if Helper.test?
|
|
87
101
|
yield(nil) if block_given?
|
|
@@ -869,7 +883,7 @@ module FastlaneCore
|
|
|
869
883
|
return upload(app_id, dir, package_path: package_path, asset_path: asset_path)
|
|
870
884
|
ensure
|
|
871
885
|
if use_api_key
|
|
872
|
-
|
|
886
|
+
@transporter_executor.cleanup(api_key)
|
|
873
887
|
end
|
|
874
888
|
end
|
|
875
889
|
|
|
@@ -967,7 +981,7 @@ module FastlaneCore
|
|
|
967
981
|
return provider_ids
|
|
968
982
|
ensure
|
|
969
983
|
if use_api_key
|
|
970
|
-
|
|
984
|
+
@transporter_executor.cleanup(api_key)
|
|
971
985
|
end
|
|
972
986
|
end
|
|
973
987
|
|
data/match/lib/match/runner.rb
CHANGED
|
@@ -387,6 +387,8 @@ module Match
|
|
|
387
387
|
|
|
388
388
|
if Helper.mac?
|
|
389
389
|
installed_profile = FastlaneCore::ProvisioningProfile.install(stored_profile_path, keychain_path)
|
|
390
|
+
else
|
|
391
|
+
UI.message("Skipping installation of provisioning profile as it would not work on this operating system.")
|
|
390
392
|
end
|
|
391
393
|
|
|
392
394
|
if params[:output_path]
|
|
@@ -2,6 +2,7 @@ require 'open-uri'
|
|
|
2
2
|
require 'ostruct'
|
|
3
3
|
|
|
4
4
|
require_relative '../../module'
|
|
5
|
+
require_relative '../interface'
|
|
5
6
|
|
|
6
7
|
module Match
|
|
7
8
|
module Storage
|
|
@@ -36,7 +37,7 @@ module Match
|
|
|
36
37
|
url = URI("#{file_url}/download")
|
|
37
38
|
|
|
38
39
|
begin
|
|
39
|
-
destination_file =
|
|
40
|
+
destination_file = Storage.download_path(working_directory, @file.name)
|
|
40
41
|
|
|
41
42
|
create_subfolders(working_directory)
|
|
42
43
|
File.open(destination_file, "wb") do |saved_file|
|
|
@@ -158,7 +158,7 @@ module Match
|
|
|
158
158
|
|
|
159
159
|
bucket.files.each do |current_file|
|
|
160
160
|
file_path = current_file.name # e.g. "N8X438SEU2/certs/distribution/XD9G7QCACF.cer"
|
|
161
|
-
download_path =
|
|
161
|
+
download_path = Storage.download_path(self.working_directory, file_path)
|
|
162
162
|
|
|
163
163
|
FileUtils.mkdir_p(File.expand_path("..", download_path))
|
|
164
164
|
UI.verbose("Downloading file from Google Cloud Storage '#{file_path}' on bucket #{self.bucket_name}")
|
|
@@ -2,6 +2,16 @@ require_relative '../module'
|
|
|
2
2
|
|
|
3
3
|
module Match
|
|
4
4
|
module Storage
|
|
5
|
+
# Where a file named `remote_name` by the storage is written in `working_directory`.
|
|
6
|
+
# Fails when the name would place it outside, e.g. with `..` segments.
|
|
7
|
+
def self.download_path(working_directory, remote_name)
|
|
8
|
+
root = File.expand_path(working_directory)
|
|
9
|
+
path = File.expand_path(File.join(root, remote_name))
|
|
10
|
+
return path if path.start_with?(root + File::SEPARATOR)
|
|
11
|
+
|
|
12
|
+
UI.user_error!("Refusing to download #{remote_name.inspect}: it would be written outside #{root}")
|
|
13
|
+
end
|
|
14
|
+
|
|
5
15
|
class Interface
|
|
6
16
|
MATCH_VERSION_FILE_NAME = "match_version.txt"
|
|
7
17
|
# The working directory in which we download all the profiles
|
|
@@ -114,7 +114,7 @@ module Match
|
|
|
114
114
|
file_path = strip_s3_object_prefix(object.key) # :s3_object_prefix:team_id/path/to/file
|
|
115
115
|
|
|
116
116
|
# strip s3_prefix from file_path
|
|
117
|
-
download_path =
|
|
117
|
+
download_path = Storage.download_path(self.working_directory, file_path)
|
|
118
118
|
|
|
119
119
|
FileUtils.mkdir_p(File.expand_path("..", download_path))
|
|
120
120
|
UI.verbose("Downloading file from S3 '#{file_path}' on bucket #{self.s3_bucket}")
|
data/sigh/lib/sigh/manager.rb
CHANGED
|
@@ -41,7 +41,11 @@ module Sigh
|
|
|
41
41
|
ENV["SIGH_UDID"] = ENV["SIGH_UUID"] = uuid if uuid
|
|
42
42
|
ENV["SIGH_NAME"] = name if name
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
if Helper.mac?
|
|
45
|
+
FastlaneCore::ProvisioningProfile.install(profile, keychain_path)
|
|
46
|
+
else
|
|
47
|
+
UI.message("Skipping installation of provisioning profile as it would not work on this operating system.")
|
|
48
|
+
end
|
|
45
49
|
end
|
|
46
50
|
end
|
|
47
51
|
end
|
|
@@ -252,6 +252,8 @@ module Spaceship
|
|
|
252
252
|
else
|
|
253
253
|
# Log to file by default
|
|
254
254
|
path = File.join(Dir.tmpdir, "spaceship#{Time.now.to_i}_#{Process.pid}_#{Thread.current.object_id}.log")
|
|
255
|
+
# The log holds every request and response body: create it readable by its owner only
|
|
256
|
+
File.open(path, File::WRONLY | File::CREAT | File::APPEND, 0o600) {}
|
|
255
257
|
@logger = Logger.new(path)
|
|
256
258
|
end
|
|
257
259
|
|
|
@@ -279,10 +281,12 @@ module Spaceship
|
|
|
279
281
|
def store_cookie(path: nil)
|
|
280
282
|
path ||= persistent_cookie_path
|
|
281
283
|
FileUtils.mkdir_p(File.expand_path("..", path))
|
|
284
|
+
File.chmod(0o700, File.expand_path("..", path))
|
|
282
285
|
|
|
283
286
|
# really important to specify the session to true
|
|
284
287
|
# otherwise myacinfo and more won't be stored
|
|
285
288
|
@cookie.save(path, :yaml, session: true)
|
|
289
|
+
File.chmod(0o600, path)
|
|
286
290
|
return File.read(path)
|
|
287
291
|
end
|
|
288
292
|
|
|
@@ -880,6 +884,8 @@ module Spaceship
|
|
|
880
884
|
begin
|
|
881
885
|
if File.exist?(persistent_cookie_path)
|
|
882
886
|
puts("Loading session from '#{persistent_cookie_path}'") if Spaceship::Globals.verbose?
|
|
887
|
+
File.chmod(0o700, File.expand_path("..", persistent_cookie_path))
|
|
888
|
+
File.chmod(0o600, persistent_cookie_path)
|
|
883
889
|
@cookie.load(persistent_cookie_path)
|
|
884
890
|
return true
|
|
885
891
|
end
|
|
@@ -1144,7 +1150,7 @@ module Spaceship
|
|
|
1144
1150
|
|
|
1145
1151
|
def log_response(method, url, response, headers = nil, &block)
|
|
1146
1152
|
url ||= extract_key_from_block('url', &block)
|
|
1147
|
-
body = response.body.kind_of?(String) ? response.body.force_encoding(Encoding::UTF_8) : response.body
|
|
1153
|
+
body = response.body.kind_of?(String) ? response.body.dup.force_encoding(Encoding::UTF_8) : response.body
|
|
1148
1154
|
logger.debug("<< #{method.upcase} #{url}: #{response.status} #{body}")
|
|
1149
1155
|
end
|
|
1150
1156
|
|
metadata
CHANGED
|
@@ -1,36 +1,36 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fastlane
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.241.
|
|
4
|
+
version: 2.241.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
|
-
- Felix Krause
|
|
8
|
-
- Jimmy Dee
|
|
9
|
-
- Connor Tumbleson
|
|
10
|
-
- Manish Rathi
|
|
11
|
-
- Daniel Jankowski
|
|
12
|
-
- Kohki Miki
|
|
13
7
|
- Josh Holtz
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
- Olivier Halligon
|
|
8
|
+
- Jimmy Dee
|
|
9
|
+
- Joshua Liebowitz
|
|
17
10
|
- Max Ott
|
|
11
|
+
- Olivier Halligon
|
|
18
12
|
- Helmut Januschka
|
|
19
|
-
- Luka Mirosevic
|
|
20
13
|
- Satoshi Namai
|
|
21
|
-
- Roger Oba
|
|
22
14
|
- Danielle Tomlinson
|
|
23
|
-
-
|
|
15
|
+
- Manish Rathi
|
|
24
16
|
- Iulian Onofrei
|
|
25
|
-
-
|
|
26
|
-
- Jan Piotrowski
|
|
27
|
-
- Jérôme Lacoste
|
|
17
|
+
- Kohki Miki
|
|
28
18
|
- Jorge Revuelta H
|
|
19
|
+
- Andrew McBurney
|
|
20
|
+
- Connor Tumbleson
|
|
21
|
+
- Jan Piotrowski
|
|
29
22
|
- Maksym Grebenets
|
|
30
|
-
-
|
|
31
|
-
-
|
|
23
|
+
- Manu Wallner
|
|
24
|
+
- Stefan Natchev
|
|
32
25
|
- Łukasz Grabowski
|
|
33
26
|
- Matthew Ellis
|
|
27
|
+
- Roger Oba
|
|
28
|
+
- Luka Mirosevic
|
|
29
|
+
- Aaron Brager
|
|
30
|
+
- Fumiya Nakamura
|
|
31
|
+
- Jérôme Lacoste
|
|
32
|
+
- Daniel Jankowski
|
|
33
|
+
- Felix Krause
|
|
34
34
|
bindir: bin
|
|
35
35
|
cert_chain: []
|
|
36
36
|
date: 1980-01-02 00:00:00.000000000 Z
|