fastlane 2.165.0 → 2.170.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (119) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +76 -76
  3. data/cert/lib/cert/options.rb +3 -3
  4. data/cert/lib/cert/runner.rb +1 -1
  5. data/deliver/lib/deliver/app_screenshot.rb +6 -2
  6. data/deliver/lib/deliver/loader.rb +136 -18
  7. data/deliver/lib/deliver/upload_metadata.rb +4 -10
  8. data/deliver/lib/deliver/upload_screenshots.rb +1 -64
  9. data/fastlane/lib/fastlane/actions/actions_helper.rb +1 -1
  10. data/fastlane/lib/fastlane/actions/add_git_tag.rb +9 -2
  11. data/fastlane/lib/fastlane/actions/appledoc.rb +1 -1
  12. data/fastlane/lib/fastlane/actions/docs/capture_ios_screenshots.md +4 -0
  13. data/fastlane/lib/fastlane/actions/docs/frame_screenshots.md +1 -1
  14. data/fastlane/lib/fastlane/actions/docs/sync_code_signing.md +1 -1
  15. data/fastlane/lib/fastlane/actions/docs/upload_to_app_store.md.erb +4 -0
  16. data/fastlane/lib/fastlane/actions/onesignal.rb +13 -3
  17. data/fastlane/lib/fastlane/actions/slather.rb +2 -2
  18. data/fastlane/lib/fastlane/actions/spm.rb +6 -0
  19. data/fastlane/lib/fastlane/actions/update_fastlane.rb +29 -8
  20. data/fastlane/lib/fastlane/actions/upload_app_privacy_details_to_app_store.rb +289 -0
  21. data/fastlane/lib/fastlane/actions/upload_to_app_store.rb +3 -3
  22. data/fastlane/lib/fastlane/actions/xcode_install.rb +8 -5
  23. data/fastlane/lib/fastlane/cli_tools_distributor.rb +2 -2
  24. data/fastlane/lib/fastlane/features.rb +1 -1
  25. data/fastlane/lib/fastlane/plugins/template/.rubocop.yml +2 -1
  26. data/fastlane/lib/fastlane/swift_fastlane_api_generator.rb +3 -0
  27. data/fastlane/lib/fastlane/swift_fastlane_function.rb +1 -1
  28. data/fastlane/lib/fastlane/version.rb +1 -1
  29. data/fastlane/swift/Deliverfile.swift +1 -1
  30. data/fastlane/swift/DeliverfileProtocol.swift +1 -1
  31. data/fastlane/swift/Fastfile.swift +1 -1
  32. data/fastlane/swift/Fastlane.swift +97 -26
  33. data/fastlane/swift/Gymfile.swift +1 -1
  34. data/fastlane/swift/GymfileProtocol.swift +5 -1
  35. data/fastlane/swift/LaneFileProtocol.swift +2 -2
  36. data/fastlane/swift/MainProcess.swift +2 -0
  37. data/fastlane/swift/Matchfile.swift +1 -1
  38. data/fastlane/swift/MatchfileProtocol.swift +3 -3
  39. data/fastlane/swift/Precheckfile.swift +1 -1
  40. data/fastlane/swift/PrecheckfileProtocol.swift +1 -1
  41. data/fastlane/swift/Runner.swift +1 -1
  42. data/fastlane/swift/Scanfile.swift +1 -1
  43. data/fastlane/swift/ScanfileProtocol.swift +9 -1
  44. data/fastlane/swift/Screengrabfile.swift +1 -1
  45. data/fastlane/swift/ScreengrabfileProtocol.swift +1 -1
  46. data/fastlane/swift/Snapshotfile.swift +1 -1
  47. data/fastlane/swift/SnapshotfileProtocol.swift +1 -1
  48. data/fastlane/swift/SocketClient.swift +1 -1
  49. data/fastlane_core/lib/fastlane_core/cert_checker.rb +12 -7
  50. data/fastlane_core/lib/fastlane_core/helper.rb +10 -2
  51. data/fastlane_core/lib/fastlane_core/itunes_transporter.rb +3 -3
  52. data/fastlane_core/lib/fastlane_core/provisioning_profile.rb +3 -1
  53. data/fastlane_core/lib/fastlane_core/ui/disable_colors.rb +8 -0
  54. data/gym/lib/gym/code_signing_mapping.rb +1 -1
  55. data/gym/lib/gym/generators/build_command_generator.rb +1 -0
  56. data/gym/lib/gym/options.rb +7 -1
  57. data/match/lib/match/module.rb +1 -1
  58. data/match/lib/match/nuke.rb +9 -5
  59. data/match/lib/match/options.rb +1 -1
  60. data/match/lib/match/runner.rb +1 -1
  61. data/pilot/lib/pilot/build_manager.rb +9 -3
  62. data/scan/lib/scan/detect_values.rb +3 -1
  63. data/scan/lib/scan/module.rb +4 -0
  64. data/scan/lib/scan/options.rb +16 -1
  65. data/scan/lib/scan/runner.rb +2 -2
  66. data/scan/lib/scan/test_command_generator.rb +1 -0
  67. data/snapshot/lib/assets/SnapshotHelper.swift +5 -1
  68. data/snapshot/lib/snapshot/test_command_generator.rb +1 -1
  69. data/snapshot/lib/snapshot/test_command_generator_base.rb +3 -1
  70. data/snapshot/lib/snapshot/test_command_generator_xcode_8.rb +1 -1
  71. data/spaceship/lib/spaceship/client.rb +14 -0
  72. data/spaceship/lib/spaceship/connect_api.rb +6 -0
  73. data/spaceship/lib/spaceship/connect_api/models/.app_data_usage_data_protection.rb.swp +0 -0
  74. data/spaceship/lib/spaceship/connect_api/models/age_rating_declaration.rb +3 -2
  75. data/spaceship/lib/spaceship/connect_api/models/app.rb +88 -54
  76. data/spaceship/lib/spaceship/connect_api/models/app_data_usage.rb +59 -0
  77. data/spaceship/lib/spaceship/connect_api/models/app_data_usage_category.rb +65 -0
  78. data/spaceship/lib/spaceship/connect_api/models/app_data_usage_data_protection.rb +27 -0
  79. data/spaceship/lib/spaceship/connect_api/models/app_data_usage_grouping.rb +18 -0
  80. data/spaceship/lib/spaceship/connect_api/models/app_data_usage_purposes.rb +37 -0
  81. data/spaceship/lib/spaceship/connect_api/models/app_data_usages_publish_state.rb +36 -0
  82. data/spaceship/lib/spaceship/connect_api/models/app_info.rb +16 -10
  83. data/spaceship/lib/spaceship/connect_api/models/app_info_localization.rb +8 -4
  84. data/spaceship/lib/spaceship/connect_api/models/app_preview.rb +15 -11
  85. data/spaceship/lib/spaceship/connect_api/models/app_preview_set.rb +13 -9
  86. data/spaceship/lib/spaceship/connect_api/models/app_screenshot.rb +9 -7
  87. data/spaceship/lib/spaceship/connect_api/models/app_screenshot_set.rb +15 -11
  88. data/spaceship/lib/spaceship/connect_api/models/app_store_review_attachment.rb +7 -5
  89. data/spaceship/lib/spaceship/connect_api/models/app_store_review_detail.rb +6 -4
  90. data/spaceship/lib/spaceship/connect_api/models/app_store_version.rb +55 -36
  91. data/spaceship/lib/spaceship/connect_api/models/app_store_version_localization.rb +21 -14
  92. data/spaceship/lib/spaceship/connect_api/models/app_store_version_submission.rb +3 -2
  93. data/spaceship/lib/spaceship/connect_api/models/beta_app_review_submission.rb +3 -2
  94. data/spaceship/lib/spaceship/connect_api/models/beta_feedback.rb +6 -4
  95. data/spaceship/lib/spaceship/connect_api/models/beta_group.rb +12 -2
  96. data/spaceship/lib/spaceship/connect_api/models/beta_tester.rb +12 -8
  97. data/spaceship/lib/spaceship/connect_api/models/build.rb +24 -16
  98. data/spaceship/lib/spaceship/connect_api/models/build_delivery.rb +3 -2
  99. data/spaceship/lib/spaceship/connect_api/models/bundle_id.rb +9 -6
  100. data/spaceship/lib/spaceship/connect_api/models/bundle_id_capability.rb +6 -4
  101. data/spaceship/lib/spaceship/connect_api/models/certificate.rb +12 -8
  102. data/spaceship/lib/spaceship/connect_api/models/device.rb +10 -4
  103. data/spaceship/lib/spaceship/connect_api/models/idfa_declaration.rb +6 -4
  104. data/spaceship/lib/spaceship/connect_api/models/profile.rb +12 -8
  105. data/spaceship/lib/spaceship/connect_api/models/reset_ratings_request.rb +3 -2
  106. data/spaceship/lib/spaceship/connect_api/models/sandbox_tester.rb +9 -6
  107. data/spaceship/lib/spaceship/connect_api/models/territory.rb +3 -2
  108. data/spaceship/lib/spaceship/connect_api/models/user.rb +6 -4
  109. data/spaceship/lib/spaceship/connect_api/models/user_invitation.rb +9 -6
  110. data/spaceship/lib/spaceship/connect_api/testflight/testflight.rb +12 -0
  111. data/spaceship/lib/spaceship/connect_api/tunes/tunes.rb +103 -0
  112. data/spaceship/lib/spaceship/errors.rb +19 -0
  113. data/spaceship/lib/spaceship/tunes/iap_detail.rb +1 -1
  114. data/spaceship/lib/spaceship/tunes/tunes_client.rb +2 -2
  115. data/spaceship/lib/spaceship/two_step_or_factor_client.rb +18 -6
  116. data/supply/lib/supply.rb +1 -1
  117. data/supply/lib/supply/options.rb +1 -1
  118. data/supply/lib/supply/uploader.rb +3 -2
  119. metadata +26 -18
@@ -375,9 +375,7 @@ module Deliver
375
375
  # Check folder list (an empty folder signifies a language is required)
376
376
  ignore_validation = options[:ignore_language_directory_validation]
377
377
  Loader.language_folders(options[:metadata_path], ignore_validation).each do |lang_folder|
378
- next unless File.directory?(lang_folder) # We don't want to read txt as they are non localised
379
- language = File.basename(lang_folder)
380
- enabled_languages << language unless enabled_languages.include?(language)
378
+ enabled_languages << lang_folder.basename unless enabled_languages.include?(lang_folder.basename)
381
379
  end
382
380
 
383
381
  return unless enabled_languages.include?("default")
@@ -416,10 +414,7 @@ module Deliver
416
414
  # Check folder list (an empty folder signifies a language is required)
417
415
  ignore_validation = options[:ignore_language_directory_validation]
418
416
  Loader.language_folders(options[:metadata_path], ignore_validation).each do |lang_folder|
419
- next unless File.directory?(lang_folder) # We don't want to read txt as they are non localised
420
-
421
- language = File.basename(lang_folder)
422
- enabled_languages << language unless enabled_languages.include?(language)
417
+ enabled_languages << lang_folder.basename unless enabled_languages.include?(lang_folder.basename)
423
418
  end
424
419
 
425
420
  # Mapping to strings because :default symbol can be passed in
@@ -530,14 +525,13 @@ module Deliver
530
525
  # Load localised data
531
526
  ignore_validation = options[:ignore_language_directory_validation]
532
527
  Loader.language_folders(options[:metadata_path], ignore_validation).each do |lang_folder|
533
- language = File.basename(lang_folder)
534
528
  (LOCALISED_VERSION_VALUES.keys + LOCALISED_APP_VALUES.keys).each do |key|
535
- path = File.join(lang_folder, "#{key}.txt")
529
+ path = File.join(lang_folder.path, "#{key}.txt")
536
530
  next unless File.exist?(path)
537
531
 
538
532
  UI.message("Loading '#{path}'...")
539
533
  options[key] ||= {}
540
- options[key][language] ||= File.read(path)
534
+ options[key][lang_folder.basename] ||= File.read(path)
541
535
  end
542
536
  end
543
537
 
@@ -251,70 +251,7 @@ module Deliver
251
251
 
252
252
  def collect_screenshots(options)
253
253
  return [] if options[:skip_screenshots]
254
- return collect_screenshots_for_languages(options[:screenshots_path], options[:ignore_language_directory_validation])
255
- end
256
-
257
- def collect_screenshots_for_languages(path, ignore_validation)
258
- screenshots = []
259
- extensions = '{png,jpg,jpeg}'
260
-
261
- available_languages = UploadScreenshots.available_languages.each_with_object({}) do |lang, lang_hash|
262
- lang_hash[lang.downcase] = lang
263
- end
264
-
265
- Loader.language_folders(path, ignore_validation).each do |lng_folder|
266
- language = File.basename(lng_folder)
267
-
268
- # Check to see if we need to traverse multiple platforms or just a single platform
269
- if language == Loader::APPLE_TV_DIR_NAME || language == Loader::IMESSAGE_DIR_NAME
270
- screenshots.concat(collect_screenshots_for_languages(File.join(path, language), ignore_validation))
271
- next
272
- end
273
-
274
- files = Dir.glob(File.join(lng_folder, "*.#{extensions}"), File::FNM_CASEFOLD).sort
275
- next if files.count == 0
276
-
277
- framed_screenshots_found = Dir.glob(File.join(lng_folder, "*_framed.#{extensions}"), File::FNM_CASEFOLD).count > 0
278
-
279
- UI.important("Framed screenshots are detected! 🖼 Non-framed screenshot files may be skipped. 🏃") if framed_screenshots_found
280
-
281
- language_dir_name = File.basename(lng_folder)
282
-
283
- if available_languages[language_dir_name.downcase].nil?
284
- UI.user_error!("#{language_dir_name} is not an available language. Please verify that your language codes are available in iTunesConnect. See https://developer.apple.com/library/content/documentation/LanguagesUtilities/Conceptual/iTunesConnect_Guide/Chapters/AppStoreTerritories.html for more information.")
285
- end
286
-
287
- language = available_languages[language_dir_name.downcase]
288
-
289
- files.each do |file_path|
290
- is_framed = file_path.downcase.include?("_framed.")
291
- is_watch = file_path.downcase.include?("watch")
292
-
293
- if framed_screenshots_found && !is_framed && !is_watch
294
- UI.important("🏃 Skipping screenshot file: #{file_path}")
295
- next
296
- end
297
-
298
- screenshots << AppScreenshot.new(file_path, language)
299
- end
300
- end
301
-
302
- # Checking if the device type exists in spaceship
303
- # Ex: iPhone 6.1 inch isn't supported in App Store Connect but need
304
- # to have it in there for frameit support
305
- unaccepted_device_shown = false
306
- screenshots.select! do |screenshot|
307
- exists = !screenshot.device_type.nil?
308
- unless exists
309
- UI.important("Unaccepted device screenshots are detected! 🚫 Screenshot file will be skipped. 🏃") unless unaccepted_device_shown
310
- unaccepted_device_shown = true
311
-
312
- UI.important("🏃 Skipping screenshot file: #{screenshot.path} - Not an accepted App Store Connect device...")
313
- end
314
- exists
315
- end
316
-
317
- return screenshots
254
+ return Loader.load_app_screenshots(options[:screenshots_path], options[:ignore_language_directory_validation])
318
255
  end
319
256
 
320
257
  # helper method so Spaceship::Tunes.client.available_languages is easier to test
@@ -5,7 +5,7 @@ module Fastlane
5
5
  PLATFORM_NAME = :PLATFORM_NAME
6
6
  ENVIRONMENT = :ENVIRONMENT
7
7
 
8
- # A slighly decorated hash that will store and fetch sensitive data
8
+ # A slightly decorated hash that will store and fetch sensitive data
9
9
  # but not display it while iterating keys and values
10
10
  class LaneContextValues < Hash
11
11
  def initialize
@@ -6,7 +6,13 @@ module Fastlane
6
6
  # lane name in lane_context could be nil because you can just call $fastlane add_git_tag which has no context
7
7
  lane_name = Actions.lane_context[Actions::SharedValues::LANE_NAME].to_s.delete(' ') # no spaces allowed
8
8
 
9
- tag = options[:tag] || "#{options[:grouping]}/#{lane_name}/#{options[:prefix]}#{options[:build_number]}#{options[:postfix]}"
9
+ if options[:tag]
10
+ tag = options[:tag]
11
+ elsif options[:build_number]
12
+ tag = "#{options[:grouping]}/#{lane_name}/#{options[:prefix]}#{options[:build_number]}#{options[:postfix]}"
13
+ else
14
+ UI.user_error!("No value found for 'tag' or 'build_number'. At least one of them must be provided. Note that if you do specify a tag, all other arguments are ignored.")
15
+ end
10
16
  message = options[:message] || "#{tag} (fastlane)"
11
17
 
12
18
  cmd = ['git tag']
@@ -64,7 +70,8 @@ module Fastlane
64
70
  description: "The build number. Defaults to the result of increment_build_number if you\'re using it",
65
71
  default_value: Actions.lane_context[Actions::SharedValues::BUILD_NUMBER],
66
72
  default_value_dynamic: true,
67
- is_string: false),
73
+ is_string: false,
74
+ optional: true),
68
75
  FastlaneCore::ConfigItem.new(key: :message,
69
76
  env_name: "FL_GIT_TAG_MESSAGE",
70
77
  description: "The tag message. Defaults to the tag's name",
@@ -55,7 +55,7 @@ module Fastlane
55
55
 
56
56
  def self.run(params)
57
57
  unless Helper.test?
58
- UI.message("Install using `brew install homebrew/boneyard/appledoc`")
58
+ UI.message("Install using `brew install appledoc`")
59
59
  UI.user_error!("appledoc not installed") if `which appledoc`.length == 0
60
60
  end
61
61
 
@@ -282,6 +282,10 @@ launch_arguments([
282
282
  ])
283
283
  ```
284
284
 
285
+ ## Xcode Environment Variables
286
+
287
+ _snapshot_ includes `FASTLANE_SNAPSHOT=YES` and `FASTLANE_LANGUAGE=<language>` as arguments when executing `xcodebuild`. This means you may use these environment variables in a custom build phase run script to do any additional configuration.
288
+
285
289
  # How does it work?
286
290
 
287
291
  The easiest solution would be to just render the UIWindow into a file. That's not possible because UI Tests don't run on a main thread. So _snapshot_ uses a different approach:
@@ -237,7 +237,7 @@ The `keyword.strings` and `title.strings` are standard `.strings` file you alrea
237
237
  **Notes**
238
238
 
239
239
  - These `.strings` files **MUST** be utf-8 (UTF-8) or utf-16 encoded (UTF-16 BE with BOM). They also must begin with an empty line. If you are having trouble see [issue #1740](https://github.com/fastlane/fastlane/issues/1740)
240
- - You **MUST** provide a background if you want titles. _frameit_ will not add the tiles if a background is not specified.
240
+ - You **MUST** provide a background if you want titles. _frameit_ will not add the titles if a background is not specified.
241
241
 
242
242
  ### Screenshot orientation
243
243
 
@@ -390,7 +390,7 @@ lane :beta do
390
390
  end
391
391
  ```
392
392
 
393
- By using the `force_for_new_devices` parameter, _match_ will check if the device count has changed since the last time you ran _match_, and automatically re-generate the provisioning profile if necessary. You can also use `force: true` to re-generate the provisioning profile on each run.
393
+ By using the `force_for_new_devices` parameter, _match_ will check if the (enabled) device count has changed since the last time you ran _match_, and automatically re-generate the provisioning profile if necessary. You can also use `force: true` to re-generate the provisioning profile on each run.
394
394
 
395
395
  _**Important:** The `force_for_new_devices` parameter is ignored for App Store provisioning profiles since they don't contain any device information._
396
396
 
@@ -439,6 +439,10 @@ Use the `submission_information` parameter for additional submission specifiers,
439
439
  fastlane deliver submit_build --build_number 830 --submission_information "{\"export_compliance_uses_encryption\": false, \"add_id_info_uses_idfa\": false }"
440
440
  ```
441
441
 
442
+ ### App Privacy Details
443
+
444
+ Starting on December 8, 2020, Apple announced that developers are required to provide app privacy details that will help users understand an app's privacy practies. _deliver_ does not allow for updating of this information but this can be done with the _upload_app_privacy_details_to_app_store_ action. More information on [Uploading App Privacy Details](https://docs.fastlane.tools/uploading-app-privacy-details)
445
+
442
446
  # Credentials
443
447
 
444
448
  A detailed description about how your credentials are handled is available in a [credentials_manager](https://github.com/fastlane/fastlane/tree/master/credentials_manager).
@@ -17,6 +17,7 @@ module Fastlane
17
17
  apns_p12_password = params[:apns_p12_password]
18
18
  android_token = params[:android_token]
19
19
  android_gcm_sender_id = params[:android_gcm_sender_id]
20
+ organization_id = params[:organization_id]
20
21
 
21
22
  has_app_id = !app_id.empty?
22
23
  has_app_name = !app_name.empty?
@@ -43,6 +44,7 @@ module Fastlane
43
44
 
44
45
  payload["gcm_key"] = android_token unless android_token.nil?
45
46
  payload["android_gcm_sender_id"] = android_gcm_sender_id unless android_gcm_sender_id.nil?
47
+ payload["organization_id"] = organization_id unless organization_id.nil?
46
48
 
47
49
  # here's the actual lifting - POST or PUT to OneSignal
48
50
 
@@ -135,7 +137,13 @@ module Fastlane
135
137
  env_name: "APNS_ENV",
136
138
  description: "APNS environment",
137
139
  optional: true,
138
- default_value: 'production')
140
+ default_value: 'production'),
141
+
142
+ FastlaneCore::ConfigItem.new(key: :organization_id,
143
+ env_name: "ONE_SIGNAL_ORGANIZATION_ID",
144
+ sensitive: true,
145
+ description: "OneSignal Organization ID",
146
+ optional: true)
139
147
  ]
140
148
  end
141
149
 
@@ -163,7 +171,8 @@ module Fastlane
163
171
  android_gcm_sender_id: "Your Android GCM Sender ID (optional)",
164
172
  apns_p12: "Path to Apple .p12 file (optional)",
165
173
  apns_p12_password: "Password for .p12 file (optional)",
166
- apns_env: "production/sandbox (defaults to production)"
174
+ apns_env: "production/sandbox (defaults to production)",
175
+ organization_id: "Onesignal organization id (optional)"
167
176
  )',
168
177
  'onesignal(
169
178
  app_id: "Your OneSignal App ID",
@@ -173,7 +182,8 @@ module Fastlane
173
182
  android_gcm_sender_id: "Your Android GCM Sender ID (optional)",
174
183
  apns_p12: "Path to Apple .p12 file (optional)",
175
184
  apns_p12_password: "Password for .p12 file (optional)",
176
- apns_env: "production/sandbox (defaults to production)"
185
+ apns_env: "production/sandbox (defaults to production)",
186
+ organization_id: "Onesignal organization id (optional)"
177
187
  )'
178
188
  ]
179
189
  end
@@ -272,8 +272,8 @@ module Fastlane
272
272
  FastlaneCore::ConfigItem.new(key: :binary_basename,
273
273
  env_name: "FL_SLATHER_BINARY_BASENAME",
274
274
  description: "Basename of the binary file, this should match the name of your bundle excluding its extension (i.e. YourApp [for YourApp.app bundle])",
275
- is_string: false,
276
- default_value: false),
275
+ type: Array,
276
+ optional: true),
277
277
  FastlaneCore::ConfigItem.new(key: :binary_file,
278
278
  env_name: "FL_SLATHER_BINARY_FILE",
279
279
  description: "Binary file name to be used for code coverage",
@@ -11,6 +11,7 @@ module Fastlane
11
11
  cmd << "--disable-sandbox" if params[:disable_sandbox]
12
12
  cmd << "--verbose" if params[:verbose]
13
13
  cmd << params[:command] if package_commands.include?(params[:command])
14
+ cmd << "--enable-code-coverage" if params[:enable_code_coverage] && params[:command] == 'generate-xcodeproj'
14
15
  if params[:xcconfig]
15
16
  cmd << "--xcconfig-overrides #{params[:xcconfig]}"
16
17
  end
@@ -44,6 +45,11 @@ module Fastlane
44
45
  verify_block: proc do |value|
45
46
  UI.user_error!("Please pass a valid command. Use one of the following: #{available_commands.join(', ')}") unless available_commands.include?(value)
46
47
  end),
48
+ FastlaneCore::ConfigItem.new(key: :enable_code_coverage,
49
+ env_name: "FL_SPM_ENABLE_CODE_COVERAGE",
50
+ description: "Enables code coverage for the generated Xcode project when using the generate-xcodeproj command",
51
+ is_string: false,
52
+ optional: true),
47
53
  FastlaneCore::ConfigItem.new(key: :build_path,
48
54
  env_name: "FL_SPM_BUILD_PATH",
49
55
  description: "Specify build/cache directory [default: ./.build]",
@@ -45,25 +45,36 @@ module Fastlane
45
45
  end
46
46
 
47
47
  # suppress updater output - very noisy
48
- Gem::DefaultUserInteraction.ui = Gem::SilentUI.new
48
+ Gem::DefaultUserInteraction.ui = Gem::SilentUI.new unless FastlaneCore::Globals.verbose?
49
49
 
50
50
  update_needed.each do |tool_info|
51
- tool = tool_info[0]
51
+ tool = self.get_gem_name(tool_info)
52
52
  local_version = Gem::Version.new(highest_versions[tool].version)
53
53
  latest_official_version = FastlaneCore::UpdateChecker.fetch_latest(tool)
54
54
 
55
55
  UI.message("Updating #{tool} from #{local_version.to_s.yellow} to #{latest_official_version.to_s.yellow}... 🚀")
56
56
 
57
- # Approximate_recommendation will create a string like "~> 0.10" from a version 0.10.0, e.g. one that is valid for versions >= 0.10 and <1.0
58
- requirement_version = local_version.approximate_recommendation
59
- updater.update_gem(tool, Gem::Requirement.new(requirement_version))
57
+ if Helper.homebrew?
58
+ Helper.backticks('brew upgrade fastlane')
59
+ else
60
+ # Approximate_recommendation will create a string like "~> 0.10" from a version 0.10.0, e.g. one that is valid for versions >= 0.10 and <1.0
61
+ requirement_version = local_version.approximate_recommendation
62
+ updater.update_gem(tool, Gem::Requirement.new(requirement_version))
63
+ end
60
64
 
61
65
  UI.success("Finished updating #{tool}")
62
66
  end
63
67
 
64
- UI.message("Cleaning up old versions...")
65
- cleaner.options[:args] = tools_to_update
66
- cleaner.execute
68
+ unless Helper.homebrew?
69
+ UI.message("Cleaning up old versions...")
70
+ cleaner.options[:args] = tools_to_update
71
+ cleaner.execute
72
+ end
73
+
74
+ if FastlaneCore::FastlaneFolder.swift?
75
+ upgrader = SwiftRunnerUpgrader.new
76
+ upgrader.upgrade_if_needed!
77
+ end
67
78
 
68
79
  UI.message("fastlane.tools successfully updated! I will now restart myself... 😴")
69
80
 
@@ -71,6 +82,16 @@ module Fastlane
71
82
  exec("FL_NO_UPDATE=true #{$PROGRAM_NAME} #{ARGV.join(' ')}")
72
83
  end
73
84
 
85
+ def self.get_gem_name(tool_info)
86
+ if tool_info.kind_of?(Array)
87
+ return tool_info[0]
88
+ elsif tool_info.respond_to?(:name) # Gem::NameTuple in RubyGems >= 3.1.0
89
+ return tool_info.name
90
+ else
91
+ UI.crash!("Unknown gem update information returned from RubyGems. Please file a new issue for this... 🤷")
92
+ end
93
+ end
94
+
74
95
  def self.description
75
96
  "Makes sure fastlane-tools are up-to-date when running fastlane"
76
97
  end
@@ -0,0 +1,289 @@
1
+ module Fastlane
2
+ module Actions
3
+ class UploadAppPrivacyDetailsToAppStoreAction < Action
4
+ DEFAULT_PATH = Fastlane::Helper.fastlane_enabled_folder_path
5
+ DEFAULT_FILE_NAME = "app_privacy_details.json"
6
+
7
+ def self.run(params)
8
+ require 'spaceship'
9
+
10
+ # Prompts select team if multiple teams and none specified
11
+ UI.message("Login to App Store Connect (#{params[:username]})")
12
+ Spaceship::ConnectAPI.login(params[:username], use_portal: false, use_tunes: true, tunes_team_id: params[:team_id], team_name: params[:team_name])
13
+ UI.message("Login successful")
14
+
15
+ # Get App
16
+ app = Spaceship::ConnectAPI::App.find(params[:app_identifier])
17
+ unless app
18
+ UI.user_error!("Could not find app with bundle identifier '#{params[:app_identifier]}' on account #{params[:username]}")
19
+ end
20
+
21
+ # Attempt to load JSON file
22
+ usages_config = load_json_file(params)
23
+
24
+ # Start interactive questions to generate and save JSON file
25
+ unless usages_config
26
+ usages_config = ask_interactive_questions_for_json
27
+
28
+ if params[:skip_json_file_saving]
29
+ UI.message("Skipping JSON file saving...")
30
+ else
31
+ json = JSON.pretty_generate(usages_config)
32
+ path = output_path(params)
33
+
34
+ UI.message("Writing file to #{path}")
35
+ File.write(path, json)
36
+ end
37
+ end
38
+
39
+ # Process JSON file to save app data usages to API
40
+ if params[:skip_upload]
41
+ UI.message("Skipping uploading of data... (so you can verify your JSON file)")
42
+ else
43
+ upload_app_data_usages(params, app, usages_config)
44
+ end
45
+ end
46
+
47
+ def self.load_json_file(params)
48
+ path = params[:json_path]
49
+ return nil if path.nil?
50
+ return JSON.parse(File.read(path))
51
+ end
52
+
53
+ def self.output_path(params)
54
+ path = params[:output_json_path]
55
+ return File.absolute_path(path)
56
+ end
57
+
58
+ def self.ask_interactive_questions_for_json(show_intro = true)
59
+ if show_intro
60
+ UI.important("You did not provide a JSON file for updating the app data usages")
61
+ UI.important("fastlane will now run you through interactive question to generate the JSON file")
62
+ UI.important("")
63
+ UI.important("This JSON file can be saved in source control and used in this action with the :json_file option")
64
+
65
+ unless UI.confirm("Ready to start?")
66
+ UI.user_error!("Cancelled")
67
+ end
68
+ end
69
+
70
+ # Fetch categories and purposes used for generating interactive questions
71
+ categories = Spaceship::ConnectAPI::AppDataUsageCategory.all(includes: "grouping")
72
+ purposes = Spaceship::ConnectAPI::AppDataUsagePurpose.all
73
+
74
+ json = []
75
+
76
+ unless UI.confirm("Are you collecting data?")
77
+ json << {
78
+ "data_protections" => [Spaceship::ConnectAPI::AppDataUsageDataProtection::ID::DATA_NOT_COLLECTED]
79
+ }
80
+
81
+ return json
82
+ end
83
+
84
+ categories.each do |category|
85
+ # Ask if using category
86
+ next unless UI.confirm("Collect data for #{category.id}?")
87
+
88
+ purpose_names = purposes.map(&:id).join(', ')
89
+ UI.message("How will this data be used? You'll be offered with #{purpose_names}")
90
+
91
+ # Ask purposes
92
+ selected_purposes = []
93
+ loop do
94
+ purposes.each do |purpose|
95
+ selected_purposes << purpose if UI.confirm("Used for #{purpose.id}?")
96
+ end
97
+
98
+ break unless selected_purposes.empty?
99
+ break unless UI.confirm("No purposes selected. Do you want to try again?")
100
+ end
101
+
102
+ # Skip asking protections if purposes were skipped
103
+ next if selected_purposes.empty?
104
+
105
+ # Ask protections
106
+ is_linked_to_user = UI.confirm("Is #{category.id} linked to the user?")
107
+ is_used_for_tracking = UI.confirm("Is #{category.id} used for tracking purposes?")
108
+
109
+ # Map answers to values for API requests
110
+ protection_id = is_linked_to_user ? Spaceship::ConnectAPI::AppDataUsageDataProtection::ID::DATA_LINKED_TO_YOU : Spaceship::ConnectAPI::AppDataUsageDataProtection::ID::DATA_NOT_LINKED_TO_YOU
111
+ tracking_id = is_used_for_tracking ? Spaceship::ConnectAPI::AppDataUsageDataProtection::ID::DATA_USED_TO_TRACK_YOU : nil
112
+
113
+ json << {
114
+ "category" => category.id,
115
+ "purposes" => selected_purposes.map(&:id),
116
+ "data_protections" => [
117
+ protection_id, tracking_id
118
+ ].compact
119
+ }
120
+ end
121
+
122
+ # Recursively call this method if no categories were selected for data collection
123
+ if json.empty?
124
+ UI.error("No categories were selected for data collection.")
125
+ json = ask_interactive_questions_for_json(false)
126
+ end
127
+
128
+ return json
129
+ end
130
+
131
+ def self.upload_app_data_usages(params, app, usages_config)
132
+ UI.message("Preparing to upload App Data Usage")
133
+
134
+ # Delete all existing usages for new ones
135
+ all_usages = Spaceship::ConnectAPI::AppDataUsage.all(app_id: app.id, includes: "category,grouping,purpose,dataProtection", limit: 500)
136
+ all_usages.each(&:delete!)
137
+
138
+ usages_config.each do |usage_config|
139
+ category = usage_config["category"]
140
+ purposes = usage_config["purposes"] || []
141
+ data_protections = usage_config["data_protections"] || []
142
+
143
+ # There will not be any purposes if "not collecting data"
144
+ # However, an AppDataUsage still needs to be created for not collecting data
145
+ # Creating an array with nil so that purposes can be iterated over and
146
+ # that AppDataUsage can be created
147
+ purposes = [nil] if purposes.empty?
148
+
149
+ purposes.each do |purpose|
150
+ data_protections.each do |data_protection|
151
+ if data_protection == Spaceship::ConnectAPI::AppDataUsageDataProtection::ID::DATA_NOT_COLLECTED
152
+ UI.message("Setting #{data_protection}")
153
+ else
154
+ UI.message("Setting #{category} and #{purpose} to #{data_protection}")
155
+ end
156
+
157
+ Spaceship::ConnectAPI::AppDataUsage.create(
158
+ app_id: app.id,
159
+ app_data_usage_category_id: category,
160
+ app_data_usage_protection_id: data_protection,
161
+ app_data_usage_purpose_id: purpose
162
+ )
163
+ end
164
+ end
165
+ end
166
+
167
+ # Publish
168
+ if params[:skip_publish]
169
+ UI.message("Skipping app data usage publishing... (so you can verify on App Store Connect)")
170
+ else
171
+ publish_state = Spaceship::ConnectAPI::AppDataUsagesPublishState.get(app_id: app.id)
172
+ if publish_state.published
173
+ UI.important("App data usage is already published")
174
+ else
175
+ UI.important("App data usage not published! Going to publish...")
176
+ publish_state.publish!
177
+ UI.important("App data usage is now published")
178
+ end
179
+ end
180
+ end
181
+
182
+ def self.description
183
+ "Upload App Privacy Details for an app in App Store Connect"
184
+ end
185
+
186
+ def self.available_options
187
+ user = CredentialsManager::AppfileConfig.try_fetch_value(:itunes_connect_id)
188
+ user ||= CredentialsManager::AppfileConfig.try_fetch_value(:apple_id)
189
+
190
+ [
191
+ FastlaneCore::ConfigItem.new(key: :username,
192
+ env_name: "FASTLANE_USER",
193
+ description: "Your Apple ID Username for App Store Connect",
194
+ default_value: user,
195
+ default_value_dynamic: true),
196
+ FastlaneCore::ConfigItem.new(key: :app_identifier,
197
+ env_name: "UPLOAD_APP_PRIVACY_DETAILS_TO_APP_STORE_APP_IDENTIFIER",
198
+ description: "The bundle identifier of your app",
199
+ code_gen_sensitive: true,
200
+ default_value: CredentialsManager::AppfileConfig.try_fetch_value(:app_identifier),
201
+ default_value_dynamic: true),
202
+ FastlaneCore::ConfigItem.new(key: :team_id,
203
+ env_name: "FASTLANE_ITC_TEAM_ID",
204
+ description: "The ID of your App Store Connect team if you're in multiple teams",
205
+ optional: true,
206
+ is_string: false, # as we also allow integers, which we convert to strings anyway
207
+ code_gen_sensitive: true,
208
+ default_value: CredentialsManager::AppfileConfig.try_fetch_value(:itc_team_id),
209
+ default_value_dynamic: true),
210
+ FastlaneCore::ConfigItem.new(key: :team_name,
211
+ env_name: "FASTLANE_ITC_TEAM_NAME",
212
+ description: "The name of your App Store Connect team if you're in multiple teams",
213
+ optional: true,
214
+ code_gen_sensitive: true,
215
+ default_value: CredentialsManager::AppfileConfig.try_fetch_value(:itc_team_name),
216
+ default_value_dynamic: true),
217
+
218
+ # JSON paths
219
+ FastlaneCore::ConfigItem.new(key: :json_path,
220
+ env_name: "UPLOAD_APP_PRIVACY_DETAILS_TO_APP_STORE_JSON_PATH",
221
+ description: "Path to the app usage data JSON",
222
+ is_string: true,
223
+ optional: true,
224
+ verify_block: proc do |value|
225
+ UI.user_error!("Could not find JSON file at path '#{File.expand_path(value)}'") unless File.exist?(value)
226
+ UI.user_error!("'#{value}' doesn't seem to be a JSON file") unless FastlaneCore::Helper.json_file?(File.expand_path(value))
227
+ end),
228
+ FastlaneCore::ConfigItem.new(key: :output_json_path,
229
+ env_name: "UPLOAD_APP_PRIVACY_DETAILS_TO_APP_STORE_OUTPUT_JSON_PATH",
230
+ description: "Path to the app usage data JSON file generated by interactive questions",
231
+ conflicting_options: [:skip_json_file_saving],
232
+ default_value: File.join(DEFAULT_PATH, DEFAULT_FILE_NAME)),
233
+
234
+ # Skipping options
235
+ FastlaneCore::ConfigItem.new(key: :skip_json_file_saving,
236
+ env_name: "UPLOAD_APP_PRIVACY_DETAILS_TO_APP_STORE_OUTPUT_SKIP_JSON_FILE_SAVING",
237
+ description: "Whether to skip the saving of the JSON file",
238
+ conflicting_options: [:skip_output_json_path],
239
+ type: Boolean,
240
+ default_value: false),
241
+ FastlaneCore::ConfigItem.new(key: :skip_upload,
242
+ env_name: "UPLOAD_APP_PRIVACY_DETAILS_TO_APP_STORE_OUTPUT_SKIP_UPLOAD",
243
+ description: "Whether to skip the upload and only create the JSON file with interactive questions",
244
+ conflicting_options: [:skip_publish],
245
+ type: Boolean,
246
+ default_value: false),
247
+ FastlaneCore::ConfigItem.new(key: :skip_publish,
248
+ env_name: "UPLOAD_APP_PRIVACY_DETAILS_TO_APP_STORE_OUTPUT_SKIP_PUBLISH",
249
+ description: "Whether to skip the publishing",
250
+ conflicting_options: [:skip_upload],
251
+ type: Boolean,
252
+ default_value: false)
253
+ ]
254
+ end
255
+
256
+ def self.author
257
+ "joshdholtz"
258
+ end
259
+
260
+ def self.is_supported?(platform)
261
+ [:ios, :mac, :tvos].include?(platform)
262
+ end
263
+
264
+ def self.details
265
+ "Upload App Privacy Details for an app in App Store Connect. For more detail information, view https://docs.fastlane.tools/uploading-app-privacy-details"
266
+ end
267
+
268
+ def self.example_code
269
+ [
270
+ 'upload_app_privacy_details_to_app_store(
271
+ username: "your@email.com",
272
+ team_name: "Your Team",
273
+ app_identifier: "com.your.bundle"
274
+ )',
275
+ 'upload_app_privacy_details_to_app_store(
276
+ username: "your@email.com",
277
+ team_name: "Your Team",
278
+ app_identifier: "com.your.bundle",
279
+ json_path: "fastlane/app_data_usages.json"
280
+ )'
281
+ ]
282
+ end
283
+
284
+ def self.category
285
+ :production
286
+ end
287
+ end
288
+ end
289
+ end