quality_gate 0.2.2 → 0.3.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/CHANGELOG.md +22 -0
- data/README.md +292 -21
- data/docs/codex.md +37 -9
- data/docs/releasing.md +16 -4
- data/lib/generators/quality_gate/install/install_generator.rb +18 -3
- data/lib/generators/quality_gate/install/templates/agents_section.md.tt +13 -2
- data/lib/generators/quality_gate/install/templates/codex_fast.rb.tt +36 -0
- data/lib/generators/quality_gate/install/templates/codex_hooks.json.tt +1 -0
- data/lib/generators/quality_gate/install/templates/codex_verify_stop.rb.tt +28 -0
- data/lib/generators/quality_gate/install/templates/quality_gate.yml.tt +6 -0
- data/lib/generators/quality_gate/install/templates/quality_gate_fast.rb.tt +16 -3
- data/lib/generators/quality_gate/install/templates/quality_gate_workflow.yml.tt +28 -0
- data/lib/generators/quality_gate/install/templates/rails_quality_gate.yml.tt +45 -0
- data/lib/generators/quality_gate/install/templates/ruby_agents_section.md.tt +13 -2
- data/lib/generators/quality_gate/install/templates/ruby_quality_gate.yml.tt +5 -0
- data/lib/generators/quality_gate/install/templates/simplecov.rb.tt +1 -0
- data/lib/quality_gate/adapters/bundler_audit.rb +28 -3
- data/lib/quality_gate/adapters/database_consistency.rb +51 -0
- data/lib/quality_gate/adapters/database_consistency_report.rb +61 -0
- data/lib/quality_gate/adapters/database_consistency_report_item.rb +122 -0
- data/lib/quality_gate/adapters/debride.rb +67 -0
- data/lib/quality_gate/adapters/debride_report.rb +80 -0
- data/lib/quality_gate/adapters/herb.rb +303 -0
- data/lib/quality_gate/adapters/reek.rb +21 -2
- data/lib/quality_gate/adapters/rubocop.rb +23 -9
- data/lib/quality_gate/adapters/rubycritic.rb +74 -0
- data/lib/quality_gate/adapters/rubycritic_report.rb +112 -0
- data/lib/quality_gate/adapters/test_suite.rb +12 -1
- data/lib/quality_gate/baseline.rb +269 -0
- data/lib/quality_gate/baseline_run.rb +80 -0
- data/lib/quality_gate/cli.rb +129 -6
- data/lib/quality_gate/codex_fast_hook.rb +163 -0
- data/lib/quality_gate/codex_patch_files.rb +95 -0
- data/lib/quality_gate/codex_stop_hook.rb +102 -0
- data/lib/quality_gate/config.rb +20 -6
- data/lib/quality_gate/database_consistency_runner.rb +124 -0
- data/lib/quality_gate/doctor.rb +217 -0
- data/lib/quality_gate/doctor_bounded_file.rb +74 -0
- data/lib/quality_gate/doctor_command.rb +130 -0
- data/lib/quality_gate/doctor_coverage.rb +113 -0
- data/lib/quality_gate/doctor_git.rb +121 -0
- data/lib/quality_gate/doctor_hooks.rb +99 -0
- data/lib/quality_gate/doctor_launchers.rb +300 -0
- data/lib/quality_gate/doctor_path_lookup.rb +52 -0
- data/lib/quality_gate/doctor_report.rb +38 -0
- data/lib/quality_gate/init_command.rb +3 -1
- data/lib/quality_gate/installation.rb +72 -17
- data/lib/quality_gate/installer.rb +3 -0
- data/lib/quality_gate/reporters/doctor.rb +57 -0
- data/lib/quality_gate/reporters/json.rb +18 -1
- data/lib/quality_gate/reporters/markdown.rb +11 -1
- data/lib/quality_gate/reporters/text.rb +14 -6
- data/lib/quality_gate/ruby_profile.rb +121 -2
- data/lib/quality_gate/runner.rb +15 -17
- data/lib/quality_gate/version.rb +1 -1
- data/lib/quality_gate.rb +15 -0
- data/sig/quality_gate.rbs +117 -2
- metadata +30 -1
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module QualityGate
|
|
4
|
+
# Holds bounded preflight observations independently of gate findings.
|
|
5
|
+
class DoctorReport
|
|
6
|
+
STATUSES = %w[ready warning blocked unchecked not_applicable].map!(&:freeze).freeze
|
|
7
|
+
|
|
8
|
+
attr_reader :checks, :summary
|
|
9
|
+
|
|
10
|
+
def self.check(id:, status:, message:)
|
|
11
|
+
normalized_status = status.to_s
|
|
12
|
+
raise ArgumentError, "unknown Doctor status" unless STATUSES.include?(normalized_status)
|
|
13
|
+
|
|
14
|
+
{ "id" => id.to_s.dup.freeze, "status" => normalized_status.dup.freeze,
|
|
15
|
+
"message" => message.to_s.dup.freeze }.freeze
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def initialize(checks:)
|
|
19
|
+
@checks = checks.map { normalize_check(_1) }.freeze
|
|
20
|
+
@summary = STATUSES.to_h do |status|
|
|
21
|
+
[status, @checks.count { |check| check.fetch("status") == status }]
|
|
22
|
+
end.freeze
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def exit_code
|
|
26
|
+
return 2 if summary.fetch("blocked").positive?
|
|
27
|
+
return 1 if summary.fetch("warning").positive? || summary.fetch("unchecked").positive?
|
|
28
|
+
|
|
29
|
+
0
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def normalize_check(check)
|
|
35
|
+
self.class.check(id: check.fetch("id"), status: check.fetch("status"), message: check.fetch("message"))
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -16,7 +16,7 @@ module QualityGate
|
|
|
16
16
|
|
|
17
17
|
def parse(arguments)
|
|
18
18
|
reject_format!(arguments)
|
|
19
|
-
options = { profile: "ruby", skip_coverage: false, agents: false, pretend: false }
|
|
19
|
+
options = { profile: "ruby", skip_coverage: false, agents: false, codex: false, ci: false, pretend: false }
|
|
20
20
|
option_parser(options).parse!(arguments)
|
|
21
21
|
raise OptionParser::InvalidOption, arguments.join(" ") if arguments.any?
|
|
22
22
|
|
|
@@ -62,6 +62,8 @@ module QualityGate
|
|
|
62
62
|
def boolean_options(parser, options)
|
|
63
63
|
parser.on("--skip-coverage") { options[:skip_coverage] = true }
|
|
64
64
|
parser.on("--agents") { options[:agents] = true }
|
|
65
|
+
parser.on("--codex") { options[:codex] = true }
|
|
66
|
+
parser.on("--ci") { options[:ci] = true }
|
|
65
67
|
parser.on("--pretend") { options[:pretend] = true }
|
|
66
68
|
end
|
|
67
69
|
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
require "erb"
|
|
4
4
|
require "fileutils"
|
|
5
|
+
require "json"
|
|
5
6
|
require "pathname"
|
|
7
|
+
require "shellwords"
|
|
6
8
|
require "tempfile"
|
|
7
9
|
|
|
8
10
|
module QualityGate
|
|
@@ -18,6 +20,9 @@ module QualityGate
|
|
|
18
20
|
.claude/settings.json
|
|
19
21
|
CLAUDE.md
|
|
20
22
|
AGENTS.md
|
|
23
|
+
.codex/hooks.json
|
|
24
|
+
.codex/hooks/quality_gate_fast.rb
|
|
25
|
+
.codex/hooks/quality_gate_verify_stop.rb
|
|
21
26
|
].freeze
|
|
22
27
|
MARKER_START = "# quality_gate coverage — start"
|
|
23
28
|
MARKER_END = "# quality_gate coverage — end"
|
|
@@ -27,7 +32,10 @@ module QualityGate
|
|
|
27
32
|
AGENT_TEMPLATE_PATHS = {
|
|
28
33
|
".claude/hooks/quality_gate_fast.rb" => "quality_gate_fast.rb.tt",
|
|
29
34
|
".claude/hooks/quality_gate_verify_stop.rb" => "quality_gate_verify_stop.rb.tt",
|
|
30
|
-
".claude/settings.json" => "claude_settings.json.tt"
|
|
35
|
+
".claude/settings.json" => "claude_settings.json.tt",
|
|
36
|
+
".codex/hooks.json" => "codex_hooks.json.tt",
|
|
37
|
+
".codex/hooks/quality_gate_fast.rb" => "codex_fast.rb.tt",
|
|
38
|
+
".codex/hooks/quality_gate_verify_stop.rb" => "codex_verify_stop.rb.tt"
|
|
31
39
|
}.freeze
|
|
32
40
|
AGENT_CONTRACT_PATHS = %w[CLAUDE.md AGENTS.md].freeze
|
|
33
41
|
PRIOR_CLAUDE_SETTINGS = [
|
|
@@ -51,6 +59,25 @@ module QualityGate
|
|
|
51
59
|
}
|
|
52
60
|
JSON
|
|
53
61
|
].freeze
|
|
62
|
+
PRIOR_CODEX_HOOKS = lambda do |root|
|
|
63
|
+
JSON.pretty_generate(
|
|
64
|
+
"hooks" => {
|
|
65
|
+
"Stop" => [
|
|
66
|
+
{
|
|
67
|
+
"hooks" => [
|
|
68
|
+
{
|
|
69
|
+
"type" => "command",
|
|
70
|
+
"command" => Shellwords.join(
|
|
71
|
+
["ruby", File.expand_path(".codex/hooks/quality_gate_verify_stop.rb", root)]
|
|
72
|
+
),
|
|
73
|
+
"timeout" => 600
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
}
|
|
79
|
+
).b << "\n".b
|
|
80
|
+
end
|
|
54
81
|
STRONG_MIGRATIONS_PROVENANCE = "# Generated by Quality Gate."
|
|
55
82
|
HOOK_MODE = 0o755
|
|
56
83
|
DEFAULT_AGENT_ARTIFACT_MODE = 0o644
|
|
@@ -193,7 +220,7 @@ module QualityGate
|
|
|
193
220
|
private_constant :PreparedTempfile
|
|
194
221
|
private_constant :DirectoryOperations
|
|
195
222
|
private_constant :AGENT_TEMPLATE_PATHS, :AGENT_CONTRACT_PATHS
|
|
196
|
-
private_constant :PRIOR_CLAUDE_SETTINGS
|
|
223
|
+
private_constant :PRIOR_CLAUDE_SETTINGS, :PRIOR_CODEX_HOOKS
|
|
197
224
|
private_constant :AGENT_CONTRACT_START, :AGENT_CONTRACT_END
|
|
198
225
|
private_constant :HOOK_MODE, :DEFAULT_AGENT_ARTIFACT_MODE, :TEMPFILE_MODE
|
|
199
226
|
|
|
@@ -224,7 +251,7 @@ module QualityGate
|
|
|
224
251
|
relative_path = test_helper_path
|
|
225
252
|
path = safe_destination_path(relative_path)
|
|
226
253
|
return unless path
|
|
227
|
-
return skip_missing_test_helper unless File.file?(path)
|
|
254
|
+
return skip_missing_test_helper(relative_path) unless File.file?(path)
|
|
228
255
|
|
|
229
256
|
install_coverage(path, relative_path)
|
|
230
257
|
end
|
|
@@ -232,22 +259,25 @@ module QualityGate
|
|
|
232
259
|
def create_agent_integration
|
|
233
260
|
return unless invoke_behavior?
|
|
234
261
|
|
|
235
|
-
unless options[:agents]
|
|
236
|
-
record(
|
|
262
|
+
unless options[:agents] || options[:codex]
|
|
263
|
+
record(
|
|
264
|
+
:skipped,
|
|
265
|
+
"agent integration (pass --agents for Claude hooks or --codex for Codex patch feedback and Stop verification)"
|
|
266
|
+
)
|
|
237
267
|
return
|
|
238
268
|
end
|
|
239
269
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
"quality_gate_verify_stop.rb.tt",
|
|
243
|
-
".claude/hooks/quality_gate_verify_stop.rb",
|
|
244
|
-
mode: HOOK_MODE
|
|
245
|
-
)
|
|
246
|
-
install_template("claude_settings.json.tt", ".claude/settings.json")
|
|
247
|
-
install_agent_contract("CLAUDE.md")
|
|
270
|
+
install_claude_integration if options[:agents]
|
|
271
|
+
install_codex_integration if options[:codex]
|
|
248
272
|
install_agent_contract("AGENTS.md")
|
|
249
273
|
end
|
|
250
274
|
|
|
275
|
+
def create_ci_workflow
|
|
276
|
+
return unless invoke_behavior? && options[:ci]
|
|
277
|
+
|
|
278
|
+
install_template("quality_gate_workflow.yml.tt", ".github/workflows/quality_gate.yml")
|
|
279
|
+
end
|
|
280
|
+
|
|
251
281
|
def print_summary
|
|
252
282
|
return print_unsupported_behavior unless invoke_behavior?
|
|
253
283
|
|
|
@@ -261,10 +291,31 @@ module QualityGate
|
|
|
261
291
|
|
|
262
292
|
private
|
|
263
293
|
|
|
294
|
+
def install_claude_integration
|
|
295
|
+
install_template("quality_gate_fast.rb.tt", ".claude/hooks/quality_gate_fast.rb", mode: HOOK_MODE)
|
|
296
|
+
install_template(
|
|
297
|
+
"quality_gate_verify_stop.rb.tt",
|
|
298
|
+
".claude/hooks/quality_gate_verify_stop.rb",
|
|
299
|
+
mode: HOOK_MODE
|
|
300
|
+
)
|
|
301
|
+
install_template("claude_settings.json.tt", ".claude/settings.json")
|
|
302
|
+
install_agent_contract("CLAUDE.md")
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def install_codex_integration
|
|
306
|
+
install_template("codex_fast.rb.tt", ".codex/hooks/quality_gate_fast.rb", mode: HOOK_MODE)
|
|
307
|
+
install_template("codex_hooks.json.tt", ".codex/hooks.json")
|
|
308
|
+
install_template("codex_verify_stop.rb.tt", ".codex/hooks/quality_gate_verify_stop.rb", mode: HOOK_MODE)
|
|
309
|
+
end
|
|
310
|
+
|
|
264
311
|
def invoke_behavior?
|
|
265
312
|
behavior == :invoke
|
|
266
313
|
end
|
|
267
314
|
|
|
315
|
+
def agent_option?(name)
|
|
316
|
+
options.fetch(name, options.fetch(name.to_s, false))
|
|
317
|
+
end
|
|
318
|
+
|
|
268
319
|
def print_unsupported_behavior
|
|
269
320
|
say "Automatic removal is not supported. See the README installation undo instructions for manual removal."
|
|
270
321
|
end
|
|
@@ -284,7 +335,7 @@ module QualityGate
|
|
|
284
335
|
existing = existing_file.fetch(:content)
|
|
285
336
|
if existing == content.b
|
|
286
337
|
record_existing_template(path, relative_path, existing:, mode:, status: existing_file.fetch(:status))
|
|
287
|
-
elsif prior_claude_settings?(relative_path, existing)
|
|
338
|
+
elsif prior_claude_settings?(relative_path, existing) || prior_codex_hooks?(relative_path, existing)
|
|
288
339
|
return record_pretend_skip(relative_path) if options[:pretend]
|
|
289
340
|
|
|
290
341
|
replace_content(path, content, relative_path, expected: existing_file)
|
|
@@ -302,6 +353,10 @@ module QualityGate
|
|
|
302
353
|
relative_path == ".claude/settings.json" && PRIOR_CLAUDE_SETTINGS.include?(content)
|
|
303
354
|
end
|
|
304
355
|
|
|
356
|
+
def prior_codex_hooks?(relative_path, content)
|
|
357
|
+
relative_path == ".codex/hooks.json" && content == PRIOR_CODEX_HOOKS.call(destination_root).b
|
|
358
|
+
end
|
|
359
|
+
|
|
305
360
|
def install_agent_contract(relative_path)
|
|
306
361
|
path = safe_destination_path(relative_path)
|
|
307
362
|
return unless path
|
|
@@ -577,9 +632,9 @@ module QualityGate
|
|
|
577
632
|
markers
|
|
578
633
|
end
|
|
579
634
|
|
|
580
|
-
def skip_missing_test_helper
|
|
581
|
-
say "Warning: Minitest coverage wiring skipped;
|
|
582
|
-
record(:skipped, "
|
|
635
|
+
def skip_missing_test_helper(relative_path)
|
|
636
|
+
say "Warning: Minitest coverage wiring skipped; #{relative_path} is missing."
|
|
637
|
+
record(:skipped, "#{relative_path} (missing; Minitest coverage wiring skipped)")
|
|
583
638
|
end
|
|
584
639
|
|
|
585
640
|
def skip_existing_simplecov(relative_path)
|
|
@@ -15,6 +15,8 @@ module QualityGate
|
|
|
15
15
|
test_helper
|
|
16
16
|
test_command
|
|
17
17
|
agents
|
|
18
|
+
codex
|
|
19
|
+
ci
|
|
18
20
|
pretend
|
|
19
21
|
skip_coverage
|
|
20
22
|
].freeze
|
|
@@ -25,6 +27,7 @@ module QualityGate
|
|
|
25
27
|
create_initializers
|
|
26
28
|
inject_coverage
|
|
27
29
|
create_agent_integration
|
|
30
|
+
create_ci_workflow
|
|
28
31
|
].freeze
|
|
29
32
|
|
|
30
33
|
attr_reader :destination_root, :stdout, :profile
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require_relative "../doctor_report"
|
|
5
|
+
|
|
6
|
+
module QualityGate
|
|
7
|
+
module Reporters
|
|
8
|
+
# Renders Doctor's preflight checks as text or a stable JSON envelope.
|
|
9
|
+
class Doctor
|
|
10
|
+
def initialize(io:)
|
|
11
|
+
@io = io
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call(report, format:)
|
|
15
|
+
case format
|
|
16
|
+
when "json" then io.puts(JSON.generate(json_payload(report)))
|
|
17
|
+
when "text" then print_text(report)
|
|
18
|
+
else raise ArgumentError, "Doctor format must be text or json"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
attr_reader :io
|
|
25
|
+
|
|
26
|
+
def json_payload(report)
|
|
27
|
+
{
|
|
28
|
+
"scope" => "preflight",
|
|
29
|
+
"checks" => sanitized_checks(report),
|
|
30
|
+
"summary" => report.summary
|
|
31
|
+
}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def sanitized_checks(report)
|
|
35
|
+
report.checks.map do |check|
|
|
36
|
+
check.transform_values { |value| FieldSanitizer.for_json(value) }
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def print_text(report)
|
|
41
|
+
report.checks.each do |check|
|
|
42
|
+
io.puts [check.fetch("id"), check.fetch("status"), check.fetch("message")]
|
|
43
|
+
.map { FieldSanitizer.for_text(_1) }.join(" ")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
io.puts "Preflight: #{summary_text(report.summary)}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def summary_text(summary)
|
|
50
|
+
DoctorReport::STATUSES.map do |status|
|
|
51
|
+
label = status == "not_applicable" ? "not applicable" : status
|
|
52
|
+
"#{summary.fetch(status)} #{label}"
|
|
53
|
+
end.join(", ")
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -19,7 +19,7 @@ module QualityGate
|
|
|
19
19
|
attr_reader :io
|
|
20
20
|
|
|
21
21
|
def payload_for(result)
|
|
22
|
-
{
|
|
22
|
+
payload = {
|
|
23
23
|
"checks" => result.checks.map { serialize_check(_1) },
|
|
24
24
|
"findings" => result.findings.map { serialize_finding(_1) },
|
|
25
25
|
"summary" => {
|
|
@@ -28,6 +28,23 @@ module QualityGate
|
|
|
28
28
|
"failed_tools" => result.failed_tools.map { FieldSanitizer.for_json(_1) }
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
+
add_baseline(payload, result.baseline)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def add_baseline(payload, baseline)
|
|
35
|
+
payload["baseline"] = serialize_baseline(baseline) if baseline
|
|
36
|
+
payload
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def serialize_baseline(baseline)
|
|
40
|
+
{
|
|
41
|
+
"mode" => baseline.fetch(:mode),
|
|
42
|
+
"status" => baseline.fetch(:status),
|
|
43
|
+
"accepted_count" => baseline.fetch(:accepted_count),
|
|
44
|
+
"removed_count" => baseline.fetch(:removed_count),
|
|
45
|
+
"written" => baseline.fetch(:written),
|
|
46
|
+
"accepted_findings" => baseline.fetch(:accepted_findings).map { serialize_finding(_1) }
|
|
47
|
+
}
|
|
31
48
|
end
|
|
32
49
|
|
|
33
50
|
def serialize_finding(finding)
|
|
@@ -19,7 +19,17 @@ module QualityGate
|
|
|
19
19
|
attr_reader :io
|
|
20
20
|
|
|
21
21
|
def sections_for(result)
|
|
22
|
-
[
|
|
22
|
+
[
|
|
23
|
+
summary_heading(result), baseline_section(result.baseline),
|
|
24
|
+
checks_table(result.checks), findings_section(result.findings)
|
|
25
|
+
].compact
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def baseline_section(baseline)
|
|
29
|
+
return unless baseline
|
|
30
|
+
|
|
31
|
+
"Baseline **#{baseline.fetch(:mode)}**: #{baseline.fetch(:status)}, " \
|
|
32
|
+
"#{baseline.fetch(:accepted_count)} accepted, #{baseline.fetch(:removed_count)} removed."
|
|
23
33
|
end
|
|
24
34
|
|
|
25
35
|
def summary_heading(result)
|
|
@@ -10,18 +10,26 @@ module QualityGate
|
|
|
10
10
|
|
|
11
11
|
def call(result)
|
|
12
12
|
print_checks(result.checks)
|
|
13
|
-
|
|
14
|
-
result
|
|
15
|
-
io.puts(render_finding(finding))
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
io.puts("#{result.findings.length} findings, #{result.failed_tools.length} tool failures")
|
|
13
|
+
print_baseline(result.baseline) if result.baseline
|
|
14
|
+
print_findings_and_summary(result)
|
|
19
15
|
end
|
|
20
16
|
|
|
21
17
|
private
|
|
22
18
|
|
|
23
19
|
attr_reader :io
|
|
24
20
|
|
|
21
|
+
def print_findings_and_summary(result)
|
|
22
|
+
result.findings.each { |finding| io.puts(render_finding(finding)) }
|
|
23
|
+
io.puts("#{result.findings.length} findings, #{result.failed_tools.length} tool failures")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def print_baseline(baseline)
|
|
27
|
+
io.puts(
|
|
28
|
+
"Baseline #{baseline.fetch(:mode)}: #{baseline.fetch(:status)}, " \
|
|
29
|
+
"#{baseline.fetch(:accepted_count)} accepted, #{baseline.fetch(:removed_count)} removed"
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
|
|
25
33
|
def print_checks(checks)
|
|
26
34
|
return if checks.empty?
|
|
27
35
|
|
|
@@ -42,8 +42,8 @@ module QualityGate
|
|
|
42
42
|
raise ArgumentError, "#{name} must stay inside the project"
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
-
def command(value, framework)
|
|
46
|
-
command = value.nil? ?
|
|
45
|
+
def command(value, framework, defaults: DEFAULT_COMMANDS)
|
|
46
|
+
command = value.nil? ? defaults.fetch(framework) : parse_command(value)
|
|
47
47
|
validate_command!(command)
|
|
48
48
|
command.map(&:dup).freeze
|
|
49
49
|
end
|
|
@@ -167,4 +167,123 @@ module QualityGate
|
|
|
167
167
|
"test helper #{relative.inspect} does not exist; pass --skip-coverage or provide an existing helper"
|
|
168
168
|
end
|
|
169
169
|
end
|
|
170
|
+
|
|
171
|
+
# Resolves the Rails test helper and command while sharing the Ruby profile's
|
|
172
|
+
# validation for project-relative paths and shell-free argv.
|
|
173
|
+
class RailsProfile
|
|
174
|
+
DEFAULT_HELPERS = {
|
|
175
|
+
"minitest" => "test/test_helper.rb",
|
|
176
|
+
"rspec" => "spec/rails_helper.rb"
|
|
177
|
+
}.freeze
|
|
178
|
+
DEFAULT_COMMANDS = {
|
|
179
|
+
"minitest" => %w[bin/rails test].freeze,
|
|
180
|
+
"rspec" => %w[bundle exec rspec].freeze
|
|
181
|
+
}.freeze
|
|
182
|
+
TEMPLATE_MAP = { "quality_gate.yml.tt" => "rails_quality_gate.yml.tt" }.freeze
|
|
183
|
+
SUPPORTED_FRAMEWORKS = DEFAULT_HELPERS.keys.freeze
|
|
184
|
+
|
|
185
|
+
attr_reader :destination_root, :framework, :test_helper, :test_command
|
|
186
|
+
|
|
187
|
+
def initialize(destination_root:, options: {})
|
|
188
|
+
@destination_root = RubyProfileSupport.destination_root(destination_root)
|
|
189
|
+
@options = RubyProfileSupport.options(options)
|
|
190
|
+
resolve_settings
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def coverage? = @coverage
|
|
194
|
+
|
|
195
|
+
def missing_default_helper? = !!@missing_default_helper
|
|
196
|
+
|
|
197
|
+
def template_for(name)
|
|
198
|
+
TEMPLATE_MAP.fetch(name, name)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
private
|
|
202
|
+
|
|
203
|
+
attr_reader :options
|
|
204
|
+
|
|
205
|
+
def resolve_settings
|
|
206
|
+
@coverage = !option(:skip_coverage)
|
|
207
|
+
@framework = resolve_framework
|
|
208
|
+
@test_helper = resolve_test_helper
|
|
209
|
+
@test_command = RubyProfileSupport.command(option(:test_command), framework, defaults: DEFAULT_COMMANDS)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def option(key)
|
|
213
|
+
options.fetch(key, nil)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def resolve_framework
|
|
217
|
+
explicit = option(:test_framework)
|
|
218
|
+
return validate_framework(explicit) if explicit
|
|
219
|
+
|
|
220
|
+
helper = option(:test_helper)
|
|
221
|
+
return framework_for_helper(helper) if helper
|
|
222
|
+
|
|
223
|
+
detected_framework
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def detected_framework
|
|
227
|
+
available = DEFAULT_HELPERS.select { |_name, path| helper_present?(path) }.keys
|
|
228
|
+
return available.first if available.one?
|
|
229
|
+
|
|
230
|
+
return missing_default_framework if available.empty?
|
|
231
|
+
|
|
232
|
+
raise ArgumentError, "both test helpers exist; pass --test-framework minitest or rspec"
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def missing_default_framework
|
|
236
|
+
@missing_default_helper = true
|
|
237
|
+
"minitest"
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def validate_framework(value)
|
|
241
|
+
selected = value.to_s
|
|
242
|
+
return selected if SUPPORTED_FRAMEWORKS.include?(selected)
|
|
243
|
+
|
|
244
|
+
raise ArgumentError, "test_framework must be minitest or rspec"
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def framework_for_helper(value)
|
|
248
|
+
relative = RubyProfileSupport.relative_path(destination_root, value, "test_helper")
|
|
249
|
+
relative.start_with?("spec/") || relative == "spec" ? "rspec" : "minitest"
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def resolve_test_helper
|
|
253
|
+
helper = option(:test_helper) || DEFAULT_HELPERS.fetch(framework)
|
|
254
|
+
relative = RubyProfileSupport.relative_path(destination_root, helper, "test_helper")
|
|
255
|
+
return relative unless coverage?
|
|
256
|
+
|
|
257
|
+
validate_test_helper!(relative)
|
|
258
|
+
relative
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def validate_test_helper!(relative)
|
|
262
|
+
path = helper_path_without_symlinks(relative)
|
|
263
|
+
raise ArgumentError, "test_helper must not pass through symlinks" unless path
|
|
264
|
+
return if File.file?(path) || missing_default_helper?
|
|
265
|
+
|
|
266
|
+
raise ArgumentError,
|
|
267
|
+
"test helper #{relative.inspect} does not exist; pass --skip-coverage or provide an existing helper"
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def helper_present?(helper)
|
|
271
|
+
path = helper_path_without_symlinks(helper)
|
|
272
|
+
path && File.file?(path)
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def helper_path_without_symlinks(helper)
|
|
276
|
+
return if symlink_in_path?(helper)
|
|
277
|
+
|
|
278
|
+
File.join(destination_root, helper)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def symlink_in_path?(helper)
|
|
282
|
+
path = destination_root
|
|
283
|
+
helper.split(File::SEPARATOR).any? do |component|
|
|
284
|
+
path = File.join(path, component)
|
|
285
|
+
File.symlink?(path)
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
end
|
|
170
289
|
end
|
data/lib/quality_gate/runner.rb
CHANGED
|
@@ -23,22 +23,24 @@ module QualityGate
|
|
|
23
23
|
|
|
24
24
|
# Runs adapters in order and reduces their findings into one immutable result.
|
|
25
25
|
class Runner
|
|
26
|
-
Result = Data.define(:findings, :failed_tools, :checks) do
|
|
27
|
-
def initialize(findings:, checks: [])
|
|
26
|
+
Result = Data.define(:findings, :failed_tools, :checks, :baseline) do
|
|
27
|
+
def initialize(findings:, checks: [], baseline: nil)
|
|
28
28
|
copied_findings = findings.dup.freeze
|
|
29
29
|
failed_tools = copied_findings.select(&:tool_failure?).map(&:tool).uniq
|
|
30
30
|
copied_checks = RunnerValueCopy.freeze_copy(checks)
|
|
31
|
+
copied_baseline = RunnerValueCopy.freeze_copy(baseline)
|
|
31
32
|
|
|
32
33
|
super(
|
|
33
34
|
findings: copied_findings,
|
|
34
35
|
failed_tools: failed_tools.map { _1.dup.freeze }.freeze,
|
|
35
|
-
checks: copied_checks
|
|
36
|
+
checks: copied_checks,
|
|
37
|
+
baseline: copied_baseline
|
|
36
38
|
)
|
|
37
39
|
end
|
|
38
40
|
|
|
39
41
|
# Override Data#with so replacements cannot bypass the derived-state invariant.
|
|
40
|
-
def with(findings: self.findings, checks: self.checks)
|
|
41
|
-
self.class.new(findings: findings, checks: checks)
|
|
42
|
+
def with(findings: self.findings, checks: self.checks, baseline: self.baseline)
|
|
43
|
+
self.class.new(findings: findings, checks: checks, baseline: baseline)
|
|
42
44
|
end
|
|
43
45
|
|
|
44
46
|
def exit_code
|
|
@@ -52,13 +54,17 @@ module QualityGate
|
|
|
52
54
|
CHECK_SCOPES = {
|
|
53
55
|
"rubocop" => "selected_files",
|
|
54
56
|
"reek" => "selected_files",
|
|
57
|
+
"herb" => "selected_files",
|
|
55
58
|
"test_suite" => "test_suite",
|
|
56
59
|
"undercover" => "git_diff",
|
|
57
60
|
"simplecov" => "coverage_summary",
|
|
58
61
|
"brakeman" => "project",
|
|
59
|
-
"bundler_audit" => "lockfile"
|
|
62
|
+
"bundler_audit" => "lockfile",
|
|
63
|
+
"rubycritic" => "project",
|
|
64
|
+
"debride" => "project",
|
|
65
|
+
"database_consistency" => "project"
|
|
60
66
|
}.freeze
|
|
61
|
-
SELECTION_SUPPORTED_TOOLS = %w[rubocop reek].freeze
|
|
67
|
+
SELECTION_SUPPORTED_TOOLS = %w[rubocop reek herb].freeze
|
|
62
68
|
private_constant :CHECK_SCOPES, :SELECTION_SUPPORTED_TOOLS
|
|
63
69
|
|
|
64
70
|
def initialize(adapters:, config:, diagnostic_io: nil)
|
|
@@ -72,7 +78,7 @@ module QualityGate
|
|
|
72
78
|
checks = []
|
|
73
79
|
|
|
74
80
|
adapters.each do |adapter|
|
|
75
|
-
tool =
|
|
81
|
+
tool = adapter.name.to_s
|
|
76
82
|
write_progress(tool, scope_for(tool))
|
|
77
83
|
started_at = monotonic_time
|
|
78
84
|
adapter_findings = adapter.call
|
|
@@ -87,24 +93,16 @@ module QualityGate
|
|
|
87
93
|
|
|
88
94
|
attr_reader :adapters, :config, :diagnostic_io
|
|
89
95
|
|
|
90
|
-
def adapter_name(adapter)
|
|
91
|
-
adapter.name.to_s
|
|
92
|
-
end
|
|
93
|
-
|
|
94
96
|
def check_for(tool, findings, duration_ms)
|
|
95
97
|
{
|
|
96
98
|
tool: tool,
|
|
97
99
|
status: status_for(findings),
|
|
98
100
|
scope: scope_for(tool),
|
|
99
|
-
requested_files:
|
|
101
|
+
requested_files: SELECTION_SUPPORTED_TOOLS.include?(tool) ? requested_files : [],
|
|
100
102
|
duration_ms: duration_ms
|
|
101
103
|
}
|
|
102
104
|
end
|
|
103
105
|
|
|
104
|
-
def selection_supported?(tool)
|
|
105
|
-
SELECTION_SUPPORTED_TOOLS.include?(tool)
|
|
106
|
-
end
|
|
107
|
-
|
|
108
106
|
def scope_for(tool)
|
|
109
107
|
scope = CHECK_SCOPES.fetch(tool, "unknown")
|
|
110
108
|
return "project" if scope == "selected_files" && requested_files.empty?
|
data/lib/quality_gate/version.rb
CHANGED
data/lib/quality_gate.rb
CHANGED
|
@@ -14,15 +14,30 @@ module QualityGate
|
|
|
14
14
|
# Your code goes here...
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
+
require_relative "quality_gate/baseline"
|
|
18
|
+
require_relative "quality_gate/baseline_run"
|
|
17
19
|
require_relative "quality_gate/adapter"
|
|
18
20
|
require_relative "quality_gate/adapters/brakeman"
|
|
19
21
|
require_relative "quality_gate/adapters/bundler_audit"
|
|
22
|
+
require_relative "quality_gate/adapters/database_consistency"
|
|
23
|
+
require_relative "quality_gate/adapters/herb"
|
|
20
24
|
require_relative "quality_gate/adapters/reek"
|
|
21
25
|
require_relative "quality_gate/adapters/rubocop"
|
|
26
|
+
require_relative "quality_gate/adapters/rubycritic"
|
|
27
|
+
require_relative "quality_gate/adapters/debride"
|
|
22
28
|
require_relative "quality_gate/adapters/simplecov"
|
|
23
29
|
require_relative "quality_gate/adapters/test_suite"
|
|
24
30
|
require_relative "quality_gate/adapters/undercover"
|
|
25
31
|
require_relative "quality_gate/config"
|
|
26
32
|
require_relative "quality_gate/runner"
|
|
33
|
+
require_relative "quality_gate/doctor_report"
|
|
34
|
+
require_relative "quality_gate/reporters/doctor"
|
|
35
|
+
require_relative "quality_gate/doctor_bounded_file"
|
|
36
|
+
require_relative "quality_gate/doctor_launchers"
|
|
37
|
+
require_relative "quality_gate/doctor_git"
|
|
38
|
+
require_relative "quality_gate/doctor_coverage"
|
|
39
|
+
require_relative "quality_gate/doctor_hooks"
|
|
40
|
+
require_relative "quality_gate/doctor"
|
|
41
|
+
require_relative "quality_gate/doctor_command"
|
|
27
42
|
require_relative "quality_gate/cli"
|
|
28
43
|
require_relative "quality_gate/railtie" if defined?(Rails::Railtie)
|