LoginSignup
1
0

More than 5 years have passed since last update.

DroneCIでRubocopを導入した話

Last updated at Posted at 2018-12-14

やってこと

  1. ファイルの差分だけでrubocopエラーを通知する
  2. rubocopエラーはGithubAPI経由でGithubで投稿する
  3. 上のロジックをDroneCIに持っていく

最後
プルリクでrubocopエラーが起こるファイルの行ごとでエラー内容を投稿する (comment inline)

1.ファイルの差分だけでrubocopエラーを通知する

まずは、3つのgemを導入する

Gemfile
  gem 'checkstyle_filter-git'
  gem 'rubocop-select'
  gem 'rubocop-checkstyle_formatter'

run_diffcop.shを作成する

run_diffcop.sh
err=$( git fetch upstream master && git diff -z --name-only FETCH_HEAD.. \
| xargs -0 bundle exec rubocop-select \
| xargs bundle exec rubocop \
--require rubocop/formatter/checkstyle_formatter \
--format RuboCop::Formatter::CheckstyleFormatter \
| bundle exec checkstyle_filter-git diff FETCH_HEAD)
echo $err

git fetch upstream masterupstreamはプロジェクトのrepo

ローカルで試してみる

テストのファイルを作る

test_rubocop.rb
class TestRubocop
  def test
    puts "test"
  end
end

git addgit commitしておく
次は、

% sh run_diffcop.sh
<?xml version='1.0'?> 
<checkstyle> 
<file name='test_rubocop.rb'> 
<error column='0' line='1' message='Missing top-level class documentation comment.' severity='info' source='com.puppycrawl.tools.checkstyle.Style/Documentation'/> 
<error column='0' line='1' message='Missing magic comment `# frozen_string_literal: true`.' severity='info' source='com.puppycrawl.tools.checkstyle.Style/FrozenStringLiteralComment'/>
<error column='9' line='3' message='Prefer single-quoted strings when you don&apos;t need string interpolation or special symbols.' severity='info' source='com.puppycrawl.tools.checkstyle.Style/StringLiterals'/> 
</file>
</checkstyle>

ここまでは一旦xmlフォマットでエラーが出力できた
見やすくするため、parserを書く

parse_rubocop_xml.rb
require 'nokogiri'
require "cgi"
xml_doc = Nokogiri::XML(ARGV.join(" "))
error_messages = ""
xml_doc.xpath("//file").each do |elm|
  begin
    elm.children.search("error").each do |error|
      error_messages << "- #{elm.attributes["name"].value}:#{error.attributes["line"].value}: #{error.attributes["message"].value}\\n"
    end
  rescue => e
    next
  end
end
puts CGI::escapeHTML(error_messages)

上のrun_diffcop.shを追加する

run_diffcop.sh
err=$( git fetch upstream master && git diff -z --name-only FETCH_HEAD.. \
| xargs -0 bundle exec rubocop-select \
| xargs bundle exec rubocop \
--require rubocop/formatter/checkstyle_formatter \
--format RuboCop::Formatter::CheckstyleFormatter \
| bundle exec checkstyle_filter-git diff FETCH_HEAD)
echo $err

mess=`bundle exec ruby parse_rubocop_xml.rb $err`
echo $mess
% sh run_diffcop.sh
- test_rubocop.rb:1: Missing top-level class documentation comment.
- test_rubocop.rb:1: Missing magic comment `# frozen_string_literal: true`.
- test_rubocop.rb:3: Prefer single-quoted strings when you don&#39;t need string interpolation or special symbols.

2. rubocopエラーはGithubAPI経由でGithubで投稿する

run_diffcop.sh
err=$( git fetch upstream master && git diff -z --name-only FETCH_HEAD.. \
| xargs -0 bundle exec rubocop-select \
| xargs bundle exec rubocop \
--require rubocop/formatter/checkstyle_formatter \
--format RuboCop::Formatter::CheckstyleFormatter \
| bundle exec checkstyle_filter-git diff FETCH_HEAD)
echo $err

mess=`bundle exec ruby parse_rubocop_xml.rb $err`
echo $mess

if [ "$mess" ]; then
  POST_BODY="{\"body\": \"RUBOCOP WARNING!!! \n $mess "}"
  curl -XPOST \
    -H "Authorization: token GITHUB_TOKEN" \
    -H "Content-Type: application/json" \
    -d "$POST_BODY" \
    https://api.github.com/repos/:owner/:repo/issues/pr_number/comments
fi

ここまではローカルでファイルの差分だけでrubocopエラーがGithubに通知できる

3. 上のロジックをDroneCIに持っていく

.drone.yml
# skip
run_diffcop:
    image: your_image
    secrets: [
      GITHUB_ACCESS_TOKEN,
      DRONE_PULL_REQUEST,
      DRONE_COMMIT_SHA
    ]
    commands:
      - sh run_diffcop.sh
    when:
      event: [pull_request]

# skip

droneCIのenvironmentの詳細はこちら
droneCIでrubocopを実行すると、2つの変更が必要
1. git fetch upstream master は git fetch origin masterになる
2. https://api.github.com/repos/:owner/:repo/issues/pr_number/comments
https://api.github.com/repos/:owner/:repo/issues/$DRONE_PULL_REQUEST/commentsになる

run_diffcop.sh
err=$( git fetch origin master && git diff -z --name-only FETCH_HEAD.. \
| xargs -0 bundle exec rubocop-select \
| xargs bundle exec rubocop \
--require rubocop/formatter/checkstyle_formatter \
--format RuboCop::Formatter::CheckstyleFormatter \
| bundle exec checkstyle_filter-git diff FETCH_HEAD)
echo $err

mess=`bundle exec ruby parse_rubocop_xml.rb $err`
echo $mess

if [ "$mess" ]; then
  POST_BODY="{\"body\": \"RUBOCOP WARNING!!! \n $mess "}"
  curl -XPOST \
    -H "Authorization: token GITHUB_TOKEN" \
    -H "Content-Type: application/json" \
    -d "$POST_BODY" \
    https://api.github.com/repos/:owner/:repo/issues/pr_number/comments
fi

以上!
これからプルリクを更新するたびに、rubocopを実行してもらう
スクリーンショット 2018-12-14 0.13.16.png

おまけ

プルリクでrubocopエラーが起こるファイルの行ごとでエラー内容を投稿する (comment inline)

diffcop.rb

require 'nokogiri'
require "cgi"
require 'pry'
require 'httparty'

@drone_link = ARGV[0]
@commit_id = ARGV[1]
@pull_number = ARGV[2]
@github_token = ARGV[3]
@hunk_headers_data = {}

puts @commit_id
puts @github_token
puts @drone_link

def run_diffcop
  analyze_commit_diff_hunks
  mess = pretty_rubocop_error
  push_to_github mess
end

def analyze_commit_diff_hunks
  diff = fetch_commit_diff
  @hunk_headers_data = parse_diff diff
end

def fetch_commit_diff
  HTTParty.get("https://api.github.com/repos/orcainc/homeup/commits/#{@commit_id}",
    headers: {
      "Authorization": "token #{@github_token}",
      "Content-Type": "application/json",
      "Accept": "application/vnd.github.v3.diff",
      "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36"
    }
  )
end

def parse_diff diff
  res = {}
  key = nil
  diff.each_line do |str|
    new_key = str.scan(/diff --git a\/(.*)b\//).flatten[0]
    if new_key
      key = new_key.strip
      res[key] = {}
    end
    hunk_header = str.scan(/@@ \-\d+,\d+ \+(\d+),(\d+) @@/).flatten.map(&:to_i)
    if hunk_header.length > 0
      res[key]["hunks"] ||= []
      res[key]["all_hunks_pos"] ||= []
      res[key]["hunks"] << {position: hunk_header[0], range: hunk_header[1]}
      res[key]["all_hunks_pos"] << hunk_header[0]
    end
  end
  res
end

def pretty_rubocop_error
  xml_doc = Nokogiri::XML(rubocop_errors)
  error_messages = []
  xml_doc.xpath("//file").each do |elm|
    elm.children.search("error").each do |error|
      err_line = error.attributes["line"].value
      file_name = elm.attributes["name"].value
      error_messages <<  {path: file_name,
                          position: get_position_of_comment(file_name, Integer(err_line)),
                          body: error.attributes["message"].value
                          }
    end
  end
  error_messages
end

def rubocop_errors
  data_message_errors = `git fetch upstream master && git diff -z --name-only FETCH_HEAD.. \
  | xargs -0 bundle exec rubocop-select \
  | xargs bundle exec rubocop --force-exclusion --config .rubocop_v2.yml \
   --require rubocop/formatter/checkstyle_formatter \
   --format RuboCop::Formatter::CheckstyleFormatter \
  | bundle exec checkstyle_filter-git diff FETCH_HEAD`
  data_message_errors
end

def get_position_of_comment file_name, line
  comment_pos = 0
  all_pos = @hunk_headers_data[file_name]["all_hunks_pos"].dup
  chain_pos = all_pos.keep_if {|v| v < line}
  if chain_pos.length > 1
    pivot = chain_pos.pop
    chain_pos.each do |po|
      comment_pos += get_range_from_hunk file_name, po
      comment_pos += 1 # +1 for hunk header line
    end
    comment_pos += (line - pivot + 1)
  else
    comment_pos = line - all_pos[0] + 1
  end
  comment_pos
end

def get_range_from_hunk key, pos
  @hunk_headers_data[key]["hunks"].detect {|hunk| hunk[:position] == pos}[:range]
end

def push_to_github mess
  response = HTTParty.post("https://api.github.com/repos/orcainc/homeup/pulls/#{@pull_number}/reviews",
    headers: {
      "Authorization": "token #{@github_token}",
      "Content-Type": "application/json",
      "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36"
    },
    body: {
      "body": "RUBOCOP WARNING!!! \n #{@drone_link}",
      "event": "COMMENT",
      "comments": mess,
    }.to_json
  )
  puts response
end

run_diffcop

.drone.yml
run_diffcop:
    image: your_image
    secrets: [
      GITHUB_ACCESS_TOKEN,
      DRONE_PULL_REQUEST,
      DRONE_COMMIT_SHA
    ]
    commands:
      - ruby diffcop.rb $DRONE_BUILD_LINK $DRONE_COMMIT_SHA $DRONE_PULL_REQUEST $GITHUB_ACCESS_TOKEN
    when:
      event: [pull_request]

結果
スクリーンショット 2018-12-14 0.28.29.png

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0