quality_gate 0.1.1 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5c2f18d9b5a74e7d19838e915bb21ab23fe18cb1d2179e0279ec848dec71e9ee
4
- data.tar.gz: 8f6d82509a688cf01fb92186caca89a4c330470e05b9a5fb3200fa4c28339b55
3
+ metadata.gz: dc4fe18802dfc3fb409d11c8db6eb816df282e1914cd143807436998724eaf19
4
+ data.tar.gz: f1529c7f7cf1959a889fcc8a88691da70853ccb36e499718a5d56918e58add21
5
5
  SHA512:
6
- metadata.gz: '0938c529aa703c3e0cdeaad45996e715a58b0554f4b87483d1741816056a10012567ef00410d287d15fe8d39996bf99a7cb750101275ec9b7423a044a2bf5e3d'
7
- data.tar.gz: 5d822362f3278a6d219401b65379b1d4217e28db160991e7014c25e6a45403c5bd2c1f62df5766d50105d5ae40e80c50c02b5e54619ed0169dc0db723c271d77
6
+ metadata.gz: f5d316d6426037588e9b895ed9f0d0a18d6f59e06b6666d8b37fd079af1bab0dddf991c1b756385ef90a6f5ae562da322f65a704f55d71b302adf5d9de90c63b
7
+ data.tar.gz: 2a21cb8513b65b1fb5fee6187bb303ae1ece9f575c9d1ae0c46c7cff5e2f27be7e12a88c6543cb9c29fb10fb636e02d0ebd4bab5b246c6d5ba6f99f51505ab39
data/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ ## [0.2.0] - 2026-09-21
2
+
3
+ - Gate commands accept `--format markdown` and `format: markdown`, printing a heading with the tally, a checks table, and a findings list for agents and pull request comments.
4
+ - Undercover falls back to `origin/main` or `origin/master` when `origin/HEAD` and the local default branch are absent, so verify checks the diff on CI pull request checkouts instead of skipping. A missing default branch is now reported as missing rather than as a detached checkout.
5
+ - The repository's own `.undercover` keeps Undercover's default test exclusions next to the version file exclusion.
6
+
1
7
  ## [0.1.1] - 2026-09-10
2
8
 
3
9
  - Text gate output now lists each tool that ran with its status, scope, and duration, so a clean run is distinguishable from a run where nothing executed.
data/README.md CHANGED
@@ -82,15 +82,16 @@ rubocop clean project 842ms
82
82
 
83
83
  `verify --files app/models/user.rb` therefore narrows Reek, but still runs the full suite and checks Git changes. JSON includes each adapter's scope and status. When stderr is a terminal, progress names each tool before it starts; redirected output and hook invocations remain quiet except for diagnostics.
84
84
 
85
- Choose text or JSON output with `--format text` or `--format json`:
85
+ Choose text, JSON, or Markdown output with `--format text`, `--format json`, or `--format markdown`:
86
86
 
87
87
  ```sh
88
88
  bundle exec quality_gate verify --format json
89
+ bundle exec quality_gate verify --format markdown
89
90
  ```
90
91
 
91
92
  The shorter form `quality_gate version` works when the installed executable is already on your `PATH`.
92
93
 
93
- The repository config must also use exactly `text` or `json` for `format`; any other configured value is rejected as a configuration error before a gate runs.
94
+ The repository config must also use exactly `text`, `json`, or `markdown` for `format`; any other configured value is rejected as a configuration error before a gate runs.
94
95
 
95
96
  ## Output contract
96
97
 
@@ -171,6 +172,23 @@ When `--format json` is requested, input/configuration failures use the same rep
171
172
 
172
173
  Consumers should tolerate additive JSON fields. Existing finding and summary fields retain their meanings. An informational Undercover skip remains a finding and returns exit `1`; it is not proof that changed code was covered.
173
174
 
175
+ Markdown output is for pasting into agent notes, pull request comments, and chat. It prints a level-two heading with the tally, a checks table when at least one tool ran, and a findings list when there are findings:
176
+
177
+ ```markdown
178
+ ## Quality Gate: 1 findings, 0 tool failures
179
+
180
+ | Tool | Status | Scope | Duration |
181
+ | --- | --- | --- | --- |
182
+ | rubocop | findings | selected_files | 1240ms |
183
+
184
+ ### Findings
185
+
186
+ - **rubocop** warning `lib/a.rb:4` Layout/First: First message
187
+ Second message line
188
+ ```
189
+
190
+ Locations are code spans, extra message lines are indented under their finding, and pipe characters in table cells are escaped. Message punctuation is escaped so diagnostic HTML and Markdown markers display literally. Input and configuration errors keep the plain-text stderr behaviour; only JSON has a machine-readable error envelope.
191
+
174
192
  ## Configuration
175
193
 
176
194
  QualityGate looks for `.quality_gate.yml` in the working repository. Values in that file override the shipped defaults for known top-level keys:
@@ -374,7 +392,7 @@ The default verify adapters always run in order: Reek reports code smells, the t
374
392
 
375
393
  Reek uses an existing host `.reek.yml` when present and otherwise uses the shipped configuration. It reports smells as warnings and respects `--files`; without selected files it scans the project. Missing selected files are rejected by the CLI before any adapter runs.
376
394
 
377
- Undercover compares against the configured `compare_point` when present. Otherwise it uses the merge base of `HEAD` and the detected default branch: `refs/remotes/origin/HEAD` when available, then local `main`, then local `master`. If history is shallow, the default branch is missing, there is no earlier commit, or a detached checkout has no shared ancestor, verify reports an informational `undercover_skipped` finding with the reason instead of silently passing.
395
+ Undercover compares against the configured `compare_point` when present. Otherwise it uses the merge base of `HEAD` and the detected default branch: `refs/remotes/origin/HEAD` when available, then `main` (local, then `origin/main`), then `master` (local, then `origin/master`). The remote-tracking fallback covers CI pull request checkouts, which are detached and fetch the default branch only as `origin/main`. If history is shallow, the default branch is missing, there is no earlier commit, or a detached checkout has no shared ancestor, verify reports an informational `undercover_skipped` finding with the reason instead of silently passing.
378
396
 
379
397
  Each uncovered region is a warning that names its file, first line, full line range, node, uncovered lines, and uncovered branch context. If the coverage record is absent, verify reports a tool failure explaining that the SimpleCov wiring is missing. Install it with `quality_gate init --profile ruby` or the Rails generator. The test-suite and Undercover adapters ignore `--files`: the suite and the Git change define their scope.
380
398
 
@@ -1,4 +1,4 @@
1
- # # Output format: "text" or "json".
1
+ # # Output format: "text", "json", or "markdown".
2
2
  # format: text
3
3
  #
4
4
  # # Optional file paths passed to adapters; empty means no configured restriction.
@@ -1,4 +1,4 @@
1
- # # Output format: "text" or "json".
1
+ # # Output format: "text", "json", or "markdown".
2
2
  # format: text
3
3
  #
4
4
  # # Optional file paths passed to RuboCop and Reek; empty means the project scope.
@@ -10,7 +10,10 @@ module QualityGate
10
10
  COVERAGE_PATH = "coverage/coverage.json"
11
11
  SKIP_RULE = "undercover_skipped"
12
12
  CLI_FOOTER_PATTERN = /\nUndercover finished in \d+(?:\.\d+)?s\n\z/
13
- private_constant :CLI_FOOTER_PATTERN
13
+ NAMED_DEFAULT_BRANCH_REFS = %w[main master].flat_map do |branch|
14
+ ["refs/heads/#{branch}", "refs/remotes/origin/#{branch}"]
15
+ end.freeze
16
+ private_constant :CLI_FOOTER_PATTERN, :NAMED_DEFAULT_BRANCH_REFS
14
17
 
15
18
  def name = "undercover"
16
19
 
@@ -144,7 +147,7 @@ module QualityGate
144
147
 
145
148
  def skip_reason
146
149
  return "Undercover skipped: repository is shallow; set compare_point explicitly." if shallow_repository?
147
- return detached_reason if detached_head?
150
+ return detached_reason if detached_head? && default_branch_present?
148
151
  return first_commit_reason if first_commit?
149
152
  return missing_branch_reason unless default_branch_present?
150
153
 
@@ -201,7 +204,7 @@ module QualityGate
201
204
  def default_branch_ref
202
205
  return @default_branch_ref if defined?(@default_branch_ref)
203
206
 
204
- @default_branch_ref = remote_default_branch || local_default_branch
207
+ @default_branch_ref = remote_default_branch || named_default_branch
205
208
  end
206
209
 
207
210
  def remote_default_branch
@@ -217,19 +220,12 @@ module QualityGate
217
220
  branch
218
221
  end
219
222
 
220
- def local_default_branch
221
- %w[main master].each do |branch|
222
- ref = "refs/heads/#{branch}"
223
- next unless commit_exists?(ref)
224
-
225
- @default_branch_present = true
226
- @default_branch_name = branch
227
- return ref
228
- end
229
-
230
- @default_branch_present = false
231
- @default_branch_name = "main"
232
- "refs/heads/main"
223
+ # Local branches win, but a CI pull request checkout often has only the remote-tracking ref.
224
+ def named_default_branch
225
+ ref = NAMED_DEFAULT_BRANCH_REFS.find { commit_exists?(_1) }
226
+ @default_branch_present = !ref.nil?
227
+ @default_branch_name = ref ? ref.split("/").last : "main"
228
+ ref || "refs/heads/main"
233
229
  end
234
230
 
235
231
  def merge_base(branch)
@@ -166,7 +166,7 @@ module QualityGate
166
166
 
167
167
  overrides[:files] = [path]
168
168
  end
169
- parser.on("--format FORMAT", %w[text json]) { |format| overrides[:format] = format }
169
+ parser.on("--format FORMAT", Config::FORMATS) { |format| overrides[:format] = format }
170
170
  parser.on("-h", "--help") { overrides[:help] = true }
171
171
  end
172
172
  end
@@ -241,10 +241,9 @@ module QualityGate
241
241
 
242
242
  def reporter_for(format, stdout:)
243
243
  case format
244
- when "json"
245
- Reporters::Json.new(io: stdout)
246
- else
247
- Reporters::Text.new(io: stdout)
244
+ when "json" then Reporters::Json.new(io: stdout)
245
+ when "markdown" then Reporters::Markdown.new(io: stdout)
246
+ else Reporters::Text.new(io: stdout)
248
247
  end
249
248
  end
250
249
 
@@ -296,7 +295,7 @@ module QualityGate
296
295
 
297
296
  Options:
298
297
  --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)
298
+ --format FORMAT Choose text, json, or markdown output (also accepts --format=FORMAT)
300
299
  -h, --help Show this help
301
300
 
302
301
  Scope:
@@ -308,6 +307,7 @@ module QualityGate
308
307
  quality_gate fast
309
308
  quality_gate fast --files app/models/user.rb test/models
310
309
  quality_gate verify --format json
310
+ quality_gate verify --format markdown
311
311
 
312
312
  Exit status:
313
313
  0 Checks completed cleanly
@@ -326,7 +326,7 @@ module QualityGate
326
326
 
327
327
  Options:
328
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)
329
+ --format FORMAT Choose text, json, or markdown output (also accepts --format=FORMAT)
330
330
  -h, --help Show this help
331
331
 
332
332
  Scope:
@@ -362,7 +362,7 @@ module QualityGate
362
362
  --pretend Preview changes without writing them
363
363
  -h, --help Show this help
364
364
 
365
- Init writes a human-readable summary. Gate commands continue to support text and JSON output.
365
+ Init writes a human-readable summary. Gate commands continue to support text, JSON, and Markdown output.
366
366
 
367
367
  Examples:
368
368
  quality_gate init
@@ -396,7 +396,7 @@ module QualityGate
396
396
  contents = File.read(path)
397
397
  document = YAML.safe_load(contents)
398
398
  format = document.is_a?(Hash) && document["format"]
399
- return format if %w[text json].include?(format)
399
+ return format if Config::FORMATS.include?(format)
400
400
 
401
401
  nil
402
402
  rescue Psych::Exception, SystemCallError, SystemStackError
@@ -34,6 +34,7 @@ module QualityGate
34
34
  compare_point: nil,
35
35
  rubocop_config: nil
36
36
  }.freeze
37
+ FORMATS = %w[text json markdown].map!(&:freeze).freeze
37
38
  ADAPTER_LAYERS = %i[fast verify audit].freeze
38
39
  COVERAGE_KEYS = %i[minimum_line minimum_branch].freeze
39
40
  private_constant :ADAPTER_LAYERS, :COVERAGE_KEYS
@@ -95,9 +96,9 @@ module QualityGate
95
96
  end
96
97
 
97
98
  def validate_format(format, path)
98
- return if %w[text json].include?(format)
99
+ return if FORMATS.include?(format)
99
100
 
100
- fail ConfigError.new(path: path, cause_message: "format must be text or json") # rubocop:disable Style/SignalException
101
+ fail ConfigError.new(path: path, cause_message: "format must be text, json, or markdown") # rubocop:disable Style/SignalException
101
102
  end
102
103
 
103
104
  def validate_files(files, path)
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QualityGate
4
+ module Reporters
5
+ # Writes Markdown reports that agents and humans can paste into notes and reviews.
6
+ class Markdown
7
+ TABLE_HEADER = ["| Tool | Status | Scope | Duration |", "| --- | --- | --- | --- |"].freeze
8
+
9
+ def initialize(io:)
10
+ @io = io
11
+ end
12
+
13
+ def call(result)
14
+ io.puts(sections_for(result).join("\n\n"))
15
+ end
16
+
17
+ private
18
+
19
+ attr_reader :io
20
+
21
+ def sections_for(result)
22
+ [summary_heading(result), checks_table(result.checks), findings_section(result.findings)].compact
23
+ end
24
+
25
+ def summary_heading(result)
26
+ "## Quality Gate: #{result.findings.length} findings, #{result.failed_tools.length} tool failures"
27
+ end
28
+
29
+ def checks_table(checks)
30
+ return if checks.empty?
31
+
32
+ [*TABLE_HEADER, *checks.map { render_check(_1) }].join("\n")
33
+ end
34
+
35
+ def render_check(check)
36
+ cells = [check[:tool], check[:status], check[:scope]].map { table_cell(_1) }
37
+ "| #{cells.join(" | ")} | #{duration_ms_for(check)}ms |"
38
+ end
39
+
40
+ def duration_ms_for(check)
41
+ Integer(check[:duration_ms], exception: false) || 0
42
+ end
43
+
44
+ def table_cell(value)
45
+ FieldSanitizer.for_text(value).gsub("|", "\\|")
46
+ end
47
+
48
+ def findings_section(findings)
49
+ return if findings.empty?
50
+
51
+ ["### Findings", "", *findings.map { render_finding(_1) }].join("\n")
52
+ end
53
+
54
+ def render_finding(finding)
55
+ message = FieldSanitizer.for_text_message(finding.message).gsub(/[[:punct:]&&[:ascii:]]/) { "\\#{_1}" }
56
+ lines = message.split("\n", -1)
57
+ ["- #{finding_prefix(finding)}: #{lines.first}", *lines.drop(1).map { " #{_1}" }].join("\n")
58
+ end
59
+
60
+ def finding_prefix(finding)
61
+ tool = "**#{FieldSanitizer.for_text(finding.tool)}**"
62
+ severity = FieldSanitizer.for_text(finding.severity.to_s)
63
+ [tool, severity, finding_location(finding), FieldSanitizer.for_text(finding.rule)].compact.join(" ")
64
+ end
65
+
66
+ def finding_location(finding)
67
+ file = FieldSanitizer.for_text(finding.file)
68
+ return if file.empty?
69
+
70
+ code_span(finding.line.zero? ? file : "#{file}:#{finding.line}")
71
+ end
72
+
73
+ def code_span(text)
74
+ fence = "`" * (longest_backtick_run(text) + 1)
75
+ fence.length == 1 ? "`#{text}`" : "#{fence} #{text} #{fence}"
76
+ end
77
+
78
+ def longest_backtick_run(text)
79
+ text.scan(/`+/).map(&:length).max.to_i
80
+ end
81
+ end
82
+ end
83
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module QualityGate
4
- VERSION = "0.1.1"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/quality_gate.rb CHANGED
@@ -5,6 +5,7 @@ require_relative "quality_gate/exit_code"
5
5
  require_relative "quality_gate/finding"
6
6
  require_relative "quality_gate/reporters/field_sanitizer"
7
7
  require_relative "quality_gate/reporters/json"
8
+ require_relative "quality_gate/reporters/markdown"
8
9
  require_relative "quality_gate/reporters/text"
9
10
  require_relative "quality_gate/hook_log"
10
11
 
data/sig/quality_gate.rbs CHANGED
@@ -189,6 +189,11 @@ module QualityGate
189
189
  def initialize: (io: _Output) -> void
190
190
  def call: (Runner::Result result) -> nil
191
191
  end
192
+
193
+ class Markdown
194
+ def initialize: (io: _Output) -> void
195
+ def call: (Runner::Result result) -> nil
196
+ end
192
197
  end
193
198
 
194
199
  type finding_severity = :error | :warning | :info
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: quality_gate
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lucian Ghinda
@@ -219,6 +219,7 @@ files:
219
219
  - lib/quality_gate/railtie.rb
220
220
  - lib/quality_gate/reporters/field_sanitizer.rb
221
221
  - lib/quality_gate/reporters/json.rb
222
+ - lib/quality_gate/reporters/markdown.rb
222
223
  - lib/quality_gate/reporters/text.rb
223
224
  - lib/quality_gate/rubocop.rb
224
225
  - lib/quality_gate/ruby_profile.rb