From b4d1a5618d9000b5ba6bbd12f1de1d1d6adf0a84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matou=C5=A1=20Bor=C3=A1k?= Date: Sat, 5 Oct 2024 09:38:58 +0200 Subject: [PATCH] Create GitHub reporter (#188) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hello, we want to use Slim Lint in our GitHub Actions CI pipeline so I would like to add a GitHub reporter. The name of it is taken from the [Rubocop GitHub Actions formatter](https://docs.rubocop.org/rubocop/1.66/formatters.html#github-actions-formatter) which does the same thing for Rubocop. I'm also adding a sample GH Actions workflow snippet that follows the new [Rails GH CI convention](https://github.com/rails/rails/blob/6df3358abf5fb99ede40d45ef7a643b97e1a802d/railties/lib/rails/generators/rails/app/templates/github/ci.yml.tt#L46) to the README. This setup creates Pull Request annotations on Lint failures that look like this: ![image](https://github.com/user-attachments/assets/a6818d73-4bae-498f-9194-77e27efd4bba) --- README.md | 30 +++++ lib/slim_lint/reporter/github_reporter.rb | 36 ++++++ .../reporter/github_reporter_spec.rb | 105 ++++++++++++++++++ 3 files changed, 171 insertions(+) create mode 100644 lib/slim_lint/reporter/github_reporter.rb create mode 100644 spec/slim_lint/reporter/github_reporter_spec.rb diff --git a/README.md b/README.md index 692114a..e8cfe4e 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ your [SCM hooks](https://github.com/sds/overcommit). * [Linters](#linters) * [Editor Integration](#editor-integration) * [Git Integration](#git-integration) +* [GitHub Integration](#github-integration) * [Rake Integration](#rake-integration) * [Contributing](#contributing) * [Changelog](#changelog) @@ -181,6 +182,35 @@ If you'd like to integrate `slim-lint` into your Git workflow, check out [overcommit](https://github.com/sds/overcommit), a powerful and flexible Git hook manager. +## Github Integration + +To run `slim-lint` in your [GitHub Actions](https://docs.github.com/en/actions) CI pipeline, +use the `github` reporter, for example: + +```yml +on: + pull_request: + push: + branches: [ main ] + +jobs: + lint: + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: .ruby-version + bundler-cache: true + + - name: Lint Slim templates for consistent style + run: bundle exec slim-lint -r github app/views +``` + +On lint failures, this setup will create annotations in your pull requests on GitHub. + ## Rake Integration To execute `slim-lint` via a [Rake](https://github.com/ruby/rake) task, make diff --git a/lib/slim_lint/reporter/github_reporter.rb b/lib/slim_lint/reporter/github_reporter.rb new file mode 100644 index 0000000..a32a1ef --- /dev/null +++ b/lib/slim_lint/reporter/github_reporter.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module SlimLint + # Outputs lints in a format suitable for GitHub Actions. + # See https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions/. + class Reporter::GithubReporter < Reporter + def display_report(report) + sorted_lints = report.lints.sort_by { |l| [l.filename, l.line] } + + sorted_lints.each do |lint| + print_type(lint) + print_location(lint) + print_message(lint) + end + end + + private + + def print_type(lint) + if lint.error? + log.log '::error ', false + else + log.log '::warning ', false + end + end + + def print_location(lint) + log.log "file=#{lint.filename},line=#{lint.line},", false + end + + def print_message(lint) + log.log 'title=Slim Lint', false + log.log "::#{lint.message}" + end + end +end diff --git a/spec/slim_lint/reporter/github_reporter_spec.rb b/spec/slim_lint/reporter/github_reporter_spec.rb new file mode 100644 index 0000000..608ff6c --- /dev/null +++ b/spec/slim_lint/reporter/github_reporter_spec.rb @@ -0,0 +1,105 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe SlimLint::Reporter::GithubReporter do + describe '#display_report' do + let(:io) { StringIO.new } + let(:output) { io.string } + let(:logger) { SlimLint::Logger.new(io) } + let(:report) { SlimLint::Report.new(lints, []) } + let(:reporter) { described_class.new(logger) } + + subject { reporter.display_report(report) } + + context 'when there are no lints' do + let(:lints) { [] } + + it 'prints nothing' do + subject + output.should be_empty + end + end + + context 'when there are lints' do + let(:filenames) { ['some-filename.slim', 'other-filename.slim'] } + let(:lines) { [502, 724] } + let(:descriptions) { ['Description of lint 1', 'Description of lint 2'] } + let(:severities) { [:warning] * 2 } + let(:linter) { double(name: 'SomeLinter') } + + let(:lints) do + filenames.each_with_index.map do |filename, index| + SlimLint::Lint.new(linter, filename, lines[index], descriptions[index], severities[index]) + end + end + + it 'prints each lint on its own line' do + subject + output.count("\n").should == 2 + end + + it 'prints a trailing newline' do + subject + output[-1].should == "\n" + end + + it 'prints the filename in the "file" parameter for each lint' do + subject + filenames.each do |filename| + output.scan(/file=#{filename}/).count.should == 1 + end + end + + it 'prints the line number in the "line" parameter for each lint' do + subject + lines.each do |line| + output.scan(/line=#{line}/).count.should == 1 + end + end + + it 'prints a "Slim Lint" annotation title for each lint' do + subject + output.scan(/title=Slim Lint/).count.should == 2 + end + + it 'prints the description for each lint at the end of the line' do + subject + descriptions.each do |description| + output.scan(/::#{description}$/).count.should == 1 + end + end + + context 'when lints are warnings' do + it 'prints the warning severity annotation at the beginning of each line' do + subject + output.split("\n").each do |line| + line.scan(/^::warning /).count.should == 1 + end + end + end + + context 'when lints are errors' do + let(:severities) { [:error] * 2 } + + it 'prints the error severity annotation at the beginning of each line' do + subject + output.split("\n").each do |line| + line.scan(/^::error /).count.should == 1 + end + end + end + + context 'when lint has no associated linter' do + let(:linter) { nil } + + it 'prints the description for each lint' do + subject + descriptions.each do |description| + output.scan(/#{description}/).count.should == 1 + end + end + end + end + end +end