fastlane 2.189.0 → 2.193.0

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