fastlane 2.190.0 → 2.193.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +90 -90
  3. data/deliver/lib/deliver/app_screenshot.rb +2 -1
  4. data/deliver/lib/deliver/app_screenshot_iterator.rb +2 -2
  5. data/deliver/lib/deliver/loader.rb +1 -1
  6. data/deliver/lib/deliver/options.rb +6 -0
  7. data/deliver/lib/deliver/runner.rb +9 -1
  8. data/deliver/lib/deliver/screenshot_comparable.rb +62 -0
  9. data/deliver/lib/deliver/sync_screenshots.rb +200 -0
  10. data/fastlane/lib/assets/completions/completion.bash +4 -1
  11. data/fastlane/lib/assets/completions/completion.zsh +6 -5
  12. data/fastlane/lib/fastlane/actions/create_xcframework.rb +97 -17
  13. data/fastlane/lib/fastlane/actions/get_provisioning_profile.rb +1 -1
  14. data/fastlane/lib/fastlane/actions/gradle.rb +1 -1
  15. data/fastlane/lib/fastlane/actions/notarize.rb +77 -1
  16. data/fastlane/lib/fastlane/actions/push_git_tags.rb +1 -1
  17. data/fastlane/lib/fastlane/actions/sync_code_signing.rb +1 -1
  18. data/fastlane/lib/fastlane/actions/upload_to_testflight.rb +3 -1
  19. data/fastlane/lib/fastlane/actions/zip.rb +4 -3
  20. data/fastlane/lib/fastlane/features.rb +3 -0
  21. data/fastlane/lib/fastlane/version.rb +1 -1
  22. data/fastlane/swift/Deliverfile.swift +1 -1
  23. data/fastlane/swift/DeliverfileProtocol.swift +5 -1
  24. data/fastlane/swift/Fastlane.swift +82 -20
  25. data/fastlane/swift/Gymfile.swift +1 -1
  26. data/fastlane/swift/GymfileProtocol.swift +1 -1
  27. data/fastlane/swift/Matchfile.swift +1 -1
  28. data/fastlane/swift/MatchfileProtocol.swift +2 -2
  29. data/fastlane/swift/Precheckfile.swift +1 -1
  30. data/fastlane/swift/PrecheckfileProtocol.swift +1 -1
  31. data/fastlane/swift/Scanfile.swift +1 -1
  32. data/fastlane/swift/ScanfileProtocol.swift +1 -1
  33. data/fastlane/swift/Screengrabfile.swift +1 -1
  34. data/fastlane/swift/ScreengrabfileProtocol.swift +1 -1
  35. data/fastlane/swift/Snapshotfile.swift +1 -1
  36. data/fastlane/swift/SnapshotfileProtocol.swift +1 -1
  37. data/fastlane/swift/formatting/Brewfile.lock.json +8 -8
  38. data/fastlane_core/lib/fastlane_core/build_watcher.rb +25 -6
  39. data/fastlane_core/lib/fastlane_core/pkg_file_analyser.rb +5 -0
  40. data/fastlane_core/lib/fastlane_core/ui/fastlane_runner.rb +41 -0
  41. data/match/lib/match/options.rb +1 -1
  42. data/match/lib/match/runner.rb +10 -9
  43. data/pilot/lib/pilot/build_manager.rb +14 -4
  44. data/pilot/lib/pilot/manager.rb +3 -1
  45. data/pilot/lib/pilot/options.rb +20 -1
  46. data/produce/lib/produce/commands_generator.rb +28 -0
  47. data/produce/lib/produce/service.rb +16 -1
  48. data/spaceship/lib/spaceship/connect_api/api_client.rb +15 -1
  49. data/spaceship/lib/spaceship/connect_api/models/app.rb +9 -1
  50. data/spaceship/lib/spaceship/connect_api/models/build.rb +4 -0
  51. data/spaceship/lib/spaceship/connect_api/models/build_beta_detail.rb +4 -0
  52. data/spaceship/lib/spaceship/connect_api/models/capabilities.rb +27 -0
  53. data/spaceship/lib/spaceship/connect_api/provisioning/provisioning.rb +5 -0
  54. data/spaceship/lib/spaceship/connect_api/testflight/.testflight.rb.swp +0 -0
  55. data/spaceship/lib/spaceship/connect_api/testflight/testflight.rb +7 -5
  56. data/spaceship/lib/spaceship/connect_api/token.rb +2 -0
  57. data/spaceship/lib/spaceship/connect_api/users/users.rb +34 -1
  58. data/spaceship/lib/spaceship/connect_api.rb +1 -0
  59. data/supply/lib/supply/client.rb +38 -5
  60. data/supply/lib/supply/options.rb +7 -0
  61. data/supply/lib/supply/uploader.rb +1 -1
  62. metadata +39 -21
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8705b34d3c2ba652057cd97cf7482e1490cfdfdd26650ee1cb001b3e2d55553c
4
- data.tar.gz: 4fc0b307f940f973ea7d9a70fe88ef567a97fcb9398ae9dfac52d9c4041c58c9
3
+ metadata.gz: 16bbd4e5c7f94860e5d9cba08578ab2918bccfe31092380f43d859da4f8393e7
4
+ data.tar.gz: f527779d5963a3f3110c7b5b4bb6bda4c816a01e391b16919c7ef5c0c841b82d
5
5
  SHA512:
6
- metadata.gz: 8421768a3ca7030a72a000a3df67a864b7461ee3e221c1e8e91d061aa2d83eac121ed9781e75fc81e2f1e11d822309318d2f0f5aea52cfdd7ce5180c745e359f
7
- data.tar.gz: bf7ca0c1eb8f5daed3a4ea7a44ffad3898535e173805f342a3590440a248cc9d5bbb4b6a9edea98af191d20cb740316ba9b5b044a75b1c0109ac1923a5df6326
6
+ metadata.gz: 148193a7bc745d1c044cadad094875e5375c8f0ed611231695228bd1656aa3f5030018a88cf484d7c811bcd4161ac191f786f3cfcce759a9e59d70cc5a814e34
7
+ data.tar.gz: b041a08c28429bc1a32c7a08702ebca0e260ac051734541b4345cb441a3cfaeb33d08f75887346bc974380463c1c8eddae468024069ad505c5edaa2ec5fdfc1c
data/README.md CHANGED
@@ -35,23 +35,43 @@ If the above doesn't help, please [submit an issue](https://github.com/fastlane/
35
35
  <!-- This table is regenerated and resorted on each release -->
36
36
  <table id='team'>
37
37
  <tr>
38
+ <td id='iulian-onofrei'>
39
+ <a href='https://github.com/revolter'>
40
+ <img src='https://github.com/revolter.png' width='140px;'>
41
+ </a>
42
+ <h4 align='center'><a href='https://twitter.com/Revolt666'>Iulian Onofrei</a></h4>
43
+ </td>
44
+ <td id='andrew-mcburney'>
45
+ <a href='https://github.com/armcburney'>
46
+ <img src='https://github.com/armcburney.png' width='140px;'>
47
+ </a>
48
+ <h4 align='center'><a href='https://twitter.com/armcburney'>Andrew McBurney</a></h4>
49
+ </td>
50
+ <td id='maksym-grebenets'>
51
+ <a href='https://github.com/mgrebenets'>
52
+ <img src='https://github.com/mgrebenets.png' width='140px;'>
53
+ </a>
54
+ <h4 align='center'><a href='https://twitter.com/mgrebenets'>Maksym Grebenets</a></h4>
55
+ </td>
56
+ <td id='joshua-liebowitz'>
57
+ <a href='https://github.com/taquitos'>
58
+ <img src='https://github.com/taquitos.png' width='140px;'>
59
+ </a>
60
+ <h4 align='center'><a href='https://twitter.com/taquitos'>Joshua Liebowitz</a></h4>
61
+ </td>
38
62
  <td id='luka-mirosevic'>
39
63
  <a href='https://github.com/lmirosevic'>
40
64
  <img src='https://github.com/lmirosevic.png' width='140px;'>
41
65
  </a>
42
66
  <h4 align='center'><a href='https://twitter.com/lmirosevic'>Luka Mirosevic</a></h4>
43
67
  </td>
44
- <td id='aaron-brager'>
45
- <a href='https://github.com/getaaron'>
46
- <img src='https://github.com/getaaron.png' width='140px;'>
47
- </a>
48
- <h4 align='center'><a href='https://twitter.com/getaaron'>Aaron Brager</a></h4>
49
- </td>
50
- <td id='fumiya-nakamura'>
51
- <a href='https://github.com/nafu'>
52
- <img src='https://github.com/nafu.png' width='140px;'>
68
+ </tr>
69
+ <tr>
70
+ <td id='olivier-halligon'>
71
+ <a href='https://github.com/AliSoftware'>
72
+ <img src='https://github.com/AliSoftware.png' width='140px;'>
53
73
  </a>
54
- <h4 align='center'><a href='https://twitter.com/nafu003'>Fumiya Nakamura</a></h4>
74
+ <h4 align='center'><a href='https://twitter.com/aligatr'>Olivier Halligon</a></h4>
55
75
  </td>
56
76
  <td id='satoshi-namai'>
57
77
  <a href='https://github.com/ainame'>
@@ -59,19 +79,11 @@ If the above doesn't help, please [submit an issue](https://github.com/fastlane/
59
79
  </a>
60
80
  <h4 align='center'><a href='https://twitter.com/ainame'>Satoshi Namai</a></h4>
61
81
  </td>
62
- <td id='manish-rathi'>
63
- <a href='https://github.com/crazymanish'>
64
- <img src='https://github.com/crazymanish.png' width='140px;'>
65
- </a>
66
- <h4 align='center'><a href='https://twitter.com/iammanishrathi'>Manish Rathi</a></h4>
67
- </td>
68
- </tr>
69
- <tr>
70
- <td id='andrew-mcburney'>
71
- <a href='https://github.com/armcburney'>
72
- <img src='https://github.com/armcburney.png' width='140px;'>
82
+ <td id='stefan-natchev'>
83
+ <a href='https://github.com/snatchev'>
84
+ <img src='https://github.com/snatchev.png' width='140px;'>
73
85
  </a>
74
- <h4 align='center'><a href='https://twitter.com/armcburney'>Andrew McBurney</a></h4>
86
+ <h4 align='center'><a href='https://twitter.com/snatchev'>Stefan Natchev</a></h4>
75
87
  </td>
76
88
  <td id='kohki-miki'>
77
89
  <a href='https://github.com/giginet'>
@@ -79,31 +91,19 @@ If the above doesn't help, please [submit an issue](https://github.com/fastlane/
79
91
  </a>
80
92
  <h4 align='center'><a href='https://twitter.com/giginet'>Kohki Miki</a></h4>
81
93
  </td>
82
- <td id='olivier-halligon'>
83
- <a href='https://github.com/AliSoftware'>
84
- <img src='https://github.com/AliSoftware.png' width='140px;'>
85
- </a>
86
- <h4 align='center'><a href='https://twitter.com/aligatr'>Olivier Halligon</a></h4>
87
- </td>
88
- <td id='danielle-tomlinson'>
89
- <a href='https://github.com/endocrimes'>
90
- <img src='https://github.com/endocrimes.png' width='140px;'>
91
- </a>
92
- <h4 align='center'><a href='https://twitter.com/endocrimes'>Danielle Tomlinson</a></h4>
93
- </td>
94
- <td id='jorge-revuelta-h'>
95
- <a href='https://github.com/minuscorp'>
96
- <img src='https://github.com/minuscorp.png' width='140px;'>
94
+ <td id='daniel-jankowski'>
95
+ <a href='https://github.com/mollyIV'>
96
+ <img src='https://github.com/mollyIV.png' width='140px;'>
97
97
  </a>
98
- <h4 align='center'><a href='https://twitter.com/minuscorp'>Jorge Revuelta H</a></h4>
98
+ <h4 align='center'><a href='https://twitter.com/mollyIV'>Daniel Jankowski</a></h4>
99
99
  </td>
100
100
  </tr>
101
101
  <tr>
102
- <td id='manu-wallner'>
103
- <a href='https://github.com/milch'>
104
- <img src='https://github.com/milch.png' width='140px;'>
102
+ <td id='max-ott'>
103
+ <a href='https://github.com/max-ott'>
104
+ <img src='https://github.com/max-ott.png' width='140px;'>
105
105
  </a>
106
- <h4 align='center'><a href='https://twitter.com/acrooow'>Manu Wallner</a></h4>
106
+ <h4 align='center'><a href='https://twitter.com/ott_max'>Max Ott</a></h4>
107
107
  </td>
108
108
  <td id='matthew-ellis'>
109
109
  <a href='https://github.com/matthewellis'>
@@ -111,37 +111,31 @@ If the above doesn't help, please [submit an issue](https://github.com/fastlane/
111
111
  </a>
112
112
  <h4 align='center'><a href='https://twitter.com/mellis1995'>Matthew Ellis</a></h4>
113
113
  </td>
114
- <td id='felix-krause'>
115
- <a href='https://github.com/KrauseFx'>
116
- <img src='https://github.com/KrauseFx.png' width='140px;'>
117
- </a>
118
- <h4 align='center'><a href='https://twitter.com/KrauseFx'>Felix Krause</a></h4>
119
- </td>
120
- <td id='maksym-grebenets'>
121
- <a href='https://github.com/mgrebenets'>
122
- <img src='https://github.com/mgrebenets.png' width='140px;'>
114
+ <td id='jorge-revuelta-h'>
115
+ <a href='https://github.com/minuscorp'>
116
+ <img src='https://github.com/minuscorp.png' width='140px;'>
123
117
  </a>
124
- <h4 align='center'><a href='https://twitter.com/mgrebenets'>Maksym Grebenets</a></h4>
118
+ <h4 align='center'><a href='https://twitter.com/minuscorp'>Jorge Revuelta H</a></h4>
125
119
  </td>
126
- <td id='helmut-januschka'>
127
- <a href='https://github.com/hjanuschka'>
128
- <img src='https://github.com/hjanuschka.png' width='140px;'>
120
+ <td id='fumiya-nakamura'>
121
+ <a href='https://github.com/nafu'>
122
+ <img src='https://github.com/nafu.png' width='140px;'>
129
123
  </a>
130
- <h4 align='center'><a href='https://twitter.com/hjanuschka'>Helmut Januschka</a></h4>
124
+ <h4 align='center'><a href='https://twitter.com/nafu003'>Fumiya Nakamura</a></h4>
131
125
  </td>
132
- </tr>
133
- <tr>
134
126
  <td id='jimmy-dee'>
135
127
  <a href='https://github.com/jdee'>
136
128
  <img src='https://github.com/jdee.png' width='140px;'>
137
129
  </a>
138
130
  <h4 align='center'>Jimmy Dee</h4>
139
131
  </td>
140
- <td id='joshua-liebowitz'>
141
- <a href='https://github.com/taquitos'>
142
- <img src='https://github.com/taquitos.png' width='140px;'>
132
+ </tr>
133
+ <tr>
134
+ <td id='helmut-januschka'>
135
+ <a href='https://github.com/hjanuschka'>
136
+ <img src='https://github.com/hjanuschka.png' width='140px;'>
143
137
  </a>
144
- <h4 align='center'><a href='https://twitter.com/taquitos'>Joshua Liebowitz</a></h4>
138
+ <h4 align='center'><a href='https://twitter.com/hjanuschka'>Helmut Januschka</a></h4>
145
139
  </td>
146
140
  <td id='jérôme-lacoste'>
147
141
  <a href='https://github.com/lacostej'>
@@ -149,31 +143,11 @@ If the above doesn't help, please [submit an issue](https://github.com/fastlane/
149
143
  </a>
150
144
  <h4 align='center'><a href='https://twitter.com/lacostej'>Jérôme Lacoste</a></h4>
151
145
  </td>
152
- <td id='stefan-natchev'>
153
- <a href='https://github.com/snatchev'>
154
- <img src='https://github.com/snatchev.png' width='140px;'>
155
- </a>
156
- <h4 align='center'><a href='https://twitter.com/snatchev'>Stefan Natchev</a></h4>
157
- </td>
158
- <td id='iulian-onofrei'>
159
- <a href='https://github.com/revolter'>
160
- <img src='https://github.com/revolter.png' width='140px;'>
161
- </a>
162
- <h4 align='center'><a href='https://twitter.com/Revolt666'>Iulian Onofrei</a></h4>
163
- </td>
164
- </tr>
165
- <tr>
166
- <td id='roger-oba'>
167
- <a href='https://github.com/rogerluan'>
168
- <img src='https://github.com/rogerluan.png' width='140px;'>
169
- </a>
170
- <h4 align='center'><a href='https://twitter.com/rogerluan_'>Roger Oba</a></h4>
171
- </td>
172
- <td id='daniel-jankowski'>
173
- <a href='https://github.com/mollyIV'>
174
- <img src='https://github.com/mollyIV.png' width='140px;'>
146
+ <td id='danielle-tomlinson'>
147
+ <a href='https://github.com/endocrimes'>
148
+ <img src='https://github.com/endocrimes.png' width='140px;'>
175
149
  </a>
176
- <h4 align='center'><a href='https://twitter.com/mollyIV'>Daniel Jankowski</a></h4>
150
+ <h4 align='center'><a href='https://twitter.com/endocrimes'>Danielle Tomlinson</a></h4>
177
151
  </td>
178
152
  <td id='josh-holtz'>
179
153
  <a href='https://github.com/joshdholtz'>
@@ -181,11 +155,25 @@ If the above doesn't help, please [submit an issue](https://github.com/fastlane/
181
155
  </a>
182
156
  <h4 align='center'><a href='https://twitter.com/joshdholtz'>Josh Holtz</a></h4>
183
157
  </td>
184
- <td id='max-ott'>
185
- <a href='https://github.com/max-ott'>
186
- <img src='https://github.com/max-ott.png' width='140px;'>
158
+ <td id='felix-krause'>
159
+ <a href='https://github.com/KrauseFx'>
160
+ <img src='https://github.com/KrauseFx.png' width='140px;'>
187
161
  </a>
188
- <h4 align='center'><a href='https://twitter.com/ott_max'>Max Ott</a></h4>
162
+ <h4 align='center'><a href='https://twitter.com/KrauseFx'>Felix Krause</a></h4>
163
+ </td>
164
+ </tr>
165
+ <tr>
166
+ <td id='aaron-brager'>
167
+ <a href='https://github.com/getaaron'>
168
+ <img src='https://github.com/getaaron.png' width='140px;'>
169
+ </a>
170
+ <h4 align='center'><a href='https://twitter.com/getaaron'>Aaron Brager</a></h4>
171
+ </td>
172
+ <td id='manu-wallner'>
173
+ <a href='https://github.com/milch'>
174
+ <img src='https://github.com/milch.png' width='140px;'>
175
+ </a>
176
+ <h4 align='center'><a href='https://twitter.com/acrooow'>Manu Wallner</a></h4>
189
177
  </td>
190
178
  <td id='jan-piotrowski'>
191
179
  <a href='https://github.com/janpio'>
@@ -193,6 +181,18 @@ If the above doesn't help, please [submit an issue](https://github.com/fastlane/
193
181
  </a>
194
182
  <h4 align='center'><a href='https://twitter.com/Sujan'>Jan Piotrowski</a></h4>
195
183
  </td>
184
+ <td id='manish-rathi'>
185
+ <a href='https://github.com/crazymanish'>
186
+ <img src='https://github.com/crazymanish.png' width='140px;'>
187
+ </a>
188
+ <h4 align='center'><a href='https://twitter.com/iammanishrathi'>Manish Rathi</a></h4>
189
+ </td>
190
+ <td id='roger-oba'>
191
+ <a href='https://github.com/rogerluan'>
192
+ <img src='https://github.com/rogerluan.png' width='140px;'>
193
+ </a>
194
+ <h4 align='center'><a href='https://twitter.com/rogerluan_'>Roger Oba</a></h4>
195
+ </td>
196
196
  </tr>
197
197
  </table>
198
198
 
@@ -324,7 +324,8 @@ module Deliver
324
324
  is_3rd_gen = [
325
325
  "iPad Pro (12.9-inch) (3rd generation)", # default simulator name has this
326
326
  "iPad Pro (12.9-inch) (4th generation)", # default simulator name has this
327
- "ipadPro129" # downloaded screenshots name has this
327
+ "ipadPro129", # downloaded screenshots name has this,
328
+ "3GEN" # downloaded screenshots name from App Store Connect API has this
328
329
  ].any? { |key| filename.include?(key) }
329
330
  if is_3rd_gen
330
331
  if screen_size == ScreenSize::IOS_IPAD_PRO
@@ -85,8 +85,8 @@ module Deliver
85
85
  app_screenshot_set ||= localization.create_app_screenshot_set(attributes: { screenshotDisplayType: display_type })
86
86
 
87
87
  # iterate over screenshots per display size with index
88
- screenshots.each do |screenshot|
89
- yield(localization, app_screenshot_set, screenshot)
88
+ screenshots.each.with_index do |screenshot, index|
89
+ yield(localization, app_screenshot_set, screenshot, index)
90
90
  end
91
91
  end
92
92
  end
@@ -82,7 +82,7 @@ module Deliver
82
82
  #
83
83
  # @param root [String] A directory path
84
84
  # @param ignore_validation [String] Set false not to raise the error when finding invalid folder name
85
- # @return [Array<AppScreenshot>] The list of AppScreenshot that exist under given `root` directory
85
+ # @return [Array<Deliver::AppScreenshot>] The list of AppScreenshot that exist under given `root` directory
86
86
  def self.load_app_screenshots(root, ignore_validation)
87
87
  screenshots = language_folders(root, ignore_validation, true).flat_map do |language_folder|
88
88
  paths = if language_folder.framed_file_paths.count > 0
@@ -162,6 +162,12 @@ module Deliver
162
162
  description: "Clear all previously uploaded screenshots before uploading the new ones",
163
163
  type: Boolean,
164
164
  default_value: false),
165
+ FastlaneCore::ConfigItem.new(key: :sync_screenshots,
166
+ env_name: "DELIVER_SYNC_SCREENSHOTS",
167
+ description: "Sync screenshots with local ones. This is currently beta option" \
168
+ "so set true to 'FASTLANE_ENABLE_BETA_DELIVER_SYNC_SCREENSHOTS' environment variable as well",
169
+ type: Boolean,
170
+ default_value: false),
165
171
  FastlaneCore::ConfigItem.new(key: :submit_for_review,
166
172
  env_name: "DELIVER_SUBMIT_FOR_REVIEW",
167
173
  description: "Submit the new version for Review after uploading everything",
@@ -10,6 +10,7 @@ require_relative 'submit_for_review'
10
10
  require_relative 'upload_price_tier'
11
11
  require_relative 'upload_metadata'
12
12
  require_relative 'upload_screenshots'
13
+ require_relative 'sync_screenshots'
13
14
  require_relative 'detect_values'
14
15
 
15
16
  module Deliver
@@ -143,7 +144,14 @@ module Deliver
143
144
 
144
145
  # Commit
145
146
  upload_metadata.upload(options)
146
- upload_screenshots.upload(options, screenshots)
147
+
148
+ if options[:sync_screenshots]
149
+ sync_screenshots = SyncScreenshots.new(app: Deliver.cache[:app], platform: Spaceship::ConnectAPI::Platform.map(options[:platform]))
150
+ sync_screenshots.sync(screenshots)
151
+ else
152
+ upload_screenshots.upload(options, screenshots)
153
+ end
154
+
147
155
  UploadPriceTier.new.upload(options)
148
156
  end
149
157
 
@@ -0,0 +1,62 @@
1
+ require 'spaceship/connect_api/models/app_screenshot'
2
+ require 'spaceship/connect_api/models/app_screenshot_set'
3
+
4
+ require_relative 'app_screenshot'
5
+
6
+ module Deliver
7
+ # This clsas enables you to compare equality between different representations of the screenshots
8
+ # in the standard API `Array#-` that requires objects to implements `eql?` and `hash`.
9
+ class ScreenshotComparable
10
+ # A unique key value that is consist of locale, filename, and checksum.
11
+ attr_reader :key
12
+
13
+ # A hash object that contains the source data of this representation class
14
+ attr_reader :context
15
+
16
+ def self.create_from_local(screenshot:, app_screenshot_set:)
17
+ raise ArgumentError unless screenshot.kind_of?(Deliver::AppScreenshot)
18
+ raise ArgumentError unless app_screenshot_set.kind_of?(Spaceship::ConnectAPI::AppScreenshotSet)
19
+
20
+ new(
21
+ path: "#{screenshot.language}/#{File.basename(screenshot.path)}",
22
+ checksum: calculate_checksum(screenshot.path),
23
+ context: {
24
+ screenshot: screenshot,
25
+ app_screenshot_set: app_screenshot_set
26
+ }
27
+ )
28
+ end
29
+
30
+ def self.create_from_remote(app_screenshot:, locale:)
31
+ raise ArgumentError unless app_screenshot.kind_of?(Spaceship::ConnectAPI::AppScreenshot)
32
+ raise ArgumentError unless locale.kind_of?(String)
33
+
34
+ new(
35
+ path: "#{locale}/#{app_screenshot.file_name}",
36
+ checksum: app_screenshot.source_file_checksum,
37
+ context: {
38
+ app_screenshot: app_screenshot,
39
+ locale: locale
40
+ }
41
+ )
42
+ end
43
+
44
+ def self.calculate_checksum(path)
45
+ bytes = File.binread(path)
46
+ Digest::MD5.hexdigest(bytes)
47
+ end
48
+
49
+ def initialize(path:, checksum:, context:)
50
+ @key = "#{path}/#{checksum}"
51
+ @context = context
52
+ end
53
+
54
+ def eql?(other)
55
+ key == other.key
56
+ end
57
+
58
+ def hash
59
+ key.hash
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,200 @@
1
+ require 'fastlane_core'
2
+ require 'digest/md5'
3
+ require 'naturally'
4
+
5
+ require_relative 'app_screenshot'
6
+ require_relative 'app_screenshot_iterator'
7
+ require_relative 'loader'
8
+ require_relative 'screenshot_comparable'
9
+
10
+ module Deliver
11
+ class SyncScreenshots
12
+ DeleteScreenshotJob = Struct.new(:app_screenshot, :locale)
13
+ UploadScreenshotJob = Struct.new(:app_screenshot_set, :path)
14
+
15
+ class UploadResult
16
+ attr_reader :asset_delivery_state_counts, :failing_screenshots
17
+
18
+ def initialize(asset_delivery_state_counts:, failing_screenshots:)
19
+ @asset_delivery_state_counts = asset_delivery_state_counts
20
+ @failing_screenshots = failing_screenshots
21
+ end
22
+
23
+ def processing?
24
+ @asset_delivery_state_counts.fetch('UPLOAD_COMPLETE', 0) > 0
25
+ end
26
+
27
+ def screenshot_count
28
+ @asset_delivery_state_counts.fetch('COMPLETE', 0)
29
+ end
30
+ end
31
+
32
+ def initialize(app:, platform:)
33
+ @app = app
34
+ @platform = platform
35
+ end
36
+
37
+ def sync_from_path(screenshots_path)
38
+ # load local screenshots
39
+ screenshots = Deliver::Loader.load_app_screenshots(screenshots_path, true)
40
+ sync(screenshots)
41
+ end
42
+
43
+ def sync(screenshots)
44
+ UI.important('This is currently a beta feature in fastlane. This may cause some errors on your environment.')
45
+
46
+ unless FastlaneCore::Feature.enabled?('FASTLANE_ENABLE_BETA_DELIVER_SYNC_SCREENSHOTS')
47
+ UI.user_error!('Please set a value to "FASTLANE_ENABLE_BETA_DELIVER_SYNC_SCREENSHOTS" environment variable ' \
48
+ 'if you acknowleage the risk and try this out.')
49
+ end
50
+
51
+ UI.important("Will begin uploading snapshots for '#{version.version_string}' on App Store Connect")
52
+
53
+ # enable localizations that will be used
54
+ screenshots_per_language = screenshots.group_by(&:language)
55
+ enable_localizations(screenshots_per_language.keys)
56
+
57
+ # create iterator
58
+ localizations = fetch_localizations
59
+ iterator = Deliver::AppScreenshotIterator.new(localizations)
60
+
61
+ # sync local screenshots with remote settings by deleting and uploading
62
+ UI.message("Starting with the upload of screenshots...")
63
+ replace_screenshots(iterator, screenshots)
64
+
65
+ # ensure screenshots within screenshot sets are sorted in right order
66
+ sort_screenshots(iterator)
67
+
68
+ UI.important('Screenshots are synced successfully!')
69
+ end
70
+
71
+ def enable_localizations(locales)
72
+ localizations = fetch_localizations
73
+ locales_to_enable = locales - localizations.map(&:locale)
74
+ Helper.show_loading_indicator("Activating localizations for #{locales_to_enable.join(', ')}...")
75
+ locales_to_enable.each do |locale|
76
+ version.create_app_store_version_localization(attributes: { locale: locale })
77
+ end
78
+ Helper.hide_loading_indicator
79
+ end
80
+
81
+ def replace_screenshots(iterator, screenshots, retries = 3)
82
+ # delete and upload screenshots to get App Store Connect in sync
83
+ do_replace_screenshots(iterator, screenshots, create_delete_worker, create_upload_worker)
84
+
85
+ # wait for screenshots to be processed on App Store Connect end and
86
+ # ensure the number of uploaded screenshots matches the one in local
87
+ result = wait_for_complete(iterator)
88
+ return if !result.processing? && result.screenshot_count == screenshots.count
89
+
90
+ if retries.zero?
91
+ UI.crash!("Retried uploading screenshots #{retries} but there are still failures of processing screenshots." \
92
+ "Check App Store Connect console to work out which screenshots processed unsuccessfully.")
93
+ end
94
+
95
+ # retry with deleting failing screenshots
96
+ result.failing_screenshots.each(&:delete!)
97
+ replace_screenshots(iterator, screenshots, retries - 1)
98
+ end
99
+
100
+ # This is a testable method that focuses on figuring out what to update
101
+ def do_replace_screenshots(iterator, screenshots, delete_worker, upload_worker)
102
+ remote_screenshots = iterator.each_app_screenshot.map do |localization, app_screenshot_set, app_screenshot|
103
+ ScreenshotComparable.create_from_remote(app_screenshot: app_screenshot, locale: localization.locale)
104
+ end
105
+
106
+ local_screenshots = iterator.each_local_screenshot(screenshots.group_by(&:language)).map do |localization, app_screenshot_set, screenshot, index|
107
+ if index >= 10
108
+ UI.user_error!("Found #{localization.locale} has more than 10 screenshots for #{app_screenshot_set.screenshot_display_type}. "\
109
+ "Make sure containts only necessary screenshots.")
110
+ end
111
+ ScreenshotComparable.create_from_local(screenshot: screenshot, app_screenshot_set: app_screenshot_set)
112
+ end
113
+
114
+ # Thanks to `Array#-` API and `ScreenshotComparable`, working out diffs between local screenshot directory and App Store Connect
115
+ # is as easy as you can see below. The former one finds what is missing in local and the latter one is visa versa.
116
+ screenshots_to_delete = remote_screenshots - local_screenshots
117
+ screenshots_to_upload = local_screenshots - remote_screenshots
118
+
119
+ delete_jobs = screenshots_to_delete.map { |x| DeleteScreenshotJob.new(x.context[:app_screenshot], x.context[:locale]) }
120
+ delete_worker.batch_enqueue(delete_jobs)
121
+ delete_worker.start
122
+
123
+ upload_jobs = screenshots_to_upload.map { |x| UploadScreenshotJob.new(x.context[:app_screenshot_set], x.context[:screenshot].path) }
124
+ upload_worker.batch_enqueue(upload_jobs)
125
+ upload_worker.start
126
+ end
127
+
128
+ def wait_for_complete(iterator)
129
+ retry_count = 0
130
+ Helper.show_loading_indicator("Waiting for all the screenshots processed...")
131
+ loop do
132
+ failing_screenshots = []
133
+ state_counts = iterator.each_app_screenshot.map { |_, _, app_screenshot| app_screenshot }.each_with_object({}) do |app_screenshot, hash|
134
+ state = app_screenshot.asset_delivery_state['state']
135
+ hash[state] ||= 0
136
+ hash[state] += 1
137
+ failing_screenshots << app_screenshot if app_screenshot.error?
138
+ end
139
+
140
+ result = UploadResult.new(asset_delivery_state_counts: state_counts, failing_screenshots: failing_screenshots)
141
+ return result unless result.processing?
142
+
143
+ # sleep with exponential backoff
144
+ interval = 5 + (2**retry_count)
145
+ UI.message("There are still incomplete screenshots. Will check the states again in #{interval} secs - #{state_counts}")
146
+ sleep(interval)
147
+ retry_count += 1
148
+ end
149
+ ensure
150
+ Helper.hide_loading_indicator
151
+ end
152
+
153
+ def sort_screenshots(iterator)
154
+ Helper.show_loading_indicator("Sorting screenshots uploaded...")
155
+ sort_worker = create_sort_worker
156
+ sort_worker.batch_enqueue(iterator.each_app_screenshot_set.to_a.map { |_, set| set })
157
+ sort_worker.start
158
+ Helper.hide_loading_indicator
159
+ end
160
+
161
+ private
162
+
163
+ def version
164
+ @version ||= @app.get_edit_app_store_version(platform: @platform)
165
+ end
166
+
167
+ def fetch_localizations
168
+ version.get_app_store_version_localizations
169
+ end
170
+
171
+ def create_upload_worker
172
+ FastlaneCore::QueueWorker.new do |job|
173
+ UI.verbose("Uploading '#{job.path}'...")
174
+ start_time = Time.now
175
+ job.app_screenshot_set.upload_screenshot(path: job.path, wait_for_processing: false)
176
+ UI.message("Uploaded '#{job.path}'... (#{Time.now - start_time} secs)")
177
+ end
178
+ end
179
+
180
+ def create_delete_worker
181
+ FastlaneCore::QueueWorker.new do |job|
182
+ target = "id=#{job.app_screenshot.id} #{job.locale} #{job.app_screenshot.file_name}"
183
+ UI.verbose("Deleting '#{target}'")
184
+ start_time = Time.now
185
+ job.app_screenshot.delete!
186
+ UI.message("Deleted '#{target}' - (#{Time.now - start_time} secs)")
187
+ end
188
+ end
189
+
190
+ def create_sort_worker
191
+ FastlaneCore::QueueWorker.new do |app_screenshot_set|
192
+ original_ids = app_screenshot_set.app_screenshots.map(&:id)
193
+ sorted_ids = Naturally.sort(app_screenshot_set.app_screenshots, by: :file_name).map(&:id)
194
+ if original_ids != sorted_ids
195
+ app_screenshot_set.reorder_screenshots(app_screenshot_ids: sorted_ids)
196
+ end
197
+ end
198
+ end
199
+ end
200
+ end