diff --git a/.gitignore b/.gitignore index 3882e93..c6c703c 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,6 @@ xcuserdata /fastlane/*.xml /vendor -/.bundle \ No newline at end of file +/.bundle + +.vscode \ No newline at end of file diff --git a/.rubocop.yml b/.rubocop.yml index ec08ab7..038455e 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -4,16 +4,15 @@ inherit_mode: AllCops: Exclude: - - 'Carthage/**/*' - - 'Demo/Carthage/**/*' - 'vendor/**/*' Include: - 'fastlane/Pluginfile' + NewCops: enable # this would cause errors with long lanes Metrics/BlockLength: Enabled: true - ExcludedMethods: ['platform', 'for_platform'] + IgnoredMethods: ['platform', 'for_platform'] # Lane description and gem lines can be long Layout/LineLength: diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 0000000..623364b --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,104 @@ +included: + - Sources + - Tests + +disabled_rules: + - cyclomatic_complexity + - identifier_name + - force_cast + - force_try + - function_body_length + - multiple_closures_with_trailing_closure + - statement_position + - todo + - type_body_length + +opt_in_rules: + - capture_variable + - closure_end_indentation + - closure_spacing + - collection_alignment + - comma_inheritance + - contains_over_filter_count + - contains_over_filter_is_empty + - contains_over_first_not_nil + - contains_over_range_nil_comparison + - convenience_type + - discarded_notification_center_observer + - discouraged_assert + - discouraged_object_literal + - discouraged_optional_boolean + - empty_collection_literal + - empty_count + - empty_string + - empty_xctest_method + - enum_case_associated_values_count + - expiring_todo + - explicit_init + - extension_access_modifier + - fallthrough + - fatal_error_message + - file_name_no_space + - first_where + - flatmap_over_map_reduce + - identical_operands + - joined_default_parameter + - last_where + - legacy_multiple + - literal_expression_end_indentation + - modifier_order + - multiline_function_chains + - multiline_literal_brackets + - multiline_parameters + - multiline_parameters_brackets + - nslocalizedstring_key + - operator_usage_whitespace + - overridden_super_call + - override_in_extension + - pattern_matching_keywords + - prefer_self_in_static_references + - prefer_self_type_over_type_of_self + - prefer_zero_over_explicit_init + - prefixed_toplevel_constant + - private_action + - private_outlet + - private_subject + - prohibited_super_call + - raw_value_for_camel_cased_codable_enum + - redundant_nil_coalescing + - redundant_type_annotation + - required_enum_case + - return_value_from_void_function + - sorted_first_last + - static_operator + - switch_case_on_newline + - test_case_accessibility + - toggle_bool + - typesafe_array_init + - unneeded_parentheses_in_closure_argument + - unowned_variable_capture + - untyped_error_in_catch + - unused_declaration + - unused_import + - vertical_parameter_alignment_on_call + - vertical_whitespace_closing_braces + - vertical_whitespace_opening_braces + - weak_delegate + - xct_specific_matcher + +line_length: 300 +file_length: 1000 + +trailing_whitespace: + ignores_empty_lines: true + severity: warning + +type_name: + excluded: + - Id + +function_parameter_count: + warning: 8 + +nesting: + type_level: 2 \ No newline at end of file diff --git a/Gemfile b/Gemfile index 5d378ba..aa3838f 100644 --- a/Gemfile +++ b/Gemfile @@ -8,6 +8,3 @@ source 'https://rubygems.org' gem 'fastlane' gem 'xcode-install' - -plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') -eval_gemfile(plugins_path) if File.exist?(plugins_path) diff --git a/Gemfile.lock b/Gemfile.lock index a835d13..b9be0ee 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,29 +1,30 @@ GEM remote: https://rubygems.org/ specs: - CFPropertyList (3.0.3) + CFPropertyList (3.0.5) + rexml addressable (2.8.0) public_suffix (>= 2.0.2, < 5.0) artifactory (3.0.15) atomos (0.1.3) aws-eventstream (1.2.0) - aws-partitions (1.502.0) - aws-sdk-core (3.121.0) + aws-partitions (1.616.0) + aws-sdk-core (3.132.0) aws-eventstream (~> 1, >= 1.0.2) - aws-partitions (~> 1, >= 1.239.0) + aws-partitions (~> 1, >= 1.525.0) aws-sigv4 (~> 1.1) - jmespath (~> 1.0) - aws-sdk-kms (1.48.0) - aws-sdk-core (~> 3, >= 3.120.0) + jmespath (~> 1, >= 1.6.1) + aws-sdk-kms (1.58.0) + aws-sdk-core (~> 3, >= 3.127.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.103.0) - aws-sdk-core (~> 3, >= 3.120.0) + aws-sdk-s3 (1.114.0) + aws-sdk-core (~> 3, >= 3.127.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.4) - aws-sigv4 (1.4.0) + aws-sigv4 (1.5.1) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) - claide (1.0.3) + claide (1.1.0) colored (1.2) colored2 (3.1.2) commander (4.6.0) @@ -33,19 +34,20 @@ GEM rake (>= 12.0.0, < 14.0.0) domain_name (0.5.20190701) unf (>= 0.0.5, < 1.0.0) - dotenv (2.7.6) - emoji_regex (3.2.2) - excon (0.85.0) - faraday (1.7.2) + dotenv (2.8.1) + emoji_regex (3.2.3) + excon (0.92.4) + faraday (1.10.1) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) faraday-excon (~> 1.1) - faraday-httpclient (~> 1.0.1) + faraday-httpclient (~> 1.0) + faraday-multipart (~> 1.0) faraday-net_http (~> 1.0) - faraday-net_http_persistent (~> 1.1) + faraday-net_http_persistent (~> 1.0) faraday-patron (~> 1.0) faraday-rack (~> 1.0) - multipart-post (>= 1.2, < 3) + faraday-retry (~> 1.0) ruby2_keywords (>= 0.0.4) faraday-cookie_jar (0.0.7) faraday (>= 0.8.0) @@ -54,14 +56,17 @@ GEM faraday-em_synchrony (1.0.0) faraday-excon (1.1.0) faraday-httpclient (1.0.1) + faraday-multipart (1.0.4) + multipart-post (~> 2) faraday-net_http (1.0.1) faraday-net_http_persistent (1.2.0) faraday-patron (1.0.0) faraday-rack (1.0.0) - faraday_middleware (1.1.0) + faraday-retry (1.0.3) + faraday_middleware (1.2.0) faraday (~> 1.0) - fastimage (2.2.5) - fastlane (2.194.0) + fastimage (2.2.6) + fastlane (2.208.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -100,12 +105,10 @@ GEM xcodeproj (>= 1.13.0, < 2.0.0) xcpretty (~> 0.3.0) xcpretty-travis-formatter (>= 0.0.3) - fastlane-plugin-trainer (0.4.1) - trainer (>= 0.7.0) gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.11.0) - google-apis-core (>= 0.4, < 2.a) - google-apis-core (0.4.1) + google-apis-androidpublisher_v3 (0.25.0) + google-apis-core (>= 0.7, < 2.a) + google-apis-core (0.7.0) addressable (~> 2.5, >= 2.5.1) googleauth (>= 0.16.2, < 2.a) httpclient (>= 2.8.1, < 3.a) @@ -114,53 +117,53 @@ GEM retriable (>= 2.0, < 4.a) rexml webrick - google-apis-iamcredentials_v1 (0.7.0) - google-apis-core (>= 0.4, < 2.a) - google-apis-playcustomapp_v1 (0.5.0) - google-apis-core (>= 0.4, < 2.a) - google-apis-storage_v1 (0.6.0) - google-apis-core (>= 0.4, < 2.a) + google-apis-iamcredentials_v1 (0.13.0) + google-apis-core (>= 0.7, < 2.a) + google-apis-playcustomapp_v1 (0.10.0) + google-apis-core (>= 0.7, < 2.a) + google-apis-storage_v1 (0.17.0) + google-apis-core (>= 0.7, < 2.a) google-cloud-core (1.6.0) google-cloud-env (~> 1.0) google-cloud-errors (~> 1.0) - google-cloud-env (1.5.0) - faraday (>= 0.17.3, < 2.0) - google-cloud-errors (1.1.0) - google-cloud-storage (1.34.1) - addressable (~> 2.5) + google-cloud-env (1.6.0) + faraday (>= 0.17.3, < 3.0) + google-cloud-errors (1.2.0) + google-cloud-storage (1.38.0) + addressable (~> 2.8) digest-crc (~> 0.4) google-apis-iamcredentials_v1 (~> 0.1) - google-apis-storage_v1 (~> 0.1) + google-apis-storage_v1 (~> 0.17.0) google-cloud-core (~> 1.6) googleauth (>= 0.16.2, < 2.a) mini_mime (~> 1.0) - googleauth (0.17.1) - faraday (>= 0.17.3, < 2.0) + googleauth (1.2.0) + faraday (>= 0.17.3, < 3.a) jwt (>= 1.4, < 3.0) memoist (~> 0.16) multi_json (~> 1.11) os (>= 0.9, < 2.0) - signet (~> 0.15) + signet (>= 0.16, < 2.a) highline (2.0.3) - http-cookie (1.0.4) + http-cookie (1.0.5) domain_name (~> 0.5) httpclient (2.8.3) - jmespath (1.4.0) - json (2.5.1) - jwt (2.2.3) + jmespath (1.6.1) + json (2.6.2) + jwt (2.4.1) memoist (0.16.2) mini_magick (4.11.0) - mini_mime (1.1.1) + mini_mime (1.1.2) multi_json (1.15.0) multipart-post (2.0.0) nanaimo (0.3.0) naturally (2.2.1) optparse (0.1.1) - os (1.1.1) + os (1.1.4) plist (3.6.0) - public_suffix (4.0.6) + public_suffix (4.0.7) rake (13.0.6) - representable (3.1.1) + representable (3.2.0) declarative (< 0.1.0) trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) @@ -170,9 +173,9 @@ GEM ruby2_keywords (0.0.5) rubyzip (2.3.2) security (0.1.3) - signet (0.16.0) + signet (0.17.0) addressable (~> 2.8) - faraday (>= 0.17.3, < 2.0) + faraday (>= 0.17.5, < 3.a) jwt (>= 1.5, < 3.0) multi_json (~> 1.10) simctl (1.6.8) @@ -181,10 +184,7 @@ GEM terminal-notifier (2.0.0) terminal-table (1.8.0) unicode-display_width (~> 1.1, >= 1.1.1) - trailblazer-option (0.1.1) - trainer (0.9.1) - fastlane (>= 2.25.0) - plist (>= 3.1.0, < 4.0.0) + trailblazer-option (0.1.2) tty-cursor (0.7.1) tty-screen (0.8.1) tty-spinner (0.9.3) @@ -192,14 +192,14 @@ GEM uber (0.1.0) unf (0.1.4) unf_ext - unf_ext (0.0.8) + unf_ext (0.0.8.2) unicode-display_width (1.8.0) webrick (1.7.0) word_wrap (1.0.0) - xcode-install (2.8.0) - claide (>= 0.9.1, < 1.1.0) + xcode-install (2.8.1) + claide (>= 0.9.1) fastlane (>= 2.1.0, < 3.0.0) - xcodeproj (1.21.0) + xcodeproj (1.22.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) @@ -216,8 +216,7 @@ PLATFORMS DEPENDENCIES fastlane - fastlane-plugin-trainer xcode-install BUNDLED WITH - 1.17.2 + 2.3.7 diff --git a/Package.swift b/Package.swift index 18fb235..1cbcc6a 100644 --- a/Package.swift +++ b/Package.swift @@ -3,13 +3,13 @@ import PackageDescription struct ProjectSettings { - static let marketingVersion: String = "3.0.2" + static let marketingVersion: String = "3.1.0" } let package = Package( name: "SRGLogger", platforms: [ - .iOS(.v9), + .iOS(.v12), .tvOS(.v12), .watchOS(.v5) ], diff --git a/Sources/SRGLoggerSwift/SRGLogger.swift b/Sources/SRGLoggerSwift/SRGLogger.swift index 285cb4b..ad3eb82 100644 --- a/Sources/SRGLoggerSwift/SRGLogger.swift +++ b/Sources/SRGLoggerSwift/SRGLogger.swift @@ -9,44 +9,44 @@ import SRGLogger /** * Log a given message at the verbose level. */ -public func SRGLogVerbose(subsystem: String?, category : String?, message: String, file: String = #file, function: String = #function, line: UInt = #line) { +public func SRGLogVerbose(subsystem: String?, category: String?, message: String, file: String = #file, function: String = #function, line: UInt = #line) { SRGLogger.logMessage({ () -> String in return message - }, level: SRGLogLevel.verbose, subsystem: subsystem, category: category, file: file, function: function, line: line); + }, level: SRGLogLevel.verbose, subsystem: subsystem, category: category, file: file, function: function, line: line) } /** * Log a given message at the debug level. */ -public func SRGLogDebug(subsystem: String?, category : String?, message: String, file: String = #file, function: String = #function, line: UInt = #line) { +public func SRGLogDebug(subsystem: String?, category: String?, message: String, file: String = #file, function: String = #function, line: UInt = #line) { SRGLogger.logMessage({ () -> String in return message - }, level: SRGLogLevel.debug, subsystem: subsystem, category: category, file: file, function: function, line: line); + }, level: SRGLogLevel.debug, subsystem: subsystem, category: category, file: file, function: function, line: line) } /** * Log a given message at the info level. */ -public func SRGLogInfo(subsystem: String?, category : String?, message: String, file: String = #file, function: String = #function, line: UInt = #line) { +public func SRGLogInfo(subsystem: String?, category: String?, message: String, file: String = #file, function: String = #function, line: UInt = #line) { SRGLogger.logMessage({ () -> String in return message - }, level: SRGLogLevel.info, subsystem: subsystem, category: category, file: file, function: function, line: line); + }, level: SRGLogLevel.info, subsystem: subsystem, category: category, file: file, function: function, line: line) } /** * Log a given message at the warning level. */ -public func SRGLogWarning(subsystem: String?, category : String?, message: String, file: String = #file, function: String = #function, line: UInt = #line) { +public func SRGLogWarning(subsystem: String?, category: String?, message: String, file: String = #file, function: String = #function, line: UInt = #line) { SRGLogger.logMessage({ () -> String in return message - }, level: SRGLogLevel.warning, subsystem: subsystem, category: category, file: file, function: function, line: line); + }, level: SRGLogLevel.warning, subsystem: subsystem, category: category, file: file, function: function, line: line) } /** * Log a given message at the error level. */ -public func SRGLogError(subsystem: String?, category : String?, message: String, file: String = #file, function: String = #function, line: UInt = #line) { +public func SRGLogError(subsystem: String?, category: String?, message: String, file: String = #file, function: String = #function, line: UInt = #line) { SRGLogger.logMessage({ () -> String in return message - }, level: SRGLogLevel.error, subsystem: subsystem, category: category, file: file, function: function, line: line); + }, level: SRGLogLevel.error, subsystem: subsystem, category: category, file: file, function: function, line: line) } diff --git a/Tests/SRGLoggerSwiftTests/LoggerTestCase.swift b/Tests/SRGLoggerSwiftTests/LoggerTestCase.swift index 34ec7d7..7e2466f 100644 --- a/Tests/SRGLoggerSwiftTests/LoggerTestCase.swift +++ b/Tests/SRGLoggerSwiftTests/LoggerTestCase.swift @@ -8,7 +8,7 @@ import SRGLoggerSwift import XCTest func TestLogError(category: String?, message: String, file: String = #file, function: String = #function, line: UInt = #line) { - SRGLogError(subsystem: "ch.srgssr.logger-tests", category: category, message: message, file: file, function: function, line: line); + SRGLogError(subsystem: "ch.srgssr.logger-tests", category: category, message: message, file: file, function: function, line: line) } class LoggerSwiftTestCase: XCTestCase { @@ -18,7 +18,7 @@ class LoggerSwiftTestCase: XCTestCase { let string = "Hello, World!" SRGLogError(subsystem: "ch.srgssr.logger-tests", category: "Test-Swift", message: "Error with string '\(string)'") - SRGLogError(subsystem: "ch.srgssr.logger-tests", category: "Test-Swift", message: "Error with dictionary \([ "key" : "value" ])") + SRGLogError(subsystem: "ch.srgssr.logger-tests", category: "Test-Swift", message: "Error with dictionary \([ "key": "value" ])") TestLogError(category: "Test-Swift", message: "Error!") } diff --git a/docs/README.md b/docs/README.md index d6b3d39..383a453 100644 --- a/docs/README.md +++ b/docs/README.md @@ -25,7 +25,7 @@ The library does not provide any logging to [NSLogger](https://github.com/fpille ## Compatibility -The library is suitable for applications running on iOS 9, tvOS 12, watchOS 5 and above. The project is meant to be compiled with the latest Xcode version. +The library is suitable for applications running on iOS 12, tvOS 12, watchOS 5 and above. The project is meant to be compiled with the latest Xcode version. ## Contributing diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 4b2e74e..e263574 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -9,7 +9,7 @@ # All lines starting with a # are ignored when running `fastlane` # This is the minimum version number required. -fastlane_version '2.96.0' +fastlane_version '2.201.0' default_platform :ios @@ -17,7 +17,7 @@ platform :ios do before_all do ensure_git_status_clean - xcversion(version: '~> 13') + xcversion(version: '~> 14') end desc 'Run library tests' @@ -25,12 +25,9 @@ platform :ios do clean_result_files clean_derived_data - Device = Struct.new(:platform, :name) - TestBuild = Struct.new(:scheme, :scheme_suffix, :in_workspace) - - iphone11 = Device.new('iOS', 'iPhone 11') + iphone13 = Device.new('iOS', 'iPhone 13') appletv = Device.new('tvOS', 'Apple TV') - devices = [iphone11, appletv] + devices = [iphone13, appletv] scheme = swift_package_name swift_package_tests = TestBuild.new(scheme, '-Package', false) @@ -40,8 +37,7 @@ platform :ios do srg_run_tests(devices, test_builds) # Produce JUnit files for CI - srg_trainer - override_junit_test_suite_names(test_builds) + srg_junit_files end after_all do @@ -54,6 +50,9 @@ platform :ios do end end +Device = Struct.new :platform, :name +TestBuild = Struct.new :scheme, :scheme_suffix, :in_workspace + def swift_package_name JSON.parse((sh 'swift package dump-package'))['name'] end @@ -70,13 +69,11 @@ end def srg_run_tests(devices, test_builds) devices.each do |device| test_builds.each do |test_build| - begin - srg_xcodebuild(device, test_build) - rescue StandardError => e - raise e unless e.message.include? '** TEST FAILED **' + srg_xcodebuild(device, test_build) + rescue StandardError => e + raise e unless e.message.include? '** TEST FAILED **' - UI.important('One or more tests failed on ' + device.platform + ' (' + srg_xcodebuild_scheme(test_build) + '). ⚠️') - end + UI.important("One or more tests failed on #{device.platform} (#{srg_xcodebuild_scheme(test_build)}). ⚠️") end end end @@ -84,6 +81,7 @@ end def srg_xcodebuild(device, test_build) xcodebuild( test: true, + xcargs: srg_test_xcargs, workspace: srg_xcodebuild_workspace(test_build), scheme: srg_xcodebuild_scheme(test_build), destination: srg_xcodebuild_destination(device), @@ -92,8 +90,12 @@ def srg_xcodebuild(device, test_build) ) end +def srg_test_xcargs + '-retry-tests-on-failure -testLanguage en -testRegion en-US' +end + def srg_xcodebuild_workspace(test_build) - test_build.in_workspace ? 'Tests/' + srg_xcodebuild_scheme(test_build) + '.xcworkspace' : nil + test_build.in_workspace ? "Tests/#{srg_xcodebuild_scheme(test_build)}.xcworkspace" : nil end def srg_xcodebuild_scheme(test_build) @@ -101,22 +103,30 @@ def srg_xcodebuild_scheme(test_build) end def srg_xcodebuild_destination(device) - 'platform=' + device.platform + ' Simulator,name=' + device.name + "platform=#{device.platform} Simulator,name=#{device.name}" end def srg_xcodebuild_result_bundle_path(device, test_build) - result_bundle_folder_path + test_build.scheme + '-' + device.platform + "#{result_bundle_folder_path}#{test_build.scheme}-#{device.platform}" end def srg_xcodebuild_derived_data_path '.build/DerivedData' end +def srg_junit_files + srg_trainer + override_junit_test_suite_names + expose_junit_files +end + # Convert xcresults to JUnit files def srg_trainer trainer( path: result_bundle_folder_path, output_directory: './fastlane', + extension: raw_extension, + output_remove_retry_attempts: true, fail_build: false ) end @@ -125,21 +135,31 @@ def result_bundle_folder_path './fastlane/xcresult/' end +def raw_extension + '.rawjunit' +end + # Override JUnit test suite names to split iOS and tvOS test results -def override_junit_test_suite_names(test_builds) - test_builds.each do |test_build| - Dir[test_build.scheme + '-*.xml'].each do |file_name| - override_junit_test_suite_name(file_name) - end +def override_junit_test_suite_names + Dir["*#{raw_extension}"].each do |file_name| + override_junit_test_suite_name(file_name) end end def override_junit_test_suite_name(file_name) platform = file_name.split('.').first.split('-').last file = File.open(file_name, 'r') - xml = file.read.gsub('Tests" tests="', '-' + platform + '" tests="') - xml = xml.gsub('-tests" tests="', '-' + platform + '" tests="') - File.open(file_name, 'w') { |f| f.write(xml) } + xml = file.read.gsub('Tests" tests="', "-#{platform}\" tests=\"") + xml = xml.gsub('-tests" tests="', "-#{platform}\" tests=\"") + File.write(file_name, xml) +end + +def expose_junit_files + Dir["*#{raw_extension}"].each do |file_name| + # Rename JUnit files with correct extension + new_file_name = file_name.gsub(raw_extension, '.xml') + File.rename(file_name, new_file_name) unless File.exist?(new_file_name) + end end # More information about multiple platforms in fastlane: https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Platforms.md diff --git a/fastlane/Pluginfile b/fastlane/Pluginfile deleted file mode 100644 index 72dfbbd..0000000 --- a/fastlane/Pluginfile +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -# Autogenerated by fastlane -# -# Ensure this file is checked in to source control! - -gem 'fastlane-plugin-trainer' diff --git a/fastlane/README.md b/fastlane/README.md index 45a7da7..d812e80 100644 --- a/fastlane/README.md +++ b/fastlane/README.md @@ -1,29 +1,32 @@ fastlane documentation -================ +---- + # Installation Make sure you have the latest version of the Xcode command line tools installed: -``` +```sh xcode-select --install ``` -Install _fastlane_ using -``` -[sudo] gem install fastlane -NV -``` -or alternatively using `brew install fastlane` +For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane) # Available Actions + ## iOS + ### ios tests + +```sh +[bundle exec] fastlane ios tests ``` -fastlane ios tests -``` + Run library tests ---- This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. -More information about fastlane can be found on [fastlane.tools](https://fastlane.tools). -The documentation of fastlane can be found on [docs.fastlane.tools](https://docs.fastlane.tools). + +More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools). + +The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools).