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,290 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module QualityGate
6
+ # Reports a configuration file that could not be parsed or read.
7
+ class ConfigError < Error
8
+ attr_reader :path
9
+
10
+ def initialize(path:, cause_message:)
11
+ @path = path.dup.freeze
12
+ super("Could not load config #{@path}: #{cause_message}")
13
+ end
14
+ end
15
+
16
+ # Loads and exposes quality gate settings resolved against shipped defaults.
17
+ # rubocop:disable Metrics/ClassLength
18
+ class Config
19
+ DEFAULTS = {
20
+ format: "text",
21
+ files: [].freeze,
22
+ adapters: {
23
+ fast: ["rubocop"].freeze,
24
+ verify: %w[reek test_suite undercover].map!(&:freeze).freeze,
25
+ audit: %w[brakeman bundler_audit].map!(&:freeze).freeze
26
+ }.freeze,
27
+ commands: {
28
+ fast: {}.freeze,
29
+ verify: { test_suite: %w[bin/rails test].map!(&:freeze).freeze }.freeze,
30
+ audit: {}.freeze
31
+ }.freeze,
32
+ timeouts: { default: 120, rubocop: 10, test_suite: 120, undercover: 120 }.freeze,
33
+ coverage: nil,
34
+ compare_point: nil,
35
+ rubocop_config: nil
36
+ }.freeze
37
+ ADAPTER_LAYERS = %i[fast verify audit].freeze
38
+ COVERAGE_KEYS = %i[minimum_line minimum_branch].freeze
39
+ private_constant :ADAPTER_LAYERS, :COVERAGE_KEYS
40
+
41
+ class << self
42
+ def defaults = DEFAULTS
43
+
44
+ def load(dir:)
45
+ path = File.join(dir, ".quality_gate.yml")
46
+ return new(DEFAULTS) unless File.exist?(path)
47
+
48
+ load_file(path)
49
+ rescue Psych::Exception, SystemCallError, SystemStackError => e
50
+ fail ConfigError.new(path: path, cause_message: e.message) # rubocop:disable Style/SignalException
51
+ end
52
+
53
+ private
54
+
55
+ def symbolize_keys(value)
56
+ case value
57
+ when Hash
58
+ value.to_h { |key, child| [key.to_sym, symbolize_keys(child)] }
59
+ when Array
60
+ value.map { |child| symbolize_keys(child) }
61
+ else
62
+ value
63
+ end
64
+ end
65
+
66
+ def load_file(path)
67
+ document = YAML.safe_load(File.read(path))
68
+ validate_document(document, path)
69
+ overrides = symbolize_keys(document || {})
70
+ validate_known_settings(overrides, path)
71
+
72
+ new(
73
+ deep_merge(DEFAULTS, known_overrides(overrides)),
74
+ path: path,
75
+ unknown_keys: unknown_keys(overrides)
76
+ )
77
+ end
78
+
79
+ def known_overrides(overrides)
80
+ overrides.select { |key, _value| DEFAULTS.key?(key) }
81
+ end
82
+
83
+ def unknown_keys(overrides)
84
+ overrides.keys.reject { |key| DEFAULTS.key?(key) }.map(&:to_s)
85
+ end
86
+
87
+ def validate_known_settings(overrides, path) # rubocop:disable Metrics/AbcSize
88
+ validate_format(overrides[:format], path) if overrides.key?(:format)
89
+ validate_files(overrides[:files], path) if overrides.key?(:files)
90
+ validate_adapters(overrides[:adapters], path) if overrides.key?(:adapters)
91
+ validate_commands(overrides[:commands], path) if overrides.key?(:commands)
92
+ validate_coverage(overrides[:coverage], path) if overrides.key?(:coverage)
93
+ validate_compare_point(overrides[:compare_point], path) if overrides.key?(:compare_point)
94
+ validate_rubocop_config(overrides[:rubocop_config], path) if overrides.key?(:rubocop_config)
95
+ end
96
+
97
+ def validate_format(format, path)
98
+ return if %w[text json].include?(format)
99
+
100
+ fail ConfigError.new(path: path, cause_message: "format must be text or json") # rubocop:disable Style/SignalException
101
+ end
102
+
103
+ def validate_files(files, path)
104
+ unless files.is_a?(Array)
105
+ fail ConfigError.new(path: path, cause_message: "files must be an array") # rubocop:disable Style/SignalException
106
+ end
107
+
108
+ return if files.all? { |file| file.is_a?(String) && !file.empty? }
109
+
110
+ fail ConfigError.new( # rubocop:disable Style/SignalException
111
+ path: path,
112
+ cause_message: "files entries must be non-empty strings"
113
+ )
114
+ end
115
+
116
+ def validate_adapters(adapters, path)
117
+ unless adapters.is_a?(Hash)
118
+ fail ConfigError.new(path: path, cause_message: "adapters must be a mapping") # rubocop:disable Style/SignalException
119
+ end
120
+
121
+ invalid_keys = adapters.keys - ADAPTER_LAYERS
122
+ if invalid_keys.any?
123
+ fail ConfigError.new(path: path, cause_message: "adapters keys must be fast, verify, or audit") # rubocop:disable Style/SignalException
124
+ end
125
+
126
+ adapters.each do |layer, adapter_names|
127
+ validate_adapter_names(layer, adapter_names, path)
128
+ end
129
+ end
130
+
131
+ def validate_adapter_names(layer, adapter_names, path)
132
+ unless adapter_names.is_a?(Array)
133
+ fail ConfigError.new(path: path, cause_message: "adapters.#{layer} must be an array") # rubocop:disable Style/SignalException
134
+ end
135
+
136
+ return if adapter_names.all? { |name| name.is_a?(String) && !name.empty? }
137
+
138
+ fail ConfigError.new(path: path, cause_message: "adapters.#{layer} entries must be non-empty strings") # rubocop:disable Style/SignalException
139
+ end
140
+
141
+ def validate_commands(commands, path)
142
+ validate_layer_mapping(commands, "commands", path)
143
+
144
+ commands.each do |layer, layer_commands|
145
+ unless layer_commands.is_a?(Hash)
146
+ fail ConfigError.new(path: path, cause_message: "commands.#{layer} must be a mapping") # rubocop:disable Style/SignalException
147
+ end
148
+
149
+ layer_commands.each do |command_name, argv|
150
+ validate_command_argv(layer, command_name, argv, path)
151
+ end
152
+ end
153
+ end
154
+
155
+ def validate_layer_mapping(value, setting, path)
156
+ unless value.is_a?(Hash)
157
+ fail ConfigError.new(path: path, cause_message: "#{setting} must be a mapping") # rubocop:disable Style/SignalException
158
+ end
159
+
160
+ return if (value.keys - ADAPTER_LAYERS).empty?
161
+
162
+ cause = "#{setting} keys must be fast, verify, or audit"
163
+ fail ConfigError.new(path: path, cause_message: cause) # rubocop:disable Style/SignalException
164
+ end
165
+
166
+ def validate_command_argv(layer, command_name, argv, path)
167
+ unless argv.is_a?(Array) && argv.any?
168
+ cause = "commands.#{layer}.#{command_name} must be an argv array"
169
+ fail ConfigError.new(path: path, cause_message: cause) # rubocop:disable Style/SignalException
170
+ end
171
+
172
+ return if argv.all? { _1.is_a?(String) && !_1.empty? }
173
+
174
+ cause = "commands.#{layer}.#{command_name} entries must be non-empty strings"
175
+ fail ConfigError.new(path: path, cause_message: cause) # rubocop:disable Style/SignalException
176
+ end
177
+
178
+ def validate_compare_point(compare_point, path)
179
+ return if compare_point.nil?
180
+ return if compare_point.is_a?(String) && !compare_point.empty?
181
+
182
+ cause = "compare_point must be nil or a non-empty String"
183
+ fail ConfigError.new(path: path, cause_message: cause) # rubocop:disable Style/SignalException
184
+ end
185
+
186
+ def validate_coverage(coverage, path)
187
+ unless coverage.is_a?(Hash) && coverage.any?
188
+ cause = "coverage must be a non-empty mapping"
189
+ fail ConfigError.new(path: path, cause_message: cause) # rubocop:disable Style/SignalException
190
+ end
191
+
192
+ invalid_key = (coverage.keys - COVERAGE_KEYS).first
193
+ if invalid_key
194
+ cause = "unknown coverage key #{invalid_key}"
195
+ fail ConfigError.new(path: path, cause_message: cause) # rubocop:disable Style/SignalException
196
+ end
197
+
198
+ coverage.each do |key, value|
199
+ validate_coverage_value(key, value, path)
200
+ end
201
+ end
202
+
203
+ def validate_coverage_value(key, value, path)
204
+ unless value.is_a?(Numeric)
205
+ cause = "coverage.#{key} must be Numeric"
206
+ fail ConfigError.new(path: path, cause_message: cause) # rubocop:disable Style/SignalException
207
+ end
208
+
209
+ return if value.finite? && value.between?(0, 100)
210
+
211
+ cause = "coverage.#{key} must be within 0..100"
212
+ fail ConfigError.new(path: path, cause_message: cause) # rubocop:disable Style/SignalException
213
+ end
214
+
215
+ def validate_rubocop_config(rubocop_config, path)
216
+ return if rubocop_config.nil?
217
+ return if rubocop_config.is_a?(String) && !rubocop_config.empty?
218
+
219
+ fail ConfigError.new(path: path, cause_message: "rubocop_config must be nil or a non-empty String") # rubocop:disable Style/SignalException
220
+ end
221
+
222
+ def validate_document(document, path)
223
+ unless document.nil? || document.is_a?(Hash)
224
+ cause = "expected a mapping, got #{document.class}"
225
+ fail ConfigError.new(path: path, cause_message: cause) # rubocop:disable Style/SignalException
226
+ end
227
+
228
+ validate_keys(document, path)
229
+ end
230
+
231
+ def validate_keys(value, path)
232
+ case value
233
+ when Hash
234
+ value.each do |key, child|
235
+ validate_key(key, path)
236
+ validate_keys(child, path)
237
+ end
238
+ when Array
239
+ value.each { |child| validate_keys(child, path) }
240
+ end
241
+ end
242
+
243
+ def validate_key(key, path)
244
+ return if key.is_a?(String)
245
+
246
+ cause = "configuration keys must be strings, got #{key.class}"
247
+ fail ConfigError.new(path: path, cause_message: cause) # rubocop:disable Style/SignalException
248
+ end
249
+
250
+ def deep_merge(defaults, overrides)
251
+ defaults.merge(overrides) do |_key, default, override|
252
+ if default.is_a?(Hash) && override.is_a?(Hash)
253
+ deep_merge(default, override)
254
+ else
255
+ override
256
+ end
257
+ end
258
+ end
259
+ end
260
+
261
+ attr_reader :path, :unknown_keys
262
+
263
+ def initialize(settings, path: nil, unknown_keys: [])
264
+ @settings = deep_freeze(settings)
265
+ @path = path&.dup&.freeze
266
+ @unknown_keys = deep_freeze(unknown_keys)
267
+ end
268
+
269
+ def to_h = @settings
270
+
271
+ def fetch(key) = @settings.fetch(key)
272
+
273
+ private
274
+
275
+ def deep_freeze(value)
276
+ case value
277
+ when Hash
278
+ value.each do |key, child|
279
+ deep_freeze(key)
280
+ deep_freeze(child)
281
+ end
282
+ when Array
283
+ value.each { |child| deep_freeze(child) }
284
+ end
285
+
286
+ value.freeze
287
+ end
288
+ end
289
+ # rubocop:enable Metrics/ClassLength
290
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QualityGate
4
+ # Provides stable process-independent result codes for command execution.
5
+ module ExitCode
6
+ CLEAN = 0
7
+ FINDINGS = 1
8
+ TOOL_FAILURE = 2
9
+
10
+ @all = [CLEAN, FINDINGS, TOOL_FAILURE].freeze
11
+
12
+ def self.all = @all
13
+ end
14
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ QualityGate::Finding = Data.define(:tool, :file, :line, :rule, :severity, :message)
4
+
5
+ QualityGate::Finding::SEVERITIES = %i[error warning info].freeze
6
+ QualityGate::Finding::TOOL_FAILURE_RULE = "tool_failure"
7
+
8
+ QualityGate::Finding.class_eval do
9
+ def initialize(**attributes)
10
+ super(
11
+ **attributes,
12
+ tool: copy_string(:tool, attributes.fetch(:tool)),
13
+ file: copy_string(:file, attributes.fetch(:file)),
14
+ line: validate_line(attributes.fetch(:line)),
15
+ rule: copy_string(:rule, attributes.fetch(:rule)),
16
+ message: copy_string(:message, attributes.fetch(:message))
17
+ )
18
+ end
19
+
20
+ def self.tool_failure(tool:, message:)
21
+ new(
22
+ tool: tool,
23
+ file: "",
24
+ line: 0,
25
+ rule: self::TOOL_FAILURE_RULE,
26
+ severity: :error,
27
+ message: message
28
+ )
29
+ end
30
+
31
+ def with(**attributes) = self.class.new(**to_h.merge(attributes))
32
+
33
+ def tool_failure? = rule == self.class::TOOL_FAILURE_RULE
34
+
35
+ private
36
+
37
+ def copy_string(member, value)
38
+ raise TypeError, "#{member} must be a String" unless value.is_a?(String)
39
+
40
+ value.dup.freeze
41
+ end
42
+
43
+ def validate_line(value)
44
+ raise TypeError, "line must be an Integer" unless value.is_a?(Integer)
45
+ raise ArgumentError, "line must be greater than or equal to 0" if value.negative?
46
+
47
+ value
48
+ end
49
+ end
50
+ # rubocop:enable Metrics/BlockLength
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module QualityGate
6
+ # Reads the advisory health log written by the automatic hooks.
7
+ class HookLog
8
+ DEFAULT_PATH = "log/quality_gate_hooks.jsonl"
9
+ MAX_SCAN_BYTES = 1024 * 1024
10
+ MAX_LINE_BYTES = 64 * 1024
11
+ OUTCOMES = %w[
12
+ no_file skipped deleted clean findings unavailable
13
+ verify_skipped verify_debounced verify_clean verify_blocked verify_unavailable verify_cap
14
+ ].map!(&:freeze).freeze
15
+ private_constant :MAX_SCAN_BYTES, :MAX_LINE_BYTES, :OUTCOMES
16
+
17
+ attr_reader :path
18
+
19
+ def initialize(path:)
20
+ @path = path.dup.freeze
21
+ end
22
+
23
+ def recent(limit: 20)
24
+ return [] unless limit.positive?
25
+
26
+ with_log_file { |io| recent_records(io, limit) }
27
+ end
28
+
29
+ def unavailable_count(limit: 20)
30
+ recent(limit: limit).count { %w[unavailable verify_unavailable].include?(_1.fetch("outcome")) }
31
+ end
32
+
33
+ def warning_line(limit: 20)
34
+ count = unavailable_count(limit: limit)
35
+ return if count.zero?
36
+
37
+ "Warning: #{count} of the last #{limit} automatic checks could not run; " \
38
+ "check bundle install and the Quality Gate hook setup."
39
+ end
40
+
41
+ private
42
+
43
+ def with_log_file
44
+ expected = File.lstat(path)
45
+ return [] unless expected.file?
46
+
47
+ File.open(path, read_flags) do |io|
48
+ return [] unless same_regular_file?(io.stat, expected, File.lstat(path))
49
+
50
+ io.binmode
51
+ yield io
52
+ end
53
+ rescue SystemCallError, IOError
54
+ []
55
+ end
56
+
57
+ def recent_records(io, limit)
58
+ records = []
59
+ tail_bytes(io).split("\n".b).reverse_each do |line|
60
+ record = parse_line(line)
61
+ records << record if record
62
+ break if records.length >= limit
63
+ end
64
+ records.reverse
65
+ end
66
+
67
+ def read_flags
68
+ %i[NONBLOCK NOFOLLOW].reduce(File::RDONLY) do |flags, name|
69
+ File.const_defined?(name) ? flags | File.const_get(name) : flags
70
+ end
71
+ end
72
+
73
+ def same_regular_file?(opened, expected, current)
74
+ [opened, expected, current].all?(&:file?) &&
75
+ [opened.dev, opened.ino] == [expected.dev, expected.ino] &&
76
+ [opened.dev, opened.ino] == [current.dev, current.ino]
77
+ end
78
+
79
+ def tail_bytes(io)
80
+ size = io.stat.size
81
+ length = [size, MAX_SCAN_BYTES].min
82
+ offset = size - length
83
+ starts_at_boundary = line_boundary?(io, offset)
84
+ io.seek(offset, IO::SEEK_SET)
85
+ bytes = io.read(length) || "".b
86
+
87
+ discard_partial_line(bytes, starts_at_boundary)
88
+ end
89
+
90
+ def line_boundary?(io, offset)
91
+ return true if offset.zero?
92
+
93
+ io.seek(offset - 1, IO::SEEK_SET)
94
+ io.read(1) == "\n".b
95
+ end
96
+
97
+ def discard_partial_line(bytes, starts_at_boundary)
98
+ return bytes if starts_at_boundary
99
+
100
+ newline = bytes.index("\n".b)
101
+ return "".b unless newline
102
+
103
+ bytes.byteslice(newline + 1, bytes.bytesize - newline - 1)
104
+ end
105
+
106
+ def parse_line(line)
107
+ return if line.bytesize > MAX_LINE_BYTES
108
+
109
+ line = line.dup.force_encoding(Encoding::UTF_8)
110
+ return unless line.valid_encoding?
111
+
112
+ record = JSON.parse(line)
113
+ record if valid_record?(record)
114
+ rescue JSON::ParserError
115
+ nil
116
+ end
117
+
118
+ def valid_record?(record)
119
+ record.is_a?(Hash) &&
120
+ record["ts"].is_a?(String) &&
121
+ record.key?("file") &&
122
+ (record["file"].nil? || record["file"].is_a?(String)) &&
123
+ OUTCOMES.include?(record["outcome"]) &&
124
+ valid_duration?(record["duration_ms"])
125
+ end
126
+
127
+ def valid_duration?(duration)
128
+ duration.is_a?(Numeric) && duration.finite? && duration >= 0
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+
5
+ module QualityGate
6
+ # Parses and executes the framework-independent project initializer.
7
+ class InitCommand
8
+ class << self
9
+ def run(arguments, stdout:, stderr:, dir:)
10
+ installer(parse(arguments), dir: dir, stdout: stdout).call
11
+ rescue OptionParser::ParseError, ArgumentError => e
12
+ report_failure(e, stderr)
13
+ end
14
+
15
+ private
16
+
17
+ def parse(arguments)
18
+ reject_format!(arguments)
19
+ options = { profile: "ruby", skip_coverage: false, agents: false, pretend: false }
20
+ option_parser(options).parse!(arguments)
21
+ raise OptionParser::InvalidOption, arguments.join(" ") if arguments.any?
22
+
23
+ options
24
+ end
25
+
26
+ def installer(options, dir:, stdout:)
27
+ require_relative "installer"
28
+
29
+ Installer.new(destination_root: dir, options: options, stdout: stdout)
30
+ end
31
+
32
+ def reject_format!(arguments = nil)
33
+ return unless arguments.nil? || arguments.any? { _1 == "--format" || _1.start_with?("--format=") }
34
+
35
+ raise OptionParser::InvalidArgument, "init does not support --format; init output is text only"
36
+ end
37
+
38
+ def option_parser(options)
39
+ OptionParser.new.tap { |parser| parser_definitions(parser, options) }
40
+ end
41
+
42
+ def parser_definitions(parser, options)
43
+ profile_options(parser, options)
44
+ test_options(parser, options)
45
+ boolean_options(parser, options)
46
+ parser.on("--format FORMAT") { reject_format! }
47
+ parser.on("-h", "--help") { options[:help] = true }
48
+ end
49
+
50
+ def profile_options(parser, options)
51
+ parser.on("--profile PROFILE", %w[ruby]) { |profile| options[:profile] = profile }
52
+ parser.on("--test-framework FRAMEWORK", %w[minitest rspec]) do |framework|
53
+ options[:test_framework] = framework
54
+ end
55
+ end
56
+
57
+ def test_options(parser, options)
58
+ parser.on("--test-helper PATH") { |path| options[:test_helper] = path }
59
+ parser.on("--test-command COMMAND") { |command| options[:test_command] = command }
60
+ end
61
+
62
+ def boolean_options(parser, options)
63
+ parser.on("--skip-coverage") { options[:skip_coverage] = true }
64
+ parser.on("--agents") { options[:agents] = true }
65
+ parser.on("--pretend") { options[:pretend] = true }
66
+ end
67
+
68
+ def report_failure(error, stderr)
69
+ report_error(error, stderr)
70
+ write_line(stderr, "Usage: quality_gate init [options]") if error.is_a?(OptionParser::ParseError)
71
+ ExitCode::TOOL_FAILURE
72
+ end
73
+
74
+ def report_error(error, stderr)
75
+ write_line(stderr, "Error: #{error.message}")
76
+ ExitCode::TOOL_FAILURE
77
+ rescue StandardError
78
+ write_line(stderr, "Error: quality_gate init failed")
79
+ ExitCode::TOOL_FAILURE
80
+ end
81
+
82
+ def write_line(io, message)
83
+ io.puts(message)
84
+ true
85
+ rescue StandardError
86
+ false
87
+ end
88
+ end
89
+ end
90
+ end