fastlane 2.188.0 → 2.192.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +88 -88
  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/app_store_connect_api_key.rb +1 -1
  13. data/fastlane/lib/fastlane/actions/bundle_install.rb +13 -1
  14. data/fastlane/lib/fastlane/actions/clean_cocoapods_cache.rb +25 -1
  15. data/fastlane/lib/fastlane/actions/create_xcframework.rb +97 -17
  16. data/fastlane/lib/fastlane/actions/docs/capture_android_screenshots.md +2 -2
  17. data/fastlane/lib/fastlane/actions/docs/upload_to_app_store.md.erb +2 -2
  18. data/fastlane/lib/fastlane/actions/notarize.rb +77 -1
  19. data/fastlane/lib/fastlane/actions/push_git_tags.rb +1 -1
  20. data/fastlane/lib/fastlane/actions/zip.rb +86 -21
  21. data/fastlane/lib/fastlane/features.rb +3 -0
  22. data/fastlane/lib/fastlane/version.rb +1 -1
  23. data/fastlane/swift/Deliverfile.swift +1 -1
  24. data/fastlane/swift/DeliverfileProtocol.swift +5 -1
  25. data/fastlane/swift/Fastlane.swift +120 -20
  26. data/fastlane/swift/Gymfile.swift +1 -1
  27. data/fastlane/swift/GymfileProtocol.swift +1 -1
  28. data/fastlane/swift/Matchfile.swift +1 -1
  29. data/fastlane/swift/MatchfileProtocol.swift +1 -1
  30. data/fastlane/swift/Precheckfile.swift +1 -1
  31. data/fastlane/swift/PrecheckfileProtocol.swift +1 -1
  32. data/fastlane/swift/Scanfile.swift +1 -1
  33. data/fastlane/swift/ScanfileProtocol.swift +5 -1
  34. data/fastlane/swift/Screengrabfile.swift +1 -1
  35. data/fastlane/swift/ScreengrabfileProtocol.swift +1 -1
  36. data/fastlane/swift/Snapshotfile.swift +1 -1
  37. data/fastlane/swift/SnapshotfileProtocol.swift +1 -1
  38. data/fastlane/swift/formatting/Brewfile.lock.json +9 -9
  39. data/fastlane_core/lib/fastlane_core/keychain_importer.rb +11 -4
  40. data/fastlane_core/lib/fastlane_core/ui/disable_colors.rb +1 -0
  41. data/fastlane_core/lib/fastlane_core/ui/fastlane_runner.rb +41 -0
  42. data/pilot/lib/pilot/build_manager.rb +0 -1
  43. data/precheck/lib/precheck/module.rb +2 -0
  44. data/precheck/lib/precheck/options.rb +3 -3
  45. data/produce/lib/produce/commands_generator.rb +28 -0
  46. data/produce/lib/produce/service.rb +16 -1
  47. data/scan/lib/scan/detect_values.rb +22 -13
  48. data/scan/lib/scan/module.rb +1 -0
  49. data/scan/lib/scan/options.rb +12 -1
  50. data/scan/lib/scan/test_command_generator.rb +29 -6
  51. data/scan/lib/scan/xcpretty_reporter_options_generator.rb +1 -1
  52. data/sigh/lib/sigh/options.rb +2 -1
  53. data/spaceship/lib/spaceship/client.rb +6 -0
  54. data/spaceship/lib/spaceship/connect_api/api_client.rb +15 -1
  55. data/spaceship/lib/spaceship/connect_api/models/app.rb +9 -1
  56. data/spaceship/lib/spaceship/connect_api/models/capabilities.rb +27 -0
  57. data/spaceship/lib/spaceship/connect_api/models/user.rb +17 -3
  58. data/spaceship/lib/spaceship/connect_api/models/user_invitation.rb +26 -5
  59. data/spaceship/lib/spaceship/connect_api/provisioning/provisioning.rb +5 -0
  60. data/spaceship/lib/spaceship/connect_api/testflight/.testflight.rb.swp +0 -0
  61. data/spaceship/lib/spaceship/connect_api/testflight/client.rb +3 -0
  62. data/spaceship/lib/spaceship/connect_api/testflight/testflight.rb +46 -5
  63. data/spaceship/lib/spaceship/connect_api/token.rb +4 -1
  64. data/spaceship/lib/spaceship/connect_api/tunes/client.rb +3 -0
  65. data/spaceship/lib/spaceship/connect_api/users/client.rb +3 -0
  66. data/spaceship/lib/spaceship/connect_api/users/users.rb +58 -3
  67. data/spaceship/lib/spaceship/connect_api.rb +1 -0
  68. data/spaceship/lib/spaceship/tunes/tunes_client.rb +3 -0
  69. data/supply/lib/supply/client.rb +40 -1
  70. data/supply/lib/supply/options.rb +12 -0
  71. data/supply/lib/supply/uploader.rb +1 -1
  72. metadata +41 -25
  73. data/spaceship/lib/spaceship/connect_api/models/.bundle_id.rb.swp +0 -0
  74. data/spaceship/lib/spaceship/portal/.app.rb.swp +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 23a8e6b064fc103d7a8d701428ac360a7cb03ef0761d6dc067ae392dacd02bbf
4
- data.tar.gz: 11a4781c02ca5777fceac1364311be8726f6a4fc6f8943f5215a9c753c66c127
3
+ metadata.gz: f1c0c051ec3343adfcc8dcc927954b9fe5e76bf56d813be5d4f0823695dcb835
4
+ data.tar.gz: 328239727a6f80cca28a6524784cb2f4e1b39454f0c6f2e2cf036735c26f6811
5
5
  SHA512:
6
- metadata.gz: f4ebfae993bfb3db9bc6dcf91c4d526b98b9202c6813f894579979ad52a9bffe74522eaa8b2a2a36b7a0d87be275cac4ce677a9d9864746bc92d5c7d64737e12
7
- data.tar.gz: b1aacae9f83f897fd56bc9792d3a7c9842f0ffcf7caf64963b80dd7283f304116232d46608512af53804fdbb6075b1dffab7339776c0a1cff85b75992f5bf53e
6
+ metadata.gz: c430d54e72ceb927d096d02ac12578e196194405a6398768656ded1b0274d6fdb1988487d400b6c3e7434e02b82f372ac200959f524afaa9374d385a4ab00a46
7
+ data.tar.gz: 3800cfae6b4add96d08f98fa780c721d2444c4e81e1bf24e437e5e7e9f455682b198a7bc63246738fd758da7f0437dfb311d410632b0d22faa4af0e58e7d0301
data/README.md CHANGED
@@ -35,49 +35,29 @@ 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='matthew-ellis'>
39
- <a href='https://github.com/matthewellis'>
40
- <img src='https://github.com/matthewellis.png' width='140px;'>
41
- </a>
42
- <h4 align='center'><a href='https://twitter.com/mellis1995'>Matthew Ellis</a></h4>
43
- </td>
44
- <td id='josh-holtz'>
45
- <a href='https://github.com/joshdholtz'>
46
- <img src='https://github.com/joshdholtz.png' width='140px;'>
47
- </a>
48
- <h4 align='center'><a href='https://twitter.com/joshdholtz'>Josh Holtz</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
38
  <td id='stefan-natchev'>
57
39
  <a href='https://github.com/snatchev'>
58
40
  <img src='https://github.com/snatchev.png' width='140px;'>
59
41
  </a>
60
42
  <h4 align='center'><a href='https://twitter.com/snatchev'>Stefan Natchev</a></h4>
61
43
  </td>
62
- <td id='roger-oba'>
63
- <a href='https://github.com/rogerluan'>
64
- <img src='https://github.com/rogerluan.png' width='140px;'>
44
+ <td id='maksym-grebenets'>
45
+ <a href='https://github.com/mgrebenets'>
46
+ <img src='https://github.com/mgrebenets.png' width='140px;'>
65
47
  </a>
66
- <h4 align='center'><a href='https://twitter.com/rogerluan_'>Roger Oba</a></h4>
48
+ <h4 align='center'><a href='https://twitter.com/mgrebenets'>Maksym Grebenets</a></h4>
67
49
  </td>
68
- </tr>
69
- <tr>
70
- <td id='manish-rathi'>
71
- <a href='https://github.com/crazymanish'>
72
- <img src='https://github.com/crazymanish.png' width='140px;'>
50
+ <td id='aaron-brager'>
51
+ <a href='https://github.com/getaaron'>
52
+ <img src='https://github.com/getaaron.png' width='140px;'>
73
53
  </a>
74
- <h4 align='center'><a href='https://twitter.com/iammanishrathi'>Manish Rathi</a></h4>
54
+ <h4 align='center'><a href='https://twitter.com/getaaron'>Aaron Brager</a></h4>
75
55
  </td>
76
- <td id='danielle-tomlinson'>
77
- <a href='https://github.com/endocrimes'>
78
- <img src='https://github.com/endocrimes.png' width='140px;'>
56
+ <td id='satoshi-namai'>
57
+ <a href='https://github.com/ainame'>
58
+ <img src='https://github.com/ainame.png' width='140px;'>
79
59
  </a>
80
- <h4 align='center'><a href='https://twitter.com/endocrimes'>Danielle Tomlinson</a></h4>
60
+ <h4 align='center'><a href='https://twitter.com/ainame'>Satoshi Namai</a></h4>
81
61
  </td>
82
62
  <td id='jorge-revuelta-h'>
83
63
  <a href='https://github.com/minuscorp'>
@@ -85,37 +65,19 @@ If the above doesn't help, please [submit an issue](https://github.com/fastlane/
85
65
  </a>
86
66
  <h4 align='center'><a href='https://twitter.com/minuscorp'>Jorge Revuelta H</a></h4>
87
67
  </td>
88
- <td id='jimmy-dee'>
89
- <a href='https://github.com/jdee'>
90
- <img src='https://github.com/jdee.png' width='140px;'>
91
- </a>
92
- <h4 align='center'>Jimmy Dee</h4>
93
- </td>
94
- <td id='iulian-onofrei'>
95
- <a href='https://github.com/revolter'>
96
- <img src='https://github.com/revolter.png' width='140px;'>
97
- </a>
98
- <h4 align='center'><a href='https://twitter.com/Revolt666'>Iulian Onofrei</a></h4>
99
- </td>
100
68
  </tr>
101
69
  <tr>
102
- <td id='helmut-januschka'>
103
- <a href='https://github.com/hjanuschka'>
104
- <img src='https://github.com/hjanuschka.png' width='140px;'>
105
- </a>
106
- <h4 align='center'><a href='https://twitter.com/hjanuschka'>Helmut Januschka</a></h4>
107
- </td>
108
- <td id='satoshi-namai'>
109
- <a href='https://github.com/ainame'>
110
- <img src='https://github.com/ainame.png' width='140px;'>
70
+ <td id='olivier-halligon'>
71
+ <a href='https://github.com/AliSoftware'>
72
+ <img src='https://github.com/AliSoftware.png' width='140px;'>
111
73
  </a>
112
- <h4 align='center'><a href='https://twitter.com/ainame'>Satoshi Namai</a></h4>
74
+ <h4 align='center'><a href='https://twitter.com/aligatr'>Olivier Halligon</a></h4>
113
75
  </td>
114
- <td id='luka-mirosevic'>
115
- <a href='https://github.com/lmirosevic'>
116
- <img src='https://github.com/lmirosevic.png' width='140px;'>
76
+ <td id='manu-wallner'>
77
+ <a href='https://github.com/milch'>
78
+ <img src='https://github.com/milch.png' width='140px;'>
117
79
  </a>
118
- <h4 align='center'><a href='https://twitter.com/lmirosevic'>Luka Mirosevic</a></h4>
80
+ <h4 align='center'><a href='https://twitter.com/acrooow'>Manu Wallner</a></h4>
119
81
  </td>
120
82
  <td id='joshua-liebowitz'>
121
83
  <a href='https://github.com/taquitos'>
@@ -123,57 +85,75 @@ If the above doesn't help, please [submit an issue](https://github.com/fastlane/
123
85
  </a>
124
86
  <h4 align='center'><a href='https://twitter.com/taquitos'>Joshua Liebowitz</a></h4>
125
87
  </td>
126
- <td id='andrew-mcburney'>
127
- <a href='https://github.com/armcburney'>
128
- <img src='https://github.com/armcburney.png' width='140px;'>
88
+ <td id='luka-mirosevic'>
89
+ <a href='https://github.com/lmirosevic'>
90
+ <img src='https://github.com/lmirosevic.png' width='140px;'>
129
91
  </a>
130
- <h4 align='center'><a href='https://twitter.com/armcburney'>Andrew McBurney</a></h4>
92
+ <h4 align='center'><a href='https://twitter.com/lmirosevic'>Luka Mirosevic</a></h4>
131
93
  </td>
132
- </tr>
133
- <tr>
134
- <td id='olivier-halligon'>
135
- <a href='https://github.com/AliSoftware'>
136
- <img src='https://github.com/AliSoftware.png' width='140px;'>
94
+ <td id='jan-piotrowski'>
95
+ <a href='https://github.com/janpio'>
96
+ <img src='https://github.com/janpio.png' width='140px;'>
137
97
  </a>
138
- <h4 align='center'><a href='https://twitter.com/aligatr'>Olivier Halligon</a></h4>
98
+ <h4 align='center'><a href='https://twitter.com/Sujan'>Jan Piotrowski</a></h4>
139
99
  </td>
100
+ </tr>
101
+ <tr>
140
102
  <td id='daniel-jankowski'>
141
103
  <a href='https://github.com/mollyIV'>
142
104
  <img src='https://github.com/mollyIV.png' width='140px;'>
143
105
  </a>
144
106
  <h4 align='center'><a href='https://twitter.com/mollyIV'>Daniel Jankowski</a></h4>
145
107
  </td>
108
+ <td id='helmut-januschka'>
109
+ <a href='https://github.com/hjanuschka'>
110
+ <img src='https://github.com/hjanuschka.png' width='140px;'>
111
+ </a>
112
+ <h4 align='center'><a href='https://twitter.com/hjanuschka'>Helmut Januschka</a></h4>
113
+ </td>
146
114
  <td id='jérôme-lacoste'>
147
115
  <a href='https://github.com/lacostej'>
148
116
  <img src='https://github.com/lacostej.png' width='140px;'>
149
117
  </a>
150
118
  <h4 align='center'><a href='https://twitter.com/lacostej'>Jérôme Lacoste</a></h4>
151
119
  </td>
152
- <td id='jan-piotrowski'>
153
- <a href='https://github.com/janpio'>
154
- <img src='https://github.com/janpio.png' width='140px;'>
120
+ <td id='matthew-ellis'>
121
+ <a href='https://github.com/matthewellis'>
122
+ <img src='https://github.com/matthewellis.png' width='140px;'>
155
123
  </a>
156
- <h4 align='center'><a href='https://twitter.com/Sujan'>Jan Piotrowski</a></h4>
124
+ <h4 align='center'><a href='https://twitter.com/mellis1995'>Matthew Ellis</a></h4>
157
125
  </td>
158
- <td id='manu-wallner'>
159
- <a href='https://github.com/milch'>
160
- <img src='https://github.com/milch.png' width='140px;'>
126
+ <td id='fumiya-nakamura'>
127
+ <a href='https://github.com/nafu'>
128
+ <img src='https://github.com/nafu.png' width='140px;'>
161
129
  </a>
162
- <h4 align='center'><a href='https://twitter.com/acrooow'>Manu Wallner</a></h4>
130
+ <h4 align='center'><a href='https://twitter.com/nafu003'>Fumiya Nakamura</a></h4>
163
131
  </td>
164
132
  </tr>
165
133
  <tr>
166
- <td id='max-ott'>
167
- <a href='https://github.com/max-ott'>
168
- <img src='https://github.com/max-ott.png' width='140px;'>
134
+ <td id='danielle-tomlinson'>
135
+ <a href='https://github.com/endocrimes'>
136
+ <img src='https://github.com/endocrimes.png' width='140px;'>
169
137
  </a>
170
- <h4 align='center'><a href='https://twitter.com/ott_max'>Max Ott</a></h4>
138
+ <h4 align='center'><a href='https://twitter.com/endocrimes'>Danielle Tomlinson</a></h4>
171
139
  </td>
172
- <td id='aaron-brager'>
173
- <a href='https://github.com/getaaron'>
174
- <img src='https://github.com/getaaron.png' width='140px;'>
140
+ <td id='roger-oba'>
141
+ <a href='https://github.com/rogerluan'>
142
+ <img src='https://github.com/rogerluan.png' width='140px;'>
175
143
  </a>
176
- <h4 align='center'><a href='https://twitter.com/getaaron'>Aaron Brager</a></h4>
144
+ <h4 align='center'><a href='https://twitter.com/rogerluan_'>Roger Oba</a></h4>
145
+ </td>
146
+ <td id='iulian-onofrei'>
147
+ <a href='https://github.com/revolter'>
148
+ <img src='https://github.com/revolter.png' width='140px;'>
149
+ </a>
150
+ <h4 align='center'><a href='https://twitter.com/Revolt666'>Iulian Onofrei</a></h4>
151
+ </td>
152
+ <td id='jimmy-dee'>
153
+ <a href='https://github.com/jdee'>
154
+ <img src='https://github.com/jdee.png' width='140px;'>
155
+ </a>
156
+ <h4 align='center'>Jimmy Dee</h4>
177
157
  </td>
178
158
  <td id='felix-krause'>
179
159
  <a href='https://github.com/KrauseFx'>
@@ -181,17 +161,37 @@ If the above doesn't help, please [submit an issue](https://github.com/fastlane/
181
161
  </a>
182
162
  <h4 align='center'><a href='https://twitter.com/KrauseFx'>Felix Krause</a></h4>
183
163
  </td>
164
+ </tr>
165
+ <tr>
166
+ <td id='max-ott'>
167
+ <a href='https://github.com/max-ott'>
168
+ <img src='https://github.com/max-ott.png' width='140px;'>
169
+ </a>
170
+ <h4 align='center'><a href='https://twitter.com/ott_max'>Max Ott</a></h4>
171
+ </td>
172
+ <td id='manish-rathi'>
173
+ <a href='https://github.com/crazymanish'>
174
+ <img src='https://github.com/crazymanish.png' width='140px;'>
175
+ </a>
176
+ <h4 align='center'><a href='https://twitter.com/iammanishrathi'>Manish Rathi</a></h4>
177
+ </td>
184
178
  <td id='kohki-miki'>
185
179
  <a href='https://github.com/giginet'>
186
180
  <img src='https://github.com/giginet.png' width='140px;'>
187
181
  </a>
188
182
  <h4 align='center'><a href='https://twitter.com/giginet'>Kohki Miki</a></h4>
189
183
  </td>
190
- <td id='fumiya-nakamura'>
191
- <a href='https://github.com/nafu'>
192
- <img src='https://github.com/nafu.png' width='140px;'>
184
+ <td id='andrew-mcburney'>
185
+ <a href='https://github.com/armcburney'>
186
+ <img src='https://github.com/armcburney.png' width='140px;'>
193
187
  </a>
194
- <h4 align='center'><a href='https://twitter.com/nafu003'>Fumiya Nakamura</a></h4>
188
+ <h4 align='center'><a href='https://twitter.com/armcburney'>Andrew McBurney</a></h4>
189
+ </td>
190
+ <td id='josh-holtz'>
191
+ <a href='https://github.com/joshdholtz'>
192
+ <img src='https://github.com/joshdholtz.png' width='140px;'>
193
+ </a>
194
+ <h4 align='center'><a href='https://twitter.com/joshdholtz'>Josh Holtz</a></h4>
195
195
  </td>
196
196
  </tr>
197
197
  </table>
@@ -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