fastlane 2.154.0 → 2.156.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +74 -74
  3. data/deliver/lib/deliver/app_screenshot_iterator.rb +98 -0
  4. data/deliver/lib/deliver/html_generator.rb +8 -1
  5. data/deliver/lib/deliver/queue_worker.rb +64 -0
  6. data/deliver/lib/deliver/upload_screenshots.rb +122 -125
  7. data/fastlane/lib/fastlane/actions/create_keychain.rb +5 -1
  8. data/fastlane/lib/fastlane/actions/sync_code_signing.rb +5 -0
  9. data/fastlane/lib/fastlane/version.rb +1 -1
  10. data/fastlane/swift/Deliverfile.swift +1 -1
  11. data/fastlane/swift/DeliverfileProtocol.swift +1 -1
  12. data/fastlane/swift/Fastlane.swift +375 -182
  13. data/fastlane/swift/Gymfile.swift +1 -1
  14. data/fastlane/swift/GymfileProtocol.swift +1 -1
  15. data/fastlane/swift/Matchfile.swift +1 -1
  16. data/fastlane/swift/MatchfileProtocol.swift +6 -2
  17. data/fastlane/swift/Precheckfile.swift +1 -1
  18. data/fastlane/swift/PrecheckfileProtocol.swift +1 -1
  19. data/fastlane/swift/Scanfile.swift +1 -1
  20. data/fastlane/swift/ScanfileProtocol.swift +1 -1
  21. data/fastlane/swift/Screengrabfile.swift +1 -1
  22. data/fastlane/swift/ScreengrabfileProtocol.swift +1 -1
  23. data/fastlane/swift/Snapshotfile.swift +1 -1
  24. data/fastlane/swift/SnapshotfileProtocol.swift +5 -1
  25. data/fastlane_core/lib/fastlane_core/project.rb +1 -0
  26. data/gym/lib/gym/detect_values.rb +6 -3
  27. data/gym/lib/gym/generators/build_command_generator.rb +0 -1
  28. data/gym/lib/gym/module.rb +22 -0
  29. data/gym/lib/gym/runner.rb +8 -10
  30. data/match/lib/match/generator.rb +6 -0
  31. data/match/lib/match/options.rb +7 -2
  32. data/match/lib/match/runner.rb +12 -5
  33. data/match/lib/match/spaceship_ensure.rb +7 -9
  34. data/match/lib/match/storage/google_cloud_storage.rb +1 -1
  35. data/scan/lib/scan/test_command_generator.rb +3 -1
  36. data/screengrab/lib/screengrab/runner.rb +7 -7
  37. data/sigh/lib/sigh/download_all.rb +42 -27
  38. data/sigh/lib/sigh/module.rb +26 -0
  39. data/sigh/lib/sigh/options.rb +2 -2
  40. data/sigh/lib/sigh/runner.rb +100 -35
  41. data/snapshot/lib/snapshot/options.rb +5 -0
  42. data/snapshot/lib/snapshot/test_command_generator.rb +3 -2
  43. data/snapshot/lib/snapshot/test_command_generator_base.rb +3 -1
  44. data/snapshot/lib/snapshot/test_command_generator_xcode_8.rb +4 -1
  45. data/spaceship/lib/spaceship/.client.rb.swp +0 -0
  46. data/spaceship/lib/spaceship/.spaceauth_runner.rb.swp +0 -0
  47. data/spaceship/lib/spaceship/.two_step_or_factor_client.rb.swp +0 -0
  48. data/spaceship/lib/spaceship/connect_api/client.rb +2 -0
  49. data/spaceship/lib/spaceship/connect_api/model.rb +1 -1
  50. data/spaceship/lib/spaceship/connect_api/models/app.rb +3 -1
  51. data/spaceship/lib/spaceship/connect_api/models/bundle_id.rb +17 -5
  52. data/spaceship/lib/spaceship/connect_api/models/bundle_id_capability.rb +41 -7
  53. data/spaceship/lib/spaceship/connect_api/models/profile.rb +32 -1
  54. data/spaceship/lib/spaceship/connect_api/provisioning/client.rb +46 -4
  55. data/spaceship/lib/spaceship/connect_api/provisioning/provisioning.rb +41 -0
  56. data/supply/lib/supply/client.rb +2 -1
  57. data/supply/lib/supply/options.rb +8 -1
  58. metadata +24 -19
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 70c5a33bcf7a92f427b7c09b26e1269a00807a2975f484c6be1159df830a0ffb
4
- data.tar.gz: 36610a4cd508042f7a9632fe30088e84609b805bb6722f5b2752589a69cce12a
3
+ metadata.gz: 70c9faed2541a1cead4b030cf41328487f06a8b17e129a6d4ad8428de8fcaf25
4
+ data.tar.gz: 262293381bd7435f0b9dcac69ed77870c31eb59cfb023a97b09e747e89f7015a
5
5
  SHA512:
6
- metadata.gz: 6195726b19d99f0d0b7b8980e2facb389f18189bbba530d09d7dabdc60c6959e1a00b7bc4c4f8c43a0b6bdfc4211d580909ef8e941a83fb061583bae1eec0cae
7
- data.tar.gz: f0854bb26324cb229428f971159d2b7713fb137ef5c37dff1eee843bb52652afc3bf4621bf98aa0438b94fa476971bd064546ad4a5780108dba4475f438c74a8
6
+ metadata.gz: c27c2225cc7459a8d10e3c9b91dff5fa878d2b5c07f4cb11991132422f1de975a4549b5d0b460c9e213c7cb3c255e4c7caa655ab9b3369a78cdd7e6eb7258ff6
7
+ data.tar.gz: 37b00b6000617888e7940b7a55c2b57ad8cc8575d119c3583bfab9d10244831dc6007093d9e4d3933513abccd889f8eb6a6fcc14bcf6692e35a693ade259a769
data/README.md CHANGED
@@ -34,43 +34,55 @@ If the above doesn't help, please [submit an issue](https://github.com/fastlane/
34
34
  <!-- This table is regenerated and resorted on each release -->
35
35
  <table id='team'>
36
36
  <tr>
37
+ <td id='fumiya-nakamura'>
38
+ <a href='https://github.com/nafu'>
39
+ <img src='https://github.com/nafu.png?size=140'>
40
+ </a>
41
+ <h4 align='center'><a href='https://twitter.com/nafu003'>Fumiya Nakamura</a></h4>
42
+ </td>
43
+ <td id='danielle-tomlinson'>
44
+ <a href='https://github.com/endocrimes'>
45
+ <img src='https://github.com/endocrimes.png?size=140'>
46
+ </a>
47
+ <h4 align='center'><a href='https://twitter.com/endocrimes'>Danielle Tomlinson</a></h4>
48
+ </td>
37
49
  <td id='jérôme-lacoste'>
38
50
  <a href='https://github.com/lacostej'>
39
51
  <img src='https://github.com/lacostej.png?size=140'>
40
52
  </a>
41
53
  <h4 align='center'><a href='https://twitter.com/lacostej'>Jérôme Lacoste</a></h4>
42
54
  </td>
55
+ <td id='olivier-halligon'>
56
+ <a href='https://github.com/AliSoftware'>
57
+ <img src='https://github.com/AliSoftware.png?size=140'>
58
+ </a>
59
+ <h4 align='center'><a href='https://twitter.com/aligatr'>Olivier Halligon</a></h4>
60
+ </td>
43
61
  <td id='kohki-miki'>
44
62
  <a href='https://github.com/giginet'>
45
63
  <img src='https://github.com/giginet.png?size=140'>
46
64
  </a>
47
65
  <h4 align='center'><a href='https://twitter.com/giginet'>Kohki Miki</a></h4>
48
66
  </td>
49
- <td id='maksym-grebenets'>
50
- <a href='https://github.com/mgrebenets'>
51
- <img src='https://github.com/mgrebenets.png?size=140'>
52
- </a>
53
- <h4 align='center'><a href='https://twitter.com/mgrebenets'>Maksym Grebenets</a></h4>
54
- </td>
55
- <td id='daniel-jankowski'>
56
- <a href='https://github.com/mollyIV'>
57
- <img src='https://github.com/mollyIV.png?size=140'>
67
+ </tr>
68
+ <tr>
69
+ <td id='jorge-revuelta-h'>
70
+ <a href='https://github.com/minuscorp'>
71
+ <img src='https://github.com/minuscorp.png?size=140'>
58
72
  </a>
59
- <h4 align='center'><a href='https://twitter.com/mollyIV'>Daniel Jankowski</a></h4>
73
+ <h4 align='center'><a href='https://twitter.com/minuscorp'>Jorge Revuelta H</a></h4>
60
74
  </td>
61
- <td id='olivier-halligon'>
62
- <a href='https://github.com/AliSoftware'>
63
- <img src='https://github.com/AliSoftware.png?size=140'>
75
+ <td id='luka-mirosevic'>
76
+ <a href='https://github.com/lmirosevic'>
77
+ <img src='https://github.com/lmirosevic.png?size=140'>
64
78
  </a>
65
- <h4 align='center'><a href='https://twitter.com/aligatr'>Olivier Halligon</a></h4>
79
+ <h4 align='center'><a href='https://twitter.com/lmirosevic'>Luka Mirosevic</a></h4>
66
80
  </td>
67
- </tr>
68
- <tr>
69
- <td id='stefan-natchev'>
70
- <a href='https://github.com/snatchev'>
71
- <img src='https://github.com/snatchev.png?size=140'>
81
+ <td id='manu-wallner'>
82
+ <a href='https://github.com/milch'>
83
+ <img src='https://github.com/milch.png?size=140'>
72
84
  </a>
73
- <h4 align='center'><a href='https://twitter.com/snatchev'>Stefan Natchev</a></h4>
85
+ <h4 align='center'><a href='https://twitter.com/acrooow'>Manu Wallner</a></h4>
74
86
  </td>
75
87
  <td id='jan-piotrowski'>
76
88
  <a href='https://github.com/janpio'>
@@ -84,63 +96,51 @@ If the above doesn't help, please [submit an issue](https://github.com/fastlane/
84
96
  </a>
85
97
  <h4 align='center'><a href='https://twitter.com/hjanuschka'>Helmut Januschka</a></h4>
86
98
  </td>
87
- <td id='fumiya-nakamura'>
88
- <a href='https://github.com/nafu'>
89
- <img src='https://github.com/nafu.png?size=140'>
90
- </a>
91
- <h4 align='center'><a href='https://twitter.com/nafu003'>Fumiya Nakamura</a></h4>
92
- </td>
99
+ </tr>
100
+ <tr>
93
101
  <td id='iulian-onofrei'>
94
102
  <a href='https://github.com/revolter'>
95
103
  <img src='https://github.com/revolter.png?size=140'>
96
104
  </a>
97
105
  <h4 align='center'><a href='https://twitter.com/Revolt666'>Iulian Onofrei</a></h4>
98
106
  </td>
99
- </tr>
100
- <tr>
101
- <td id='danielle-tomlinson'>
102
- <a href='https://github.com/endocrimes'>
103
- <img src='https://github.com/endocrimes.png?size=140'>
104
- </a>
105
- <h4 align='center'><a href='https://twitter.com/endocrimes'>Danielle Tomlinson</a></h4>
106
- </td>
107
- <td id='josh-holtz'>
108
- <a href='https://github.com/joshdholtz'>
109
- <img src='https://github.com/joshdholtz.png?size=140'>
107
+ <td id='matthew-ellis'>
108
+ <a href='https://github.com/matthewellis'>
109
+ <img src='https://github.com/matthewellis.png?size=140'>
110
110
  </a>
111
- <h4 align='center'><a href='https://twitter.com/joshdholtz'>Josh Holtz</a></h4>
111
+ <h4 align='center'><a href='https://twitter.com/mellis1995'>Matthew Ellis</a></h4>
112
112
  </td>
113
- <td id='jimmy-dee'>
114
- <a href='https://github.com/jdee'>
115
- <img src='https://github.com/jdee.png?size=140'>
113
+ <td id='max-ott'>
114
+ <a href='https://github.com/max-ott'>
115
+ <img src='https://github.com/max-ott.png?size=140'>
116
116
  </a>
117
- <h4 align='center'>Jimmy Dee</h4>
117
+ <h4 align='center'><a href='https://twitter.com/ott_max'>Max Ott</a></h4>
118
118
  </td>
119
- <td id='joshua-liebowitz'>
120
- <a href='https://github.com/taquitos'>
121
- <img src='https://github.com/taquitos.png?size=140'>
119
+ <td id='daniel-jankowski'>
120
+ <a href='https://github.com/mollyIV'>
121
+ <img src='https://github.com/mollyIV.png?size=140'>
122
122
  </a>
123
- <h4 align='center'><a href='https://twitter.com/taquitos'>Joshua Liebowitz</a></h4>
123
+ <h4 align='center'><a href='https://twitter.com/mollyIV'>Daniel Jankowski</a></h4>
124
124
  </td>
125
- <td id='andrew-mcburney'>
126
- <a href='https://github.com/armcburney'>
127
- <img src='https://github.com/armcburney.png?size=140'>
125
+ <td id='aaron-brager'>
126
+ <a href='https://github.com/getaaron'>
127
+ <img src='https://github.com/getaaron.png?size=140'>
128
128
  </a>
129
- <h4 align='center'><a href='https://twitter.com/armcburney'>Andrew McBurney</a></h4>
129
+ <h4 align='center'><a href='https://twitter.com/getaaron'>Aaron Brager</a></h4>
130
130
  </td>
131
131
  </tr>
132
132
  <tr>
133
- <td id='manu-wallner'>
134
- <a href='https://github.com/milch'>
135
- <img src='https://github.com/milch.png?size=140'>
133
+ <td id='jimmy-dee'>
134
+ <a href='https://github.com/jdee'>
135
+ <img src='https://github.com/jdee.png?size=140'>
136
136
  </a>
137
- <h4 align='center'><a href='https://twitter.com/acrooow'>Manu Wallner</a></h4>
137
+ <h4 align='center'>Jimmy Dee</h4>
138
138
  </td>
139
- <td id='matthew-ellis'>
140
- <a href='https://github.com/matthewellis'>
141
- <img src='https://github.com/matthewellis.png?size=140'>
139
+ <td id='josh-holtz'>
140
+ <a href='https://github.com/joshdholtz'>
141
+ <img src='https://github.com/joshdholtz.png?size=140'>
142
142
  </a>
143
- <h4 align='center'><a href='https://twitter.com/mellis1995'>Matthew Ellis</a></h4>
143
+ <h4 align='center'><a href='https://twitter.com/joshdholtz'>Josh Holtz</a></h4>
144
144
  </td>
145
145
  <td id='felix-krause'>
146
146
  <a href='https://github.com/KrauseFx'>
@@ -148,31 +148,31 @@ If the above doesn't help, please [submit an issue](https://github.com/fastlane/
148
148
  </a>
149
149
  <h4 align='center'><a href='https://twitter.com/KrauseFx'>Felix Krause</a></h4>
150
150
  </td>
151
- <td id='jorge-revuelta-h'>
152
- <a href='https://github.com/minuscorp'>
153
- <img src='https://github.com/minuscorp.png?size=140'>
151
+ <td id='joshua-liebowitz'>
152
+ <a href='https://github.com/taquitos'>
153
+ <img src='https://github.com/taquitos.png?size=140'>
154
154
  </a>
155
- <h4 align='center'><a href='https://twitter.com/minuscorp'>Jorge Revuelta H</a></h4>
155
+ <h4 align='center'><a href='https://twitter.com/taquitos'>Joshua Liebowitz</a></h4>
156
156
  </td>
157
- <td id='aaron-brager'>
158
- <a href='https://github.com/getaaron'>
159
- <img src='https://github.com/getaaron.png?size=140'>
157
+ <td id='maksym-grebenets'>
158
+ <a href='https://github.com/mgrebenets'>
159
+ <img src='https://github.com/mgrebenets.png?size=140'>
160
160
  </a>
161
- <h4 align='center'><a href='https://twitter.com/getaaron'>Aaron Brager</a></h4>
161
+ <h4 align='center'><a href='https://twitter.com/mgrebenets'>Maksym Grebenets</a></h4>
162
162
  </td>
163
163
  </tr>
164
164
  <tr>
165
- <td id='max-ott'>
166
- <a href='https://github.com/max-ott'>
167
- <img src='https://github.com/max-ott.png?size=140'>
165
+ <td id='stefan-natchev'>
166
+ <a href='https://github.com/snatchev'>
167
+ <img src='https://github.com/snatchev.png?size=140'>
168
168
  </a>
169
- <h4 align='center'><a href='https://twitter.com/ott_max'>Max Ott</a></h4>
169
+ <h4 align='center'><a href='https://twitter.com/snatchev'>Stefan Natchev</a></h4>
170
170
  </td>
171
- <td id='luka-mirosevic'>
172
- <a href='https://github.com/lmirosevic'>
173
- <img src='https://github.com/lmirosevic.png?size=140'>
171
+ <td id='andrew-mcburney'>
172
+ <a href='https://github.com/armcburney'>
173
+ <img src='https://github.com/armcburney.png?size=140'>
174
174
  </a>
175
- <h4 align='center'><a href='https://twitter.com/lmirosevic'>Luka Mirosevic</a></h4>
175
+ <h4 align='center'><a href='https://twitter.com/armcburney'>Andrew McBurney</a></h4>
176
176
  </td>
177
177
  </table>
178
178
 
@@ -0,0 +1,98 @@
1
+ module Deliver
2
+ # This is a convinient class that enumerates app store connect's screenshots in various degrees.
3
+ class AppScreenshotIterator
4
+ NUMBER_OF_THREADS = Helper.test? ? 1 : [ENV.fetch("DELIVER_NUMBER_OF_THREADS", 10).to_i, 10].min
5
+
6
+ # @param localizations [Array<Spaceship::ConnectAPI::AppStoreVersionLocalization>]
7
+ def initialize(localizations)
8
+ @localizations = localizations
9
+ end
10
+
11
+ # Iterate app_screenshot_set over localizations
12
+ #
13
+ # @yield [localization, app_screenshot_set]
14
+ # @yieldparam [optional, Spaceship::ConnectAPI::AppStoreVersionLocalization] localization
15
+ # @yieldparam [optional, Spaceship::ConnectAPI::AppStoreScreenshotSet] app_screenshot_set
16
+ def each_app_screenshot_set(&block)
17
+ return enum_for(__method__) unless block_given?
18
+
19
+ # Collect app_screenshot_sets from localizations in parallel but
20
+ # limit the number of threads working at a time with using `lazy` and `force` controls
21
+ # to not attack App Store Connect
22
+ results = @localizations.each_slice(NUMBER_OF_THREADS).lazy.map do |localizations|
23
+ localizations.map do |localization|
24
+ Thread.new do
25
+ [localization, localization.get_app_screenshot_sets]
26
+ end
27
+ end
28
+ end.flat_map do |threads|
29
+ threads.map { |t| t.join.value }
30
+ end.force
31
+
32
+ results.each do |localization, app_screenshot_sets|
33
+ app_screenshot_sets.each do |app_screenshot_set|
34
+ yield(localization, app_screenshot_set)
35
+ end
36
+ end
37
+ end
38
+
39
+ # Iterate app_screenshot over localizations and app_screenshot_sets
40
+ #
41
+ # @yield [localization, app_screenshot_set, app_screenshot]
42
+ # @yieldparam [optional, Spaceship::ConnectAPI::AppStoreVersionLocalization] localization
43
+ # @yieldparam [optional, Spaceship::ConnectAPI::AppStoreScreenshotSet] app_screenshot_set
44
+ # @yieldparam [optional, Spaceship::ConnectAPI::AppStoreScreenshot] app_screenshot
45
+ def each_app_screenshot(&block)
46
+ return enum_for(__method__) unless block_given?
47
+
48
+ each_app_screenshot_set do |localization, app_screenshot_set|
49
+ app_screenshot_set.app_screenshots.each do |app_screenshot|
50
+ yield(localization, app_screenshot_set, app_screenshot)
51
+ end
52
+ end
53
+ end
54
+
55
+ # Iterate given local app_screenshot over localizations and app_screenshot_sets with index within each app_screenshot_set
56
+ #
57
+ # @param screenshots_per_language [Hash<String, Array<Deliver::AppScreenshot>]
58
+ # @yield [localization, app_screenshot_set, app_screenshot, index]
59
+ # @yieldparam [optional, Spaceship::ConnectAPI::AppStoreVersionLocalization] localization
60
+ # @yieldparam [optional, Spaceship::ConnectAPI::AppStoreScreenshotSet] app_screenshot_set
61
+ # @yieldparam [optional, Deliver::AppScreenshot] screenshot
62
+ # @yieldparam [optional, Integer] index a number reperesents which position the screenshot will be
63
+ def each_local_screenshot(screenshots_per_language, &block)
64
+ return enum_for(__method__, screenshots_per_language) unless block_given?
65
+
66
+ # Iterate over all the screenshots per language and display_type
67
+ # and then enqueue them to worker one by one if it's not duplciated on App Store Connect
68
+ screenshots_per_language.map do |language, screenshots_for_language|
69
+ localization = @localizations.find { |l| l.locale == language }
70
+ [localization, screenshots_for_language]
71
+ end.reject do |localization, _|
72
+ localization.nil?
73
+ end.each do |localization, screenshots_for_language|
74
+ iterate_over_screenshots_per_language(localization, screenshots_for_language, &block)
75
+ end
76
+ end
77
+
78
+ private
79
+
80
+ def iterate_over_screenshots_per_language(localization, screenshots_for_language, &block)
81
+ app_screenshot_sets_per_display_type = localization.get_app_screenshot_sets.map { |set| [set.screenshot_display_type, set] }.to_h
82
+ screenshots_per_display_type = screenshots_for_language.reject { |screenshot| screenshot.device_type.nil? }.group_by(&:device_type)
83
+
84
+ screenshots_per_display_type.each do |display_type, screenshots|
85
+ # Create AppScreenshotSet for given display_type if it doesn't exsit
86
+ app_screenshot_set = app_screenshot_sets_per_display_type[display_type]
87
+ app_screenshot_set ||= localization.create_app_screenshot_set(attributes: { screenshotDisplayType: display_type })
88
+ iterate_over_screenshots_per_display_type(localization, app_screenshot_set, screenshots, &block)
89
+ end
90
+ end
91
+
92
+ def iterate_over_screenshots_per_display_type(localization, app_screenshot_set, screenshots, &block)
93
+ screenshots.each.with_index do |screenshot, index|
94
+ yield(localization, app_screenshot_set, screenshot, index)
95
+ end
96
+ end
97
+ end
98
+ end
@@ -1,3 +1,5 @@
1
+ require 'spaceship'
2
+
1
3
  require_relative 'module'
2
4
 
3
5
  module Deliver
@@ -51,7 +53,12 @@ module Deliver
51
53
  @app_name ||= options[:app].name
52
54
 
53
55
  @languages = options[:description].keys if options[:description]
54
- @languages ||= options[:app].latest_version.description.languages
56
+ @languages ||= begin
57
+ platform = Spaceship::ConnectAPI::Platform.map(options[:platform])
58
+ version = options[:app].get_edit_app_store_version(platform: platform)
59
+
60
+ version.get_app_store_version_localizations.collect(&:locale)
61
+ end
55
62
 
56
63
  html_path = File.join(Deliver::ROOT, "lib/assets/summary.html.erb")
57
64
  html = ERB.new(File.read(html_path)).result(binding) # https://web.archive.org/web/20160430190141/www.rrn.dk/rubys-erb-templating-system
@@ -0,0 +1,64 @@
1
+ require 'thread'
2
+
3
+ module Deliver
4
+ # This dispatches jobs to worker threads and make it work in parallel.
5
+ # It's suitable for I/O bounds works and not for CPU bounds works.
6
+ # Use this when you have all the items that you'll process in advance.
7
+ # Simply enqueue them to this and call `QueueWorker#start`.
8
+ class QueueWorker
9
+ # @param concurrency (Numeric) - A number of threads to be created
10
+ # @param block (Proc) - A task you want to execute with enqueued items
11
+ def initialize(concurrency, &block)
12
+ @concurrency = concurrency
13
+ @block = block
14
+ @queue = Queue.new
15
+ end
16
+
17
+ # @param job (Object) - An arbitary object that keeps parameters
18
+ def enqueue(job)
19
+ @queue.push(job)
20
+ end
21
+
22
+ # Call this after you enqueuned all the jobs you want to process
23
+ # This method blocks current thread until all the enqueued jobs are processed
24
+ def start
25
+ threads = []
26
+ @concurrency.times do
27
+ threads << Thread.new do
28
+ while running? && !empty?
29
+ job = @queue.pop
30
+ @block.call(job) if job
31
+ end
32
+ end
33
+ end
34
+
35
+ wait_for_complete
36
+ threads.each(&:join)
37
+ end
38
+
39
+ private
40
+
41
+ def running?
42
+ !@queue.closed?
43
+ end
44
+
45
+ def empty?
46
+ @queue.empty?
47
+ end
48
+
49
+ def wait_for_complete
50
+ wait_thread = Thread.new do
51
+ loop do
52
+ if @queue.empty?
53
+ @queue.close
54
+ break
55
+ end
56
+
57
+ sleep(1)
58
+ end
59
+ end
60
+
61
+ wait_thread.join
62
+ end
63
+ end
64
+ end
@@ -4,10 +4,17 @@ require 'digest/md5'
4
4
  require_relative 'app_screenshot'
5
5
  require_relative 'module'
6
6
  require_relative 'loader'
7
+ require_relative 'queue_worker'
8
+ require_relative 'app_screenshot_iterator'
7
9
 
8
10
  module Deliver
9
11
  # upload screenshots to App Store Connect
10
12
  class UploadScreenshots
13
+ DeleteScreenshotJob = Struct.new(:app_screenshot, :localization, :app_screenshot_set)
14
+ UploadScreenshotJob = Struct.new(:app_screenshot_set, :path)
15
+
16
+ NUMBER_OF_THREADS = Helper.test? ? 1 : [ENV.fetch("DELIVER_NUMBER_OF_THREADS", 10).to_i, 10].min
17
+
11
18
  def upload(options, screenshots)
12
19
  return if options[:skip_screenshots]
13
20
  return if options[:edit_live]
@@ -50,57 +57,47 @@ module Deliver
50
57
  localizations = version.get_app_store_version_localizations
51
58
  end
52
59
 
53
- upload_screenshots(screenshots_per_language, localizations, options)
60
+ upload_screenshots(localizations, screenshots_per_language)
61
+
62
+ Helper.show_loading_indicator("Sorting screenshots uploaded...")
63
+ sort_screenshots(localizations)
64
+ Helper.hide_loading_indicator
65
+
66
+ UI.success("Successfully uploaded screenshots to App Store Connect")
54
67
  end
55
68
 
56
69
  def delete_screenshots(localizations, screenshots_per_language, tries: 5)
57
70
  tries -= 1
58
71
 
59
- # Get localizations on version
60
- localizations.each do |localization|
61
- # Only delete screenshots if trying to upload
62
- next unless screenshots_per_language.keys.include?(localization.locale)
63
-
64
- # Iterate over all screenshots for each set and delete
65
- screenshot_sets = localization.get_app_screenshot_sets
66
-
67
- # Multi threading delete on single localization
68
- threads = []
69
- errors = []
70
-
71
- screenshot_sets.each do |screenshot_set|
72
- UI.message("Removing all previously uploaded screenshots for '#{localization.locale}' '#{screenshot_set.screenshot_display_type}'...")
73
- screenshot_set.app_screenshots.each do |screenshot|
74
- UI.verbose("Deleting screenshot - #{localization.locale} #{screenshot_set.screenshot_display_type} #{screenshot.id}")
75
- threads << Thread.new do
76
- begin
77
- screenshot.delete!
78
- UI.verbose("Deleted screenshot - #{localization.locale} #{screenshot_set.screenshot_display_type} #{screenshot.id}")
79
- rescue => error
80
- UI.verbose("Failed to delete screenshot - #{localization.locale} #{screenshot_set.screenshot_display_type} #{screenshot.id}")
81
- errors << error
82
- end
83
- end
84
- end
72
+ worker = QueueWorker.new(NUMBER_OF_THREADS) do |job|
73
+ start_time = Time.now
74
+ target = "#{job.localization.locale} #{job.app_screenshot_set.screenshot_display_type} #{job.app_screenshot.id}"
75
+ begin
76
+ UI.verbose("Deleting '#{target}'")
77
+ job.app_screenshot.delete!
78
+ UI.message("Deleted '#{target}' - (#{Time.now - start_time} secs)")
79
+ rescue => error
80
+ UI.error("Failed to delete screenshot #{target} - (#{Time.now - start_time} secs)")
81
+ UI.error(error.message)
85
82
  end
83
+ end
86
84
 
87
- sleep(1) # Feels bad but sleeping a bit to let the threads catchup
88
-
89
- unless threads.empty?
90
- Helper.show_loading_indicator("Waiting for screenshots to be deleted for '#{localization.locale}'... (might be slow)") unless FastlaneCore::Globals.verbose?
91
- threads.each(&:join)
92
- Helper.hide_loading_indicator unless FastlaneCore::Globals.verbose?
93
- end
85
+ iterator = AppScreenshotIterator.new(localizations)
86
+ iterator.each_app_screenshot do |localization, app_screenshot_set, app_screenshot|
87
+ # Only delete screenshots if trying to upload
88
+ next unless screenshots_per_language.keys.include?(localization.locale)
94
89
 
95
- # Crash if any errors happen while deleting
96
- errors.each do |error|
97
- UI.error(error.message)
98
- end
90
+ UI.verbose("Queued delete sceeenshot job for #{localization.locale} #{app_screenshot_set.screenshot_display_type} #{app_screenshot.id}")
91
+ worker.enqueue(DeleteScreenshotJob.new(app_screenshot, localization, app_screenshot_set))
99
92
  end
100
93
 
94
+ worker.start
95
+
101
96
  # Verify all screenshots have been deleted
102
97
  # Sometimes API requests will fail but screenshots will still be deleted
103
- count = count_screenshots(localizations)
98
+ count = iterator.each_app_screenshot_set.map { |_, app_screenshot_set| app_screenshot_set }
99
+ .reduce(0) { |sum, app_screenshot_set| sum + app_screenshot_set.app_screenshots.size }
100
+
104
101
  UI.important("Number of screenshots not deleted: #{count}")
105
102
  if count > 0
106
103
  if tries.zero?
@@ -114,113 +111,107 @@ module Deliver
114
111
  end
115
112
  end
116
113
 
117
- def count_screenshots(localizations)
118
- count = 0
119
- localizations.each do |localization|
120
- screenshot_sets = localization.get_app_screenshot_sets
121
- screenshot_sets.each do |screenshot_set|
122
- count += screenshot_set.app_screenshots.size
123
- end
124
- end
125
-
126
- return count
127
- end
128
-
129
- def upload_screenshots(screenshots_per_language, localizations, options)
130
- # Check if should wait for processing
131
- # Default to waiting if submitting for review (since needed for submission)
132
- # Otherwise use enviroment variable
133
- if ENV["DELIVER_SKIP_WAIT_FOR_SCREENSHOT_PROCESSING"].nil?
134
- wait_for_processing = options[:submit_for_review]
135
- UI.verbose("Setting wait_for_processing from ':submit_for_review' option")
136
- else
137
- UI.verbose("Setting wait_for_processing from 'DELIVER_SKIP_WAIT_FOR_SCREENSHOT_PROCESSING' environment variable")
138
- wait_for_processing = !FastlaneCore::Env.truthy?("DELIVER_SKIP_WAIT_FOR_SCREENSHOT_PROCESSING")
139
- end
140
-
141
- if wait_for_processing
142
- UI.important("Will wait for screenshot image processing")
143
- UI.important("Set env DELIVER_SKIP_WAIT_FOR_SCREENSHOT_PROCESSING=true to skip waiting for screenshots to process")
144
- else
145
- UI.important("Skipping the wait for screenshot image processing (which may affect submission)")
146
- UI.important("Set env DELIVER_SKIP_WAIT_FOR_SCREENSHOT_PROCESSING=false to wait for screenshots to process")
147
- end
114
+ def upload_screenshots(localizations, screenshots_per_language, tries: 5)
115
+ tries -= 1
148
116
 
149
117
  # Upload screenshots
150
- indized = {} # per language and device type
151
-
152
- screenshots_per_language.each do |language, screenshots_for_language|
153
- # Find localization to upload screenshots to
154
- localization = localizations.find do |l|
155
- l.locale == language
118
+ worker = QueueWorker.new(NUMBER_OF_THREADS) do |job|
119
+ begin
120
+ UI.verbose("Uploading '#{job.path}'...")
121
+ start_time = Time.now
122
+ job.app_screenshot_set.upload_screenshot(path: job.path, wait_for_processing: false)
123
+ UI.message("Uploaded '#{job.path}'... (#{Time.now - start_time} secs)")
124
+ rescue => error
125
+ UI.error(error)
156
126
  end
127
+ end
157
128
 
158
- unless localization
159
- UI.error("Couldn't find localization on version for #{language}")
129
+ number_of_screenshots = 0
130
+ iterator = AppScreenshotIterator.new(localizations)
131
+ iterator.each_local_screenshot(screenshots_per_language) do |localization, app_screenshot_set, screenshot, index|
132
+ if index >= 10
133
+ UI.error("Too many screenshots found for device '#{screenshot.device_type}' in '#{screenshot.language}', skipping this one (#{screenshot.path})")
160
134
  next
161
135
  end
162
136
 
163
- indized[localization.locale] ||= {}
137
+ checksum = UploadScreenshots.calculate_checksum(screenshot.path)
138
+ duplicate = app_screenshot_set.app_screenshots.any? { |s| s.source_file_checksum == checksum }
164
139
 
165
- # Create map to find screenshot set to add screenshot to
166
- app_screenshot_sets_map = {}
167
- app_screenshot_sets = localization.get_app_screenshot_sets
168
- app_screenshot_sets.each do |app_screenshot_set|
169
- app_screenshot_sets_map[app_screenshot_set.screenshot_display_type] = app_screenshot_set
140
+ # Enqueue uploading job if it's not duplicated otherwise screenshot will be skipped
141
+ if duplicate
142
+ UI.message("Previous uploaded. Skipping '#{screenshot.path}'...")
143
+ else
144
+ worker.enqueue(UploadScreenshotJob.new(app_screenshot_set, screenshot.path))
145
+ end
170
146
 
171
- # Set initial screnshot count
172
- indized[localization.locale][app_screenshot_set.screenshot_display_type] ||= {
173
- count: app_screenshot_set.app_screenshots.size,
174
- checksums: []
175
- }
147
+ number_of_screenshots += 1
148
+ end
176
149
 
177
- checksums = app_screenshot_set.app_screenshots.map(&:source_file_checksum).uniq
178
- indized[localization.locale][app_screenshot_set.screenshot_display_type][:checksums] = checksums
179
- end
150
+ worker.start
180
151
 
181
- UI.message("Uploading #{screenshots_for_language.length} screenshots for language #{language}")
182
- screenshots_for_language.each do |screenshot|
183
- display_type = screenshot.device_type
184
- set = app_screenshot_sets_map[display_type]
152
+ UI.verbose('Uploading jobs are completed')
185
153
 
186
- if display_type.nil?
187
- UI.error("Error... Screenshot size #{screenshot.screen_size} not valid for App Store Connect")
188
- next
189
- end
154
+ Helper.show_loading_indicator("Waiting for all the screenshots processed...")
155
+ states = wait_for_complete(iterator)
156
+ Helper.hide_loading_indicator
157
+ retry_upload_screenshots_if_needed(iterator, states, number_of_screenshots, tries, localizations, screenshots_per_language)
190
158
 
191
- unless set
192
- set = localization.create_app_screenshot_set(attributes: {
193
- screenshotDisplayType: display_type
194
- })
195
- app_screenshot_sets_map[display_type] = set
159
+ UI.message("Successfully uploaded all screenshots")
160
+ end
196
161
 
197
- indized[localization.locale][set.screenshot_display_type] = {
198
- count: 0,
199
- checksums: []
200
- }
201
- end
162
+ # Verify all screenshots have been processed
163
+ def wait_for_complete(iterator)
164
+ loop do
165
+ states = iterator.each_app_screenshot.map { |_, _, app_screenshot| app_screenshot }.each_with_object({}) do |app_screenshot, hash|
166
+ state = app_screenshot.asset_delivery_state['state']
167
+ hash[state] ||= 0
168
+ hash[state] += 1
169
+ end
202
170
 
203
- index = indized[localization.locale][set.screenshot_display_type][:count]
171
+ is_processing = states.fetch('UPLOAD_COMPLETE', 0) > 0
172
+ return states unless is_processing
204
173
 
205
- if index >= 10
206
- UI.error("Too many screenshots found for device '#{screenshot.device_type}' in '#{screenshot.language}', skipping this one (#{screenshot.path})")
207
- next
174
+ UI.verbose("There are still incomplete screenshots - #{states}")
175
+ sleep(5)
176
+ end
177
+ end
178
+
179
+ # Verify all screenshots states on App Store Connect are okay
180
+ def retry_upload_screenshots_if_needed(iterator, states, number_of_screenshots, tries, localizations, screenshots_per_language)
181
+ is_failure = states.fetch("FAILED", 0) > 0
182
+ is_missing_screenshot = states.reduce(0) { |sum, (k, v)| sum + v } != number_of_screenshots
183
+ if is_failure || is_missing_screenshot
184
+ if tries.zero?
185
+ incomplete_screenshot_count = states.reject { |k, v| k == 'COMPLETE' }.reduce(0) { |sum, (k, v)| sum + v }
186
+ UI.user_error!("Failed verification of all screenshots uploaded... #{incomplete_screenshot_count} incomplete screenshot(s) still exist")
187
+ else
188
+ UI.error("Failed to upload all screenshots... Tries remaining: #{tries}")
189
+ # Delete bad entries before retry
190
+ iterator.each_app_screenshot do |_, _, app_screenshot|
191
+ app_screenshot.delete! unless app_screenshot.complete?
208
192
  end
193
+ upload_screenshots(localizations, screenshots_per_language, tries: tries)
194
+ end
195
+ end
196
+ end
209
197
 
210
- bytes = File.binread(screenshot.path)
211
- checksum = Digest::MD5.hexdigest(bytes)
212
- duplicate = indized[localization.locale][set.screenshot_display_type][:checksums].include?(checksum)
198
+ def sort_screenshots(localizations)
199
+ iterator = AppScreenshotIterator.new(localizations)
213
200
 
214
- if duplicate
215
- UI.message("Previous uploaded. Skipping '#{screenshot.path}'...")
216
- else
217
- indized[localization.locale][set.screenshot_display_type][:count] += 1
218
- UI.message("Uploading '#{screenshot.path}'...")
219
- set.upload_screenshot(path: screenshot.path, wait_for_processing: wait_for_processing)
220
- end
201
+ # Re-order screenshots within app_screenshot_set
202
+ worker = QueueWorker.new(NUMBER_OF_THREADS) do |app_screenshot_set|
203
+ original_ids = app_screenshot_set.app_screenshots.map(&:id)
204
+ sorted_ids = app_screenshot_set.app_screenshots.sort_by(&:file_name).map(&:id)
205
+ if original_ids != sorted_ids
206
+ app_screenshot_set.reorder_screenshots(app_screenshot_ids: sorted_ids)
221
207
  end
222
208
  end
223
- UI.success("Successfully uploaded screenshots to App Store Connect")
209
+
210
+ iterator.each_app_screenshot_set do |_, app_screenshot_set|
211
+ worker.enqueue(app_screenshot_set)
212
+ end
213
+
214
+ worker.start
224
215
  end
225
216
 
226
217
  def collect_screenshots(options)
@@ -299,5 +290,11 @@ module Deliver
299
290
  Spaceship::Tunes.client.available_languages
300
291
  end
301
292
  end
293
+
294
+ # helper method to mock this step in tests
295
+ def self.calculate_checksum(path)
296
+ bytes = File.binread(path)
297
+ Digest::MD5.hexdigest(bytes)
298
+ end
302
299
  end
303
300
  end