ETLane 0.1.42
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 +7 -0
- data/Lanes/CommonFastfile +322 -0
- data/Lanes/ExampleAppfile +31 -0
- data/Scripts/Package.resolved +16 -0
- data/Scripts/Package.swift +26 -0
- data/Scripts/README.md +3 -0
- metadata +49 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: '09b7a6a431d95a787e1c8071545b9d6808d4cd20e50e51b5912b83cc38b52e4d'
|
4
|
+
data.tar.gz: 7e68797c237de69e55ea50012fbc88b61f70a39b80beb44544987850976e6e72
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f26cdb1c032c33f01a51e6a3fd97d5a452e968e0d7eb56274780cac8f7506e1b881b6aad548a942061f694596d2364e4bbb1d159a9d1b5dcbf9ce6b01bdb721f
|
7
|
+
data.tar.gz: 646ff2101de9044f35330d239062677cc30308cf603baeeccc8a899893a80db736a8050fcb8694c8dea09c318b11a5e9a650a3a2bd61c6cfa448ce61bfb54277
|
@@ -0,0 +1,322 @@
|
|
1
|
+
fastlane_version "2.68.2"
|
2
|
+
|
3
|
+
default_platform :ios
|
4
|
+
|
5
|
+
platform :ios do
|
6
|
+
|
7
|
+
product_bundle_identifier = CredentialsManager::AppfileConfig.try_fetch_value(:app_identifier)
|
8
|
+
distribute_group_name = ENV["DISTRIBUTE_GROUP_NAME"]
|
9
|
+
project_name = ENV["PROJECT_NAME"]
|
10
|
+
|
11
|
+
build_folder = File.join(Dir.pwd, "..", "build")
|
12
|
+
last_commit_path = File.join(Dir.pwd, "last_commit")
|
13
|
+
scripts_dir = File.join(Dir.pwd, "../Pods/ETLane/Scripts")
|
14
|
+
|
15
|
+
desc "Push a new beta build to TestFlight"
|
16
|
+
lane :beta do |options|
|
17
|
+
setup_new_session(options)
|
18
|
+
provisioning_profile = "AppStore_#{product_bundle_identifier}"
|
19
|
+
username = options[:username]
|
20
|
+
|
21
|
+
get_certificates(output_path: build_folder, username: username)
|
22
|
+
profile_uuid = get_provisioning_profile(output_path: build_folder, username: username, app_identifier: product_bundle_identifier)
|
23
|
+
provisioningProfiles = Hash.new
|
24
|
+
provisioningProfiles[product_bundle_identifier] = profile_uuid
|
25
|
+
extension_bundle_ids = ENV["EXTENSION_BUNDLE_IDS"].split(",")
|
26
|
+
for extension_bundle_id in extension_bundle_ids do
|
27
|
+
extension_bundle_identifier = "#{product_bundle_identifier}.#{extension_bundle_id}"
|
28
|
+
extension_profile_uuid = get_provisioning_profile(output_path: build_folder, username: username, app_identifier: extension_bundle_identifier)
|
29
|
+
provisioningProfiles[extension_bundle_identifier] = extension_profile_uuid
|
30
|
+
end
|
31
|
+
|
32
|
+
team_id = CredentialsManager::AppfileConfig.try_fetch_value(:team_id)
|
33
|
+
build_app(
|
34
|
+
workspace: "#{project_name}.xcworkspace",
|
35
|
+
export_xcargs: "PROVISIONING_PROFILE_SPECIFIER='#{provisioning_profile}' DEVELOPMENT_TEAM='#{team_id}' CODE_SIGN_STYLE='Manual'",
|
36
|
+
scheme: project_name,
|
37
|
+
output_directory: build_folder,
|
38
|
+
include_bitcode: false,
|
39
|
+
skip_profile_detection: false,
|
40
|
+
export_options: {
|
41
|
+
method: "app-store",
|
42
|
+
signingStyle: "manual",
|
43
|
+
provisioningProfiles: provisioningProfiles
|
44
|
+
},
|
45
|
+
)
|
46
|
+
last_commit = File.read(last_commit_path)
|
47
|
+
changelog = changelog_from_git_commits(
|
48
|
+
quiet: true,
|
49
|
+
between: [last_commit, "HEAD"], # Optional, lets you specify a revision/tag range between which to collect commit info
|
50
|
+
pretty: "– %s",# Optional, lets you provide a custom format to apply to each commit when generating the changelog text
|
51
|
+
date_format: "short",# Optional, lets you provide an additional date format to dates within the pretty-formatted string
|
52
|
+
match_lightweight_tag: false, # Optional, lets you ignore lightweight (non-annotated) tags when searching for the last tag
|
53
|
+
merge_commit_filtering: "exclude_merges" # Optional, lets you filter out merge commits
|
54
|
+
)
|
55
|
+
puts changelog
|
56
|
+
build_number = get_build_number()
|
57
|
+
version = get_version_number(target: project_name) + " (" + build_number + ")"
|
58
|
+
branch_name = "feature/#{build_number}"
|
59
|
+
sh("git", "checkout", "-B", branch_name)
|
60
|
+
increment_and_push()
|
61
|
+
ENV["FL_CHANGELOG"] = nil
|
62
|
+
|
63
|
+
beta_app_review_info = {}
|
64
|
+
if ENV["ET_BETA_APP_REVIEW_INFO"]
|
65
|
+
beta_app_review_info = eval(ENV["ET_BETA_APP_REVIEW_INFO"])
|
66
|
+
end
|
67
|
+
|
68
|
+
exception = nil
|
69
|
+
begin
|
70
|
+
upload_to_testflight(
|
71
|
+
username: username,
|
72
|
+
beta_app_review_info: beta_app_review_info,
|
73
|
+
changelog: "",
|
74
|
+
groups: ["#{distribute_group_name}"]
|
75
|
+
)
|
76
|
+
tag = get_version_number(target: project_name) + "." + build_number
|
77
|
+
add_git_tag(
|
78
|
+
tag: tag
|
79
|
+
)
|
80
|
+
rescue => ex
|
81
|
+
exception = ex
|
82
|
+
UI.error(ex)
|
83
|
+
end
|
84
|
+
|
85
|
+
if exception == nil
|
86
|
+
if changelog
|
87
|
+
# Сохраним текущий комит
|
88
|
+
File.write(last_commit_path, last_git_commit[:commit_hash])
|
89
|
+
git_add(path: last_commit_path)
|
90
|
+
commit_bump(message: "Freeze changelog")
|
91
|
+
end
|
92
|
+
end
|
93
|
+
work_branch = ENV["ET_BRANCH"] || 'master'
|
94
|
+
sh("git", "checkout", work_branch)
|
95
|
+
sh("git", "pull", "origin", work_branch)
|
96
|
+
sh("git", "merge", branch_name)
|
97
|
+
sh("git", "branch", "-D", branch_name)
|
98
|
+
push_to_git_remote
|
99
|
+
if exception == nil
|
100
|
+
post_message(changelog: changelog, version: version)
|
101
|
+
else
|
102
|
+
raise exception
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
106
|
+
|
107
|
+
lane :metadata do |options|
|
108
|
+
setup_new_session(options)
|
109
|
+
username = options[:username]
|
110
|
+
skip_screenshots = options[:skip_screenshots] != nil ? options[:skip_screenshots] : false
|
111
|
+
upload_preview = options[:upload_preview] != nil ? options[:upload_preview] : false
|
112
|
+
skip_metadata = options[:skip_metadata] != nil ? options[:skip_metadata] : false
|
113
|
+
google_sheet_tsv = ENV["GOOGLE_SHEET_TSV"]
|
114
|
+
|
115
|
+
metadata_path = File.join(build_folder, "metadata")
|
116
|
+
FileUtils.rm_rf(metadata_path)
|
117
|
+
screenshots_path = File.join(build_folder, "screenshots")
|
118
|
+
preview_path = File.join(build_folder, "previews")
|
119
|
+
download_screenshots = !skip_screenshots
|
120
|
+
if download_screenshots
|
121
|
+
FileUtils.rm_rf(screenshots_path)
|
122
|
+
end
|
123
|
+
|
124
|
+
cocoapods(
|
125
|
+
try_repo_update_on_error: true,
|
126
|
+
use_bundle_exec: true,
|
127
|
+
)
|
128
|
+
|
129
|
+
Dir.chdir(scripts_dir) do
|
130
|
+
sh(
|
131
|
+
"swift", "run", "Resources",
|
132
|
+
google_sheet_tsv,
|
133
|
+
"--output", "#{build_folder}",
|
134
|
+
"--download-screenshots", "#{download_screenshots}",
|
135
|
+
"--figma-token", "#{ENV["FIGMA_TOKEN"]}",
|
136
|
+
"--figma-project-id", "#{ENV["FIGMA_PROJECT_ID"]}",
|
137
|
+
"--figma-page", "#{ENV["FIGMA_SCREENSHOTS_PAGE_ID"]}"
|
138
|
+
)
|
139
|
+
end
|
140
|
+
# include_in_app_purchases` flag to `false`
|
141
|
+
# run_precheck_before_submit to false
|
142
|
+
ENV["DELIVER_METADATA_PATH"] = metadata_path
|
143
|
+
ENV["DELIVER_SCREENSHOTS_PATH"] = "#{screenshots_path}"
|
144
|
+
ENV["DELIVER_SKIP_METADATA"] = "#{skip_metadata}"
|
145
|
+
ENV["DELIVER_SKIP_SCREENSHOTS"] = "#{skip_screenshots}"
|
146
|
+
ENV["DELIVER_SKIP_BINARY_UPLOAD"] = "true"
|
147
|
+
ENV["DELIVER_FORCE"] = "true"
|
148
|
+
ENV["DELIVER_SUBMIT_FOR_REVIEW"] = "false"
|
149
|
+
ENV["DELIVER_USERNAME"] = username
|
150
|
+
ENV["DELIVER_IGNORE_LANGUAGE_DIRECTORY_VALIDATION"] = "true"
|
151
|
+
ENV["DELIVER_OVERWRITE_SCREENSHOTS"] = "true"
|
152
|
+
ENV["PRECHECK_INCLUDE_IN_APP_PURCHASES"] = "false"
|
153
|
+
ENV["DELIVER_RUN_PRECHECK_BEFORE_SUBMIT"] = "false"
|
154
|
+
|
155
|
+
begin
|
156
|
+
deliver(
|
157
|
+
# edit_live: true,
|
158
|
+
)
|
159
|
+
rescue => ex
|
160
|
+
UI.error(ex)
|
161
|
+
if ex.to_s.start_with?("The app name you entered is already being used.")
|
162
|
+
UI.error("Try to detect issue...")
|
163
|
+
deliver(
|
164
|
+
# edit_live: true,
|
165
|
+
individual_metadata_items: ['name', 'keywords', 'description'],
|
166
|
+
)
|
167
|
+
else
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
if upload_preview
|
172
|
+
ENV["FASTLANE_ITUNES_TRANSPORTER_PATH"] = "/Applications/Transporter.app/Contents/itms/"
|
173
|
+
ENV["FASTLANE_ITUNES_TRANSPORTER_USE_SHELL_SCRIPT"] = "1"
|
174
|
+
previews(username: username)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
lane :post_message do |options|
|
179
|
+
version = options[:version] ? options[:version] : get_version_number(target: project_name) + " (" + get_build_number() + ")"
|
180
|
+
changelog = options[:changelog]
|
181
|
+
slack(
|
182
|
+
message: "App successfully uploaded to TestFlight.",
|
183
|
+
success: true,
|
184
|
+
default_payloads: [],
|
185
|
+
attachment_properties: {
|
186
|
+
fields: [
|
187
|
+
{
|
188
|
+
title: "Build number",
|
189
|
+
value: version,
|
190
|
+
},
|
191
|
+
{
|
192
|
+
title: "Changelog",
|
193
|
+
value: changelog,
|
194
|
+
},
|
195
|
+
]
|
196
|
+
}
|
197
|
+
)
|
198
|
+
end
|
199
|
+
|
200
|
+
# fastlane action new_version bump_type:patch|minor|major
|
201
|
+
lane :new_version do |options|
|
202
|
+
bump_type = options[:bump_type] ? options[:bump_type] : "patch"
|
203
|
+
increment_and_push(bump_type: bump_type, push: true)
|
204
|
+
end
|
205
|
+
|
206
|
+
lane :increment_and_push do |options|
|
207
|
+
increment_build_number
|
208
|
+
bump_type = options[:bump_type]
|
209
|
+
if bump_type
|
210
|
+
increment_version_number(
|
211
|
+
bump_type: bump_type
|
212
|
+
)
|
213
|
+
end
|
214
|
+
commit_bump(message: "Bump up version")
|
215
|
+
if options[:push]
|
216
|
+
push_to_git_remote
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
lane :rlz_minor do
|
221
|
+
rlz(bump_type: "minor")
|
222
|
+
end
|
223
|
+
|
224
|
+
lane :rlz do |options|
|
225
|
+
version = options[:version]
|
226
|
+
if version.to_s.empty?
|
227
|
+
bump_type = options[:bump_type]
|
228
|
+
if bump_type.to_s.empty?
|
229
|
+
version_bump_podspec(bump_type: "patch")
|
230
|
+
else
|
231
|
+
version_bump_podspec(bump_type: bump_type)
|
232
|
+
end
|
233
|
+
version = lane_context[SharedValues::PODSPEC_VERSION_NUMBER]
|
234
|
+
else
|
235
|
+
version_bump_podspec(version_number: version)
|
236
|
+
end
|
237
|
+
|
238
|
+
git_commit(
|
239
|
+
message: "Bump up version to #{version}",
|
240
|
+
path: ["./*"]
|
241
|
+
)
|
242
|
+
add_git_tag(
|
243
|
+
tag: version
|
244
|
+
)
|
245
|
+
push_to_git_remote
|
246
|
+
pod_push(allow_warnings: true, use_bundle_exec: true)
|
247
|
+
end
|
248
|
+
|
249
|
+
lane :commit_bump do |options|
|
250
|
+
commit_version_bump(
|
251
|
+
message: options[:message],
|
252
|
+
force: true,
|
253
|
+
xcodeproj: "#{project_name}.xcodeproj"
|
254
|
+
)
|
255
|
+
end
|
256
|
+
|
257
|
+
lane :add_group_to_tf_build do |options|
|
258
|
+
fastlane_require 'spaceship'
|
259
|
+
|
260
|
+
spaceship = Spaceship::Tunes.login(options[:username])
|
261
|
+
spaceship.team_id = fastlane_itc_team_id
|
262
|
+
app = Spaceship::Tunes::Application.find(product_bundle_identifier)
|
263
|
+
build = Spaceship::TestFlight::Build.latest(app_id: app.apple_id, platform: 'ios')
|
264
|
+
group = Spaceship::TestFlight::Group.find(app_id: app.apple_id, group_name: distribute_group_name)
|
265
|
+
build.add_group!(group)
|
266
|
+
# Find team id
|
267
|
+
# teamInfo = spaceship.teams.select { |team| team['contentProvider']['name'].strip.downcase == team_name.strip.downcase }.first
|
268
|
+
# team_id = teamInfo['contentProvider']['contentProviderId'] if teamInfo
|
269
|
+
end
|
270
|
+
|
271
|
+
# Получить полный список всех team_id & itc_team_id нужные для Appfile
|
272
|
+
lane :first_time do |options|
|
273
|
+
require "spaceship"
|
274
|
+
applePassword = options[:password]
|
275
|
+
apple_id = options[:username] ? username = options[:username] : CredentialsManager::AppfileConfig.try_fetch_value(:apple_id)
|
276
|
+
clientTunes = Spaceship::Tunes.login(apple_id, applePassword)
|
277
|
+
client = Spaceship::Portal.login(apple_id, applePassword)
|
278
|
+
|
279
|
+
strClientTunes = ""
|
280
|
+
clientTunes.teams.each do |team|
|
281
|
+
UI.message "#{team['contentProvider']['name']} (#{team['contentProvider']['contentProviderId']})"
|
282
|
+
strClientTunes << "#{team['contentProvider']['name']} (#{team['contentProvider']['contentProviderId']})||"
|
283
|
+
end
|
284
|
+
puts "ItunesTeamNames: #{strClientTunes[0..-3]}"
|
285
|
+
|
286
|
+
strDevPortal = ""
|
287
|
+
client.teams.each do |team|
|
288
|
+
UI.message "#{team['name']} (#{team['teamId']})"
|
289
|
+
strDevPortal << "#{team['name']} (#{team['teamId']})||"
|
290
|
+
end
|
291
|
+
puts "DevTeamNames: #{strDevPortal[0..-3]}"
|
292
|
+
end
|
293
|
+
|
294
|
+
lane :setup_new_session do |options|
|
295
|
+
if ENV["ET_USE_SESSION"]
|
296
|
+
session = retrieve_fastlane_session(options)
|
297
|
+
ENV["FASTLANE_SESSION"] = session
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
lane :retrieve_fastlane_session do |options|
|
302
|
+
# runs shell
|
303
|
+
username = options[:username]
|
304
|
+
spaceauth_output = `bundle exec fastlane spaceauth -u #{username}`
|
305
|
+
# regex the output for the value we need
|
306
|
+
fastlane_session_regex = %r{Pass the following via the FASTLANE_SESSION environment variable:\n(?<session>.+)\n\n\nExample:\n.+}
|
307
|
+
new_session = nil
|
308
|
+
if match = spaceauth_output.match(fastlane_session_regex)
|
309
|
+
# Strip out the fancy formatting
|
310
|
+
new_session = match[:session].gsub("\e[4m\e[36m", "").gsub("\e[0m\e[0m", "")
|
311
|
+
end
|
312
|
+
|
313
|
+
# Yell and quit if unable to parse out session from spaceauth output
|
314
|
+
if new_session.nil?
|
315
|
+
puts "Unable to obtain new session via fastlane spaceauth"
|
316
|
+
exit 1
|
317
|
+
else
|
318
|
+
new_session
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
app_identifier "" # The bundle identifier of your app
|
2
|
+
apple_id "e.tyutyuev@gmail.com" # Your Apple email address
|
3
|
+
# You can run getTeamNames from CommonFastfile to get it
|
4
|
+
team_name ""
|
5
|
+
team_id "" # Developer Portal Team ID
|
6
|
+
itc_team_id "" # AppsetoreConnect Portal Team ID
|
7
|
+
|
8
|
+
# https://my.slack.com/services/new/incoming-webhook/
|
9
|
+
ENV["SLACK_URL"] = ""
|
10
|
+
ENV["PROJECT_NAME"] = ""
|
11
|
+
ENV["DISTRIBUTE_GROUP_NAME"] = ""
|
12
|
+
ENV["FASTLANE_SKIP_DOCS"] = "1"
|
13
|
+
ENV["EXTENSION_BUNDLE_IDS"] = ""
|
14
|
+
ENV["GOOGLE_SHEET_TSV"] = ""
|
15
|
+
ENV["DELIVER_REJECT_IF_POSSIBLE"] = "true"
|
16
|
+
ENV["PILOT_BETA_APP_FEEDBACK"] = "teanet@mail.ru"
|
17
|
+
ENV["PILOT_DISTRIBUTE_EXTERNAL"] = "false"
|
18
|
+
ENV["PILOT_SKIP_WAITING_FOR_BUILD_PROCESSING"] = "false"
|
19
|
+
ENV["ET_BETA_APP_REVIEW_INFO"] = {
|
20
|
+
contact_email: "teanet@mail.ru",
|
21
|
+
contact_first_name: "Eugene",
|
22
|
+
contact_last_name: "Tutuev",
|
23
|
+
contact_phone: "79833087335",
|
24
|
+
demo_account_required: false,
|
25
|
+
demo_account_name: "",
|
26
|
+
demo_account_password: "",
|
27
|
+
notes: ""
|
28
|
+
}.to_s
|
29
|
+
ENV["FIGMA_TOKEN"] = ""
|
30
|
+
ENV["FIGMA_PROJECT_ID"] = ""
|
31
|
+
|
@@ -0,0 +1,16 @@
|
|
1
|
+
{
|
2
|
+
"object": {
|
3
|
+
"pins": [
|
4
|
+
{
|
5
|
+
"package": "swift-argument-parser",
|
6
|
+
"repositoryURL": "https://github.com/apple/swift-argument-parser",
|
7
|
+
"state": {
|
8
|
+
"branch": null,
|
9
|
+
"revision": "9564d61b08a5335ae0a36f789a7d71493eacadfc",
|
10
|
+
"version": "0.3.2"
|
11
|
+
}
|
12
|
+
}
|
13
|
+
]
|
14
|
+
},
|
15
|
+
"version": 1
|
16
|
+
}
|
@@ -0,0 +1,26 @@
|
|
1
|
+
// swift-tools-version:5.1
|
2
|
+
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
3
|
+
|
4
|
+
import PackageDescription
|
5
|
+
|
6
|
+
let package = Package(
|
7
|
+
name: "Scripts",
|
8
|
+
platforms: [.macOS(.v10_15)],
|
9
|
+
dependencies: [
|
10
|
+
.package(url: "https://github.com/apple/swift-argument-parser", .upToNextMinor(from: "0.3.0")),
|
11
|
+
],
|
12
|
+
targets: [
|
13
|
+
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
14
|
+
// Targets can depend on other targets in this package, and on products in packages which this package depends on.
|
15
|
+
.target(
|
16
|
+
name: "Common",
|
17
|
+
dependencies: []),
|
18
|
+
.target(
|
19
|
+
name: "Resources",
|
20
|
+
dependencies: [
|
21
|
+
"Common",
|
22
|
+
.product(name: "ArgumentParser", package: "swift-argument-parser"),
|
23
|
+
]
|
24
|
+
),
|
25
|
+
]
|
26
|
+
)
|
data/Scripts/README.md
ADDED
metadata
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ETLane
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.42
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- teanet
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-05-28 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: 'Xcode helper for upload builds and metadata
|
14
|
+
|
15
|
+
'
|
16
|
+
email:
|
17
|
+
executables: []
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files: []
|
20
|
+
files:
|
21
|
+
- Lanes/CommonFastfile
|
22
|
+
- Lanes/ExampleAppfile
|
23
|
+
- Scripts/Package.resolved
|
24
|
+
- Scripts/Package.swift
|
25
|
+
- Scripts/README.md
|
26
|
+
homepage: https://github.com/teanet/ETLane
|
27
|
+
licenses:
|
28
|
+
- MIT
|
29
|
+
metadata: {}
|
30
|
+
post_install_message:
|
31
|
+
rdoc_options: []
|
32
|
+
require_paths:
|
33
|
+
- lib
|
34
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
35
|
+
requirements:
|
36
|
+
- - ">="
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
version: '0'
|
39
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
requirements: []
|
45
|
+
rubygems_version: 3.0.3
|
46
|
+
signing_key:
|
47
|
+
specification_version: 4
|
48
|
+
summary: A short description of ETLane.
|
49
|
+
test_files: []
|