fastlane 2.232.2 → 2.233.1

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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +102 -102
  3. data/credentials_manager/lib/credentials_manager/appfile_config.rb +4 -0
  4. data/deliver/lib/deliver/options.rb +23 -0
  5. data/deliver/lib/deliver/runner.rb +17 -12
  6. data/deliver/lib/deliver/sync_app_previews.rb +204 -0
  7. data/fastlane/lib/fastlane/actions/app_store_connect_api_key.rb +5 -1
  8. data/fastlane/lib/fastlane/actions/docs/upload_to_app_store.md.erb +20 -4
  9. data/fastlane/lib/fastlane/actions/docs/upload_to_testflight.md +3 -0
  10. data/fastlane/lib/fastlane/actions/resign.rb +13 -2
  11. data/fastlane/lib/fastlane/actions/swiftlint.rb +8 -1
  12. data/fastlane/lib/fastlane/actions/upload_to_app_store.rb +1 -1
  13. data/fastlane/lib/fastlane/helper/s3_client_helper.rb +5 -2
  14. data/fastlane/lib/fastlane/version.rb +1 -1
  15. data/fastlane/swift/Deliverfile.swift +1 -1
  16. data/fastlane/swift/DeliverfileProtocol.swift +29 -1
  17. data/fastlane/swift/Fastlane.swift +105 -9
  18. data/fastlane/swift/Gymfile.swift +1 -1
  19. data/fastlane/swift/GymfileProtocol.swift +8 -1
  20. data/fastlane/swift/Matchfile.swift +1 -1
  21. data/fastlane/swift/MatchfileProtocol.swift +8 -1
  22. data/fastlane/swift/Precheckfile.swift +1 -1
  23. data/fastlane/swift/PrecheckfileProtocol.swift +1 -1
  24. data/fastlane/swift/Scanfile.swift +1 -1
  25. data/fastlane/swift/ScanfileProtocol.swift +1 -1
  26. data/fastlane/swift/Screengrabfile.swift +1 -1
  27. data/fastlane/swift/ScreengrabfileProtocol.swift +1 -1
  28. data/fastlane/swift/Snapshotfile.swift +1 -1
  29. data/fastlane/swift/SnapshotfileProtocol.swift +1 -1
  30. data/fastlane_core/lib/fastlane_core/itunes_transporter.rb +48 -17
  31. data/fastlane_core/lib/fastlane_core/video_utils.rb +202 -0
  32. data/frameit/lib/frameit/device_types.rb +2 -2
  33. data/gym/lib/gym/generators/build_command_generator.rb +2 -1
  34. data/gym/lib/gym/options.rb +5 -0
  35. data/match/lib/match/generator.rb +3 -1
  36. data/match/lib/match/options.rb +5 -0
  37. data/match/lib/match/runner.rb +12 -7
  38. data/match/lib/match/storage/s3_storage.rb +4 -1
  39. data/match/lib/match/storage.rb +1 -0
  40. data/pilot/lib/pilot/build_manager.rb +4 -12
  41. data/pilot/lib/pilot/options.rb +4 -0
  42. data/precheck/lib/precheck/rules/rules_data/curse_word_hashes/README.md +54 -0
  43. data/precheck/lib/precheck/rules/rules_data/curse_word_hashes/en_us.txt +2 -1
  44. data/scan/lib/scan/detect_values.rb +11 -3
  45. data/sigh/lib/assets/resign.sh +17 -5
  46. data/sigh/lib/sigh/commands_generator.rb +1 -0
  47. data/sigh/lib/sigh/manager.rb +6 -6
  48. data/sigh/lib/sigh/resign.rb +9 -6
  49. data/spaceship/lib/spaceship/connect_api/models/app_preview_set.rb +54 -17
  50. data/spaceship/lib/spaceship/connect_api/models/app_store_version_localization.rb +1 -2
  51. data/spaceship/lib/spaceship/connect_api/tunes/tunes.rb +2 -2
  52. data/trainer/lib/trainer/legacy_xcresult.rb +27 -20
  53. data/trainer/lib/trainer/xcresult/test_suite.rb +4 -1
  54. metadata +24 -21
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b51c417477b7551fedb50df2cee8534dff03c5d626bbc087593f0f9bd5016db8
4
- data.tar.gz: 441b8c56863434b00e2853292d5995de56f3226ee779d00c0a1f7d466b2a410d
3
+ metadata.gz: c0eca61fa0396713a78e6de08eaca27b99061aa3316be3f456fd34b5aea05368
4
+ data.tar.gz: 1d31711ddcd3b0c745587cc2eb1adbcadefe599f40442efe06c65f4b78c50e8c
5
5
  SHA512:
6
- metadata.gz: 5985fb114fa5eeb03f19b81452e0d4094cba4c05684985280b04adf042eac10d80c43ebbcc264cf94ab3b30e1934f7639d9e6011f75278b50075158532e4c52a
7
- data.tar.gz: 9d50eaca265539d2a64e073f6fed2032080dbbc40bc94f6b70ee23a2caeb122f6e03714deb7028d000b621b52de1b0fe4e62c5ce9b1b7e0d4adef9ba21a7a61c
6
+ metadata.gz: 44249e561611f1f9cd093155561bc034e6c257abc1b8a0d45f2a7b36f8a8daf857cb0a9ec2a4efb40c056d135f595dff8313c4df157beb17ff0e7f823e37d1d9
7
+ data.tar.gz: 16e33277c3dfa404406a1664e6c1cb240e2df037052e437a3f525c7943956606d67b45688af945119ec29a034ef9ba3a8bb95505217edff32831d644838f9f30
data/README.md CHANGED
@@ -35,55 +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='manish-rathi'>
39
- <a href='https://github.com/crazymanish'>
40
- <img src='https://github.com/crazymanish.png' width='140px;'>
41
- </a>
42
- <h4 align='center'><a href='https://twitter.com/iammanishrathi'>Manish Rathi</a></h4>
43
- </td>
44
- <td id='connor-tumbleson'>
45
- <a href='https://github.com/ibotpeaches'>
46
- <img src='https://github.com/ibotpeaches.png' width='140px;'>
47
- </a>
48
- <h4 align='center'><a href='https://twitter.com/ibotpeaches'>Connor Tumbleson</a></h4>
49
- </td>
50
- <td id='satoshi-namai'>
51
- <a href='https://github.com/ainame'>
52
- <img src='https://github.com/ainame.png' width='140px;'>
53
- </a>
54
- <h4 align='center'><a href='https://twitter.com/ainame'>Satoshi Namai</a></h4>
55
- </td>
56
38
  <td id='jorge-revuelta-h'>
57
39
  <a href='https://github.com/minuscorp'>
58
40
  <img src='https://github.com/minuscorp.png' width='140px;'>
59
41
  </a>
60
42
  <h4 align='center'><a href='https://twitter.com/minuscorp'>Jorge Revuelta H</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;'>
65
- </a>
66
- <h4 align='center'><a href='https://twitter.com/rogerluan_'>Roger Oba</a></h4>
67
- </td>
68
- </tr>
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;'>
73
- </a>
74
- <h4 align='center'><a href='https://twitter.com/ott_max'>Max Ott</a></h4>
75
- </td>
76
- <td id='helmut-januschka'>
77
- <a href='https://github.com/hjanuschka'>
78
- <img src='https://github.com/hjanuschka.png' width='140px;'>
44
+ <td id='łukasz-grabowski'>
45
+ <a href='https://github.com/lucgrabowski'>
46
+ <img src='https://github.com/lucgrabowski.png' width='140px;'>
79
47
  </a>
80
- <h4 align='center'><a href='https://twitter.com/hjanuschka'>Helmut Januschka</a></h4>
48
+ <h4 align='center'>Łukasz Grabowski</h4>
81
49
  </td>
82
- <td id='danielle-tomlinson'>
83
- <a href='https://github.com/endocrimes'>
84
- <img src='https://github.com/endocrimes.png' width='140px;'>
50
+ <td id='fumiya-nakamura'>
51
+ <a href='https://github.com/nafu'>
52
+ <img src='https://github.com/nafu.png' width='140px;'>
85
53
  </a>
86
- <h4 align='center'><a href='https://twitter.com/endocrimes'>Danielle Tomlinson</a></h4>
54
+ <h4 align='center'><a href='https://twitter.com/nafu003'>Fumiya Nakamura</a></h4>
87
55
  </td>
88
56
  <td id='josh-holtz'>
89
57
  <a href='https://github.com/joshdholtz'>
@@ -91,83 +59,69 @@ If the above doesn't help, please [submit an issue](https://github.com/fastlane/
91
59
  </a>
92
60
  <h4 align='center'><a href='https://twitter.com/joshdholtz'>Josh Holtz</a></h4>
93
61
  </td>
94
- <td id='matthew-ellis'>
95
- <a href='https://github.com/matthewellis'>
96
- <img src='https://github.com/matthewellis.png' width='140px;'>
62
+ <td id='aaron-brager'>
63
+ <a href='https://github.com/getaaron'>
64
+ <img src='https://github.com/getaaron.png' width='140px;'>
97
65
  </a>
98
- <h4 align='center'><a href='https://twitter.com/mellis1995'>Matthew Ellis</a></h4>
66
+ <h4 align='center'><a href='https://twitter.com/getaaron'>Aaron Brager</a></h4>
99
67
  </td>
100
68
  </tr>
101
69
  <tr>
70
+ <td id='jérôme-lacoste'>
71
+ <a href='https://github.com/lacostej'>
72
+ <img src='https://github.com/lacostej.png' width='140px;'>
73
+ </a>
74
+ <h4 align='center'><a href='https://twitter.com/lacostej'>Jérôme Lacoste</a></h4>
75
+ </td>
102
76
  <td id='andrew-mcburney'>
103
77
  <a href='https://github.com/armcburney'>
104
78
  <img src='https://github.com/armcburney.png' width='140px;'>
105
79
  </a>
106
80
  <h4 align='center'><a href='https://twitter.com/armcburney'>Andrew McBurney</a></h4>
107
81
  </td>
108
- <td id='daniel-jankowski'>
109
- <a href='https://github.com/mollyIV'>
110
- <img src='https://github.com/mollyIV.png' width='140px;'>
111
- </a>
112
- <h4 align='center'><a href='https://twitter.com/mollyIV'>Daniel Jankowski</a></h4>
113
- </td>
114
- <td id='łukasz-grabowski'>
115
- <a href='https://github.com/lucgrabowski'>
116
- <img src='https://github.com/lucgrabowski.png' width='140px;'>
117
- </a>
118
- <h4 align='center'>Łukasz Grabowski</h4>
119
- </td>
120
- <td id='jimmy-dee'>
121
- <a href='https://github.com/jdee'>
122
- <img src='https://github.com/jdee.png' width='140px;'>
123
- </a>
124
- <h4 align='center'>Jimmy Dee</h4>
125
- </td>
126
82
  <td id='kohki-miki'>
127
83
  <a href='https://github.com/giginet'>
128
84
  <img src='https://github.com/giginet.png' width='140px;'>
129
85
  </a>
130
86
  <h4 align='center'><a href='https://twitter.com/giginet'>Kohki Miki</a></h4>
131
87
  </td>
132
- </tr>
133
- <tr>
134
88
  <td id='olivier-halligon'>
135
89
  <a href='https://github.com/AliSoftware'>
136
90
  <img src='https://github.com/AliSoftware.png' width='140px;'>
137
91
  </a>
138
92
  <h4 align='center'><a href='https://twitter.com/aligatr'>Olivier Halligon</a></h4>
139
93
  </td>
140
- <td id='aaron-brager'>
141
- <a href='https://github.com/getaaron'>
142
- <img src='https://github.com/getaaron.png' width='140px;'>
94
+ <td id='connor-tumbleson'>
95
+ <a href='https://github.com/ibotpeaches'>
96
+ <img src='https://github.com/ibotpeaches.png' width='140px;'>
143
97
  </a>
144
- <h4 align='center'><a href='https://twitter.com/getaaron'>Aaron Brager</a></h4>
98
+ <h4 align='center'><a href='https://twitter.com/ibotpeaches'>Connor Tumbleson</a></h4>
145
99
  </td>
146
- <td id='joshua-liebowitz'>
147
- <a href='https://github.com/taquitos'>
148
- <img src='https://github.com/taquitos.png' width='140px;'>
100
+ </tr>
101
+ <tr>
102
+ <td id='satoshi-namai'>
103
+ <a href='https://github.com/ainame'>
104
+ <img src='https://github.com/ainame.png' width='140px;'>
149
105
  </a>
150
- <h4 align='center'><a href='https://twitter.com/taquitos'>Joshua Liebowitz</a></h4>
106
+ <h4 align='center'><a href='https://twitter.com/ainame'>Satoshi Namai</a></h4>
151
107
  </td>
152
- <td id='manu-wallner'>
153
- <a href='https://github.com/milch'>
154
- <img src='https://github.com/milch.png' width='140px;'>
108
+ <td id='maksym-grebenets'>
109
+ <a href='https://github.com/mgrebenets'>
110
+ <img src='https://github.com/mgrebenets.png' width='140px;'>
155
111
  </a>
156
- <h4 align='center'><a href='https://twitter.com/acrooow'>Manu Wallner</a></h4>
112
+ <h4 align='center'><a href='https://twitter.com/mgrebenets'>Maksym Grebenets</a></h4>
157
113
  </td>
158
- <td id='luka-mirosevic'>
159
- <a href='https://github.com/lmirosevic'>
160
- <img src='https://github.com/lmirosevic.png' width='140px;'>
114
+ <td id='stefan-natchev'>
115
+ <a href='https://github.com/snatchev'>
116
+ <img src='https://github.com/snatchev.png' width='140px;'>
161
117
  </a>
162
- <h4 align='center'><a href='https://twitter.com/lmirosevic'>Luka Mirosevic</a></h4>
118
+ <h4 align='center'><a href='https://twitter.com/snatchev'>Stefan Natchev</a></h4>
163
119
  </td>
164
- </tr>
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;'>
120
+ <td id='max-ott'>
121
+ <a href='https://github.com/max-ott'>
122
+ <img src='https://github.com/max-ott.png' width='140px;'>
169
123
  </a>
170
- <h4 align='center'><a href='https://twitter.com/lacostej'>Jérôme Lacoste</a></h4>
124
+ <h4 align='center'><a href='https://twitter.com/ott_max'>Max Ott</a></h4>
171
125
  </td>
172
126
  <td id='iulian-onofrei'>
173
127
  <a href='https://github.com/revolter'>
@@ -175,37 +129,83 @@ If the above doesn't help, please [submit an issue](https://github.com/fastlane/
175
129
  </a>
176
130
  <h4 align='center'><a href='https://twitter.com/Revolt666'>Iulian Onofrei</a></h4>
177
131
  </td>
132
+ </tr>
133
+ <tr>
134
+ <td id='matthew-ellis'>
135
+ <a href='https://github.com/matthewellis'>
136
+ <img src='https://github.com/matthewellis.png' width='140px;'>
137
+ </a>
138
+ <h4 align='center'><a href='https://twitter.com/mellis1995'>Matthew Ellis</a></h4>
139
+ </td>
140
+ <td id='manish-rathi'>
141
+ <a href='https://github.com/crazymanish'>
142
+ <img src='https://github.com/crazymanish.png' width='140px;'>
143
+ </a>
144
+ <h4 align='center'><a href='https://twitter.com/iammanishrathi'>Manish Rathi</a></h4>
145
+ </td>
146
+ <td id='danielle-tomlinson'>
147
+ <a href='https://github.com/endocrimes'>
148
+ <img src='https://github.com/endocrimes.png' width='140px;'>
149
+ </a>
150
+ <h4 align='center'><a href='https://twitter.com/endocrimes'>Danielle Tomlinson</a></h4>
151
+ </td>
152
+ <td id='joshua-liebowitz'>
153
+ <a href='https://github.com/taquitos'>
154
+ <img src='https://github.com/taquitos.png' width='140px;'>
155
+ </a>
156
+ <h4 align='center'><a href='https://twitter.com/taquitos'>Joshua Liebowitz</a></h4>
157
+ </td>
158
+ <td id='jan-piotrowski'>
159
+ <a href='https://github.com/janpio'>
160
+ <img src='https://github.com/janpio.png' width='140px;'>
161
+ </a>
162
+ <h4 align='center'><a href='https://twitter.com/Sujan'>Jan Piotrowski</a></h4>
163
+ </td>
164
+ </tr>
165
+ <tr>
166
+ <td id='helmut-januschka'>
167
+ <a href='https://github.com/hjanuschka'>
168
+ <img src='https://github.com/hjanuschka.png' width='140px;'>
169
+ </a>
170
+ <h4 align='center'><a href='https://twitter.com/hjanuschka'>Helmut Januschka</a></h4>
171
+ </td>
178
172
  <td id='felix-krause'>
179
173
  <a href='https://github.com/KrauseFx'>
180
174
  <img src='https://github.com/KrauseFx.png' width='140px;'>
181
175
  </a>
182
176
  <h4 align='center'><a href='https://twitter.com/KrauseFx'>Felix Krause</a></h4>
183
177
  </td>
184
- <td id='stefan-natchev'>
185
- <a href='https://github.com/snatchev'>
186
- <img src='https://github.com/snatchev.png' width='140px;'>
178
+ <td id='jimmy-dee'>
179
+ <a href='https://github.com/jdee'>
180
+ <img src='https://github.com/jdee.png' width='140px;'>
187
181
  </a>
188
- <h4 align='center'><a href='https://twitter.com/snatchev'>Stefan Natchev</a></h4>
182
+ <h4 align='center'>Jimmy Dee</h4>
189
183
  </td>
190
- <td id='maksym-grebenets'>
191
- <a href='https://github.com/mgrebenets'>
192
- <img src='https://github.com/mgrebenets.png' width='140px;'>
184
+ <td id='daniel-jankowski'>
185
+ <a href='https://github.com/mollyIV'>
186
+ <img src='https://github.com/mollyIV.png' width='140px;'>
193
187
  </a>
194
- <h4 align='center'><a href='https://twitter.com/mgrebenets'>Maksym Grebenets</a></h4>
188
+ <h4 align='center'><a href='https://twitter.com/mollyIV'>Daniel Jankowski</a></h4>
189
+ </td>
190
+ <td id='manu-wallner'>
191
+ <a href='https://github.com/milch'>
192
+ <img src='https://github.com/milch.png' width='140px;'>
193
+ </a>
194
+ <h4 align='center'><a href='https://twitter.com/acrooow'>Manu Wallner</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='luka-mirosevic'>
199
+ <a href='https://github.com/lmirosevic'>
200
+ <img src='https://github.com/lmirosevic.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/lmirosevic'>Luka Mirosevic</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='roger-oba'>
205
+ <a href='https://github.com/rogerluan'>
206
+ <img src='https://github.com/rogerluan.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/rogerluan_'>Roger Oba</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)
@@ -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