Skip to content

codeclimate (GitLab Code Quality compatible) output support #79

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Feb 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,30 @@ There is a GitHub Actions action available to get linter feedback in workflows:

* [puppet-lint-action](https://github.com/marketplace/actions/puppet-lint-action)

## Integration with GitLab Code Quality

[GitLab](https://gitlab.com/) users can use the `--codeclimate-report-file` configuration option to generate a report for use with the
[Code Quality](https://docs.gitlab.com/ee/ci/testing/code_quality.html) feature.

The easiest way to set this option, (and without having to modify rake tasks), is with the `CODECLIMATE_REPORT_FILE` environment variable.

For example, the following GitLab job sets the environment variable and
[archives the report](https://docs.gitlab.com/ee/ci/yaml/artifacts_reports.html#artifactsreportscodequality) produced.
```yaml
validate lint check rubocop-Ruby 2.7.2-Puppet ~> 7:
stage: syntax
image: ruby:2.7.2
script:
- bundle exec rake validate lint check rubocop
variables:
PUPPET_GEM_VERSION: '~> 7'
CODECLIMATE_REPORT_FILE: 'gl-code-quality-report.json'
artifacts:
reports:
codequality: gl-code-quality-report.json
expire_in: 1 week
```

## Options

See `puppet-lint --help` for a full list of command line options and checks.
Expand Down
18 changes: 11 additions & 7 deletions lib/puppet-lint.rb
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ def format_message(message)
puts format % message

puts " #{message[:reason]}" if message[:kind] == :ignored && !message[:reason].nil?
print_context(message)
end

# Internal: Format a problem message and print it to STDOUT so GitHub Actions
Expand Down Expand Up @@ -154,7 +155,8 @@ def get_context(message)
def print_context(message)
return if message[:check] == 'documentation'
return if message[:kind] == :fixed
line = get_context(message)
line = message[:context]
return unless line
offset = line.index(%r{\S}) || 1
puts "\n #{line.strip}"
printf("%#{message[:column] + 2 - offset}s\n\n", '^')
Expand All @@ -168,19 +170,21 @@ def print_context(message)
# Returns array of problem.
def report(problems)
json = []
print_stdout = !(configuration.json || configuration.sarif)

problems.each do |message|
next if message[:kind] == :ignored && !PuppetLint.configuration.show_ignored

message[:KIND] = message[:kind].to_s.upcase

next unless message[:kind] == :fixed || [message[:kind], :all].include?(configuration.error_level)

if configuration.json || configuration.sarif
message['context'] = get_context(message) if configuration.with_context
json << message
else
message[:context] = get_context(message) if configuration.with_context

json << message

if print_stdout
format_message(message)
print_context(message) if configuration.with_context
print_github_annotation(message) if configuration.github_actions
end
end
Expand Down Expand Up @@ -225,7 +229,7 @@ def run

# Public: Print any problems that were found out to stdout.
#
# Returns nothing.
# Returns an array of problems.
def print_problems
report(@problems)
end
Expand Down
5 changes: 5 additions & 0 deletions lib/puppet-lint/bin.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
require 'pathname'
require 'uri'
require 'puppet-lint/optparser'
require 'puppet-lint/report/codeclimate'

# Internal: The logic of the puppet-lint bin script, contained in a class for
# ease of testing.
Expand Down Expand Up @@ -104,6 +105,10 @@ def run
puts JSON.pretty_generate(all_problems)
end

if PuppetLint.configuration.codeclimate_report_file
PuppetLint::Report::CodeClimateReporter.write_report_file(all_problems, PuppetLint.configuration.codeclimate_report_file)
end

return_val
rescue PuppetLint::NoCodeError
puts 'puppet-lint: no file specified or specified file does not exist'
Expand Down
1 change: 1 addition & 0 deletions lib/puppet-lint/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -153,5 +153,6 @@ def defaults
self.show_ignored = false
self.ignore_paths = ['vendor/**/*.pp']
self.github_actions = ENV.key?('GITHUB_ACTION')
self.codeclimate_report_file = ENV['CODECLIMATE_REPORT_FILE']
end
end
4 changes: 4 additions & 0 deletions lib/puppet-lint/optparser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ def self.build(args = [])
PuppetLint.configuration.sarif = true
end

opts.on('--codeclimate-report-file FILE', 'Save a code climate compatible report to this file') do |file|
PuppetLint.configuration.codeclimate_report_file = file
end

opts.on('--list-checks', 'List available check names.') do
PuppetLint.configuration.list_checks = true
end
Expand Down
48 changes: 48 additions & 0 deletions lib/puppet-lint/report/codeclimate.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# frozen_string_literal: true

require 'digest'
require 'json'

class PuppetLint::Report
# Formats problems and writes them to a file as a code climate compatible report.
class CodeClimateReporter
def self.write_report_file(problems, report_file)
report = []
problems.each do |messages|
messages.each do |message|
case message[:kind]
when :warning
severity = 'minor'
when :error
severity = 'major'
else
next
end

issue = {
type: :issue,
check_name: message[:check],
description: message[:message],
categories: [:Style],
severity: severity,
location: {
path: message[:path],
lines: {
begin: message[:line],
end: message[:line],
}
},
}

issue[:fingerprint] = Digest::MD5.hexdigest(Marshal.dump(issue))

if message.key?(:description) && message.key?(:help_uri)
issue[:content] = "#{message[:description].chomp('.')}. See [this page](#{message[:help_uri]}) for more information about the `#{message[:check]}` check."
end
report << issue
end
end
File.write(report_file, "#{JSON.pretty_generate(report)}\n")
end
end
end
9 changes: 8 additions & 1 deletion lib/puppet-lint/tasks/puppet-lint.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
require 'puppet-lint/optparser'
require 'rake'
require 'rake/tasklib'
require 'puppet-lint/report/codeclimate'

# Public: A Rake task that can be loaded and used with everything you need.
#
Expand Down Expand Up @@ -90,19 +91,25 @@ def define(args, &task_block)
RakeFileUtils.send(:verbose, true) do
linter = PuppetLint.new
matched_files = FileList[@pattern]
all_problems = []

matched_files = matched_files.exclude(*@ignore_paths)

matched_files.to_a.each do |puppet_file|
next unless File.file?(puppet_file)
linter.file = puppet_file
linter.run
linter.print_problems
all_problems << linter.print_problems

if PuppetLint.configuration.fix && linter.problems.none? { |e| e[:check] == :syntax }
IO.write(puppet_file, linter.manifest)
end
end

if PuppetLint.configuration.codeclimate_report_file
PuppetLint::Report::CodeClimateReporter.write_report_file(all_problems, PuppetLint.configuration.codeclimate_report_file)
end

abort if linter.errors? || (
linter.warnings? && PuppetLint.configuration.fail_on_warnings
)
Expand Down
38 changes: 38 additions & 0 deletions spec/fixtures/test/reports/code_climate.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
[
{
"type": "issue",
"check_name": "autoloader_layout",
"description": "test::foo not in autoload module layout",
"categories": [
"Style"
],
"severity": "major",
"location": {
"path": "spec/fixtures/test/manifests/fail.pp",
"lines": {
"begin": 2,
"end": 2
}
},
"fingerprint": "f12bce7a776454ab9ffac2d20dcc34ba",
"content": "Test the manifest tokens for any classes or defined types that are not in an appropriately named file for the autoloader to detect and record an error of each instance found. See [this page](https://puppet.com/docs/puppet/latest/style_guide.html#separate-files) for more information about the `autoloader_layout` check."
},
{
"type": "issue",
"check_name": "parameter_order",
"description": "optional parameter listed before required parameter",
"categories": [
"Style"
],
"severity": "minor",
"location": {
"path": "spec/fixtures/test/manifests/warning.pp",
"lines": {
"begin": 2,
"end": 2
}
},
"fingerprint": "e34cf289e008446b633d1be7cf58120e",
"content": "Test the manifest tokens for any parameterised classes or defined types that take parameters and record a warning if there are any optional parameters listed before required parameters. See [this page](https://puppet.com/docs/puppet/latest/style_guide.html#display-order-of-parameters) for more information about the `parameter_order` check."
}
]
24 changes: 24 additions & 0 deletions spec/unit/puppet-lint/bin_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,30 @@ def initialize(args)
end
end

context 'when outputting code climate report' do
let(:report_file) do
Tempfile.new('report_file.json')
end

let(:args) do
[
'--codeclimate-report-file',
report_file.path,
'spec/fixtures/test/manifests/fail.pp',
'spec/fixtures/test/manifests/warning.pp',
]
end

after(:each) do
report_file.unlink
end

it 'creates a code climate report' do
expect(bin.exitstatus).to eq(1)
expect(FileUtils.compare_file(report_file.path, 'spec/fixtures/test/reports/code_climate.json')).to be_truthy
end
end

context 'when hiding ignored problems' do
let(:args) do
[
Expand Down
32 changes: 21 additions & 11 deletions spec/unit/puppet-lint/configuration_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,17 +55,18 @@
end

expect(config.settings).to eq(
'with_filename' => false,
'fail_on_warnings' => false,
'error_level' => :all,
'log_format' => '',
'sarif' => false,
'with_context' => false,
'fix' => false,
'github_actions' => false,
'show_ignored' => false,
'json' => false,
'ignore_paths' => ['vendor/**/*.pp'],
'with_filename' => false,
'fail_on_warnings' => false,
'codeclimate_report_file' => nil,
'error_level' => :all,
'log_format' => '',
'sarif' => false,
'with_context' => false,
'fix' => false,
'github_actions' => false,
'show_ignored' => false,
'json' => false,
'ignore_paths' => ['vendor/**/*.pp'],
)
end

Expand All @@ -78,6 +79,15 @@
expect(config.settings['github_actions']).to be(true)
end

it 'defaults codeclimate_report_file to the CODECLIMATE_REPORT_FILE environment variable' do
override_env do
ENV['CODECLIMATE_REPORT_FILE'] = '/path/to/report.json'
config.defaults
end

expect(config.settings['codeclimate_report_file']).to eq('/path/to/report.json')
end

def override_env
old_env = ENV.to_h
yield
Expand Down