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.
Files changed (62) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +14 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +571 -0
  5. data/config/37signals.yml +26 -0
  6. data/config/cops.yml +37 -0
  7. data/config/reek.yml +6 -0
  8. data/config/rubocop.yml +60 -0
  9. data/config/ruby.yml +8 -0
  10. data/docs/codex.md +31 -0
  11. data/docs/dogfood-log.md +47 -0
  12. data/docs/incidents.md +18 -0
  13. data/docs/releasing.md +46 -0
  14. data/exe/quality_gate +6 -0
  15. data/lib/generators/quality_gate/install/install_generator.rb +66 -0
  16. data/lib/generators/quality_gate/install/templates/agents_section.md.tt +15 -0
  17. data/lib/generators/quality_gate/install/templates/bullet.rb.tt +10 -0
  18. data/lib/generators/quality_gate/install/templates/claude_settings.json.tt +29 -0
  19. data/lib/generators/quality_gate/install/templates/hook_log_filesystem.rb.tt +76 -0
  20. data/lib/generators/quality_gate/install/templates/quality_gate.yml.tt +42 -0
  21. data/lib/generators/quality_gate/install/templates/quality_gate_fast.rb.tt +248 -0
  22. data/lib/generators/quality_gate/install/templates/quality_gate_verify_stop.rb.tt +466 -0
  23. data/lib/generators/quality_gate/install/templates/rubocop.yml.tt +1 -0
  24. data/lib/generators/quality_gate/install/templates/ruby_agents_section.md.tt +15 -0
  25. data/lib/generators/quality_gate/install/templates/ruby_quality_gate.yml.tt +40 -0
  26. data/lib/generators/quality_gate/install/templates/ruby_rubocop.yml.tt +1 -0
  27. data/lib/generators/quality_gate/install/templates/ruby_simplecov.rb.tt +11 -0
  28. data/lib/generators/quality_gate/install/templates/simplecov.rb.tt +10 -0
  29. data/lib/generators/quality_gate/install/templates/strong_migrations.rb.tt +10 -0
  30. data/lib/quality_gate/adapter.rb +328 -0
  31. data/lib/quality_gate/adapters/brakeman.rb +125 -0
  32. data/lib/quality_gate/adapters/bundler_audit.rb +235 -0
  33. data/lib/quality_gate/adapters/reek.rb +108 -0
  34. data/lib/quality_gate/adapters/rubocop.rb +142 -0
  35. data/lib/quality_gate/adapters/simplecov.rb +103 -0
  36. data/lib/quality_gate/adapters/test_suite.rb +81 -0
  37. data/lib/quality_gate/adapters/undercover.rb +357 -0
  38. data/lib/quality_gate/cli.rb +451 -0
  39. data/lib/quality_gate/config.rb +290 -0
  40. data/lib/quality_gate/exit_code.rb +14 -0
  41. data/lib/quality_gate/finding.rb +50 -0
  42. data/lib/quality_gate/hook_log.rb +131 -0
  43. data/lib/quality_gate/init_command.rb +90 -0
  44. data/lib/quality_gate/installation.rb +1577 -0
  45. data/lib/quality_gate/installer.rb +104 -0
  46. data/lib/quality_gate/railtie.rb +16 -0
  47. data/lib/quality_gate/reporters/field_sanitizer.rb +42 -0
  48. data/lib/quality_gate/reporters/json.rb +59 -0
  49. data/lib/quality_gate/reporters/text.rb +45 -0
  50. data/lib/quality_gate/rubocop.rb +34 -0
  51. data/lib/quality_gate/ruby_profile.rb +170 -0
  52. data/lib/quality_gate/runner.rb +150 -0
  53. data/lib/quality_gate/version.rb +5 -0
  54. data/lib/quality_gate.rb +27 -0
  55. data/lib/rubocop/cop/quality_gate/association_default_block_value.rb +46 -0
  56. data/lib/rubocop/cop/quality_gate/broadcast_in_controller.rb +64 -0
  57. data/lib/rubocop/cop/quality_gate/controller_instance_variables.rb +47 -0
  58. data/lib/rubocop/cop/quality_gate/prefer_after_save_commit.rb +84 -0
  59. data/lib/rubocop/cop/quality_gate/private_only_concern.rb +77 -0
  60. data/llm.txt +13 -0
  61. data/sig/quality_gate.rbs +226 -0
  62. metadata +260 -0
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require_relative "installation"
5
+ require_relative "ruby_profile"
6
+
7
+ module QualityGate
8
+ # Installs the framework-independent Quality Gate files into a Ruby project.
9
+ class Installer
10
+ include Installation
11
+
12
+ VALID_OPTIONS = %i[
13
+ profile
14
+ test_framework
15
+ test_helper
16
+ test_command
17
+ agents
18
+ pretend
19
+ skip_coverage
20
+ ].freeze
21
+ DEFAULT_TEMPLATE_ROOT = File.expand_path("../generators/quality_gate/install/templates", __dir__).freeze
22
+ INSTALL_STEPS = %i[
23
+ create_settings_file
24
+ create_rules_file
25
+ create_initializers
26
+ inject_coverage
27
+ create_agent_integration
28
+ ].freeze
29
+
30
+ attr_reader :destination_root, :stdout, :profile
31
+
32
+ def initialize(destination_root:, options: {}, stdout: $stdout)
33
+ @options = normalize_options(options)
34
+ @profile = RubyProfile.new(destination_root:, options: @options)
35
+ @destination_root = @profile.destination_root
36
+ @stdout = stdout
37
+ @template_source_root = DEFAULT_TEMPLATE_ROOT
38
+ end
39
+
40
+ def options
41
+ @options.merge(skip_initializers: true)
42
+ end
43
+
44
+ def call
45
+ INSTALL_STEPS.each { |step| public_send(step) }
46
+ print_summary
47
+
48
+ results.fetch(:needs_person, []).empty? ? 0 : 1
49
+ end
50
+
51
+ def behavior
52
+ :invoke
53
+ end
54
+
55
+ def test_helper_path
56
+ profile.test_helper
57
+ end
58
+
59
+ def template_for(name)
60
+ profile.template_for(name)
61
+ end
62
+
63
+ private
64
+
65
+ attr_reader :template_source_root
66
+
67
+ def normalize_options(value)
68
+ normalized = option_pairs(value).to_h
69
+ validate_options!(normalized)
70
+ normalized.freeze
71
+ end
72
+
73
+ def option_pairs(value)
74
+ raise ArgumentError, "options must be a mapping" unless value.respond_to?(:to_h)
75
+
76
+ value.to_h.map { |key, option_value| [key.to_sym, option_value] }
77
+ rescue TypeError, NoMethodError
78
+ raise ArgumentError, "option keys must be symbols or strings"
79
+ end
80
+
81
+ def validate_options!(normalized)
82
+ unknown = normalized.keys - VALID_OPTIONS
83
+ return if unknown.empty?
84
+
85
+ raise ArgumentError, "unknown installer option(s): #{unknown.map(&:inspect).join(", ")}"
86
+ end
87
+
88
+ def create_new_file(relative_path, content)
89
+ write_new_file(File.join(destination_root, relative_path), content)
90
+ end
91
+
92
+ def write_new_file(path, content)
93
+ FileUtils.mkdir_p(File.dirname(path))
94
+ File.open(path, File::WRONLY | File::CREAT | File::EXCL, 0o644) do |file|
95
+ file.binmode
96
+ file.write(content)
97
+ end
98
+ end
99
+
100
+ def say(message)
101
+ stdout.puts(message)
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ if defined?(Rails::Railtie)
4
+ module QualityGate
5
+ # Integrates Quality Gate's generator and task files with Rails applications.
6
+ class Railtie < Rails::Railtie
7
+ generators do
8
+ require_relative "../generators/quality_gate/install/install_generator"
9
+ end
10
+
11
+ rake_tasks do
12
+ Dir[File.expand_path("../tasks/**/*.rake", __dir__)].sort.each { load _1 }
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QualityGate
4
+ module Reporters
5
+ # Normalizes reporter string fields without mutating source findings.
6
+ module FieldSanitizer
7
+ REPLACEMENT_CHARACTER = "\uFFFD"
8
+
9
+ module_function
10
+
11
+ def for_text(value)
12
+ sanitize_text(scrub_utf8(value), preserve_newlines: false)
13
+ end
14
+
15
+ def for_text_message(value)
16
+ sanitize_text(scrub_utf8(value), preserve_newlines: true)
17
+ end
18
+
19
+ def for_json(value)
20
+ scrub_utf8(value)
21
+ end
22
+
23
+ def scrub_utf8(value)
24
+ value.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: REPLACEMENT_CHARACTER)
25
+ end
26
+
27
+ def sanitize_text(value, preserve_newlines:)
28
+ value.each_char.map { sanitize_character(_1, preserve_newlines) }.join.strip
29
+ end
30
+
31
+ def sanitize_character(character, preserve_newlines)
32
+ return character if preserve_newlines && character == "\n"
33
+ return "" if preserve_newlines && character == "\e"
34
+ return character unless character.match?(/[[:cntrl:]\u2028\u2029]/)
35
+
36
+ " "
37
+ end
38
+ end
39
+
40
+ private_constant :FieldSanitizer
41
+ end
42
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module QualityGate
6
+ module Reporters
7
+ # Writes machine-readable reports as one JSON object per run.
8
+ class Json
9
+ def initialize(io:)
10
+ @io = io
11
+ end
12
+
13
+ def call(result)
14
+ io.puts(::JSON.generate(payload_for(result)))
15
+ end
16
+
17
+ private
18
+
19
+ attr_reader :io
20
+
21
+ def payload_for(result)
22
+ {
23
+ "checks" => result.checks.map { serialize_check(_1) },
24
+ "findings" => result.findings.map { serialize_finding(_1) },
25
+ "summary" => {
26
+ "findings" => result.findings.length,
27
+ "tool_failures" => result.failed_tools.length,
28
+ "failed_tools" => result.failed_tools.map { FieldSanitizer.for_json(_1) }
29
+ }
30
+ }
31
+ end
32
+
33
+ def serialize_finding(finding)
34
+ {
35
+ "tool" => FieldSanitizer.for_json(finding.tool),
36
+ "file" => FieldSanitizer.for_json(finding.file),
37
+ "line" => finding.line,
38
+ "rule" => FieldSanitizer.for_json(finding.rule),
39
+ "severity" => FieldSanitizer.for_json(finding.severity.to_s),
40
+ "message" => FieldSanitizer.for_json(finding.message)
41
+ }
42
+ end
43
+
44
+ def serialize_check(check)
45
+ {
46
+ "tool" => FieldSanitizer.for_json(check_value(check, :tool)),
47
+ "status" => FieldSanitizer.for_json(check_value(check, :status)),
48
+ "scope" => FieldSanitizer.for_json(check_value(check, :scope)),
49
+ "requested_files" => check_value(check, :requested_files).map { FieldSanitizer.for_json(_1) },
50
+ "duration_ms" => check_value(check, :duration_ms)
51
+ }
52
+ end
53
+
54
+ def check_value(check, key)
55
+ check.fetch(key) { check.fetch(key.to_s) }
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QualityGate
4
+ module Reporters
5
+ # Writes line-oriented reports for humans.
6
+ class Text
7
+ def initialize(io:)
8
+ @io = io
9
+ end
10
+
11
+ def call(result)
12
+ result.findings.each do |finding|
13
+ io.puts(render_finding(finding))
14
+ end
15
+
16
+ io.puts("#{result.findings.length} findings, #{result.failed_tools.length} tool failures")
17
+ end
18
+
19
+ private
20
+
21
+ attr_reader :io
22
+
23
+ def render_finding(finding)
24
+ lines = FieldSanitizer.for_text_message(finding.message).split("\n", -1)
25
+ ["#{finding_prefix(finding)} #{lines.first}", *lines.drop(1).map { " #{_1}" }].join("\n")
26
+ end
27
+
28
+ def finding_prefix(finding)
29
+ [
30
+ FieldSanitizer.for_text(finding.tool),
31
+ FieldSanitizer.for_text(finding.severity.to_s),
32
+ finding_location(finding),
33
+ FieldSanitizer.for_text(finding.rule)
34
+ ].compact.join(" ")
35
+ end
36
+
37
+ def finding_location(finding)
38
+ file = FieldSanitizer.for_text(finding.file)
39
+ return if file.empty?
40
+
41
+ finding.line.zero? ? file : "#{file}:#{finding.line}"
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubocop"
4
+ require "lint_roller"
5
+ require_relative "version"
6
+ require_relative "../rubocop/cop/quality_gate/association_default_block_value"
7
+ require_relative "../rubocop/cop/quality_gate/prefer_after_save_commit"
8
+ require_relative "../rubocop/cop/quality_gate/private_only_concern"
9
+ require_relative "../rubocop/cop/quality_gate/broadcast_in_controller"
10
+ require_relative "../rubocop/cop/quality_gate/controller_instance_variables"
11
+
12
+ module QualityGate
13
+ # Loads optional Rails conventions through RuboCop's plugin interface.
14
+ class RuboCopPlugin < LintRoller::Plugin
15
+ def about
16
+ LintRoller::About.new(
17
+ name: "quality_gate", version: VERSION,
18
+ homepage: "https://github.com/lucianghinda/quality_gate",
19
+ description: "Conservative, opt-in Rails convention checks."
20
+ )
21
+ end
22
+
23
+ def supported?(context)
24
+ context.engine == :rubocop
25
+ end
26
+
27
+ def rules(_context)
28
+ LintRoller::Rules.new(
29
+ type: :path, config_format: :rubocop,
30
+ value: Pathname.new(File.expand_path("../../config/cops.yml", __dir__))
31
+ )
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,170 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+ require "json"
5
+ require "shellwords"
6
+
7
+ module QualityGate
8
+ module RubyProfileSupport
9
+ DEFAULT_COMMANDS = {
10
+ "minitest" => %w[bundle exec rake test].freeze,
11
+ "rspec" => %w[bundle exec rspec].freeze
12
+ }.freeze
13
+ private_constant :DEFAULT_COMMANDS
14
+
15
+ module_function
16
+
17
+ def options(value)
18
+ raise ArgumentError, "options must be a mapping" unless value.respond_to?(:to_h)
19
+
20
+ value.to_h.transform_keys(&:to_sym)
21
+ rescue NoMethodError
22
+ raise ArgumentError, "option keys must be symbols or strings"
23
+ end
24
+
25
+ def destination_root(value)
26
+ path = value.to_s
27
+ raise ArgumentError, "destination_root must be a non-empty directory" if path.empty?
28
+
29
+ expanded = File.expand_path(path)
30
+ return expanded if File.directory?(expanded)
31
+
32
+ raise ArgumentError, "destination_root must be an existing directory: #{path}"
33
+ end
34
+
35
+ def relative_path(root_path, value, name)
36
+ validate_path_value!(value, name)
37
+ raise ArgumentError, "#{name} must be relative to the project" if Pathname.new(value).absolute?
38
+
39
+ relative = Pathname.new(File.expand_path(value, root_path)).relative_path_from(Pathname.new(root_path)).to_s
40
+ return relative unless outside_project?(relative)
41
+
42
+ raise ArgumentError, "#{name} must stay inside the project"
43
+ end
44
+
45
+ def command(value, framework)
46
+ command = value.nil? ? DEFAULT_COMMANDS.fetch(framework) : parse_command(value)
47
+ validate_command!(command)
48
+ command.map(&:dup).freeze
49
+ end
50
+
51
+ def parse_command(value)
52
+ return value.dup if value.is_a?(Array)
53
+ return parse_shellwords(value) if value.is_a?(String)
54
+
55
+ raise ArgumentError, "test_command must be a command string or argv array"
56
+ end
57
+
58
+ def parse_shellwords(value)
59
+ Shellwords.split(value)
60
+ rescue ArgumentError => e
61
+ raise ArgumentError, "test_command could not be parsed: #{e.message}"
62
+ end
63
+
64
+ def validate_path_value!(value, name)
65
+ return if value.is_a?(String) && !value.empty?
66
+
67
+ raise ArgumentError, "#{name} must be a non-empty path"
68
+ end
69
+
70
+ def validate_command!(command)
71
+ valid = command.is_a?(Array) && command.any? && command.all? { _1.is_a?(String) && !_1.empty? }
72
+ return if valid
73
+
74
+ raise ArgumentError, "test_command must not be empty"
75
+ end
76
+
77
+ def outside_project?(relative)
78
+ relative == ".." || relative.start_with?("../")
79
+ end
80
+ end
81
+ private_constant :RubyProfileSupport
82
+
83
+ # Resolves the framework, helper, command, and templates for a plain Ruby project.
84
+ class RubyProfile
85
+ DEFAULT_HELPERS = {
86
+ "minitest" => "test/test_helper.rb",
87
+ "rspec" => "spec/spec_helper.rb"
88
+ }.freeze
89
+ TEMPLATE_MAP = {
90
+ "quality_gate.yml.tt" => "ruby_quality_gate.yml.tt",
91
+ "rubocop.yml.tt" => "ruby_rubocop.yml.tt",
92
+ "simplecov.rb.tt" => "ruby_simplecov.rb.tt",
93
+ "agents_section.md.tt" => "ruby_agents_section.md.tt"
94
+ }.freeze
95
+ SUPPORTED_FRAMEWORKS = DEFAULT_HELPERS.keys.freeze
96
+ private_constant :DEFAULT_HELPERS, :TEMPLATE_MAP, :SUPPORTED_FRAMEWORKS
97
+
98
+ attr_reader :destination_root, :test_helper, :test_command
99
+
100
+ def initialize(destination_root:, options: {})
101
+ @destination_root = RubyProfileSupport.destination_root(destination_root)
102
+ @options = RubyProfileSupport.options(options)
103
+ resolve_settings
104
+ end
105
+
106
+ def coverage? = @coverage
107
+
108
+ def template_for(name)
109
+ TEMPLATE_MAP.fetch(name, name)
110
+ end
111
+
112
+ private
113
+
114
+ attr_reader :options, :framework
115
+
116
+ def resolve_settings
117
+ validate_profile!
118
+ @coverage = !option(:skip_coverage)
119
+ @framework = resolve_framework
120
+ @test_helper = resolve_test_helper
121
+ @test_command = RubyProfileSupport.command(option(:test_command), framework)
122
+ end
123
+
124
+ def option(key)
125
+ options.fetch(key, nil)
126
+ end
127
+
128
+ def validate_profile!
129
+ return if (option(:profile) || "ruby").to_s == "ruby"
130
+
131
+ raise ArgumentError, "unsupported profile; only ruby is supported"
132
+ end
133
+
134
+ def resolve_framework
135
+ explicit = option(:test_framework)
136
+ return validate_framework(explicit) if explicit
137
+ return "minitest" if option(:test_helper)
138
+
139
+ detected_framework
140
+ end
141
+
142
+ def validate_framework(value)
143
+ framework = value.to_s
144
+ return framework if SUPPORTED_FRAMEWORKS.include?(framework)
145
+
146
+ raise ArgumentError, "test_framework must be minitest or rspec"
147
+ end
148
+
149
+ def detected_framework
150
+ available = DEFAULT_HELPERS.select { |_framework, helper| helper_present?(helper) }.keys
151
+ return available.first if available.one?
152
+ return "minitest" if available.empty?
153
+
154
+ raise ArgumentError, "both test helpers exist; pass --test-framework minitest or rspec"
155
+ end
156
+
157
+ def helper_present?(helper)
158
+ File.file?(File.join(destination_root, helper))
159
+ end
160
+
161
+ def resolve_test_helper
162
+ helper = option(:test_helper) || DEFAULT_HELPERS.fetch(framework)
163
+ relative = RubyProfileSupport.relative_path(destination_root, helper, "test_helper")
164
+ return relative if !coverage? || helper_present?(relative)
165
+
166
+ raise ArgumentError,
167
+ "test helper #{relative.inspect} does not exist; pass --skip-coverage or provide an existing helper"
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QualityGate
4
+ module RunnerValueCopy
5
+ module_function
6
+
7
+ def freeze_copy(value)
8
+ case value
9
+ when Array
10
+ value.map { freeze_copy(_1) }.freeze
11
+ when Hash
12
+ value.each_with_object({}) do |(key, child), copy|
13
+ copy[freeze_copy(key)] = freeze_copy(child)
14
+ end.freeze
15
+ when String
16
+ value.dup.freeze
17
+ else
18
+ value
19
+ end
20
+ end
21
+ end
22
+ private_constant :RunnerValueCopy
23
+
24
+ # Runs adapters in order and reduces their findings into one immutable result.
25
+ class Runner
26
+ Result = Data.define(:findings, :failed_tools, :checks) do
27
+ def initialize(findings:, checks: [])
28
+ copied_findings = findings.dup.freeze
29
+ failed_tools = copied_findings.select(&:tool_failure?).map(&:tool).uniq
30
+ copied_checks = RunnerValueCopy.freeze_copy(checks)
31
+
32
+ super(
33
+ findings: copied_findings,
34
+ failed_tools: failed_tools.map { _1.dup.freeze }.freeze,
35
+ checks: copied_checks
36
+ )
37
+ end
38
+
39
+ # 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
+ end
43
+
44
+ def exit_code
45
+ return ExitCode::TOOL_FAILURE if failed_tools.any?
46
+ return ExitCode::FINDINGS if findings.any?
47
+
48
+ ExitCode::CLEAN
49
+ end
50
+ end
51
+
52
+ CHECK_SCOPES = {
53
+ "rubocop" => "selected_files",
54
+ "reek" => "selected_files",
55
+ "test_suite" => "test_suite",
56
+ "undercover" => "git_diff",
57
+ "simplecov" => "coverage_summary",
58
+ "brakeman" => "project",
59
+ "bundler_audit" => "lockfile"
60
+ }.freeze
61
+ SELECTION_SUPPORTED_TOOLS = %w[rubocop reek].freeze
62
+ private_constant :CHECK_SCOPES, :SELECTION_SUPPORTED_TOOLS
63
+
64
+ def initialize(adapters:, config:, diagnostic_io: nil)
65
+ @adapters = adapters.dup.freeze
66
+ @config = config
67
+ @diagnostic_io = diagnostic_io
68
+ end
69
+
70
+ def call
71
+ findings = []
72
+ checks = []
73
+
74
+ adapters.each do |adapter|
75
+ tool = adapter_name(adapter)
76
+ write_progress(tool, scope_for(tool))
77
+ started_at = monotonic_time
78
+ adapter_findings = adapter.call
79
+ findings.concat(adapter_findings)
80
+ checks << check_for(tool, adapter_findings, elapsed_ms_since(started_at))
81
+ end
82
+
83
+ Result.new(findings:, checks:)
84
+ end
85
+
86
+ private
87
+
88
+ attr_reader :adapters, :config, :diagnostic_io
89
+
90
+ def adapter_name(adapter)
91
+ adapter.name.to_s
92
+ end
93
+
94
+ def check_for(tool, findings, duration_ms)
95
+ {
96
+ tool: tool,
97
+ status: status_for(findings),
98
+ scope: scope_for(tool),
99
+ requested_files: selection_supported?(tool) ? requested_files : [],
100
+ duration_ms: duration_ms
101
+ }
102
+ end
103
+
104
+ def selection_supported?(tool)
105
+ SELECTION_SUPPORTED_TOOLS.include?(tool)
106
+ end
107
+
108
+ def scope_for(tool)
109
+ scope = CHECK_SCOPES.fetch(tool, "unknown")
110
+ return "project" if scope == "selected_files" && requested_files.empty?
111
+
112
+ scope
113
+ end
114
+
115
+ def requested_files
116
+ config.fetch(:files).map(&:to_s)
117
+ end
118
+
119
+ def status_for(findings)
120
+ return "tool_failure" if findings.any?(&:tool_failure?)
121
+ return "skipped" if findings.any? { _1.rule == "undercover_skipped" }
122
+ return "findings" if findings.any?
123
+
124
+ "clean"
125
+ end
126
+
127
+ def elapsed_ms_since(started_at)
128
+ [(monotonic_time - started_at) * 1000, 0].max.round
129
+ end
130
+
131
+ def monotonic_time
132
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
133
+ end
134
+
135
+ def write_progress(tool, scope)
136
+ return unless diagnostic_io
137
+
138
+ diagnostic_io.puts("Running #{sanitize_progress(tool)} (scope: #{scope})...")
139
+ rescue StandardError
140
+ nil
141
+ end
142
+
143
+ def sanitize_progress(value)
144
+ value.to_s
145
+ .encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "�")
146
+ .gsub(/(?:[[:cntrl:]]|[\u2028\u2029])+/, " ")
147
+ .strip
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QualityGate
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "quality_gate/version"
4
+ require_relative "quality_gate/exit_code"
5
+ require_relative "quality_gate/finding"
6
+ require_relative "quality_gate/reporters/field_sanitizer"
7
+ require_relative "quality_gate/reporters/json"
8
+ require_relative "quality_gate/reporters/text"
9
+ require_relative "quality_gate/hook_log"
10
+
11
+ module QualityGate
12
+ class Error < StandardError; end
13
+ # Your code goes here...
14
+ end
15
+
16
+ require_relative "quality_gate/adapter"
17
+ require_relative "quality_gate/adapters/brakeman"
18
+ require_relative "quality_gate/adapters/bundler_audit"
19
+ require_relative "quality_gate/adapters/reek"
20
+ require_relative "quality_gate/adapters/rubocop"
21
+ require_relative "quality_gate/adapters/simplecov"
22
+ require_relative "quality_gate/adapters/test_suite"
23
+ require_relative "quality_gate/adapters/undercover"
24
+ require_relative "quality_gate/config"
25
+ require_relative "quality_gate/runner"
26
+ require_relative "quality_gate/cli"
27
+ require_relative "quality_gate/railtie" if defined?(Rails::Railtie)