やってこと
- ファイルの差分だけでrubocopエラーを通知する
- rubocopエラーはGithubAPI経由でGithubで投稿する
- 上のロジックを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 master
のupstream
はプロジェクトのrepo
ローカルで試してみる
テストのファイルを作る
test_rubocop.rb
class TestRubocop
def test
puts "test"
end
end
git add
とgit 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'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'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つの変更が必要
-
git fetch upstream master
はgit fetch origin master
になる -
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を実行してもらう
おまけ
プルリクで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]