fastlane 2.232.2 → 2.233.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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +98 -98
  3. data/credentials_manager/lib/credentials_manager/appfile_config.rb +4 -0
  4. data/deliver/lib/deliver/detect_values.rb +2 -0
  5. data/deliver/lib/deliver/options.rb +23 -0
  6. data/deliver/lib/deliver/runner.rb +17 -12
  7. data/deliver/lib/deliver/sync_app_previews.rb +204 -0
  8. data/fastlane/lib/fastlane/actions/app_store_connect_api_key.rb +5 -1
  9. data/fastlane/lib/fastlane/actions/docs/upload_to_app_store.md.erb +20 -4
  10. data/fastlane/lib/fastlane/actions/docs/upload_to_testflight.md +3 -0
  11. data/fastlane/lib/fastlane/actions/resign.rb +13 -2
  12. data/fastlane/lib/fastlane/actions/swiftlint.rb +8 -1
  13. data/fastlane/lib/fastlane/actions/upload_to_app_store.rb +1 -1
  14. data/fastlane/lib/fastlane/helper/s3_client_helper.rb +5 -2
  15. data/fastlane/lib/fastlane/version.rb +1 -1
  16. data/fastlane/swift/Deliverfile.swift +1 -1
  17. data/fastlane/swift/DeliverfileProtocol.swift +29 -1
  18. data/fastlane/swift/Fastlane.swift +105 -9
  19. data/fastlane/swift/Gymfile.swift +1 -1
  20. data/fastlane/swift/GymfileProtocol.swift +8 -1
  21. data/fastlane/swift/Matchfile.swift +1 -1
  22. data/fastlane/swift/MatchfileProtocol.swift +8 -1
  23. data/fastlane/swift/Precheckfile.swift +1 -1
  24. data/fastlane/swift/PrecheckfileProtocol.swift +1 -1
  25. data/fastlane/swift/Scanfile.swift +1 -1
  26. data/fastlane/swift/ScanfileProtocol.swift +1 -1
  27. data/fastlane/swift/Screengrabfile.swift +1 -1
  28. data/fastlane/swift/ScreengrabfileProtocol.swift +1 -1
  29. data/fastlane/swift/Snapshotfile.swift +1 -1
  30. data/fastlane/swift/SnapshotfileProtocol.swift +1 -1
  31. data/fastlane_core/lib/fastlane_core/itunes_transporter.rb +48 -17
  32. data/fastlane_core/lib/fastlane_core/video_utils.rb +202 -0
  33. data/frameit/lib/frameit/device_types.rb +2 -2
  34. data/gym/lib/gym/generators/build_command_generator.rb +2 -1
  35. data/gym/lib/gym/options.rb +5 -0
  36. data/match/lib/match/generator.rb +3 -1
  37. data/match/lib/match/options.rb +5 -0
  38. data/match/lib/match/runner.rb +12 -7
  39. data/match/lib/match/storage/s3_storage.rb +4 -1
  40. data/match/lib/match/storage.rb +1 -0
  41. data/pilot/lib/pilot/build_manager.rb +4 -12
  42. data/pilot/lib/pilot/options.rb +4 -0
  43. data/precheck/lib/precheck/rules/rules_data/curse_word_hashes/README.md +54 -0
  44. data/precheck/lib/precheck/rules/rules_data/curse_word_hashes/en_us.txt +2 -1
  45. data/scan/lib/scan/detect_values.rb +11 -3
  46. data/sigh/lib/assets/resign.sh +17 -5
  47. data/sigh/lib/sigh/commands_generator.rb +1 -0
  48. data/sigh/lib/sigh/manager.rb +6 -6
  49. data/sigh/lib/sigh/resign.rb +9 -6
  50. data/spaceship/lib/spaceship/connect_api/models/app_preview_set.rb +54 -17
  51. data/spaceship/lib/spaceship/connect_api/models/app_store_version_localization.rb +1 -2
  52. data/spaceship/lib/spaceship/connect_api/tunes/tunes.rb +2 -2
  53. data/trainer/lib/trainer/legacy_xcresult.rb +27 -20
  54. data/trainer/lib/trainer/xcresult/test_suite.rb +4 -1
  55. metadata +25 -22
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b51c417477b7551fedb50df2cee8534dff03c5d626bbc087593f0f9bd5016db8
4
- data.tar.gz: 441b8c56863434b00e2853292d5995de56f3226ee779d00c0a1f7d466b2a410d
3
+ metadata.gz: a1e9ccdeab76489550801ba5eca3943ce3fb8b0f6a54e68281f39879469b5476
4
+ data.tar.gz: abd22788dfc2496906517916afbc50d18ae7789e5ba8430ccfdef5b979573f2b
5
5
  SHA512:
6
- metadata.gz: 5985fb114fa5eeb03f19b81452e0d4094cba4c05684985280b04adf042eac10d80c43ebbcc264cf94ab3b30e1934f7639d9e6011f75278b50075158532e4c52a
7
- data.tar.gz: 9d50eaca265539d2a64e073f6fed2032080dbbc40bc94f6b70ee23a2caeb122f6e03714deb7028d000b621b52de1b0fe4e62c5ce9b1b7e0d4adef9ba21a7a61c
6
+ metadata.gz: a801932f673957a2ce012624ad221efd34d7d0d04deca0f7df0f6dbc7eb976bb5ad1eaf6e0d005e1e60c7e01219665afa0b514e695dc063cf75e2834e79f0aaf
7
+ data.tar.gz: 81ff84ead2d0339f588af6fae06452092a97dac684513c5f5b02af084084bd727a6b140d17d01f3d85ed1f6d374f7063828dc6a86aa5ba139d0acbdd36ccb799
data/README.md CHANGED
@@ -35,55 +35,55 @@ 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='manish-rathi'>
39
- <a href='https://github.com/crazymanish'>
40
- <img src='https://github.com/crazymanish.png' width='140px;'>
38
+ <td id='manu-wallner'>
39
+ <a href='https://github.com/milch'>
40
+ <img src='https://github.com/milch.png' width='140px;'>
41
41
  </a>
42
- <h4 align='center'><a href='https://twitter.com/iammanishrathi'>Manish Rathi</a></h4>
42
+ <h4 align='center'><a href='https://twitter.com/acrooow'>Manu Wallner</a></h4>
43
43
  </td>
44
- <td id='connor-tumbleson'>
45
- <a href='https://github.com/ibotpeaches'>
46
- <img src='https://github.com/ibotpeaches.png' width='140px;'>
44
+ <td id='danielle-tomlinson'>
45
+ <a href='https://github.com/endocrimes'>
46
+ <img src='https://github.com/endocrimes.png' width='140px;'>
47
47
  </a>
48
- <h4 align='center'><a href='https://twitter.com/ibotpeaches'>Connor Tumbleson</a></h4>
48
+ <h4 align='center'><a href='https://twitter.com/endocrimes'>Danielle Tomlinson</a></h4>
49
49
  </td>
50
- <td id='satoshi-namai'>
51
- <a href='https://github.com/ainame'>
52
- <img src='https://github.com/ainame.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;'>
53
53
  </a>
54
- <h4 align='center'><a href='https://twitter.com/ainame'>Satoshi Namai</a></h4>
54
+ <h4 align='center'><a href='https://twitter.com/getaaron'>Aaron Brager</a></h4>
55
55
  </td>
56
- <td id='jorge-revuelta-h'>
57
- <a href='https://github.com/minuscorp'>
58
- <img src='https://github.com/minuscorp.png' width='140px;'>
56
+ <td id='jimmy-dee'>
57
+ <a href='https://github.com/jdee'>
58
+ <img src='https://github.com/jdee.png' width='140px;'>
59
59
  </a>
60
- <h4 align='center'><a href='https://twitter.com/minuscorp'>Jorge Revuelta H</a></h4>
60
+ <h4 align='center'>Jimmy Dee</h4>
61
61
  </td>
62
- <td id='roger-oba'>
63
- <a href='https://github.com/rogerluan'>
64
- <img src='https://github.com/rogerluan.png' width='140px;'>
62
+ <td id='kohki-miki'>
63
+ <a href='https://github.com/giginet'>
64
+ <img src='https://github.com/giginet.png' width='140px;'>
65
65
  </a>
66
- <h4 align='center'><a href='https://twitter.com/rogerluan_'>Roger Oba</a></h4>
66
+ <h4 align='center'><a href='https://twitter.com/giginet'>Kohki Miki</a></h4>
67
67
  </td>
68
68
  </tr>
69
69
  <tr>
70
- <td id='max-ott'>
71
- <a href='https://github.com/max-ott'>
72
- <img src='https://github.com/max-ott.png' width='140px;'>
70
+ <td id='matthew-ellis'>
71
+ <a href='https://github.com/matthewellis'>
72
+ <img src='https://github.com/matthewellis.png' width='140px;'>
73
73
  </a>
74
- <h4 align='center'><a href='https://twitter.com/ott_max'>Max Ott</a></h4>
74
+ <h4 align='center'><a href='https://twitter.com/mellis1995'>Matthew Ellis</a></h4>
75
75
  </td>
76
- <td id='helmut-januschka'>
77
- <a href='https://github.com/hjanuschka'>
78
- <img src='https://github.com/hjanuschka.png' width='140px;'>
76
+ <td id='fumiya-nakamura'>
77
+ <a href='https://github.com/nafu'>
78
+ <img src='https://github.com/nafu.png' width='140px;'>
79
79
  </a>
80
- <h4 align='center'><a href='https://twitter.com/hjanuschka'>Helmut Januschka</a></h4>
80
+ <h4 align='center'><a href='https://twitter.com/nafu003'>Fumiya Nakamura</a></h4>
81
81
  </td>
82
- <td id='danielle-tomlinson'>
83
- <a href='https://github.com/endocrimes'>
84
- <img src='https://github.com/endocrimes.png' width='140px;'>
82
+ <td id='luka-mirosevic'>
83
+ <a href='https://github.com/lmirosevic'>
84
+ <img src='https://github.com/lmirosevic.png' width='140px;'>
85
85
  </a>
86
- <h4 align='center'><a href='https://twitter.com/endocrimes'>Danielle Tomlinson</a></h4>
86
+ <h4 align='center'><a href='https://twitter.com/lmirosevic'>Luka Mirosevic</a></h4>
87
87
  </td>
88
88
  <td id='josh-holtz'>
89
89
  <a href='https://github.com/joshdholtz'>
@@ -91,89 +91,89 @@ If the above doesn't help, please [submit an issue](https://github.com/fastlane/
91
91
  </a>
92
92
  <h4 align='center'><a href='https://twitter.com/joshdholtz'>Josh Holtz</a></h4>
93
93
  </td>
94
- <td id='matthew-ellis'>
95
- <a href='https://github.com/matthewellis'>
96
- <img src='https://github.com/matthewellis.png' width='140px;'>
94
+ <td id='jérôme-lacoste'>
95
+ <a href='https://github.com/lacostej'>
96
+ <img src='https://github.com/lacostej.png' width='140px;'>
97
97
  </a>
98
- <h4 align='center'><a href='https://twitter.com/mellis1995'>Matthew Ellis</a></h4>
98
+ <h4 align='center'><a href='https://twitter.com/lacostej'>Jérôme Lacoste</a></h4>
99
99
  </td>
100
100
  </tr>
101
101
  <tr>
102
- <td id='andrew-mcburney'>
103
- <a href='https://github.com/armcburney'>
104
- <img src='https://github.com/armcburney.png' width='140px;'>
105
- </a>
106
- <h4 align='center'><a href='https://twitter.com/armcburney'>Andrew McBurney</a></h4>
107
- </td>
108
102
  <td id='daniel-jankowski'>
109
103
  <a href='https://github.com/mollyIV'>
110
104
  <img src='https://github.com/mollyIV.png' width='140px;'>
111
105
  </a>
112
106
  <h4 align='center'><a href='https://twitter.com/mollyIV'>Daniel Jankowski</a></h4>
113
107
  </td>
114
- <td id='łukasz-grabowski'>
115
- <a href='https://github.com/lucgrabowski'>
116
- <img src='https://github.com/lucgrabowski.png' width='140px;'>
108
+ <td id='helmut-januschka'>
109
+ <a href='https://github.com/hjanuschka'>
110
+ <img src='https://github.com/hjanuschka.png' width='140px;'>
117
111
  </a>
118
- <h4 align='center'>Łukasz Grabowski</h4>
112
+ <h4 align='center'><a href='https://twitter.com/hjanuschka'>Helmut Januschka</a></h4>
119
113
  </td>
120
- <td id='jimmy-dee'>
121
- <a href='https://github.com/jdee'>
122
- <img src='https://github.com/jdee.png' width='140px;'>
114
+ <td id='andrew-mcburney'>
115
+ <a href='https://github.com/armcburney'>
116
+ <img src='https://github.com/armcburney.png' width='140px;'>
123
117
  </a>
124
- <h4 align='center'>Jimmy Dee</h4>
118
+ <h4 align='center'><a href='https://twitter.com/armcburney'>Andrew McBurney</a></h4>
125
119
  </td>
126
- <td id='kohki-miki'>
127
- <a href='https://github.com/giginet'>
128
- <img src='https://github.com/giginet.png' width='140px;'>
120
+ <td id='iulian-onofrei'>
121
+ <a href='https://github.com/revolter'>
122
+ <img src='https://github.com/revolter.png' width='140px;'>
129
123
  </a>
130
- <h4 align='center'><a href='https://twitter.com/giginet'>Kohki Miki</a></h4>
124
+ <h4 align='center'><a href='https://twitter.com/Revolt666'>Iulian Onofrei</a></h4>
125
+ </td>
126
+ <td id='jorge-revuelta-h'>
127
+ <a href='https://github.com/minuscorp'>
128
+ <img src='https://github.com/minuscorp.png' width='140px;'>
129
+ </a>
130
+ <h4 align='center'><a href='https://twitter.com/minuscorp'>Jorge Revuelta H</a></h4>
131
131
  </td>
132
132
  </tr>
133
133
  <tr>
134
- <td id='olivier-halligon'>
135
- <a href='https://github.com/AliSoftware'>
136
- <img src='https://github.com/AliSoftware.png' width='140px;'>
134
+ <td id='connor-tumbleson'>
135
+ <a href='https://github.com/ibotpeaches'>
136
+ <img src='https://github.com/ibotpeaches.png' width='140px;'>
137
137
  </a>
138
- <h4 align='center'><a href='https://twitter.com/aligatr'>Olivier Halligon</a></h4>
138
+ <h4 align='center'><a href='https://twitter.com/ibotpeaches'>Connor Tumbleson</a></h4>
139
139
  </td>
140
- <td id='aaron-brager'>
141
- <a href='https://github.com/getaaron'>
142
- <img src='https://github.com/getaaron.png' width='140px;'>
140
+ <td id='jan-piotrowski'>
141
+ <a href='https://github.com/janpio'>
142
+ <img src='https://github.com/janpio.png' width='140px;'>
143
143
  </a>
144
- <h4 align='center'><a href='https://twitter.com/getaaron'>Aaron Brager</a></h4>
144
+ <h4 align='center'><a href='https://twitter.com/Sujan'>Jan Piotrowski</a></h4>
145
145
  </td>
146
- <td id='joshua-liebowitz'>
147
- <a href='https://github.com/taquitos'>
148
- <img src='https://github.com/taquitos.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;'>
149
149
  </a>
150
- <h4 align='center'><a href='https://twitter.com/taquitos'>Joshua Liebowitz</a></h4>
150
+ <h4 align='center'><a href='https://twitter.com/snatchev'>Stefan Natchev</a></h4>
151
151
  </td>
152
- <td id='manu-wallner'>
153
- <a href='https://github.com/milch'>
154
- <img src='https://github.com/milch.png' width='140px;'>
152
+ <td id='manish-rathi'>
153
+ <a href='https://github.com/crazymanish'>
154
+ <img src='https://github.com/crazymanish.png' width='140px;'>
155
155
  </a>
156
- <h4 align='center'><a href='https://twitter.com/acrooow'>Manu Wallner</a></h4>
156
+ <h4 align='center'><a href='https://twitter.com/iammanishrathi'>Manish Rathi</a></h4>
157
157
  </td>
158
- <td id='luka-mirosevic'>
159
- <a href='https://github.com/lmirosevic'>
160
- <img src='https://github.com/lmirosevic.png' width='140px;'>
158
+ <td id='olivier-halligon'>
159
+ <a href='https://github.com/AliSoftware'>
160
+ <img src='https://github.com/AliSoftware.png' width='140px;'>
161
161
  </a>
162
- <h4 align='center'><a href='https://twitter.com/lmirosevic'>Luka Mirosevic</a></h4>
162
+ <h4 align='center'><a href='https://twitter.com/aligatr'>Olivier Halligon</a></h4>
163
163
  </td>
164
164
  </tr>
165
165
  <tr>
166
- <td id='jérôme-lacoste'>
167
- <a href='https://github.com/lacostej'>
168
- <img src='https://github.com/lacostej.png' width='140px;'>
166
+ <td id='roger-oba'>
167
+ <a href='https://github.com/rogerluan'>
168
+ <img src='https://github.com/rogerluan.png' width='140px;'>
169
169
  </a>
170
- <h4 align='center'><a href='https://twitter.com/lacostej'>Jérôme Lacoste</a></h4>
170
+ <h4 align='center'><a href='https://twitter.com/rogerluan_'>Roger Oba</a></h4>
171
171
  </td>
172
- <td id='iulian-onofrei'>
173
- <a href='https://github.com/revolter'>
174
- <img src='https://github.com/revolter.png' width='140px;'>
172
+ <td id='maksym-grebenets'>
173
+ <a href='https://github.com/mgrebenets'>
174
+ <img src='https://github.com/mgrebenets.png' width='140px;'>
175
175
  </a>
176
- <h4 align='center'><a href='https://twitter.com/Revolt666'>Iulian Onofrei</a></h4>
176
+ <h4 align='center'><a href='https://twitter.com/mgrebenets'>Maksym Grebenets</a></h4>
177
177
  </td>
178
178
  <td id='felix-krause'>
179
179
  <a href='https://github.com/KrauseFx'>
@@ -181,31 +181,31 @@ If the above doesn't help, please [submit an issue](https://github.com/fastlane/
181
181
  </a>
182
182
  <h4 align='center'><a href='https://twitter.com/KrauseFx'>Felix Krause</a></h4>
183
183
  </td>
184
- <td id='stefan-natchev'>
185
- <a href='https://github.com/snatchev'>
186
- <img src='https://github.com/snatchev.png' width='140px;'>
184
+ <td id='łukasz-grabowski'>
185
+ <a href='https://github.com/lucgrabowski'>
186
+ <img src='https://github.com/lucgrabowski.png' width='140px;'>
187
187
  </a>
188
- <h4 align='center'><a href='https://twitter.com/snatchev'>Stefan Natchev</a></h4>
188
+ <h4 align='center'>Łukasz Grabowski</h4>
189
189
  </td>
190
- <td id='maksym-grebenets'>
191
- <a href='https://github.com/mgrebenets'>
192
- <img src='https://github.com/mgrebenets.png' width='140px;'>
190
+ <td id='joshua-liebowitz'>
191
+ <a href='https://github.com/taquitos'>
192
+ <img src='https://github.com/taquitos.png' width='140px;'>
193
193
  </a>
194
- <h4 align='center'><a href='https://twitter.com/mgrebenets'>Maksym Grebenets</a></h4>
194
+ <h4 align='center'><a href='https://twitter.com/taquitos'>Joshua Liebowitz</a></h4>
195
195
  </td>
196
196
  </tr>
197
197
  <tr>
198
- <td id='fumiya-nakamura'>
199
- <a href='https://github.com/nafu'>
200
- <img src='https://github.com/nafu.png' width='140px;'>
198
+ <td id='satoshi-namai'>
199
+ <a href='https://github.com/ainame'>
200
+ <img src='https://github.com/ainame.png' width='140px;'>
201
201
  </a>
202
- <h4 align='center'><a href='https://twitter.com/nafu003'>Fumiya Nakamura</a></h4>
202
+ <h4 align='center'><a href='https://twitter.com/ainame'>Satoshi Namai</a></h4>
203
203
  </td>
204
- <td id='jan-piotrowski'>
205
- <a href='https://github.com/janpio'>
206
- <img src='https://github.com/janpio.png' width='140px;'>
204
+ <td id='max-ott'>
205
+ <a href='https://github.com/max-ott'>
206
+ <img src='https://github.com/max-ott.png' width='140px;'>
207
207
  </a>
208
- <h4 align='center'><a href='https://twitter.com/Sujan'>Jan Piotrowski</a></h4>
208
+ <h4 align='center'><a href='https://twitter.com/ott_max'>Max Ott</a></h4>
209
209
  </td>
210
210
  </table>
211
211
 
@@ -123,6 +123,10 @@ module CredentialsManager
123
123
  setter(:itc_provider, *args, &block)
124
124
  end
125
125
 
126
+ def provider_public_id(*args, &block)
127
+ setter(:provider_public_id, *args, &block)
128
+ end
129
+
126
130
  # Android
127
131
  def json_key_file(*args, &block)
128
132
  setter(:json_key_file, *args, &block)
@@ -58,11 +58,13 @@ module Deliver
58
58
  def find_folders(options)
59
59
  containing = Helper.fastlane_enabled? ? FastlaneCore::FastlaneFolder.path : '.'
60
60
  options[:screenshots_path] ||= File.join(containing, 'screenshots')
61
+ options[:app_previews_path] ||= File.join(containing, 'app-previews')
61
62
  options[:metadata_path] ||= File.join(containing, 'metadata')
62
63
  end
63
64
 
64
65
  def ensure_folders_created(options)
65
66
  FileUtils.mkdir_p(options[:screenshots_path])
67
+ FileUtils.mkdir_p(options[:app_previews_path])
66
68
  FileUtils.mkdir_p(options[:metadata_path])
67
69
  end
68
70
 
@@ -128,6 +128,22 @@ module Deliver
128
128
  description: "Path to the folder containing the screenshots",
129
129
  optional: true),
130
130
 
131
+ # app previews (videos)
132
+ FastlaneCore::ConfigItem.new(key: :app_previews_path,
133
+ env_name: "DELIVER_APP_PREVIEWS_PATH",
134
+ description: "Path to the folder containing localized App Preview videos",
135
+ optional: true),
136
+ FastlaneCore::ConfigItem.new(key: :preview_frame_time_code,
137
+ env_name: "DELIVER_PREVIEW_FRAME_TIME_CODE",
138
+ description: "Time code for the App Preview still frame written as hour:minute:second:centisecond (e.g. 00:00:00:01)",
139
+ optional: true,
140
+ default_value: "00:00:05:00"),
141
+ FastlaneCore::ConfigItem.new(key: :overwrite_preview_videos,
142
+ env_name: "DELIVER_OVERWRITE_PREVIEW_VIDEOS",
143
+ description: "Clear all previously uploaded App Preview videos before uploading the new ones",
144
+ type: Boolean,
145
+ default_value: false),
146
+
131
147
  # skip
132
148
  FastlaneCore::ConfigItem.new(key: :skip_binary_upload,
133
149
  env_name: "DELIVER_SKIP_BINARY_UPLOAD",
@@ -307,6 +323,13 @@ module Deliver
307
323
  code_gen_sensitive: true,
308
324
  default_value: CredentialsManager::AppfileConfig.try_fetch_value(:itc_provider),
309
325
  default_value_dynamic: true),
326
+ FastlaneCore::ConfigItem.new(key: :provider_public_id,
327
+ env_name: "DELIVER_PROVIDER_PUBLIC_ID",
328
+ description: "The provider public ID to be used with altool (--provider-public-id). This value will override the automatically detected provider value for altool uploads. Required after Xcode 26 when your account is associated with multiple providers and using username/app-password authentication",
329
+ optional: true,
330
+ code_gen_sensitive: true,
331
+ default_value: CredentialsManager::AppfileConfig.try_fetch_value(:provider_public_id),
332
+ default_value_dynamic: true),
310
333
  # rubocop:enable Layout/LineLength
311
334
 
312
335
  # precheck
@@ -11,6 +11,7 @@ require_relative 'upload_price_tier'
11
11
  require_relative 'upload_metadata'
12
12
  require_relative 'upload_screenshots'
13
13
  require_relative 'sync_screenshots'
14
+ require_relative 'sync_app_previews'
14
15
  require_relative 'detect_values'
15
16
 
16
17
  module Deliver
@@ -158,6 +159,17 @@ module Deliver
158
159
  upload_screenshots.upload(options, screenshots)
159
160
  end
160
161
 
162
+ if options[:app_previews_path]
163
+ previews = Deliver::SyncAppPreviews.new(
164
+ app: Deliver.cache[:app],
165
+ platform: Spaceship::ConnectAPI::Platform.map(options[:platform]),
166
+ app_previews_path: options[:app_previews_path],
167
+ preview_frame_time_code: options[:preview_frame_time_code],
168
+ overwrite_preview_videos: options[:overwrite_preview_videos]
169
+ )
170
+ previews.sync_from_path
171
+ end
172
+
161
173
  UploadPriceTier.new.upload(options)
162
174
  end
163
175
 
@@ -265,7 +277,7 @@ module Deliver
265
277
 
266
278
  # If App Store Connect API token, use token.
267
279
  # If api_key is specified and it is an Individual API Key, don't use token but use username.
268
- # If itc_provider was explicitly specified, use it.
280
+ # If itc_provider or provider_public_id was explicitly specified, use it.
269
281
  # If there are multiple teams, infer the provider from the selected team name.
270
282
  # If there are fewer than two teams, don't infer the provider.
271
283
  def transporter_for_selected_team
@@ -281,23 +293,16 @@ module Deliver
281
293
  api_key
282
294
  end
283
295
 
284
- # Currently no kind of transporters accept an Individual API Key. Use username and app-specific password instead.
285
- # See https://github.com/fastlane/fastlane/issues/22115
286
- is_individual_key = !api_key.nil? && api_key[:issuer_id].nil?
287
- if is_individual_key
288
- api_key = nil
289
- api_token = nil
290
- end
291
-
292
296
  unless api_token.nil?
293
297
  api_token.refresh! if api_token.expired?
294
- return FastlaneCore::ItunesTransporter.new(nil, nil, false, nil, api_token.text, altool_compatible_command: true, api_key: api_key)
298
+ return FastlaneCore::ItunesTransporter.new(nil, nil, false, nil, api_token.text, altool_compatible_command: true, api_key: api_key, provider_public_id: nil)
295
299
  end
296
300
 
297
301
  tunes_client = Spaceship::ConnectAPI.client.tunes_client
298
302
 
299
- generic_transporter = FastlaneCore::ItunesTransporter.new(options[:username], nil, false, options[:itc_provider], altool_compatible_command: true, api_key: api_key)
300
- return generic_transporter unless options[:itc_provider].nil? && tunes_client.teams.count > 1
303
+ generic_transporter = FastlaneCore::ItunesTransporter.new(options[:username], nil, false, options[:itc_provider], altool_compatible_command: true, api_key: api_key, provider_public_id: options[:provider_public_id])
304
+ return generic_transporter if options[:itc_provider] || options[:provider_public_id] || tunes_client.nil?
305
+ return generic_transporter unless tunes_client.teams.count > 1
301
306
 
302
307
  begin
303
308
  team = tunes_client.teams.find { |t| t['providerId'].to_s == tunes_client.team_id }
@@ -0,0 +1,204 @@
1
+ require "fastlane_core"
2
+ require "fastlane_core/video_utils"
3
+ require "digest/md5"
4
+
5
+ require_relative 'module'
6
+
7
+ module Deliver
8
+ class SyncAppPreviews
9
+ UploadPreviewJob = Struct.new(:localization, :preview_set, :video_path, :frame_time_code)
10
+
11
+ def initialize(app:, platform:, app_previews_path:, preview_frame_time_code: nil, overwrite_preview_videos: false)
12
+ @app = app
13
+ @platform = platform
14
+ @app_previews_path = app_previews_path
15
+ @frame_time_code = preview_frame_time_code
16
+ @overwrite = overwrite_preview_videos
17
+ end
18
+
19
+ def sync_from_path
20
+ UI.important("Uploading App Preview videos...")
21
+ validate_path!
22
+
23
+ localizations = editable_version.get_app_store_version_localizations
24
+ locale_by_code = localizations.each_with_object({}) { |l, h| h[l.locale] = l }
25
+
26
+ video_previews_per_locale = discover_videos(@app_previews_path)
27
+ if video_previews_per_locale.empty?
28
+ UI.message("No preview videos found under '#{@app_previews_path}'.")
29
+ return
30
+ end
31
+
32
+ jobs = []
33
+ video_previews_per_locale.each do |locale, previews|
34
+ process_locale_videos(locale: locale, previews: previews, locale_by_code: locale_by_code, jobs: jobs)
35
+ end
36
+
37
+ if jobs.empty?
38
+ UI.message("No new preview videos to upload.")
39
+ return
40
+ end
41
+
42
+ UI.message("Queueing #{jobs.size} preview video upload job(s) across #{video_previews_per_locale.keys.size} locale(s)...")
43
+
44
+ upload_errors = []
45
+ worker = FastlaneCore::QueueWorker.new do |job|
46
+ begin
47
+ UI.message("Uploading preview video '#{File.basename(job.video_path)}' for locale #{job.localization.locale} (set #{job.preview_set.preview_type})...")
48
+ job.preview_set.upload_preview(path: job.video_path, frame_time_code: job.frame_time_code)
49
+ UI.message("Uploaded preview video '#{File.basename(job.video_path)}' for locale #{job.localization.locale} (set #{job.preview_set.preview_type}).")
50
+ rescue => e
51
+ UI.error("Failed to upload '#{job.video_path}': #{e.class} - #{e.message}")
52
+ upload_errors << e
53
+ end
54
+ end
55
+
56
+ jobs.each { |j| worker.enqueue(j) }
57
+ worker.start
58
+ UI.message("All upload jobs finished. Sorting previews by filename...")
59
+
60
+ # sort previews in each set by file name
61
+ localizations.each do |loc|
62
+ loc.get_app_preview_sets(includes: "appPreviews").each do |set|
63
+ next unless set.app_previews && set.app_previews.length > 1
64
+ ordered_ids = set.app_previews.sort_by { |preview| preview.file_name.to_s }.map(&:id)
65
+ set.reorder_previews(app_preview_ids: ordered_ids)
66
+ end
67
+ end
68
+
69
+ unless upload_errors.empty?
70
+ UI.user_error!("#{upload_errors.size} App Preview upload error(s) occurred. First error: #{upload_errors.first.class} - #{upload_errors.first.message}")
71
+ end
72
+
73
+ UI.success("Successfully uploaded and sorted App Preview videos.")
74
+ end
75
+
76
+ private
77
+
78
+ # process videos for a single locale: enforce limits, create/reuse sets, enqueue upload jobs
79
+ def process_locale_videos(locale:, previews:, locale_by_code:, jobs:)
80
+ localization = locale_by_code[locale]
81
+ unless localization
82
+ UI.important("Locale '#{locale}' does not exist on App Store Connect for this version. Skipping its videos.")
83
+ return
84
+ end
85
+
86
+ sets_by_preview_type = localization
87
+ .get_app_preview_sets(includes: "appPreviews")
88
+ .each_with_object({}) { |set, h| h[set.preview_type] = set }
89
+
90
+ if @overwrite
91
+ delete_existing_previews(localization, sets_by_preview_type.values)
92
+ # re-fetch sets after deletes
93
+ sets_by_preview_type = localization
94
+ .get_app_preview_sets(includes: "appPreviews")
95
+ .each_with_object({}) { |set, h| h[set.preview_type] = set }
96
+ end
97
+
98
+ # group videos by preview type to enforce a max of 3 per locale AND type
99
+ videos_by_preview_type = Hash.new { |h, k| h[k] = [] }
100
+ previews.each { |item| videos_by_preview_type[item[:preview_type]] << item[:path] }
101
+
102
+ videos_by_preview_type.each do |preview_type, video_paths|
103
+ video_paths.sort!
104
+ if video_paths.size > 3
105
+ UI.important("[#{locale}] Found #{video_paths.size} '#{preview_type}' videos. Limiting to first 3 by filename.")
106
+ video_paths = video_paths.first(3)
107
+ end
108
+
109
+ preview_set = sets_by_preview_type[preview_type] || begin
110
+ UI.message("[#{locale}] Creating App Preview Set for type #{preview_type}...")
111
+ created = localization.create_app_preview_set(attributes: { previewType: preview_type })
112
+ sets_by_preview_type[preview_type] = created
113
+ end
114
+
115
+ video_paths.each do |video_path|
116
+ already_exist = (preview_set.app_previews || []).any? { |preview| preview.source_file_checksum == Digest::MD5.file(video_path).hexdigest }
117
+ if already_exist
118
+ UI.message("[#{locale}] Preview '#{File.basename(video_path)}' already uploaded (matching checksum). Skipping upload.")
119
+ next
120
+ end
121
+
122
+ jobs << UploadPreviewJob.new(localization, preview_set, video_path, @frame_time_code)
123
+ end
124
+ end
125
+ end
126
+
127
+ def validate_path!
128
+ UI.user_error!("app_previews_path is required") if @app_previews_path.to_s.empty?
129
+ UI.user_error!("app_previews_path '#{@app_previews_path}' does not exist") unless Dir.exist?(@app_previews_path)
130
+ end
131
+
132
+ def editable_version
133
+ @app.get_edit_app_store_version(platform: @platform)
134
+ end
135
+
136
+ def discover_videos(root)
137
+ extensions = %w[mp4 mov m4v]
138
+ locales = Dir.children(root).select { |subdir| File.directory?(File.join(root, subdir)) }
139
+ all_videos = {}
140
+ locales.each do |locale|
141
+ dir = File.join(root, locale)
142
+ video_paths = Dir.children(dir)
143
+ .select { |filename| extensions.include?(File.extname(filename).delete(".").downcase) }
144
+ .map { |filename| File.join(dir, filename) }
145
+ .sort
146
+ valid_previews = []
147
+ video_paths.each do |path|
148
+ # require filename to contain a known preview type token
149
+ preview_type = Spaceship::ConnectAPI::AppPreviewSet.preview_type_from_filename(File.basename(path))
150
+ unless preview_type
151
+ UI.important("[#{locale}] '#{File.basename(path)}' does not contain any known preview device type. Skipping.")
152
+ next
153
+ end
154
+
155
+ # enforce size constraint (under 500MB)
156
+ size_mb = File.size(path) / (1024.0 * 1024.0)
157
+ if size_mb > 500
158
+ UI.important("[#{locale}] '#{File.basename(path)}' is #{size_mb.round(1)}MB (> 500MB). Skipping.")
159
+ next
160
+ end
161
+
162
+ # enforce duration constraints [15s..30s]. warn if duration can't be determined
163
+ duration = FastlaneCore::VideoUtils.read_video_duration_seconds(path)
164
+ if duration
165
+ if duration < 15.0 || duration > 30.0
166
+ UI.important("[#{locale}] '#{File.basename(path)}' duration is #{duration.round(2)}s (allowed: 15–30s). Skipping.")
167
+ next
168
+ end
169
+ else
170
+ UI.important("[#{locale}] Could not determine duration for '#{File.basename(path)}'. Proceeding anyway.")
171
+ end
172
+
173
+ # validate resolution against accepted canonical sizes; warn if resolution can't be determined
174
+ res = FastlaneCore::VideoUtils.read_video_resolution(path)
175
+ if res
176
+ unless Spaceship::ConnectAPI::AppPreviewSet.validate_video_resolution(res[0], res[1], preview_type)
177
+ UI.important("[#{locale}] '#{File.basename(path)}' has invalid resolution #{res.join('x')}. Skipping.")
178
+ next
179
+ end
180
+ else
181
+ UI.important("[#{locale}] Could not determine resolution for '#{File.basename(path)}'. Proceeding anyway.")
182
+ end
183
+ valid_previews << { path: path, preview_type: preview_type }
184
+ end
185
+ all_videos[locale] = valid_previews unless valid_previews.empty?
186
+ end
187
+ all_videos
188
+ end
189
+
190
+ def delete_existing_previews(localization, sets)
191
+ sets.each do |set|
192
+ next unless set.app_previews && set.app_previews.any?
193
+ UI.message("Deleting #{set.app_previews.size} existing previews from set #{set.preview_type} for locale #{localization.locale} due to overwrite...")
194
+ set.app_previews.each do |preview|
195
+ begin
196
+ preview.delete!
197
+ rescue => e
198
+ UI.error("Failed to delete preview '#{preview.file_name}': #{e.class} - #{e.message}")
199
+ end
200
+ end
201
+ end
202
+ end
203
+ end
204
+ end
@@ -56,7 +56,7 @@ module Fastlane
56
56
  description: "The key ID"),
57
57
  FastlaneCore::ConfigItem.new(key: :issuer_id,
58
58
  env_name: "APP_STORE_CONNECT_API_KEY_ISSUER_ID",
59
- description: "The issuer ID. It can be nil if the key is individual API key",
59
+ description: "The issuer ID. It should be nil if the key is individual API key",
60
60
  optional: true),
61
61
  FastlaneCore::ConfigItem.new(key: :key_filepath,
62
62
  env_name: "APP_STORE_CONNECT_API_KEY_KEY_FILEPATH",
@@ -137,6 +137,10 @@ module Fastlane
137
137
  key_id: "D83848D23",
138
138
  issuer_id: "227b0bbf-ada8-458c-9d62-3d8022b7d07f",
139
139
  key_content: "-----BEGIN EC PRIVATE KEY-----\nfewfawefawfe\n-----END EC PRIVATE KEY-----"
140
+ )',
141
+ 'app_store_connect_api_key(
142
+ key_id: "D83848D23", # no issuer_id if the key is individual
143
+ key_content: "-----BEGIN EC PRIVATE KEY-----\nfewfawefawfe\n-----END EC PRIVATE KEY-----"
140
144
  )'
141
145
  ]
142
146
  end