expect-pty 0.5.2 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +37 -0
- data/README.md +37 -19
- data/docs/API.md +88 -0
- data/docs/MIGRATION.md +15 -0
- data/lib/expect/cleanup.rb +33 -0
- data/lib/expect/configuration.rb +87 -66
- data/lib/expect/interaction.rb +226 -207
- data/lib/expect/logging.rb +157 -156
- data/lib/expect/matcher.rb +30 -29
- data/lib/expect/pattern.rb +8 -2
- data/lib/expect/pattern_list.rb +35 -5
- data/lib/expect/redactor.rb +11 -4
- data/lib/expect/relay.rb +122 -68
- data/lib/expect/relay_writer.rb +2 -1
- data/lib/expect/result.rb +32 -8
- data/lib/expect/session.rb +508 -0
- data/lib/expect/session_resources.rb +49 -16
- data/lib/expect/terminal.rb +78 -78
- data/lib/expect/version.rb +1 -1
- data/lib/expect.rb +336 -486
- data/sig/expect.rbs +355 -0
- metadata +12 -100
- data/.rubocop.yml +0 -66
- data/CONTRIBUTING.md +0 -29
- data/Gemfile +0 -28
- data/Rakefile +0 -38
- data/benchmark/matching.rb +0 -98
- data/benchmark/redactor.rb +0 -64
- data/benchmark/relay.rb +0 -48
- data/benchmark/scaling.rb +0 -70
- data/benchmark/send_slow.rb +0 -41
- data/benchmark/support.rb +0 -114
- data/docs/COMPATIBILITY.md +0 -98
- data/docs/INTERNAL_CONTRACTS.md +0 -133
- data/docs/PERFORMANCE.md +0 -165
- data/docs/RELEASING.md +0 -84
- data/docs/VERIFICATION.md +0 -358
- data/examples/dialogue.rb +0 -30
- data/examples/kibitz/README.md +0 -81
- data/examples/kibitz/kibitz.rb +0 -142
- data/examples/kibitz/test_kibitz.rb +0 -37
- data/examples/ssh_auto.rb +0 -94
- data/examples/ssh_interact.rb +0 -159
- data/examples/ssh_login.rb +0 -64
- data/expect-pty.gemspec +0 -33
- data/script/ci +0 -121
- data/script/release.rb +0 -311
- data/test/buffer_accounting_test.rb +0 -83
- data/test/cleanup_test.rb +0 -251
- data/test/compare_upstream.rb +0 -157
- data/test/configuration_test.rb +0 -133
- data/test/deadline_test.rb +0 -237
- data/test/diagnostics_test.rb +0 -353
- data/test/edge_case_test.rb +0 -262
- data/test/fixtures/ssh_scripts/01_identity.sh +0 -4
- data/test/fixtures/ssh_scripts/02_output.sh +0 -5
- data/test/fixtures/ssh_scripts/03_delayed.sh +0 -6
- data/test/fixtures/ssh_scripts/04_failure.sh +0 -2
- data/test/fixtures/ssh_scripts/05_recovery.sh +0 -3
- data/test/integration/README.md +0 -109
- data/test/integration/ssh_scripts.rb +0 -94
- data/test/interact_test.rb +0 -253
- data/test/interconnect_test.rb +0 -425
- data/test/io_test.rb +0 -321
- data/test/kibitz_test.rb +0 -45
- data/test/lifecycle_contract_test.rb +0 -71
- data/test/literal_scan_test.rb +0 -97
- data/test/matching_test.rb +0 -211
- data/test/multi_session_test.rb +0 -66
- data/test/pattern_offset_test.rb +0 -43
- data/test/process_test.rb +0 -291
- data/test/redactor_test.rb +0 -183
- data/test/relay_recovery_test.rb +0 -451
- data/test/release_test.rb +0 -328
- data/test/ruby_api_test.rb +0 -515
- data/test/scan_reuse_test.rb +0 -109
- data/test/script_logging_test.rb +0 -124
- data/test/support/interact_probe.rb +0 -125
- data/test/support/kibitz_probe.rb +0 -177
- data/test/support/script_probe.rb +0 -158
- data/test/terminal_cleanup_test.rb +0 -349
- data/test/test_helper.rb +0 -58
- data/test/timeout_test.rb +0 -289
data/test/release_test.rb
DELETED
|
@@ -1,328 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "minitest/autorun"
|
|
4
|
-
require "minitest/mock"
|
|
5
|
-
require_relative "../script/release"
|
|
6
|
-
|
|
7
|
-
class ReleaseTest < Minitest::Test
|
|
8
|
-
def test_extracts_only_the_selected_version
|
|
9
|
-
changelog = "# Changelog\n\n## Unreleased\n\n## 0.2.0 - 2026-09-12\n\n- New API.\n\n## 0.1.1\n\n- Old API.\n"
|
|
10
|
-
assert_equal "- New API.", Release.release_notes(changelog, "0.2.0")
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def test_rejects_unreleased_missing_empty_or_invalid_versions
|
|
14
|
-
["## Unreleased\n- Pending.\n## 0.2.0\n- Ready.", "## 0.1.1\n- Old.", "## 0.2.0\n"].each do |changelog|
|
|
15
|
-
assert_raises(RuntimeError) { Release.release_notes(changelog, "0.2.0") }
|
|
16
|
-
end
|
|
17
|
-
assert_raises(RuntimeError) { Release.release_notes("## 0.2.0.rc1\n- Preview.", "0.2.0.rc1") }
|
|
18
|
-
assert_raises(RuntimeError) { Release.release_notes("## Unreleased \t\n- Pending.\n## 0.2.0\n- Ready.", "0.2.0") }
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def test_dry_run_verifies_the_artifact_without_remote_calls
|
|
22
|
-
with_package do |release, artifact|
|
|
23
|
-
release.stub(:github, ->(*) { flunk "dry run contacted GitHub" }) do
|
|
24
|
-
release.stub(:get, ->(*) { flunk "dry run contacted RubyGems" }) do
|
|
25
|
-
capture_io { release.run }
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
directory = Dir.glob(File.join("tmp", "release", Expect::VERSION, "candidate-*")).fetch(0)
|
|
29
|
-
checksum = File.read(File.join(directory, "SHA256SUMS"))
|
|
30
|
-
assert_equal "#{Digest::SHA256.file(artifact).hexdigest} #{File.basename(artifact)}\n", checksum
|
|
31
|
-
assert_equal "- Release fixture.\n", File.read(File.join(directory, "release-notes.md"))
|
|
32
|
-
File.write(artifact, "another build")
|
|
33
|
-
assert_equal checksum.split.first, Digest::SHA256.file(File.join(directory, File.basename(artifact))).hexdigest
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def test_rubygems_only_publishes_the_verified_copy_without_github
|
|
38
|
-
with_package do |_release, artifact|
|
|
39
|
-
commit_package_source
|
|
40
|
-
release = Release.new(artifact: artifact, rubygems_only: true)
|
|
41
|
-
bytes = File.binread(artifact)
|
|
42
|
-
checksum = Digest::SHA256.hexdigest(bytes)
|
|
43
|
-
published = false
|
|
44
|
-
get = lambda do |path|
|
|
45
|
-
if path.start_with?("/downloads/")
|
|
46
|
-
Struct.new(:code, :body).new("200", bytes)
|
|
47
|
-
elsif published
|
|
48
|
-
Struct.new(:code, :body).new("200", JSON.generate("sha" => checksum, "yanked" => false))
|
|
49
|
-
else
|
|
50
|
-
Struct.new(:code, :body).new("404", "")
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
push = lambda do |*arguments|
|
|
54
|
-
candidate = arguments.fetch(2)
|
|
55
|
-
assert_equal ["gem", "push", candidate, "--host", "https://rubygems.org"], arguments
|
|
56
|
-
assert_match %r{/tmp/release/#{Regexp.escape(Expect::VERSION)}/candidate-[^/]+/}, candidate
|
|
57
|
-
assert_equal bytes, File.binread(candidate)
|
|
58
|
-
refute_equal File.expand_path(artifact), candidate
|
|
59
|
-
published = true
|
|
60
|
-
end
|
|
61
|
-
release.stub(:get, get) do
|
|
62
|
-
release.stub(:system, push) do
|
|
63
|
-
release.stub(:github, ->(*) { flunk "RubyGems-only release contacted GitHub" }) do
|
|
64
|
-
release.stub(:publish_github, -> { flunk "RubyGems-only release published to GitHub" }) do
|
|
65
|
-
[nil, "true"].each do |github_actions|
|
|
66
|
-
with_environment("GITHUB_ACTIONS" => github_actions,
|
|
67
|
-
"GEM_HOST_API_KEY" => (github_actions ? "test-only-api-key" : nil)) do
|
|
68
|
-
published = false
|
|
69
|
-
output, = capture_io { release.run }
|
|
70
|
-
assert_includes output, "SHA256 verified"
|
|
71
|
-
assert published
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
def test_ci_publish_requires_an_api_key_before_pushing
|
|
82
|
-
release = Release.new(rubygems_only: true)
|
|
83
|
-
release.stub(:registry_version, nil) do
|
|
84
|
-
release.stub(:system, ->(*) { flunk "pushed without a CI API key" }) do
|
|
85
|
-
[nil, ""].each do |api_key|
|
|
86
|
-
with_environment("GITHUB_ACTIONS" => "true", "GEM_HOST_API_KEY" => api_key) do
|
|
87
|
-
error = assert_raises(RuntimeError) { release.send(:publish_rubygems) }
|
|
88
|
-
assert_includes error.message, "RUBYGEMS_API_KEY"
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
def test_rubygems_only_rejects_uncommitted_source_before_publishing
|
|
96
|
-
with_package do |_release, artifact|
|
|
97
|
-
release = Release.new(artifact: artifact, rubygems_only: true)
|
|
98
|
-
release.stub(:capture, " M payload.rb") do
|
|
99
|
-
release.stub(:get, ->(*) { flunk "uncommitted source contacted RubyGems" }) do
|
|
100
|
-
assert_match "Commit all source changes", assert_raises(RuntimeError) { release.run }.message
|
|
101
|
-
end
|
|
102
|
-
end
|
|
103
|
-
end
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
def test_rubygems_only_rechecks_source_after_verification
|
|
107
|
-
with_package do |_release, artifact|
|
|
108
|
-
commit_package_source
|
|
109
|
-
release = Release.new(artifact: artifact, rubygems_only: true)
|
|
110
|
-
statuses = ["", " M payload.rb"]
|
|
111
|
-
original = release.method(:capture)
|
|
112
|
-
capture = ->(*arguments) { arguments.include?("rev-parse") ? original.call(*arguments) : statuses.shift }
|
|
113
|
-
release.stub(:capture, capture) do
|
|
114
|
-
release.stub(:get, ->(*) { flunk "changed source contacted RubyGems" }) do
|
|
115
|
-
capture_io do
|
|
116
|
-
assert_match "Source changed", assert_raises(RuntimeError) { release.run }.message
|
|
117
|
-
end
|
|
118
|
-
end
|
|
119
|
-
end
|
|
120
|
-
end
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
def test_rejects_a_package_from_different_source
|
|
124
|
-
with_package do |release, _artifact|
|
|
125
|
-
File.write("payload.rb", "puts :changed\n")
|
|
126
|
-
assert_match "Artifact differs from source", assert_raises(RuntimeError) { release.send(:verify_package) }.message
|
|
127
|
-
end
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
def test_rejects_ignored_source_collected_by_the_package_glob
|
|
131
|
-
with_package do |_release, artifact|
|
|
132
|
-
commit_package_source
|
|
133
|
-
FileUtils.mkdir_p("lib")
|
|
134
|
-
File.write("lib/local_only.rb", "puts :local_only\n")
|
|
135
|
-
File.write(".git/info/exclude", "lib/local_only.rb\n", mode: "a")
|
|
136
|
-
rebuild_package
|
|
137
|
-
assert_empty git("status", "--porcelain")
|
|
138
|
-
|
|
139
|
-
error = publishing_source_error(artifact)
|
|
140
|
-
assert_match "not in release commit: lib/local_only.rb", error.message
|
|
141
|
-
# 提交前的 dry-run 仍可检验候选包,但不能以此证明发布来源。
|
|
142
|
-
capture_io { Release.new(artifact: artifact, dry_run: true).run }
|
|
143
|
-
end
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
def test_rejects_source_changes_hidden_from_git_status
|
|
147
|
-
with_package do |_release, artifact|
|
|
148
|
-
commit_package_source
|
|
149
|
-
git("update-index", "--assume-unchanged", "payload.rb")
|
|
150
|
-
File.binwrite("payload.rb", "puts :changed\n\n")
|
|
151
|
-
rebuild_package
|
|
152
|
-
assert_empty git("status", "--porcelain")
|
|
153
|
-
assert_match "differs from release commit: payload.rb", publishing_source_error(artifact).message
|
|
154
|
-
end
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
def test_rejects_executable_mode_changes_hidden_from_git_status
|
|
158
|
-
with_package do |_release, artifact|
|
|
159
|
-
commit_package_source
|
|
160
|
-
git("config", "core.filemode", "false")
|
|
161
|
-
File.chmod(0o755, "payload.rb")
|
|
162
|
-
rebuild_package
|
|
163
|
-
assert_empty git("status", "--porcelain")
|
|
164
|
-
assert_match "permissions differ from release commit: payload.rb", publishing_source_error(artifact).message
|
|
165
|
-
end
|
|
166
|
-
end
|
|
167
|
-
|
|
168
|
-
def test_rejects_altered_homepage_metadata
|
|
169
|
-
with_package do |release, _artifact|
|
|
170
|
-
spec = Gem::Specification.load(File.expand_path("expect-pty.gemspec")).dup
|
|
171
|
-
spec.homepage = "https://example.invalid/altered"
|
|
172
|
-
capture_io { Gem::Package.build(spec) }
|
|
173
|
-
assert_match "metadata", assert_raises(RuntimeError) { release.send(:verify_package) }.message
|
|
174
|
-
end
|
|
175
|
-
end
|
|
176
|
-
|
|
177
|
-
def test_rejects_different_installation_metadata_even_with_identical_source_files
|
|
178
|
-
with_package do |release, _artifact|
|
|
179
|
-
expected = Gem::Specification.load(File.expand_path("expect-pty.gemspec")).dup
|
|
180
|
-
expected.add_runtime_dependency "unexpected-dependency", "= 1.0.0"
|
|
181
|
-
Gem::Specification.stub(:load, expected) do
|
|
182
|
-
assert_match "metadata", assert_raises(RuntimeError) { release.send(:verify_package) }.message
|
|
183
|
-
end
|
|
184
|
-
expected = Gem::Specification.load(File.expand_path("expect-pty.gemspec")).dup
|
|
185
|
-
expected.extensions = ["payload.rb"]
|
|
186
|
-
Gem::Specification.stub(:load, expected) do
|
|
187
|
-
assert_match "metadata", assert_raises(RuntimeError) { release.send(:verify_package) }.message
|
|
188
|
-
end
|
|
189
|
-
end
|
|
190
|
-
end
|
|
191
|
-
|
|
192
|
-
def test_registry_errors_are_not_treated_as_an_unpublished_version
|
|
193
|
-
release = Release.new
|
|
194
|
-
response = Struct.new(:code, :body).new("503", "unavailable")
|
|
195
|
-
release.stub(:get, response) { assert_raises(RuntimeError) { release.send(:registry_version) } }
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
def test_rejects_overwriting_a_published_version_or_reusing_a_yanked_version
|
|
199
|
-
release = Release.new
|
|
200
|
-
release.instance_variable_set(:@sha256, "expected")
|
|
201
|
-
[{ "sha" => "different", "yanked" => false }, { "sha" => "expected", "yanked" => true }].each do |version|
|
|
202
|
-
assert_raises(RuntimeError) { release.send(:verify_registry_checksum, version) }
|
|
203
|
-
end
|
|
204
|
-
end
|
|
205
|
-
|
|
206
|
-
def test_existing_registry_version_is_downloaded_and_verified_without_pushing_again
|
|
207
|
-
release = Release.new
|
|
208
|
-
bytes = "published gem"
|
|
209
|
-
checksum = Digest::SHA256.hexdigest(bytes)
|
|
210
|
-
release.instance_variable_set(:@sha256, checksum)
|
|
211
|
-
response = Struct.new(:code, :body).new("200", bytes)
|
|
212
|
-
release.stub(:registry_version, { "sha" => checksum, "yanked" => false }) do
|
|
213
|
-
release.stub(:system, ->(*) { flunk "repushed an existing gem" }) do
|
|
214
|
-
release.stub(:get, response) { capture_io { release.send(:publish_rubygems) } }
|
|
215
|
-
response.body = "different bytes"
|
|
216
|
-
release.stub(:get, response) { assert_raises(RuntimeError) { release.send(:publish_rubygems) } }
|
|
217
|
-
end
|
|
218
|
-
end
|
|
219
|
-
end
|
|
220
|
-
|
|
221
|
-
def test_remote_tag_must_point_to_the_verified_commit
|
|
222
|
-
release = Release.new
|
|
223
|
-
release.instance_variable_set(:@commit, "verified")
|
|
224
|
-
capture = ->(*arguments) { arguments.include?("rev-parse") ? "verified" : "" }
|
|
225
|
-
github = lambda do |path, **|
|
|
226
|
-
{ "status" => (path.end_with?("...main") ? "ahead" : "behind") }
|
|
227
|
-
end
|
|
228
|
-
release.stub(:capture, capture) do
|
|
229
|
-
release.stub(:github, github) do
|
|
230
|
-
assert_match "another commit", assert_raises(RuntimeError) { release.send(:verify_remote_source) }.message
|
|
231
|
-
end
|
|
232
|
-
end
|
|
233
|
-
end
|
|
234
|
-
|
|
235
|
-
def test_package_validation_uses_archive_modes_regardless_of_umask
|
|
236
|
-
with_package do |release, artifact|
|
|
237
|
-
File.chmod(0o755, "payload.rb")
|
|
238
|
-
capture_io { Gem::Package.build(Gem::Specification.load(File.expand_path("expect-pty.gemspec"))) }
|
|
239
|
-
previous_umask = File.umask(0o077)
|
|
240
|
-
begin
|
|
241
|
-
release.send(:verify_package)
|
|
242
|
-
assert File.file?(artifact)
|
|
243
|
-
File.chmod(0o644, "payload.rb")
|
|
244
|
-
assert_match "permissions differ", assert_raises(RuntimeError) { release.send(:verify_package) }.message
|
|
245
|
-
ensure
|
|
246
|
-
File.umask(previous_umask)
|
|
247
|
-
end
|
|
248
|
-
end
|
|
249
|
-
end
|
|
250
|
-
|
|
251
|
-
def test_incomplete_github_asset_has_an_explicit_recovery_message
|
|
252
|
-
release = Release.new
|
|
253
|
-
release.instance_variable_set(:@checksum_file, "SHA256SUMS")
|
|
254
|
-
remote = { "assets" => [{ "name" => "SHA256SUMS", "state" => "starter" }] }
|
|
255
|
-
release.stub(:github_release, remote) do
|
|
256
|
-
error = assert_raises(RuntimeError) { release.send(:publish_github) }
|
|
257
|
-
assert_match "Incomplete GitHub asset: SHA256SUMS", error.message
|
|
258
|
-
assert_match "stop any active upload", error.message
|
|
259
|
-
end
|
|
260
|
-
end
|
|
261
|
-
|
|
262
|
-
private
|
|
263
|
-
|
|
264
|
-
def git(*)
|
|
265
|
-
output, error, status = Open3.capture3("git", *)
|
|
266
|
-
assert status.success?, error
|
|
267
|
-
output
|
|
268
|
-
end
|
|
269
|
-
|
|
270
|
-
def commit_package_source
|
|
271
|
-
git("init", "--quiet")
|
|
272
|
-
git("config", "core.hooksPath", File::NULL)
|
|
273
|
-
File.write(".git/info/exclude", "*.gem\ntmp/\n")
|
|
274
|
-
git("add", "--", "payload.rb", "CHANGELOG.md", "expect-pty.gemspec")
|
|
275
|
-
git("-c", "user.name=Release Test", "-c", "user.email=release-test@example.invalid",
|
|
276
|
-
"-c", "commit.gpgsign=false", "commit", "--quiet", "-m", "Release fixture")
|
|
277
|
-
end
|
|
278
|
-
|
|
279
|
-
def rebuild_package
|
|
280
|
-
# Gem::Specification.load 会缓存已加载的 gemspec;重新执行才能看到新的 glob 文件。
|
|
281
|
-
Gem::Specification.reset
|
|
282
|
-
spec = Gem::Specification.load(File.expand_path("expect-pty.gemspec"))
|
|
283
|
-
capture_io { Gem::Package.build(spec) }
|
|
284
|
-
end
|
|
285
|
-
|
|
286
|
-
def publishing_source_error(artifact)
|
|
287
|
-
release = Release.new(artifact: artifact, rubygems_only: true)
|
|
288
|
-
release.stub(:get, ->(*) { flunk "uncommitted package content reached RubyGems" }) do
|
|
289
|
-
error = nil
|
|
290
|
-
capture_io { error = assert_raises(RuntimeError) { release.run } }
|
|
291
|
-
error
|
|
292
|
-
end
|
|
293
|
-
end
|
|
294
|
-
|
|
295
|
-
# 发布测试显式控制凭据环境,避免本机登录状态或 CI 标记影响用例结果。
|
|
296
|
-
def with_environment(values)
|
|
297
|
-
previous = values.to_h { |name, _value| [name, ENV.fetch(name, nil)] }
|
|
298
|
-
begin
|
|
299
|
-
values.each { |name, value| ENV[name] = value }
|
|
300
|
-
yield
|
|
301
|
-
ensure
|
|
302
|
-
previous.each { |name, value| ENV[name] = value }
|
|
303
|
-
end
|
|
304
|
-
end
|
|
305
|
-
|
|
306
|
-
def with_package
|
|
307
|
-
Dir.mktmpdir("expect-release-test-") do |directory|
|
|
308
|
-
Dir.chdir(directory) do
|
|
309
|
-
File.write("payload.rb", "puts :original\n")
|
|
310
|
-
File.write("CHANGELOG.md", "## #{Expect::VERSION}\n\n- Release fixture.\n")
|
|
311
|
-
File.write("expect-pty.gemspec", <<~RUBY)
|
|
312
|
-
Gem::Specification.new do |spec|
|
|
313
|
-
spec.name = "expect-pty"
|
|
314
|
-
spec.version = "#{Expect::VERSION}"
|
|
315
|
-
spec.summary = "Release test fixture"
|
|
316
|
-
spec.authors = ["Test"]
|
|
317
|
-
spec.license = "MIT"
|
|
318
|
-
spec.homepage = "https://github.com/gatework/expect-ruby"
|
|
319
|
-
spec.files = ["payload.rb", "CHANGELOG.md", "expect-pty.gemspec"] + Dir["lib/**/*.rb"]
|
|
320
|
-
end
|
|
321
|
-
RUBY
|
|
322
|
-
artifact = nil
|
|
323
|
-
capture_io { artifact = Gem::Package.build(Gem::Specification.load(File.expand_path("expect-pty.gemspec"))) }
|
|
324
|
-
yield Release.new(artifact: artifact, dry_run: true), artifact
|
|
325
|
-
end
|
|
326
|
-
end
|
|
327
|
-
end
|
|
328
|
-
end
|