quality_gate 0.1.1 → 0.2.1
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 +10 -0
- data/README.md +58 -5
- 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 +42 -22
- 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: 661dea9969d01c65c60ef01523cbdfc2ebae74be253bd3f78b5420b34aad934a
|
|
4
|
+
data.tar.gz: 6a34cb55d86a89885c635dd0c1b77d203c5432c4f85035b232510e23baa1aad2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0475a1a8e435e68effb9ea72bfbe3a82c69be9fb66e9c9e14c6cb983aec9354adb2754848a181dba35a128032e6131b1a3ed852defe5f838e1cb8d90589cc05a
|
|
7
|
+
data.tar.gz: 017605cf3e353c4e1861f03fd0e7fa2ac1ee1c6cedb6ba8fdfdbe99827bf1d186997b3ac794774f0259a23eb36b20a08f6a5957c8e67b7a43d65860755af4399
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
## [0.2.1] - 2026-09-22
|
|
2
|
+
|
|
3
|
+
- Undercover reports stale coverage as an actionable `stale_coverage` finding with gate exit 1, telling operators to re-run the test suite before the coverage gate, instead of a parse failure with exit 2. Unknown validation reasons also fail closed and include their value; malformed output and CLI exit-status checks remain strict.
|
|
4
|
+
|
|
5
|
+
## [0.2.0] - 2026-09-21
|
|
6
|
+
|
|
7
|
+
- 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.
|
|
8
|
+
- 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.
|
|
9
|
+
- The repository's own `.undercover` keeps Undercover's default test exclusions next to the version file exclusion.
|
|
10
|
+
|
|
1
11
|
## [0.1.1] - 2026-09-10
|
|
2
12
|
|
|
3
13
|
- 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
|
|
|
@@ -563,14 +581,49 @@ Clone the repository and install its dependencies:
|
|
|
563
581
|
bin/setup
|
|
564
582
|
```
|
|
565
583
|
|
|
566
|
-
Run the tests, lint checks, or complete default task:
|
|
584
|
+
Run the tests in two separate Ruby processes, lint checks, or complete serial default task:
|
|
567
585
|
|
|
568
586
|
```sh
|
|
569
|
-
bundle exec rake test
|
|
587
|
+
bundle exec rake test:parallel
|
|
570
588
|
bundle exec rubocop
|
|
571
589
|
bundle exec rake
|
|
572
590
|
```
|
|
573
591
|
|
|
592
|
+
`TEST_WORKERS=4 bundle exec rake test:parallel` changes the worker count.
|
|
593
|
+
`bundle exec rake test` preserves the serial runner and its Minitest filtering
|
|
594
|
+
options; `test:parallel` accepts the same `N`, `X`, and `A` filters. Named latency
|
|
595
|
+
checks (`QUALITY_GATE_ACCEPTANCE_TIMING=1`) always use one worker so concurrent
|
|
596
|
+
tests do not distort their measurements.
|
|
597
|
+
|
|
598
|
+
This repository's `quality_gate verify` uses the parallel task. Each worker writes
|
|
599
|
+
isolated coverage results; the parent merges them and generates reports only after
|
|
600
|
+
every worker succeeds and supplies coverage. `TEST_WORKERS=1 bundle exec quality_gate verify`
|
|
601
|
+
runs the same coverage pipeline with one worker. CI runs RuboCop separately and
|
|
602
|
+
lets verify run the full suite once per Ruby version.
|
|
603
|
+
|
|
604
|
+
To investigate test performance with [TestProf](https://github.com/test-prof/test-prof),
|
|
605
|
+
run the serial suite without coverage instrumentation:
|
|
606
|
+
|
|
607
|
+
```sh
|
|
608
|
+
QUALITY_GATE_PROFILE=1 COVERAGE=0 bundle exec rake test
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
This opt-in run prints a TagProf breakdown by test directory and an EventProf
|
|
612
|
+
report for `subprocess.quality_gate`, including the ten slowest suites and tests.
|
|
613
|
+
The event measures synchronous `Open3.capture2`, `capture2e`, and `capture3`
|
|
614
|
+
calls, including time waiting for child processes. It does not measure work
|
|
615
|
+
inside those processes or the adapter's separate `popen`/wait implementation.
|
|
616
|
+
The profiling switch is consumed before tests run so nested test projects keep
|
|
617
|
+
their normal output. Ordinary runs do not enable the profilers, and TestProf is
|
|
618
|
+
only a development/test dependency.
|
|
619
|
+
|
|
620
|
+
Use the serial task for a single report; parallel profiling produces a separate
|
|
621
|
+
report per worker. Existing Minitest filters still apply, for example:
|
|
622
|
+
|
|
623
|
+
```sh
|
|
624
|
+
QUALITY_GATE_PROFILE=1 COVERAGE=0 bundle exec rake test N=/test_clean_fixture/
|
|
625
|
+
```
|
|
626
|
+
|
|
574
627
|
The gem is linted by the configuration it ships, so the Sandi Metz budgets apply to its own code. Violations that predate those budgets are frozen file by file in `.rubocop_todo.yml`, which keeps the gate green while forcing new code to meet the budgets. That file is a ratchet: it may only shrink. When you change a file listed there, bring it under the budget and delete its entry; never add one. Regenerate it only after such a burn-down:
|
|
575
628
|
|
|
576
629
|
```sh
|
|
@@ -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
|
|
|
@@ -36,8 +39,7 @@ module QualityGate
|
|
|
36
39
|
|
|
37
40
|
stdout, stderr, status = capture_with_remaining_timeout(command)
|
|
38
41
|
validate_cli_envelope!(stdout, stderr, status)
|
|
39
|
-
findings = parse(stdout)
|
|
40
|
-
validate_cli_result!(status.exitstatus, findings)
|
|
42
|
+
findings = parse(stdout, exitstatus: status.exitstatus)
|
|
41
43
|
validate_findings!(findings, expected_tool: tool)
|
|
42
44
|
findings
|
|
43
45
|
rescue StandardError => e
|
|
@@ -46,11 +48,13 @@ module QualityGate
|
|
|
46
48
|
clear_call_budget
|
|
47
49
|
end
|
|
48
50
|
|
|
49
|
-
def parse(stdout)
|
|
51
|
+
def parse(stdout, exitstatus: nil)
|
|
50
52
|
document = parsed_document(stdout)
|
|
51
53
|
warnings, summary = report_parts(document)
|
|
52
54
|
findings = warnings.map { finding_from(_1) }
|
|
53
55
|
validate_summary!(summary, warnings)
|
|
56
|
+
validate_cli_result!(exitstatus, warnings)
|
|
57
|
+
findings << validation_finding(document["validation"]) unless document["validation"].nil?
|
|
54
58
|
findings
|
|
55
59
|
rescue JSON::ParserError, KeyError, TypeError, ArgumentError => e
|
|
56
60
|
raise ParseError.new(tool: name, reason: e.message)
|
|
@@ -94,8 +98,10 @@ module QualityGate
|
|
|
94
98
|
cli_contract_error!("Undercover output is missing its terminal timing footer")
|
|
95
99
|
end
|
|
96
100
|
|
|
97
|
-
def validate_cli_result!(exitstatus,
|
|
98
|
-
|
|
101
|
+
def validate_cli_result!(exitstatus, warnings)
|
|
102
|
+
return if exitstatus.nil?
|
|
103
|
+
|
|
104
|
+
expected_exitstatus = warnings.empty? ? 0 : 1
|
|
99
105
|
return if exitstatus == expected_exitstatus
|
|
100
106
|
|
|
101
107
|
cli_contract_error!("Undercover exit status #{exitstatus} disagrees with its warning count")
|
|
@@ -117,7 +123,6 @@ module QualityGate
|
|
|
117
123
|
summary = document.fetch("summary")
|
|
118
124
|
raise TypeError, "warnings must be an Array" unless warnings.is_a?(Array)
|
|
119
125
|
raise TypeError, "summary must be a mapping" unless summary.is_a?(Hash)
|
|
120
|
-
raise TypeError, "validation must be nil" unless document["validation"].nil?
|
|
121
126
|
|
|
122
127
|
[warnings, summary]
|
|
123
128
|
end
|
|
@@ -142,9 +147,31 @@ module QualityGate
|
|
|
142
147
|
)
|
|
143
148
|
end
|
|
144
149
|
|
|
150
|
+
def validation_finding(validation)
|
|
151
|
+
raise TypeError, "validation must be a String or nil" unless validation.is_a?(String)
|
|
152
|
+
|
|
153
|
+
Finding.new(
|
|
154
|
+
tool: name,
|
|
155
|
+
file: "",
|
|
156
|
+
line: 0,
|
|
157
|
+
rule: validation == "stale_coverage" ? "stale_coverage" : "undercover_validation",
|
|
158
|
+
severity: :error,
|
|
159
|
+
message: validation_message(validation)
|
|
160
|
+
)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def validation_message(validation)
|
|
164
|
+
if validation == "stale_coverage"
|
|
165
|
+
"Undercover coverage data predates the changed files. Re-run the test suite before the coverage gate."
|
|
166
|
+
else
|
|
167
|
+
"Undercover validation failed: #{validation.inspect}. Coverage cannot be trusted; " \
|
|
168
|
+
"resolve the validation issue and re-run the test suite before the coverage gate."
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
145
172
|
def skip_reason
|
|
146
173
|
return "Undercover skipped: repository is shallow; set compare_point explicitly." if shallow_repository?
|
|
147
|
-
return detached_reason if detached_head?
|
|
174
|
+
return detached_reason if detached_head? && default_branch_present?
|
|
148
175
|
return first_commit_reason if first_commit?
|
|
149
176
|
return missing_branch_reason unless default_branch_present?
|
|
150
177
|
|
|
@@ -201,7 +228,7 @@ module QualityGate
|
|
|
201
228
|
def default_branch_ref
|
|
202
229
|
return @default_branch_ref if defined?(@default_branch_ref)
|
|
203
230
|
|
|
204
|
-
@default_branch_ref = remote_default_branch ||
|
|
231
|
+
@default_branch_ref = remote_default_branch || named_default_branch
|
|
205
232
|
end
|
|
206
233
|
|
|
207
234
|
def remote_default_branch
|
|
@@ -217,19 +244,12 @@ module QualityGate
|
|
|
217
244
|
branch
|
|
218
245
|
end
|
|
219
246
|
|
|
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"
|
|
247
|
+
# Local branches win, but a CI pull request checkout often has only the remote-tracking ref.
|
|
248
|
+
def named_default_branch
|
|
249
|
+
ref = NAMED_DEFAULT_BRANCH_REFS.find { commit_exists?(_1) }
|
|
250
|
+
@default_branch_present = !ref.nil?
|
|
251
|
+
@default_branch_name = ref ? ref.split("/").last : "main"
|
|
252
|
+
ref || "refs/heads/main"
|
|
233
253
|
end
|
|
234
254
|
|
|
235
255
|
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.1
|
|
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
|