quality_gate 0.1.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 +7 -0
- data/CHANGELOG.md +14 -0
- data/LICENSE.txt +21 -0
- data/README.md +571 -0
- data/config/37signals.yml +26 -0
- data/config/cops.yml +37 -0
- data/config/reek.yml +6 -0
- data/config/rubocop.yml +60 -0
- data/config/ruby.yml +8 -0
- data/docs/codex.md +31 -0
- data/docs/dogfood-log.md +47 -0
- data/docs/incidents.md +18 -0
- data/docs/releasing.md +46 -0
- data/exe/quality_gate +6 -0
- data/lib/generators/quality_gate/install/install_generator.rb +66 -0
- data/lib/generators/quality_gate/install/templates/agents_section.md.tt +15 -0
- data/lib/generators/quality_gate/install/templates/bullet.rb.tt +10 -0
- data/lib/generators/quality_gate/install/templates/claude_settings.json.tt +29 -0
- data/lib/generators/quality_gate/install/templates/hook_log_filesystem.rb.tt +76 -0
- data/lib/generators/quality_gate/install/templates/quality_gate.yml.tt +42 -0
- data/lib/generators/quality_gate/install/templates/quality_gate_fast.rb.tt +248 -0
- data/lib/generators/quality_gate/install/templates/quality_gate_verify_stop.rb.tt +466 -0
- data/lib/generators/quality_gate/install/templates/rubocop.yml.tt +1 -0
- data/lib/generators/quality_gate/install/templates/ruby_agents_section.md.tt +15 -0
- data/lib/generators/quality_gate/install/templates/ruby_quality_gate.yml.tt +40 -0
- data/lib/generators/quality_gate/install/templates/ruby_rubocop.yml.tt +1 -0
- data/lib/generators/quality_gate/install/templates/ruby_simplecov.rb.tt +11 -0
- data/lib/generators/quality_gate/install/templates/simplecov.rb.tt +10 -0
- data/lib/generators/quality_gate/install/templates/strong_migrations.rb.tt +10 -0
- data/lib/quality_gate/adapter.rb +328 -0
- data/lib/quality_gate/adapters/brakeman.rb +125 -0
- data/lib/quality_gate/adapters/bundler_audit.rb +235 -0
- data/lib/quality_gate/adapters/reek.rb +108 -0
- data/lib/quality_gate/adapters/rubocop.rb +142 -0
- data/lib/quality_gate/adapters/simplecov.rb +103 -0
- data/lib/quality_gate/adapters/test_suite.rb +81 -0
- data/lib/quality_gate/adapters/undercover.rb +357 -0
- data/lib/quality_gate/cli.rb +451 -0
- data/lib/quality_gate/config.rb +290 -0
- data/lib/quality_gate/exit_code.rb +14 -0
- data/lib/quality_gate/finding.rb +50 -0
- data/lib/quality_gate/hook_log.rb +131 -0
- data/lib/quality_gate/init_command.rb +90 -0
- data/lib/quality_gate/installation.rb +1577 -0
- data/lib/quality_gate/installer.rb +104 -0
- data/lib/quality_gate/railtie.rb +16 -0
- data/lib/quality_gate/reporters/field_sanitizer.rb +42 -0
- data/lib/quality_gate/reporters/json.rb +59 -0
- data/lib/quality_gate/reporters/text.rb +45 -0
- data/lib/quality_gate/rubocop.rb +34 -0
- data/lib/quality_gate/ruby_profile.rb +170 -0
- data/lib/quality_gate/runner.rb +150 -0
- data/lib/quality_gate/version.rb +5 -0
- data/lib/quality_gate.rb +27 -0
- data/lib/rubocop/cop/quality_gate/association_default_block_value.rb +46 -0
- data/lib/rubocop/cop/quality_gate/broadcast_in_controller.rb +64 -0
- data/lib/rubocop/cop/quality_gate/controller_instance_variables.rb +47 -0
- data/lib/rubocop/cop/quality_gate/prefer_after_save_commit.rb +84 -0
- data/lib/rubocop/cop/quality_gate/private_only_concern.rb +77 -0
- data/llm.txt +13 -0
- data/sig/quality_gate.rbs +226 -0
- metadata +260 -0
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
|
|
5
|
+
module QualityGate
|
|
6
|
+
# Parses command-line input and coordinates configured quality gates.
|
|
7
|
+
# rubocop:disable Metrics/ClassLength
|
|
8
|
+
class CLI
|
|
9
|
+
SUBCOMMANDS = %w[fast verify audit version init].map!(&:freeze).freeze
|
|
10
|
+
GATE_SUBCOMMANDS = SUBCOMMANDS.first(3).freeze
|
|
11
|
+
GATE_TOOLS = {
|
|
12
|
+
"fast" => %w[rubocop].freeze,
|
|
13
|
+
"verify" => %w[reek test_suite undercover].freeze,
|
|
14
|
+
"audit" => %w[brakeman bundler_audit].freeze
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
class << self
|
|
18
|
+
def run(argv, stdout: $stdout, stderr: $stderr, dir: Dir.pwd)
|
|
19
|
+
output_format = nil
|
|
20
|
+
arguments = argv.dup
|
|
21
|
+
output_format = requested_format(arguments)
|
|
22
|
+
subcommand = arguments.shift
|
|
23
|
+
|
|
24
|
+
early_result = early_command(subcommand, arguments, stdout: stdout, stderr: stderr, dir: dir)
|
|
25
|
+
return early_result unless early_result.nil?
|
|
26
|
+
|
|
27
|
+
output_format ||= configured_format(dir)
|
|
28
|
+
execute_command(
|
|
29
|
+
subcommand,
|
|
30
|
+
arguments,
|
|
31
|
+
stdout: stdout,
|
|
32
|
+
stderr: stderr,
|
|
33
|
+
dir: dir,
|
|
34
|
+
format: output_format
|
|
35
|
+
)
|
|
36
|
+
rescue OptionParser::ParseError => e
|
|
37
|
+
report_option_error(e, stdout: stdout, stderr: stderr, format: output_format)
|
|
38
|
+
rescue StandardError => e
|
|
39
|
+
report_error(e, stdout: stdout, stderr: stderr, format: output_format)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def print_version(stdout, stderr)
|
|
45
|
+
return ExitCode::CLEAN if write_line(stdout, VERSION)
|
|
46
|
+
|
|
47
|
+
write_diagnostic(stderr, "Error", "could not write version output")
|
|
48
|
+
ExitCode::TOOL_FAILURE
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def invalid_command(subcommand, stdout:, stderr:, format:)
|
|
52
|
+
return report_json_error("unknown subcommand #{subcommand}", stdout: stdout, stderr: stderr) if format == "json"
|
|
53
|
+
|
|
54
|
+
usage(stderr)
|
|
55
|
+
ExitCode::TOOL_FAILURE
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def early_command(subcommand, arguments, stdout:, stderr:, dir:)
|
|
59
|
+
if help_requested?(subcommand, arguments)
|
|
60
|
+
return print_help(stdout, stderr, gate: help_gate(subcommand, arguments))
|
|
61
|
+
end
|
|
62
|
+
return print_version(stdout, stderr) if subcommand == "version" && arguments.empty?
|
|
63
|
+
return run_init(arguments, stdout: stdout, stderr: stderr, dir: dir) if subcommand == "init"
|
|
64
|
+
|
|
65
|
+
nil
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def run_init(arguments, stdout:, stderr:, dir:)
|
|
69
|
+
require_relative "init_command" unless defined?(QualityGate::InitCommand)
|
|
70
|
+
|
|
71
|
+
InitCommand.run(arguments, stdout: stdout, stderr: stderr, dir: dir)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def execute_command(subcommand, arguments, stdout:, stderr:, dir:, format:)
|
|
75
|
+
unless GATE_SUBCOMMANDS.include?(subcommand)
|
|
76
|
+
return invalid_command(subcommand, stdout: stdout, stderr: stderr, format: format)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
emit_hook_warning(dir, stderr)
|
|
80
|
+
run_gate(subcommand, arguments, stdout: stdout, stderr: stderr, dir: dir)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def run_gate(subcommand, arguments, stdout:, stderr:, dir:)
|
|
84
|
+
overrides = parse(arguments)
|
|
85
|
+
config = Config.load(dir: dir)
|
|
86
|
+
return ExitCode::TOOL_FAILURE unless warn_unknown_keys(config, stderr)
|
|
87
|
+
|
|
88
|
+
settings = config.to_h.merge(overrides)
|
|
89
|
+
validate_paths!(settings.fetch(:files), dir)
|
|
90
|
+
validate_simplecov_configuration!(subcommand, settings, config.path || File.join(dir, ".quality_gate.yml"))
|
|
91
|
+
dispatch(subcommand, settings, stdout: stdout, stderr: stderr)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def emit_hook_warning(dir, stderr)
|
|
95
|
+
path = File.join(dir, HookLog::DEFAULT_PATH)
|
|
96
|
+
warning = HookLog.new(path: path).warning_line
|
|
97
|
+
write_line(stderr, warning) if warning
|
|
98
|
+
rescue StandardError
|
|
99
|
+
nil
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def report_option_error(error, stdout:, stderr:, format:)
|
|
103
|
+
return report_json_error(error, stdout: stdout, stderr: stderr) if format == "json"
|
|
104
|
+
|
|
105
|
+
write_error(stderr, error)
|
|
106
|
+
usage(stderr)
|
|
107
|
+
ExitCode::TOOL_FAILURE
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def report_error(error, stdout:, stderr:, format:)
|
|
111
|
+
return report_json_error(error, stdout: stdout, stderr: stderr) if format == "json"
|
|
112
|
+
|
|
113
|
+
write_error(stderr, error)
|
|
114
|
+
ExitCode::TOOL_FAILURE
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def warn_unknown_keys(config, stderr)
|
|
118
|
+
config.unknown_keys.all? do |key|
|
|
119
|
+
write_diagnostic(stderr, "Warning", "unknown config key #{key}")
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def usage(stderr)
|
|
124
|
+
write_line(stderr, "Usage: quality_gate <#{SUBCOMMANDS.join("|")}> [options]")
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def write_line(io, message)
|
|
128
|
+
io.puts message
|
|
129
|
+
true
|
|
130
|
+
rescue StandardError
|
|
131
|
+
false
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def write_diagnostic(io, prefix, detail)
|
|
135
|
+
write_line(io, "#{prefix}: #{normalize_diagnostic(detail)}")
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def write_error(io, error)
|
|
139
|
+
write_diagnostic(io, "Error", error.message)
|
|
140
|
+
rescue StandardError
|
|
141
|
+
write_diagnostic(io, "Error", "quality_gate failed")
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def normalize_diagnostic(detail)
|
|
145
|
+
detail.to_s
|
|
146
|
+
.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "�")
|
|
147
|
+
.gsub(/(?:[[:cntrl:]]|[\u2028\u2029])+/, " ")
|
|
148
|
+
.strip
|
|
149
|
+
rescue StandardError
|
|
150
|
+
"unavailable diagnostic"
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def parse(arguments)
|
|
154
|
+
overrides = {}
|
|
155
|
+
option_parser(overrides).parse!(arguments)
|
|
156
|
+
append_remaining_files(overrides, arguments)
|
|
157
|
+
validate_files(overrides)
|
|
158
|
+
|
|
159
|
+
overrides
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def option_parser(overrides)
|
|
163
|
+
OptionParser.new.tap do |parser|
|
|
164
|
+
parser.on("--files PATH") do |path|
|
|
165
|
+
raise OptionParser::InvalidOption, "--files" if overrides.key?(:files)
|
|
166
|
+
|
|
167
|
+
overrides[:files] = [path]
|
|
168
|
+
end
|
|
169
|
+
parser.on("--format FORMAT", %w[text json]) { |format| overrides[:format] = format }
|
|
170
|
+
parser.on("-h", "--help") { overrides[:help] = true }
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def append_remaining_files(overrides, arguments)
|
|
175
|
+
if overrides.key?(:files)
|
|
176
|
+
overrides[:files].concat(arguments)
|
|
177
|
+
elsif arguments.any?
|
|
178
|
+
raise OptionParser::InvalidOption, arguments.join(" ")
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def validate_files(overrides)
|
|
183
|
+
return unless overrides.key?(:files) && overrides[:files].any?(&:empty?)
|
|
184
|
+
|
|
185
|
+
raise OptionParser::InvalidArgument, "--files requires a non-empty path"
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def validate_paths!(files, dir)
|
|
189
|
+
missing = files.reject { |path| File.exist?(File.expand_path(path, dir)) }
|
|
190
|
+
return if missing.empty?
|
|
191
|
+
|
|
192
|
+
raise ArgumentError, "missing files or directories: #{missing.join(", ")}"
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def validate_simplecov_configuration!(subcommand, settings, path)
|
|
196
|
+
adapters = settings.fetch(:adapters).fetch(subcommand.to_sym)
|
|
197
|
+
return unless adapters.include?("simplecov")
|
|
198
|
+
|
|
199
|
+
coverage = settings.fetch(:coverage)
|
|
200
|
+
return if coverage.is_a?(Hash) && %i[minimum_line minimum_branch].any? { coverage.key?(_1) }
|
|
201
|
+
|
|
202
|
+
cause = "simplecov adapter requires coverage.minimum_line and/or coverage.minimum_branch"
|
|
203
|
+
raise ConfigError.new(path: path, cause_message: cause)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def dispatch(subcommand, settings, stdout:, stderr:)
|
|
207
|
+
config = Config.new(settings)
|
|
208
|
+
diagnostic_io = terminal_diagnostic_io(stderr)
|
|
209
|
+
result = Runner.new(
|
|
210
|
+
adapters: adapters_for(subcommand, settings: settings, config: config, diagnostic_io: stderr),
|
|
211
|
+
config: config,
|
|
212
|
+
diagnostic_io: diagnostic_io
|
|
213
|
+
).call
|
|
214
|
+
|
|
215
|
+
begin
|
|
216
|
+
reporter_for(settings.fetch(:format), stdout: stdout).call(result)
|
|
217
|
+
rescue StandardError => e
|
|
218
|
+
write_error(stderr, e)
|
|
219
|
+
return ExitCode::TOOL_FAILURE
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
result.exit_code
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def adapters_for(subcommand, settings:, config:, diagnostic_io:)
|
|
226
|
+
settings.fetch(:adapters).fetch(subcommand.to_sym).map do |adapter_name|
|
|
227
|
+
build_adapter(
|
|
228
|
+
adapter_name.to_s,
|
|
229
|
+
config: config,
|
|
230
|
+
files: settings.fetch(:files),
|
|
231
|
+
diagnostic_io: diagnostic_io
|
|
232
|
+
)
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def build_adapter(adapter_name, config:, files:, diagnostic_io:)
|
|
237
|
+
registry.fetch(adapter_name) do
|
|
238
|
+
return UnknownAdapter.new(adapter_name)
|
|
239
|
+
end.new(config: config, files: files, diagnostic_io: diagnostic_io)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def reporter_for(format, stdout:)
|
|
243
|
+
case format
|
|
244
|
+
when "json"
|
|
245
|
+
Reporters::Json.new(io: stdout)
|
|
246
|
+
else
|
|
247
|
+
Reporters::Text.new(io: stdout)
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def help_requested?(subcommand, arguments)
|
|
252
|
+
return true if %w[-h --help].include?(subcommand)
|
|
253
|
+
return true if subcommand == "help"
|
|
254
|
+
|
|
255
|
+
(GATE_SUBCOMMANDS.include?(subcommand) || subcommand == "init") && help_argument?(arguments)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def help_argument?(arguments)
|
|
259
|
+
arguments.take_while { _1 != "--" }.any? { %w[-h --help].include?(_1) }
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def help_gate(subcommand, arguments)
|
|
263
|
+
return arguments.find { GATE_SUBCOMMANDS.include?(_1) || _1 == "init" } if subcommand == "help"
|
|
264
|
+
return "init" if subcommand == "init"
|
|
265
|
+
return subcommand if GATE_SUBCOMMANDS.include?(subcommand)
|
|
266
|
+
|
|
267
|
+
nil
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def print_help(stdout, stderr, gate: nil)
|
|
271
|
+
return write_help_failure(stderr) unless write_line(stdout, help_text(gate))
|
|
272
|
+
|
|
273
|
+
ExitCode::CLEAN
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def write_help_failure(stderr)
|
|
277
|
+
write_diagnostic(stderr, "Error", "could not write help output")
|
|
278
|
+
ExitCode::TOOL_FAILURE
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def help_text(gate)
|
|
282
|
+
return init_help_text if gate == "init"
|
|
283
|
+
return gate_help_text(gate) if gate
|
|
284
|
+
|
|
285
|
+
<<~TEXT
|
|
286
|
+
Usage: quality_gate <fast|verify|audit|version|init> [options]
|
|
287
|
+
|
|
288
|
+
Run one Quality Gate check for the current project.
|
|
289
|
+
|
|
290
|
+
Commands:
|
|
291
|
+
fast RuboCop (quick feedback for selected files)
|
|
292
|
+
verify Reek, test_suite, and Undercover (tests and coverage)
|
|
293
|
+
audit Brakeman and bundler_audit (security checks)
|
|
294
|
+
version Print the Quality Gate version
|
|
295
|
+
init Install a plain Ruby project configuration
|
|
296
|
+
|
|
297
|
+
Options:
|
|
298
|
+
--files PATH [PATH ...] Replace configured paths for this run; files and directories are allowed
|
|
299
|
+
--format FORMAT Choose text or json output (also accepts --format=FORMAT)
|
|
300
|
+
-h, --help Show this help
|
|
301
|
+
|
|
302
|
+
Scope:
|
|
303
|
+
Without --files, the configured files are used. With --files, the command-line paths replace them.
|
|
304
|
+
RuboCop and Reek scan selected paths. The test suite runs in full; Undercover checks the Git diff.
|
|
305
|
+
Brakeman scans the application and bundler-audit scans the lockfile.
|
|
306
|
+
|
|
307
|
+
Examples:
|
|
308
|
+
quality_gate fast
|
|
309
|
+
quality_gate fast --files app/models/user.rb test/models
|
|
310
|
+
quality_gate verify --format json
|
|
311
|
+
|
|
312
|
+
Exit status:
|
|
313
|
+
0 Checks completed cleanly
|
|
314
|
+
1 Checks completed with findings
|
|
315
|
+
2 Quality Gate could not complete (input, config, or tool failure)
|
|
316
|
+
TEXT
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def gate_help_text(gate)
|
|
320
|
+
tools = GATE_TOOLS.fetch(gate).join(", ")
|
|
321
|
+
|
|
322
|
+
<<~TEXT
|
|
323
|
+
Usage: quality_gate #{gate} [options]
|
|
324
|
+
|
|
325
|
+
Run the #{gate} gate using these default tools: #{tools}.
|
|
326
|
+
|
|
327
|
+
Options:
|
|
328
|
+
--files PATH [PATH ...] Replace configured paths for this run; files and directories are allowed
|
|
329
|
+
--format FORMAT Choose text or json output (also accepts --format=FORMAT)
|
|
330
|
+
-h, --help Show this help
|
|
331
|
+
|
|
332
|
+
Scope:
|
|
333
|
+
Without --files, the configured files are used. With --files, the command-line paths replace them.
|
|
334
|
+
RuboCop and Reek scan selected paths. The test suite runs in full; Undercover checks the Git diff.
|
|
335
|
+
Brakeman scans the application and bundler-audit scans the lockfile.
|
|
336
|
+
|
|
337
|
+
Examples:
|
|
338
|
+
quality_gate #{gate}
|
|
339
|
+
quality_gate #{gate} --files app/models/user.rb test/models
|
|
340
|
+
quality_gate #{gate} --format json
|
|
341
|
+
|
|
342
|
+
Exit status:
|
|
343
|
+
0 Checks completed cleanly
|
|
344
|
+
1 Checks completed with findings
|
|
345
|
+
2 Quality Gate could not complete (input, config, or tool failure)
|
|
346
|
+
TEXT
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def init_help_text
|
|
350
|
+
<<~TEXT
|
|
351
|
+
Usage: quality_gate init [options]
|
|
352
|
+
|
|
353
|
+
Install a plain Ruby Quality Gate configuration (Ruby is the default profile).
|
|
354
|
+
|
|
355
|
+
Options:
|
|
356
|
+
--profile PROFILE Select the setup profile (ruby)
|
|
357
|
+
--test-framework NAME Select minitest or rspec
|
|
358
|
+
--test-helper PATH Use a custom test helper
|
|
359
|
+
--test-command COMMAND Override the test command
|
|
360
|
+
--skip-coverage Omit coverage and Undercover setup
|
|
361
|
+
--agents Install optional agent hooks and contracts
|
|
362
|
+
--pretend Preview changes without writing them
|
|
363
|
+
-h, --help Show this help
|
|
364
|
+
|
|
365
|
+
Init writes a human-readable summary. Gate commands continue to support text and JSON output.
|
|
366
|
+
|
|
367
|
+
Examples:
|
|
368
|
+
quality_gate init
|
|
369
|
+
quality_gate init --test-framework rspec
|
|
370
|
+
quality_gate init --pretend
|
|
371
|
+
TEXT
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def requested_format(arguments)
|
|
375
|
+
format = nil
|
|
376
|
+
explicit = false
|
|
377
|
+
arguments.take_while { _1 != "--" }.each_with_index do |argument, index|
|
|
378
|
+
if argument.start_with?("--format=")
|
|
379
|
+
explicit = true
|
|
380
|
+
format = argument.delete_prefix("--format=")
|
|
381
|
+
elsif argument == "--format"
|
|
382
|
+
explicit = true
|
|
383
|
+
format = arguments[index + 1]
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
explicit && format.nil? ? :invalid : format
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def configured_format(dir)
|
|
391
|
+
return unless dir.is_a?(String)
|
|
392
|
+
|
|
393
|
+
path = File.join(dir, ".quality_gate.yml")
|
|
394
|
+
return unless File.file?(path)
|
|
395
|
+
|
|
396
|
+
contents = File.read(path)
|
|
397
|
+
document = YAML.safe_load(contents)
|
|
398
|
+
format = document.is_a?(Hash) && document["format"]
|
|
399
|
+
return format if %w[text json].include?(format)
|
|
400
|
+
|
|
401
|
+
nil
|
|
402
|
+
rescue Psych::Exception, SystemCallError, SystemStackError
|
|
403
|
+
nil
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def terminal_diagnostic_io(io)
|
|
407
|
+
io if io.respond_to?(:tty?) && io.tty?
|
|
408
|
+
rescue StandardError
|
|
409
|
+
nil
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def report_json_error(error, stdout:, stderr:)
|
|
413
|
+
result = Runner::Result.new(
|
|
414
|
+
findings: [Finding.tool_failure(tool: "quality_gate", message: safe_error_message(error))]
|
|
415
|
+
)
|
|
416
|
+
reporter_for("json", stdout: stdout).call(result)
|
|
417
|
+
ExitCode::TOOL_FAILURE
|
|
418
|
+
rescue StandardError => e
|
|
419
|
+
write_error(stderr, e)
|
|
420
|
+
ExitCode::TOOL_FAILURE
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def safe_error_message(error)
|
|
424
|
+
message = error.is_a?(String) ? error : error.message
|
|
425
|
+
message.is_a?(String) ? message : message.to_s
|
|
426
|
+
rescue StandardError
|
|
427
|
+
"quality_gate failed"
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
def registry
|
|
431
|
+
{
|
|
432
|
+
"reek" => QualityGate::Adapters::Reek,
|
|
433
|
+
"rubocop" => QualityGate::Adapters::RuboCop,
|
|
434
|
+
"simplecov" => QualityGate::Adapters::SimpleCov,
|
|
435
|
+
"test_suite" => QualityGate::Adapters::TestSuite,
|
|
436
|
+
"undercover" => QualityGate::Adapters::Undercover,
|
|
437
|
+
"brakeman" => QualityGate::Adapters::Brakeman,
|
|
438
|
+
"bundler_audit" => QualityGate::Adapters::BundlerAudit
|
|
439
|
+
}.freeze
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
UnknownAdapter = Data.define(:name) do
|
|
443
|
+
def call
|
|
444
|
+
[Finding.tool_failure(tool: name, message: "unknown adapter #{name}")]
|
|
445
|
+
end
|
|
446
|
+
end
|
|
447
|
+
private_constant :UnknownAdapter
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
# rubocop:enable Metrics/ClassLength
|
|
451
|
+
end
|