fastlane 2.154.0 → 2.156.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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