fastlane 2.240.1 → 2.241.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.
- checksums.yaml +4 -4
- data/README.md +57 -177
- data/bin/fastlane +4 -2
- data/credentials_manager/README.md +1 -1
- data/deliver/lib/deliver/app_screenshot.rb +0 -4
- data/deliver/lib/deliver/commands_generator.rb +1 -1
- data/deliver/lib/deliver/download_screenshots.rb +8 -1
- data/deliver/lib/deliver/sync_screenshots.rb +27 -28
- data/deliver/lib/deliver/upload_app_clip_default_experience_header_images.rb +3 -3
- data/deliver/lib/deliver/upload_metadata.rb +12 -16
- data/deliver/lib/deliver/upload_screenshots.rb +12 -14
- data/docs/README.md +25 -0
- data/fastlane/lib/assets/TeamTable.md.erb +28 -0
- data/fastlane/lib/fastlane/actions/appaloosa.rb +2 -1
- data/fastlane/lib/fastlane/actions/appium.rb +1 -1
- data/fastlane/lib/fastlane/actions/apteligent.rb +8 -1
- data/fastlane/lib/fastlane/actions/build_and_upload_to_appetize.rb +1 -1
- data/fastlane/lib/fastlane/actions/clean_build_artifacts.rb +3 -0
- data/fastlane/lib/fastlane/actions/danger.rb +5 -5
- data/fastlane/lib/fastlane/actions/docs/build_app.md +1 -2
- data/fastlane/lib/fastlane/actions/docs/create_app_online.md +1 -2
- data/fastlane/lib/fastlane/actions/docs/get_certificates.md +0 -2
- data/fastlane/lib/fastlane/actions/docs/get_push_certificate.md +1 -3
- data/fastlane/lib/fastlane/actions/docs/sync_code_signing.md +13 -3
- data/fastlane/lib/fastlane/actions/ensure_no_debug_code.rb +2 -2
- data/fastlane/lib/fastlane/actions/get_managed_play_store_publishing_rights.rb +1 -0
- data/fastlane/lib/fastlane/actions/github_api.rb +1 -2
- data/fastlane/lib/fastlane/actions/hg_commit_version_bump.rb +1 -1
- data/fastlane/lib/fastlane/actions/increment_build_number.rb +1 -1
- data/fastlane/lib/fastlane/actions/increment_version_number.rb +1 -1
- data/fastlane/lib/fastlane/actions/install_xcode_plugin.rb +16 -6
- data/fastlane/lib/fastlane/actions/lcov.rb +9 -2
- data/fastlane/lib/fastlane/actions/nexus_upload.rb +16 -4
- data/fastlane/lib/fastlane/actions/notification.rb +3 -2
- data/fastlane/lib/fastlane/actions/oclint.rb +8 -8
- data/fastlane/lib/fastlane/actions/push_to_git_remote.rb +9 -4
- data/fastlane/lib/fastlane/actions/register_device.rb +1 -1
- data/fastlane/lib/fastlane/actions/say.rb +2 -4
- data/fastlane/lib/fastlane/actions/set_pod_key.rb +3 -3
- data/fastlane/lib/fastlane/actions/sonar.rb +16 -16
- data/fastlane/lib/fastlane/actions/sourcedocs.rb +8 -8
- data/fastlane/lib/fastlane/actions/splunkmint.rb +8 -1
- data/fastlane/lib/fastlane/actions/update_project_provisioning.rb +1 -1
- data/fastlane/lib/fastlane/actions/upload_to_app_store.rb +5 -2
- data/fastlane/lib/fastlane/actions/upload_to_testflight.rb +5 -2
- data/fastlane/lib/fastlane/actions/verify_build.rb +1 -4
- data/fastlane/lib/fastlane/actions/verify_pod_keys.rb +3 -1
- data/fastlane/lib/fastlane/actions/xcode_server_get_assets.rb +20 -19
- data/fastlane/lib/fastlane/actions/xcodebuild.rb +5 -6
- data/fastlane/lib/fastlane/actions/zip.rb +2 -1
- data/fastlane/lib/fastlane/auto_complete.rb +1 -1
- data/fastlane/lib/fastlane/cli_tools_distributor.rb +14 -0
- data/fastlane/lib/fastlane/documentation/markdown_docs_generator.rb +23 -0
- data/fastlane/lib/fastlane/environment_printer.rb +12 -25
- data/fastlane/lib/fastlane/fast_file.rb +2 -1
- data/fastlane/lib/fastlane/fastlane_require.rb +5 -1
- data/fastlane/lib/fastlane/helper/sh_helper.rb +16 -6
- data/fastlane/lib/fastlane/new_action.rb +1 -1
- data/fastlane/lib/fastlane/plugins/plugin_fetcher.rb +1 -1
- data/fastlane/lib/fastlane/plugins/plugin_info_collector.rb +1 -1
- data/fastlane/lib/fastlane/plugins/plugin_manager.rb +36 -4
- data/fastlane/lib/fastlane/plugins/template/%gem_name%.gemspec.erb +1 -1
- data/fastlane/lib/fastlane/plugins/template/.github/workflows/test.yml +1 -1
- data/fastlane/lib/fastlane/plugins/template/.rubocop.yml +3 -7
- data/fastlane/lib/fastlane/swift_lane_manager.rb +22 -4
- data/fastlane/lib/fastlane/version.rb +3 -2
- data/fastlane/swift/Deliverfile.swift +1 -1
- data/fastlane/swift/DeliverfileProtocol.swift +1 -1
- data/fastlane/swift/Fastlane.swift +55 -9
- data/fastlane/swift/Gymfile.swift +1 -1
- data/fastlane/swift/GymfileProtocol.swift +1 -1
- data/fastlane/swift/MainProcess.swift +190 -20
- data/fastlane/swift/Matchfile.swift +1 -1
- data/fastlane/swift/MatchfileProtocol.swift +10 -3
- data/fastlane/swift/Precheckfile.swift +1 -1
- data/fastlane/swift/PrecheckfileProtocol.swift +1 -1
- data/fastlane/swift/Scanfile.swift +1 -1
- data/fastlane/swift/ScanfileProtocol.swift +8 -1
- data/fastlane/swift/Screengrabfile.swift +1 -1
- data/fastlane/swift/ScreengrabfileProtocol.swift +1 -1
- data/fastlane/swift/Snapshotfile.swift +1 -1
- data/fastlane/swift/SnapshotfileProtocol.swift +1 -1
- data/fastlane/swift/formatting/Rakefile +1 -0
- data/fastlane/swift/main.swift +4 -1
- data/fastlane_core/README.md +1 -1
- data/fastlane_core/lib/fastlane_core/analytics/analytics_event_builder.rb +1 -17
- data/fastlane_core/lib/fastlane_core/configuration/configuration.rb +11 -8
- data/fastlane_core/lib/fastlane_core/device_manager.rb +2 -2
- data/fastlane_core/lib/fastlane_core/helper.rb +34 -5
- data/fastlane_core/lib/fastlane_core/itunes_transporter.rb +0 -14
- data/fastlane_core/lib/fastlane_core/keychain_importer.rb +24 -27
- data/fastlane_core/lib/fastlane_core/languages.rb +0 -1
- data/fastlane_core/lib/fastlane_core/print_table.rb +5 -2
- data/fastlane_core/lib/fastlane_core/provisioning_profile.rb +10 -19
- data/fastlane_core/lib/fastlane_core/secrets.rb +77 -0
- data/fastlane_core/lib/fastlane_core/ui/fastlane_runner.rb +3 -3
- data/fastlane_core/lib/fastlane_core/ui/github_issue_inspector_reporter.rb +1 -1
- data/fastlane_core/lib/fastlane_core/ui/implementations/shell.rb +2 -1
- data/fastlane_core/lib/fastlane_core.rb +1 -0
- data/frameit/lib/frameit/device.rb +1 -1
- data/frameit/lib/frameit/frame_downloader.rb +2 -0
- data/frameit/lib/frameit/runner.rb +3 -2
- data/frameit/lib/frameit/screenshot.rb +0 -1
- data/gym/lib/gym/generators/build_command_generator.rb +4 -4
- data/gym/lib/gym/generators/package_command_generator_xcode7.rb +3 -3
- data/gym/lib/gym/runner.rb +1 -1
- data/internal/README.md +1 -1
- data/match/lib/match/generator.rb +1 -0
- data/match/lib/match/options.rb +13 -5
- data/match/lib/match/storage/git_storage.rb +43 -7
- data/match/lib/match/storage/gitlab/secure_file.rb +2 -1
- data/pem/lib/pem/module.rb +0 -4
- data/precheck/lib/precheck/rule_processor.rb +1 -0
- data/produce/lib/produce/developer_center.rb +49 -2
- data/produce/lib/produce/itunes_connect.rb +1 -1
- data/produce/lib/produce/module.rb +21 -0
- data/produce/lib/produce/options.rb +17 -0
- data/produce/lib/produce/service.rb +3 -5
- data/produce/lib/produce.rb +0 -1
- data/scan/lib/scan/options.rb +7 -0
- data/scan/lib/scan/runner.rb +1 -1
- data/scan/lib/scan/test_command_generator.rb +12 -9
- data/scan/lib/scan/test_result_parser.rb +1 -0
- data/scan/lib/scan/xcpretty_reporter_options_generator.rb +2 -2
- data/screengrab/lib/screengrab/page.html.erb +12 -7
- data/screengrab/lib/screengrab/reports_generator.rb +1 -1
- data/sigh/lib/assets/resign.sh +17 -0
- data/sigh/lib/sigh/local_manage.rb +1 -1
- data/sigh/lib/sigh/options.rb +5 -0
- data/sigh/lib/sigh/resign.rb +3 -3
- data/sigh/lib/sigh/runner.rb +6 -1
- data/snapshot/lib/snapshot/page.html.erb +17 -12
- data/snapshot/lib/snapshot/reports_generator.rb +1 -1
- data/snapshot/lib/snapshot/simulator_launchers/simulator_launcher_base.rb +1 -1
- data/snapshot/lib/snapshot.rb +0 -2
- data/spaceship/README.md +2 -3
- data/spaceship/lib/spaceship/base.rb +3 -1
- data/spaceship/lib/spaceship/client.rb +65 -47
- data/spaceship/lib/spaceship/connect_api/models/bundle_id.rb +13 -2
- data/spaceship/lib/spaceship/connect_api/models/bundle_id_capability.rb +6 -0
- data/spaceship/lib/spaceship/connect_api/models/profile.rb +7 -5
- data/spaceship/lib/spaceship/connect_api/provisioning/provisioning.rb +25 -0
- data/spaceship/lib/spaceship/du/upload_file.rb +1 -1
- data/spaceship/lib/spaceship/errors.rb +7 -1
- data/spaceship/lib/spaceship/globals.rb +2 -2
- data/spaceship/lib/spaceship/portal/merchant.rb +127 -2
- data/spaceship/lib/spaceship/portal/portal_client.rb +58 -0
- data/spaceship/lib/spaceship/portal/ui/select_team.rb +0 -2
- data/spaceship/lib/spaceship/portal/website_push.rb +8 -2
- data/spaceship/lib/spaceship/test_flight/client.rb +0 -22
- data/spaceship/lib/spaceship/test_flight.rb +0 -1
- data/spaceship/lib/spaceship/two_step_or_factor_client.rb +11 -0
- data/supply/lib/supply/options.rb +1 -0
- data/supply/lib/supply.rb +0 -1
- metadata +54 -36
- data/produce/lib/produce/available_default_languages.rb +0 -62
- data/snapshot/lib/snapshot/screenshot_flatten.rb +0 -20
- data/snapshot/lib/snapshot/screenshot_rotate.rb +0 -36
- data/spaceship/lib/spaceship/test_flight/app_test_info.rb +0 -40
- data/supply/lib/supply/apk_listing.rb +0 -14
- /data/fastlane/lib/assets/{custom_action_template.rb → custom_action_template.rb.template} +0 -0
|
@@ -15,7 +15,7 @@ module Frameit
|
|
|
15
15
|
attr_reader :priority_config_key
|
|
16
16
|
|
|
17
17
|
def initialize(id, formatted_name, priority, resolutions, density_ppi, default_color, platform = Platform::IOS, deliver_screen_id = nil, priority_config_key = nil)
|
|
18
|
-
|
|
18
|
+
raise "Priority mustn't be higher than #{REQUIRED_PRIORITY}" if priority > REQUIRED_PRIORITY
|
|
19
19
|
@id = id
|
|
20
20
|
@deliver_screen_id = deliver_screen_id
|
|
21
21
|
@formatted_name = formatted_name
|
|
@@ -20,6 +20,8 @@ module Frameit
|
|
|
20
20
|
|
|
21
21
|
files = JSON.parse(download_file("files.json"))
|
|
22
22
|
files.each_with_index do |current, index|
|
|
23
|
+
# files.json is downloaded: only accept plain file names, so nothing is written outside templates_path
|
|
24
|
+
UI.user_error!("Unexpected frame file name '#{current}'") if current.include?("/") || current.include?("\\") || current.start_with?(".")
|
|
23
25
|
content = download_file(current, txt: "#{index + 1} of #{files.count} files")
|
|
24
26
|
File.binwrite(File.join(templates_path, current), content)
|
|
25
27
|
end
|
|
@@ -38,8 +38,9 @@ module Frameit
|
|
|
38
38
|
if editor.should_skip?
|
|
39
39
|
UI.message("Skipping framing of screenshot #{screenshot.path}. No title provided in your Framefile.json or title.strings.")
|
|
40
40
|
else
|
|
41
|
-
Helper.
|
|
42
|
-
|
|
41
|
+
Helper.with_loading_indicator("Framing screenshot '#{full_path}'") do
|
|
42
|
+
editor.frame!
|
|
43
|
+
end
|
|
43
44
|
end
|
|
44
45
|
rescue => ex
|
|
45
46
|
UI.error(ex.to_s)
|
|
@@ -34,11 +34,11 @@ module Gym
|
|
|
34
34
|
|
|
35
35
|
options = []
|
|
36
36
|
options += project_path_array
|
|
37
|
-
options << "-sdk
|
|
38
|
-
options << "-toolchain
|
|
39
|
-
options << "-destination
|
|
37
|
+
options << "-sdk #{config[:sdk].shellescape}" if config[:sdk]
|
|
38
|
+
options << "-toolchain #{config[:toolchain].shellescape}" if config[:toolchain]
|
|
39
|
+
options << "-destination #{config[:destination].shellescape}" if config[:destination]
|
|
40
40
|
options << "-archivePath #{archive_path.shellescape}" unless config[:skip_archive]
|
|
41
|
-
options << "-resultBundlePath
|
|
41
|
+
options << "-resultBundlePath #{result_bundle_path.shellescape}" if config[:result_bundle]
|
|
42
42
|
options << "-showBuildTimingSummary" if config[:build_timing_summary]
|
|
43
43
|
if config[:use_system_scm] && !options.include?("-scmProvider system")
|
|
44
44
|
options << "-scmProvider system"
|
|
@@ -33,10 +33,10 @@ module Gym
|
|
|
33
33
|
config = Gym.config
|
|
34
34
|
|
|
35
35
|
options = []
|
|
36
|
-
options << "-exportOptionsPlist
|
|
36
|
+
options << "-exportOptionsPlist #{config_path.shellescape}"
|
|
37
37
|
options << "-archivePath #{BuildCommandGenerator.archive_path.shellescape}"
|
|
38
|
-
options << "-exportPath
|
|
39
|
-
options << "-toolchain
|
|
38
|
+
options << "-exportPath #{temporary_output_path.shellescape}"
|
|
39
|
+
options << "-toolchain #{config[:toolchain].shellescape}" if config[:toolchain]
|
|
40
40
|
options << config[:export_xcargs] if config[:export_xcargs]
|
|
41
41
|
options << config[:xcargs] if config[:xcargs]
|
|
42
42
|
|
data/gym/lib/gym/runner.rb
CHANGED
|
@@ -223,7 +223,7 @@ module Gym
|
|
|
223
223
|
UI.message("Compressing #{available_dsyms.count} dSYM(s)") unless Gym.config[:silent]
|
|
224
224
|
|
|
225
225
|
output_path = File.expand_path(File.join(Gym.config[:output_directory], Gym.config[:output_name] + ".app.dSYM.zip"))
|
|
226
|
-
command = "cd
|
|
226
|
+
command = "cd #{containing_directory.shellescape} && zip -r #{output_path.shellescape} *.dSYM"
|
|
227
227
|
Helper.backticks(command, print: !Gym.config[:silent])
|
|
228
228
|
puts("") # new line
|
|
229
229
|
|
data/internal/README.md
CHANGED
|
@@ -92,6 +92,7 @@ module Match
|
|
|
92
92
|
fail_on_name_taken: params[:fail_on_name_taken],
|
|
93
93
|
include_all_certificates: params[:include_all_certificates],
|
|
94
94
|
include_mac_in_profiles: params[:include_mac_in_profiles],
|
|
95
|
+
offline_profile: params[:offline_profile],
|
|
95
96
|
}
|
|
96
97
|
|
|
97
98
|
values[:platform] = params[:platform]
|
data/match/lib/match/options.rb
CHANGED
|
@@ -160,14 +160,14 @@ module Match
|
|
|
160
160
|
FastlaneCore::ConfigItem.new(key: :git_basic_authorization,
|
|
161
161
|
env_name: "MATCH_GIT_BASIC_AUTHORIZATION",
|
|
162
162
|
sensitive: true,
|
|
163
|
-
description: "Use a basic authorization header to access the git repo (e.g.: access via HTTPS, GitHub Actions, etc), usually a string in Base64",
|
|
163
|
+
description: "Use a basic authorization header to access the git repo (e.g.: access via HTTPS, GitHub Actions, etc), usually a string in Base64. Needs git 2.31 or later",
|
|
164
164
|
conflicting_options: [:git_bearer_authorization, :git_private_key],
|
|
165
165
|
optional: true,
|
|
166
166
|
default_value: nil),
|
|
167
167
|
FastlaneCore::ConfigItem.new(key: :git_bearer_authorization,
|
|
168
168
|
env_name: "MATCH_GIT_BEARER_AUTHORIZATION",
|
|
169
169
|
sensitive: true,
|
|
170
|
-
description: "Use a bearer authorization header to access the git repo (e.g.: access to an Azure DevOps repository), usually a string in Base64",
|
|
170
|
+
description: "Use a bearer authorization header to access the git repo (e.g.: access to an Azure DevOps repository), usually a string in Base64. Needs git 2.31 or later",
|
|
171
171
|
conflicting_options: [:git_basic_authorization, :git_private_key],
|
|
172
172
|
optional: true,
|
|
173
173
|
default_value: nil),
|
|
@@ -209,7 +209,8 @@ module Match
|
|
|
209
209
|
FastlaneCore::ConfigItem.new(key: :s3_access_key,
|
|
210
210
|
env_name: "MATCH_S3_ACCESS_KEY",
|
|
211
211
|
description: "S3 access key",
|
|
212
|
-
optional: true
|
|
212
|
+
optional: true,
|
|
213
|
+
sensitive: true),
|
|
213
214
|
FastlaneCore::ConfigItem.new(key: :s3_secret_access_key,
|
|
214
215
|
env_name: "MATCH_S3_SECRET_ACCESS_KEY",
|
|
215
216
|
description: "S3 secret access key",
|
|
@@ -247,11 +248,13 @@ module Match
|
|
|
247
248
|
FastlaneCore::ConfigItem.new(key: :job_token,
|
|
248
249
|
env_name: "CI_JOB_TOKEN",
|
|
249
250
|
description: "GitLab CI_JOB_TOKEN",
|
|
250
|
-
optional: true
|
|
251
|
+
optional: true,
|
|
252
|
+
sensitive: true),
|
|
251
253
|
FastlaneCore::ConfigItem.new(key: :private_token,
|
|
252
254
|
env_name: "PRIVATE_TOKEN",
|
|
253
255
|
description: "GitLab Access Token",
|
|
254
|
-
optional: true
|
|
256
|
+
optional: true,
|
|
257
|
+
sensitive: true),
|
|
255
258
|
|
|
256
259
|
# Keychain
|
|
257
260
|
FastlaneCore::ConfigItem.new(key: :keychain_name,
|
|
@@ -374,6 +377,11 @@ module Match
|
|
|
374
377
|
description: "Force encryption to use legacy cbc algorithm for backwards compatibility with older match versions",
|
|
375
378
|
type: Boolean,
|
|
376
379
|
default_value: false),
|
|
380
|
+
FastlaneCore::ConfigItem.new(key: :offline_profile,
|
|
381
|
+
env_name: "MATCH_OFFLINE_PROFILE",
|
|
382
|
+
description: "Enable profile with 'Offline Support' (7 day validity). Requires Apple ID login, not supported with App Store Connect API key authentication",
|
|
383
|
+
type: Boolean,
|
|
384
|
+
default_value: false),
|
|
377
385
|
|
|
378
386
|
# other
|
|
379
387
|
FastlaneCore::ConfigItem.new(key: :verbose,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require 'fastlane_core/command_executor'
|
|
2
|
+
require 'open3'
|
|
2
3
|
|
|
3
4
|
require_relative '../module'
|
|
4
5
|
require_relative './interface'
|
|
@@ -7,6 +8,9 @@ module Match
|
|
|
7
8
|
module Storage
|
|
8
9
|
# Store the code signing identities in a git repo
|
|
9
10
|
class GitStorage < Interface
|
|
11
|
+
DEFAULT_BRANCH = "main"
|
|
12
|
+
PRIVATE_KEY_ENV = "MATCH_SSH_ADD_PRIVATE_KEY"
|
|
13
|
+
|
|
10
14
|
# User provided values
|
|
11
15
|
attr_accessor :git_url
|
|
12
16
|
attr_accessor :shallow_clone
|
|
@@ -85,11 +89,6 @@ module Match
|
|
|
85
89
|
self.working_directory = Dir.mktmpdir
|
|
86
90
|
|
|
87
91
|
command = "git clone #{self.git_url.shellescape} #{self.working_directory.shellescape}"
|
|
88
|
-
# HTTP headers are supposed to be case-insensitive but
|
|
89
|
-
# Bitbucket requires `Authorization: Basic` and `Authorization Bearer` to work
|
|
90
|
-
# https://github.com/fastlane/fastlane/pull/15928
|
|
91
|
-
command << " -c http.extraheader='Authorization: Basic #{self.git_basic_authorization}'" unless self.git_basic_authorization.nil?
|
|
92
|
-
command << " -c http.extraheader='Authorization: Bearer #{self.git_bearer_authorization}'" unless self.git_bearer_authorization.nil?
|
|
93
92
|
|
|
94
93
|
if self.shallow_clone
|
|
95
94
|
command << " --depth 1"
|
|
@@ -165,8 +164,10 @@ module Match
|
|
|
165
164
|
def generate_matchfile_content
|
|
166
165
|
UI.important("Please create a new, private git repository to store the certificates and profiles there")
|
|
167
166
|
url = UI.input("URL of the Git Repo: ")
|
|
167
|
+
branch = UI.input("Branch of the Git Repo (leave blank to use '#{DEFAULT_BRANCH}'): ").to_s.strip
|
|
168
|
+
branch = DEFAULT_BRANCH if branch.empty?
|
|
168
169
|
|
|
169
|
-
return "git_url(\"#{url}\")"
|
|
170
|
+
return "git_url(\"#{url}\")\ngit_branch(\"#{branch}\")"
|
|
170
171
|
end
|
|
171
172
|
|
|
172
173
|
def list_files(file_name: "", file_ext: "")
|
|
@@ -178,7 +179,8 @@ module Match
|
|
|
178
179
|
ssh_add = File.expand_path(self.git_private_key).shellescape.to_s
|
|
179
180
|
else
|
|
180
181
|
UI.message("Private key file does not exist, will continue by using it as a raw key.")
|
|
181
|
-
|
|
182
|
+
# From the environment (git_env_values), so the key is not on bash's command line
|
|
183
|
+
ssh_add = "- <<< \"$#{PRIVATE_KEY_ENV}\""
|
|
182
184
|
end
|
|
183
185
|
return "ssh-agent bash -c 'ssh-add #{ssh_add}; #{command}'"
|
|
184
186
|
end
|
|
@@ -264,9 +266,43 @@ module Match
|
|
|
264
266
|
def git_env_values
|
|
265
267
|
env_values = { 'GIT_TERMINAL_PROMPT' => '0' }
|
|
266
268
|
env_values['GIT_SSH_COMMAND'] = non_interactive_git_ssh_command
|
|
269
|
+
env_values[PRIVATE_KEY_ENV] = self.git_private_key unless self.git_private_key.nil?
|
|
270
|
+
env_values.merge(authorization_env_values)
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Through git's environment (git 2.31+), so the header is neither on the command line nor stored in .git/config
|
|
274
|
+
def authorization_env_values
|
|
275
|
+
headers = []
|
|
276
|
+
# HTTP headers are supposed to be case-insensitive but
|
|
277
|
+
# Bitbucket requires `Authorization: Basic` and `Authorization Bearer` to work
|
|
278
|
+
# https://github.com/fastlane/fastlane/pull/15928
|
|
279
|
+
headers << "Authorization: Basic #{self.git_basic_authorization}" unless self.git_basic_authorization.nil?
|
|
280
|
+
headers << "Authorization: Bearer #{self.git_bearer_authorization}" unless self.git_bearer_authorization.nil?
|
|
281
|
+
return {} if headers.empty?
|
|
282
|
+
|
|
283
|
+
version = git_version
|
|
284
|
+
UI.user_error!("git_basic_authorization and git_bearer_authorization need git 2.31 or later, found git #{version}") if version && Gem::Version.new(version) < Gem::Version.new('2.31')
|
|
285
|
+
|
|
286
|
+
# Keep pairs the user already passes this way
|
|
287
|
+
first = ENV['GIT_CONFIG_COUNT'].to_i
|
|
288
|
+
env_values = { 'GIT_CONFIG_COUNT' => (first + headers.count).to_s }
|
|
289
|
+
headers.each_with_index do |header, i|
|
|
290
|
+
env_values["GIT_CONFIG_KEY_#{first + i}"] = 'http.extraheader'
|
|
291
|
+
env_values["GIT_CONFIG_VALUE_#{first + i}"] = header
|
|
292
|
+
end
|
|
267
293
|
env_values
|
|
268
294
|
end
|
|
269
295
|
|
|
296
|
+
# nil when git cannot be run: cloning reports that
|
|
297
|
+
def git_version
|
|
298
|
+
@git_version ||= begin
|
|
299
|
+
output, status = Open3.capture2('git', '--version')
|
|
300
|
+
output[/\d+\.\d+(\.\d+)?/] if status.success?
|
|
301
|
+
rescue Errno::ENOENT
|
|
302
|
+
nil
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
270
306
|
def non_interactive_git_ssh_command
|
|
271
307
|
ssh_command = ENV['GIT_SSH_COMMAND']
|
|
272
308
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require 'open-uri'
|
|
2
|
+
require 'ostruct'
|
|
2
3
|
|
|
3
4
|
require_relative '../../module'
|
|
4
5
|
|
|
@@ -39,7 +40,7 @@ module Match
|
|
|
39
40
|
|
|
40
41
|
create_subfolders(working_directory)
|
|
41
42
|
File.open(destination_file, "wb") do |saved_file|
|
|
42
|
-
|
|
43
|
+
url.open("rb", { @client.authentication_key => @client.authentication_value }) do |data|
|
|
43
44
|
saved_file.write(data.read)
|
|
44
45
|
end
|
|
45
46
|
|
data/pem/lib/pem/module.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
require 'ostruct'
|
|
1
2
|
require 'spaceship'
|
|
2
3
|
require_relative 'module'
|
|
4
|
+
require_relative 'service'
|
|
3
5
|
|
|
4
6
|
module Produce
|
|
5
7
|
class DeveloperCenter
|
|
@@ -86,6 +88,8 @@ module Produce
|
|
|
86
88
|
end
|
|
87
89
|
|
|
88
90
|
def create_new_app
|
|
91
|
+
return create_new_app_via_connect_api if using_connect_api?
|
|
92
|
+
|
|
89
93
|
ENV["CREATED_NEW_APP_ID"] = Time.now.to_i.to_s
|
|
90
94
|
if app_exists?
|
|
91
95
|
UI.success("[DevCenter] App '#{Produce.config[:app_identifier]}' already exists, nothing to do on the Dev Center")
|
|
@@ -118,6 +122,31 @@ module Produce
|
|
|
118
122
|
return true
|
|
119
123
|
end
|
|
120
124
|
|
|
125
|
+
# App Store Connect API key path — used instead of Spaceship.login.
|
|
126
|
+
def create_new_app_via_connect_api
|
|
127
|
+
config_enabled_services = Produce.config[:enable_services] || Produce.config[:enabled_features]
|
|
128
|
+
|
|
129
|
+
existing = Spaceship::ConnectAPI::BundleId.find(app_identifier)
|
|
130
|
+
if existing
|
|
131
|
+
UI.success("[DevCenter] App '#{Produce.config[:app_identifier]}' already exists, nothing to do on the Dev Center")
|
|
132
|
+
return true
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
app_name = Produce.config[:app_name]
|
|
136
|
+
UI.message("Creating new app '#{app_name}' on the Apple Dev Center")
|
|
137
|
+
|
|
138
|
+
connect_api_platform = Spaceship::ConnectAPI::BundleIdPlatform.map(platform)
|
|
139
|
+
bundle_id = Spaceship::ConnectAPI::BundleId.create(name: app_name, platform: connect_api_platform, identifier: app_identifier)
|
|
140
|
+
|
|
141
|
+
UI.crash!("Something went wrong when creating the new app - it's not listed in the apps list") unless bundle_id
|
|
142
|
+
|
|
143
|
+
UI.message("Created app #{bundle_id.id}")
|
|
144
|
+
enable_inline_services(config_enabled_services) if config_enabled_services && !config_enabled_services.empty?
|
|
145
|
+
UI.success("Finished creating new app '#{app_name}' on the Dev Center")
|
|
146
|
+
|
|
147
|
+
return true
|
|
148
|
+
end
|
|
149
|
+
|
|
121
150
|
def enable_services
|
|
122
151
|
app_service = Spaceship.app_service
|
|
123
152
|
enabled_clean_options = {}
|
|
@@ -161,6 +190,17 @@ module Produce
|
|
|
161
190
|
|
|
162
191
|
private
|
|
163
192
|
|
|
193
|
+
# Delegates to Service#update instead of duplicating its ~40-capability mapping.
|
|
194
|
+
# Skips any capability explicitly marked "off" — meaningless on a brand-new app.
|
|
195
|
+
def enable_inline_services(config_enabled_services)
|
|
196
|
+
to_enable = config_enabled_services.reject { |_, v| v == SERVICE_OFF }
|
|
197
|
+
return if to_enable.empty?
|
|
198
|
+
|
|
199
|
+
options = OpenStruct.new(to_enable)
|
|
200
|
+
options.define_singleton_method(:__hash__) { to_enable }
|
|
201
|
+
Produce::Service.new.update(true, options)
|
|
202
|
+
end
|
|
203
|
+
|
|
164
204
|
def platform
|
|
165
205
|
# This was added to support creation of multiple platforms
|
|
166
206
|
# Produce::ItunesConnect can take an array of platforms to create for App Store Connect
|
|
@@ -174,9 +214,16 @@ module Produce
|
|
|
174
214
|
Spaceship.app.find(app_identifier, mac: platform == "osx") != nil
|
|
175
215
|
end
|
|
176
216
|
|
|
217
|
+
def using_connect_api?
|
|
218
|
+
!connect_api_token.nil?
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def connect_api_token
|
|
222
|
+
Spaceship::ConnectAPI::Token.from(hash: Produce.config[:api_key], filepath: Produce.config[:api_key_path])
|
|
223
|
+
end
|
|
224
|
+
|
|
177
225
|
def login
|
|
178
|
-
|
|
179
|
-
Spaceship.select_team
|
|
226
|
+
Produce.authenticate_connect_api!
|
|
180
227
|
end
|
|
181
228
|
end
|
|
182
229
|
end
|
|
@@ -38,7 +38,7 @@ module Produce
|
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
# Produce.config[:itc_users]
|
|
41
|
-
|
|
41
|
+
Spaceship::ConnectAPI::App.create(
|
|
42
42
|
name: Produce.config[:app_name],
|
|
43
43
|
version_string: Produce.config[:app_version] || "1.0",
|
|
44
44
|
sku: Produce.config[:sku].to_s,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require 'fastlane_core/helper'
|
|
2
|
+
require 'spaceship'
|
|
2
3
|
|
|
3
4
|
module Produce
|
|
4
5
|
class << self
|
|
@@ -11,4 +12,24 @@ module Produce
|
|
|
11
12
|
|
|
12
13
|
ENV['FASTLANE_TEAM_ID'] ||= ENV["PRODUCE_TEAM_ID"]
|
|
13
14
|
ENV['DELIVER_USER'] ||= ENV["PRODUCE_USERNAME"]
|
|
15
|
+
|
|
16
|
+
# Shared by DeveloperCenter#login and Service#bundle_id. Prefers a configured API key, falls
|
|
17
|
+
# back to a token already set by another lane (e.g. app_store_connect_api_key), then falls
|
|
18
|
+
# back to username-based session login.
|
|
19
|
+
def self.authenticate_connect_api!
|
|
20
|
+
if (token = Spaceship::ConnectAPI::Token.from(hash: Produce.config[:api_key], filepath: Produce.config[:api_key_path]))
|
|
21
|
+
UI.message("Authenticating with App Store Connect API Key")
|
|
22
|
+
|
|
23
|
+
# `current_team_id:` avoids a live, session-based team_id lookup elsewhere in
|
|
24
|
+
# ConnectAPI's provisioning client, which has no session to use here.
|
|
25
|
+
Spaceship::ConnectAPI.client = Spaceship::ConnectAPI::Client.new(token: token, current_team_id: Produce.config[:team_id])
|
|
26
|
+
elsif !Spaceship::ConnectAPI.token.nil?
|
|
27
|
+
UI.message("Using existing authorization token for App Store Connect API")
|
|
28
|
+
else
|
|
29
|
+
UI.message("Starting login with user '#{Produce.config[:username]}'")
|
|
30
|
+
Spaceship.login(Produce.config[:username], nil)
|
|
31
|
+
Spaceship.select_team
|
|
32
|
+
UI.message("Successfully logged in")
|
|
33
|
+
end
|
|
34
|
+
end
|
|
14
35
|
end
|
|
@@ -17,6 +17,23 @@ module Produce
|
|
|
17
17
|
code_gen_sensitive: true,
|
|
18
18
|
default_value: user,
|
|
19
19
|
default_value_dynamic: true),
|
|
20
|
+
|
|
21
|
+
# App Store Connect API
|
|
22
|
+
FastlaneCore::ConfigItem.new(key: :api_key_path,
|
|
23
|
+
env_names: ["PRODUCE_API_KEY_PATH", "APP_STORE_CONNECT_API_KEY_PATH"],
|
|
24
|
+
description: "Path to your App Store Connect API Key JSON file (https://docs.fastlane.tools/app-store-connect-api/#using-fastlane-api-key-json-file)",
|
|
25
|
+
optional: true,
|
|
26
|
+
conflicting_options: [:api_key],
|
|
27
|
+
verify_block: proc do |value|
|
|
28
|
+
UI.user_error!("Couldn't find API key JSON file at path '#{value}'") unless File.exist?(value)
|
|
29
|
+
end),
|
|
30
|
+
FastlaneCore::ConfigItem.new(key: :api_key,
|
|
31
|
+
env_names: ["PRODUCE_API_KEY", "APP_STORE_CONNECT_API_KEY"],
|
|
32
|
+
description: "Your App Store Connect API Key information (https://docs.fastlane.tools/app-store-connect-api/#using-fastlane-api-key-hash-option)",
|
|
33
|
+
type: Hash,
|
|
34
|
+
optional: true,
|
|
35
|
+
sensitive: true,
|
|
36
|
+
conflicting_options: [:api_key_path]),
|
|
20
37
|
FastlaneCore::ConfigItem.new(key: :app_identifier,
|
|
21
38
|
env_name: "PRODUCE_APP_IDENTIFIER",
|
|
22
39
|
short_option: "-a",
|
|
@@ -88,7 +88,6 @@ module Produce
|
|
|
88
88
|
end
|
|
89
89
|
|
|
90
90
|
# rubocop:disable Metrics/PerceivedComplexity
|
|
91
|
-
# rubocop:disable Require/MissingRequireStatement
|
|
92
91
|
def update(on, options)
|
|
93
92
|
updated = valid_services_for(options).count
|
|
94
93
|
|
|
@@ -420,10 +419,9 @@ module Produce
|
|
|
420
419
|
|
|
421
420
|
def bundle_id
|
|
422
421
|
return @bundle_id if @bundle_id
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
UI.message("Successfully logged in")
|
|
422
|
+
|
|
423
|
+
Produce.authenticate_connect_api!
|
|
424
|
+
|
|
427
425
|
@bundle_id ||= Spaceship::ConnectAPI::BundleId.find(Produce.config[:app_identifier].to_s)
|
|
428
426
|
end
|
|
429
427
|
end
|
data/produce/lib/produce.rb
CHANGED
data/scan/lib/scan/options.rb
CHANGED
|
@@ -207,6 +207,13 @@ module Scan
|
|
|
207
207
|
conflict_block: proc do |value|
|
|
208
208
|
UI.user_error!("You can't use 'thread_sanitizer' and 'address_sanitizer' options in one run")
|
|
209
209
|
end),
|
|
210
|
+
FastlaneCore::ConfigItem.new(key: :collect_test_diagnostics,
|
|
211
|
+
env_name: "SCAN_COLLECT_TEST_DIAGNOSTICS",
|
|
212
|
+
description: "Whether verbose and long-running diagnostics (like sysdiagnoses or log archives) are collected when testing. Valid values are: on-failure or never. If not specified, the value in the test plan is used. Equivalent to -collect-test-diagnostics (Xcode 14 and up)",
|
|
213
|
+
optional: true,
|
|
214
|
+
verify_block: proc do |value|
|
|
215
|
+
UI.user_error!("Invalid collect_test_diagnostics #{value}") unless ['on-failure', 'never'].include?(value)
|
|
216
|
+
end),
|
|
210
217
|
|
|
211
218
|
# output
|
|
212
219
|
FastlaneCore::ConfigItem.new(key: :open_report,
|
data/scan/lib/scan/runner.rb
CHANGED
|
@@ -44,9 +44,9 @@ module Scan
|
|
|
44
44
|
|
|
45
45
|
options = []
|
|
46
46
|
options += project_path_array unless config[:xctestrun]
|
|
47
|
-
options << "-sdk
|
|
47
|
+
options << "-sdk #{config[:sdk].shellescape}" if config[:sdk]
|
|
48
48
|
options << destination if destination # generated in `detect_values`
|
|
49
|
-
options << "-toolchain
|
|
49
|
+
options << "-toolchain #{config[:toolchain].to_s.shellescape}" if config[:toolchain]
|
|
50
50
|
if config[:derived_data_path] && !options.include?("-derivedDataPath #{config[:derived_data_path].shellescape}")
|
|
51
51
|
options << "-derivedDataPath #{config[:derived_data_path].shellescape}"
|
|
52
52
|
end
|
|
@@ -68,6 +68,9 @@ module Scan
|
|
|
68
68
|
options << "-enableCodeCoverage #{config[:code_coverage] ? 'YES' : 'NO'}" unless config[:code_coverage].nil?
|
|
69
69
|
options << "-enableAddressSanitizer #{config[:address_sanitizer] ? 'YES' : 'NO'}" unless config[:address_sanitizer].nil?
|
|
70
70
|
options << "-enableThreadSanitizer #{config[:thread_sanitizer] ? 'YES' : 'NO'}" unless config[:thread_sanitizer].nil?
|
|
71
|
+
if config[:collect_test_diagnostics] && FastlaneCore::Helper.xcode_at_least?(14)
|
|
72
|
+
options << "-collect-test-diagnostics #{config[:collect_test_diagnostics]}"
|
|
73
|
+
end
|
|
71
74
|
if FastlaneCore::Helper.xcode_at_least?(11)
|
|
72
75
|
if config[:cloned_source_packages_path] && !options.include?("-clonedSourcePackagesDirPath #{config[:cloned_source_packages_path].shellescape}")
|
|
73
76
|
options << "-clonedSourcePackagesDirPath #{config[:cloned_source_packages_path].shellescape}"
|
|
@@ -77,14 +80,14 @@ module Scan
|
|
|
77
80
|
options << "-packageCachePath #{config[:package_cache_path].shellescape}"
|
|
78
81
|
end
|
|
79
82
|
|
|
80
|
-
options << "-testPlan
|
|
83
|
+
options << "-testPlan #{config[:testplan].shellescape}" if config[:testplan]
|
|
81
84
|
|
|
82
85
|
# detect_values will ensure that these values are present as Arrays if
|
|
83
86
|
# they are present at all
|
|
84
|
-
options += config[:only_test_configurations].map { |name| "-only-test-configuration
|
|
85
|
-
options += config[:skip_test_configurations].map { |name| "-skip-test-configuration
|
|
87
|
+
options += config[:only_test_configurations].map { |name| "-only-test-configuration #{name.shellescape}" } if config[:only_test_configurations]
|
|
88
|
+
options += config[:skip_test_configurations].map { |name| "-skip-test-configuration #{name.shellescape}" } if config[:skip_test_configurations]
|
|
86
89
|
end
|
|
87
|
-
options << "-xctestrun
|
|
90
|
+
options << "-xctestrun #{config[:xctestrun].shellescape}" if config[:xctestrun]
|
|
88
91
|
options << config[:xcargs] if config[:xcargs]
|
|
89
92
|
|
|
90
93
|
# Number of retries does not equal xcodebuild's -test-iterations number
|
|
@@ -127,7 +130,7 @@ module Scan
|
|
|
127
130
|
end
|
|
128
131
|
|
|
129
132
|
def pipe
|
|
130
|
-
pipe = ["| tee
|
|
133
|
+
pipe = ["| tee #{xcodebuild_log_path.shellescape}"]
|
|
131
134
|
|
|
132
135
|
# disable_xcpretty is now deprecated and directs to use output_style of raw
|
|
133
136
|
if Scan.config[:disable_xcpretty] || Scan.config[:output_style] == 'raw'
|
|
@@ -182,7 +185,7 @@ module Scan
|
|
|
182
185
|
formatter = []
|
|
183
186
|
if (custom_formatter = Scan.config[:xcpretty_formatter] || Scan.config[:formatter])
|
|
184
187
|
if custom_formatter.end_with?(".rb")
|
|
185
|
-
formatter << "-f
|
|
188
|
+
formatter << "-f #{custom_formatter.shellescape}"
|
|
186
189
|
else
|
|
187
190
|
formatter << "-f `#{custom_formatter}`"
|
|
188
191
|
end
|
|
@@ -234,7 +237,7 @@ module Scan
|
|
|
234
237
|
# Generate destination parameters
|
|
235
238
|
def destination
|
|
236
239
|
unless Scan.cache[:destination]
|
|
237
|
-
Scan.cache[:destination] = [*Scan.config[:destination]].map { |dst| "-destination
|
|
240
|
+
Scan.cache[:destination] = [*Scan.config[:destination]].map { |dst| "-destination #{dst.shellescape}" }.join(' ')
|
|
238
241
|
end
|
|
239
242
|
Scan.cache[:destination]
|
|
240
243
|
end
|
|
@@ -3,6 +3,7 @@ require_relative 'module'
|
|
|
3
3
|
module Scan
|
|
4
4
|
class TestResultParser
|
|
5
5
|
def parse_result(output)
|
|
6
|
+
UI.deprecated("Scan::TestResultParser is deprecated and will be removed in a future version of fastlane; scan no longer uses it. Read the test counts from the result bundle with Trainer::TestParser, or from the <testsuites> attributes of the JUnit report.")
|
|
6
7
|
unless output
|
|
7
8
|
return {
|
|
8
9
|
tests: 0,
|
|
@@ -49,7 +49,7 @@ module Scan
|
|
|
49
49
|
type = raw_type.strip
|
|
50
50
|
output_path = File.join(File.expand_path(@output_directory), determine_output_file_name(type))
|
|
51
51
|
reporter << "--report #{type}"
|
|
52
|
-
reporter << "--output
|
|
52
|
+
reporter << "--output #{output_path.shellescape}"
|
|
53
53
|
|
|
54
54
|
if type == "html" && @open_report
|
|
55
55
|
Scan.cache[:open_html_report_path] = output_path
|
|
@@ -62,7 +62,7 @@ module Scan
|
|
|
62
62
|
@temp_junit_report = Tempfile.new("junit_report")
|
|
63
63
|
Scan.cache[:temp_junit_report] = @temp_junit_report.path
|
|
64
64
|
reporter << "--report junit"
|
|
65
|
-
reporter << "--output
|
|
65
|
+
reporter << "--output #{Scan.cache[:temp_junit_report].shellescape}"
|
|
66
66
|
return reporter
|
|
67
67
|
end
|
|
68
68
|
|
|
@@ -65,18 +65,18 @@
|
|
|
65
65
|
</style>
|
|
66
66
|
</head>
|
|
67
67
|
<body><% image_counter = 0 %><% @data.each do |language, content| %>
|
|
68
|
-
<h1 id="<%= language %>" class="language"><%= language %></h1>
|
|
68
|
+
<h1 id="<%= ERB::Util.h(language) %>" class="language"><%= ERB::Util.h(language) %></h1>
|
|
69
69
|
<hr>
|
|
70
70
|
<table><% content.each do |device_name, screens| %>
|
|
71
71
|
<tr>
|
|
72
72
|
<th colspan="<%= screens.count %>">
|
|
73
|
-
<a id="<%= language %>-<%= device_name %>" class="deviceName" href="#<%= language %>-<%= device_name %>"><%= device_name %></a>
|
|
73
|
+
<a id="<%= ERB::Util.h(language) %>-<%= ERB::Util.h(device_name) %>" class="deviceName" href="#<%= ERB::Util.h(language) %>-<%= ERB::Util.h(device_name) %>"><%= ERB::Util.h(device_name) %></a>
|
|
74
74
|
</th>
|
|
75
75
|
</tr>
|
|
76
76
|
<tr><% screens.each do |screen_path| %><% next if screen_path.include?"_framed.png" %>
|
|
77
77
|
<td><% image_counter += 1 %>
|
|
78
|
-
<a href="<%= screen_path %>" target="_blank" class="screenshotLink">
|
|
79
|
-
<img class="screenshot" src="<%= screen_path %>" style="width: 100%;" alt="<%= language %> <%= device_name %>" data-counter="<%= image_counter %>">
|
|
78
|
+
<a href="<%= ERB::Util.h(screen_path) %>" target="_blank" class="screenshotLink">
|
|
79
|
+
<img class="screenshot" src="<%= ERB::Util.h(screen_path) %>" style="width: 100%;" alt="<%= ERB::Util.h(language) %> <%= ERB::Util.h(device_name) %>" data-counter="<%= image_counter %>">
|
|
80
80
|
</a>
|
|
81
81
|
</td><% end %>
|
|
82
82
|
</tr><% end %>
|
|
@@ -128,9 +128,14 @@
|
|
|
128
128
|
imageDisplay.alt = img.alt;
|
|
129
129
|
imageDisplay.dataset.counter = img.dataset.counter;
|
|
130
130
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
131
|
+
// textContent, not innerHTML: file names must not be interpreted as HTML
|
|
132
|
+
var imageTitle = document.createElement('h3');
|
|
133
|
+
imageTitle.textContent = img.alt;
|
|
134
|
+
imageInfo.textContent = '';
|
|
135
|
+
imageInfo.appendChild(imageTitle);
|
|
136
|
+
imageInfo.appendChild(document.createTextNode(decodeURI(img.src.split("/").pop())));
|
|
137
|
+
imageInfo.appendChild(document.createElement('br'));
|
|
138
|
+
imageInfo.appendChild(document.createTextNode(tmpImg.height+'\u00d7'+tmpImg.width+'px'));
|
|
134
139
|
|
|
135
140
|
overlay.style.display = "block";
|
|
136
141
|
});
|
|
@@ -30,7 +30,7 @@ module Screengrab
|
|
|
30
30
|
|
|
31
31
|
export_path = File.expand_path(export_path)
|
|
32
32
|
UI.success("Successfully created HTML file with an overview of all the screenshots: '#{export_path}'")
|
|
33
|
-
system("open
|
|
33
|
+
system("open", export_path) unless Screengrab.config[:skip_open_summary]
|
|
34
34
|
end
|
|
35
35
|
end
|
|
36
36
|
end
|