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 +4 -4
- data/CHANGELOG.md +6 -0
- data/README.md +21 -3
- data/lib/generators/quality_gate/install/templates/quality_gate.yml.tt +1 -1
- data/lib/generators/quality_gate/install/templates/ruby_quality_gate.yml.tt +1 -1
- data/lib/quality_gate/adapters/undercover.rb +12 -16
- data/lib/quality_gate/cli.rb +9 -9
- data/lib/quality_gate/config.rb +3 -2
- data/lib/quality_gate/reporters/markdown.rb +83 -0
- data/lib/quality_gate/version.rb +1 -1
- data/lib/quality_gate.rb +1 -0
- data/sig/quality_gate.rbs +5 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dc4fe18802dfc3fb409d11c8db6eb816df282e1914cd143807436998724eaf19
|
|
4
|
+
data.tar.gz: f1529c7f7cf1959a889fcc8a88691da70853ccb36e499718a5d56918e58add21
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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 `
|
|
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
|
|
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
|
|
|
@@ -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
|
-
|
|
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 ||
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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)
|
data/lib/quality_gate/cli.rb
CHANGED
|
@@ -166,7 +166,7 @@ module QualityGate
|
|
|
166
166
|
|
|
167
167
|
overrides[:files] = [path]
|
|
168
168
|
end
|
|
169
|
-
parser.on("--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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
399
|
+
return format if Config::FORMATS.include?(format)
|
|
400
400
|
|
|
401
401
|
nil
|
|
402
402
|
rescue Psych::Exception, SystemCallError, SystemStackError
|
data/lib/quality_gate/config.rb
CHANGED
|
@@ -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
|
|
99
|
+
return if FORMATS.include?(format)
|
|
99
100
|
|
|
100
|
-
fail ConfigError.new(path: path, cause_message: "format must be text or
|
|
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
|
data/lib/quality_gate/version.rb
CHANGED
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.
|
|
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
|