forked from reclaim-the-stack/rubocop-action
-
Notifications
You must be signed in to change notification settings - Fork 0
/
rubocop.rb
202 lines (156 loc) · 7.07 KB
/
rubocop.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# This script is used to run rubocop on the files that have changed in a pull request
# and then comment on the pull request with the offenses found. It also checks the
# pull request for existing comments and removes them if the offense has been fixed.
# This script is intended to run soley in the context of GitHub Actions on pull requests.
# Setup
puts "::group::Installing Rubocop gems"
versioned_rubocop_gems =
if ENV.fetch("RUBOCOP_GEM_VERSIONS").downcase == "gemfile"
require "bundler"
Bundler::LockfileParser.new(Bundler.read_file("Gemfile.lock")).specs
.select { |spec| spec.name.start_with? "rubocop" }
.map { |spec| "#{spec.name}:#{spec.version}" }
else
ENV.fetch("RUBOCOP_GEM_VERSIONS").split
end
gem_install_command = "gem install #{versioned_rubocop_gems.join(' ')} --no-document --conservative"
puts "Installing gems with:", gem_install_command
system "time #{gem_install_command}"
puts "::endgroup::"
# Script
require_relative "lib/github"
# Figure out which ruby files have changed and run Rubocop on them
github_event = JSON.parse(File.read(ENV.fetch("GITHUB_EVENT_PATH")))
pr_number = github_event.fetch("pull_request").fetch("number")
owner_and_repository = ENV.fetch("GITHUB_REPOSITORY")
outside_diff_fail = ENV.fetch("OUTSIDE_DIFF_FAIL") == "true"
changed_ruby_files = Github.pull_request_ruby_files(owner_and_repository, pr_number)
# JSON reference: https://docs.rubocop.org/rubocop/formatters.html#json-formatter
files_with_offenses =
if changed_ruby_files.any?
command = "rubocop #{changed_ruby_files.map(&:path).join(' ')} --format json --force-exclusion #{ARGV.join(' ')}"
puts "Running rubocop with: #{command}"
JSON.parse(`#{command}`).fetch("files")
else
puts "No changed Ruby files, skipping rubocop"
[]
end
# Fetch existing pull request comments
puts "Fetching PR comments from https://api.github.com/repos/#{owner_and_repository}/pulls/#{pr_number}/comments"
existing_comments = Github.get!("/repos/#{owner_and_repository}/pulls/#{pr_number}/comments")
comments_made_by_rubocop = existing_comments.select do |comment|
comment.fetch("body").include?("rubocop-comment-id")
end
# Find existing comments which no longer have offenses and delete them
fixed_comments = comments_made_by_rubocop.reject do |comment|
files_with_offenses.any? do |file|
file.fetch("path") == comment.fetch("path") &&
file.fetch("offenses").any? do |offense|
offense.fetch("location").fetch("line") == comment.fetch("line")
end
end
end
fixed_comments.each do |comment|
comment_id = comment.fetch("id")
path = comment.fetch("path")
line = comment.fetch("line")
puts "Deleting resolved comment #{comment_id} on #{path} line #{line}"
Github.delete!("/repos/#{owner_and_repository}/pulls/comments/#{comment_id}")
end
# Comment on the pull request with the offenses found
def in_diff?(changed_files, path, line)
file = changed_files.find { |changed_file| changed_file.path == path }
file&.changed_lines&.include?(line)
end
offences_outside_diff = []
files_with_offenses.each do |file|
path = file.fetch("path")
offenses_by_line = file.fetch("offenses").group_by do |offense|
offense.fetch("location").fetch("line")
end
# Group offenses by line number and make a single comment per line
offenses_by_line.each do |line, offenses|
puts "Handling #{path} line #{line} with #{offenses.count} offenses"
message = offenses.map do |offense|
correctable_prefix = "[Correctable] " if offense.fetch("correctable")
"#{correctable_prefix}#{offense.fetch('cop_name')}: #{offense.fetch('message')}"
end.join("\n")
body = <<~BODY
<!-- rubocop-comment-id: #{path}-#{line} -->
#{message}
BODY
# If there is already a comment on this line, update it if necessary.
# Otherwise create a new comment.
existing_comment = comments_made_by_rubocop.find do |comment|
comment.fetch("body").include?("<!-- rubocop-comment-id: #{path}-#{line} -->")
end
if existing_comment
comment_id = existing_comment.fetch("id")
# No need to do anything if the offense already exists and hasn't changed
if existing_comment.fetch("body") == body
puts "Skipping unchanged comment #{comment_id} on #{path} line #{line}"
next
end
puts "Updating comment #{comment_id} on #{path} line #{line}"
Github.patch("/repos/#{owner_and_repository}/pulls/comments/#{comment_id}", body: body)
elsif in_diff?(changed_ruby_files, path, line)
puts "Commenting on #{path} line #{line}"
# Somehow the commit_id should not be just the HEAD SHA: https://stackoverflow.com/a/71431370/1075108
commit_id = github_event.fetch("pull_request").fetch("head").fetch("sha")
Github.post!(
"/repos/#{owner_and_repository}/pulls/#{pr_number}/comments",
body: body,
path: path,
commit_id: commit_id,
line: line,
)
else
offences_outside_diff << { path: path, line: line, message: message, count: offenses.count }
end
end
end
# If there are any offenses outside the diff, make a separate comment for them
separate_comments = Github.get!("/repos/#{owner_and_repository}/issues/#{pr_number}/comments")
existing_separate_comment = separate_comments.find do |comment|
comment.fetch("body").include?("rubocop-comment-id: outside-diff")
end
if offences_outside_diff.any?
puts "Found #{offences_outside_diff.count} offenses outside of the diff"
body = <<~BODY
<!-- rubocop-comment-id: outside-diff -->
<details>
<summary>Rubocop offenses found outside of the diff</summary>
<br>
BODY
body += offences_outside_diff.map do |offense|
"**#{offense.fetch(:path)}:#{offense.fetch(:line)}**\n#{offense.fetch(:message)}"
end.join("\n\n")
body += "\n</details>"
if existing_separate_comment
existing_comment_id = existing_separate_comment.fetch("id")
# No need to do anything if the offense already exists and hasn't changed
if existing_separate_comment.fetch("body") == body
puts "Skipping unchanged separate comment #{existing_comment_id}"
else
puts "Updating separate comment #{existing_comment_id}"
Github.patch!("/repos/#{owner_and_repository}/issues/comments/#{existing_comment_id}", body: body)
end
else
puts "Commenting on pull request with offenses found outside the diff"
Github.post!("/repos/#{owner_and_repository}/issues/#{pr_number}/comments", body: body)
end
elsif existing_separate_comment
existing_comment_id = existing_separate_comment.fetch("id")
puts "Deleting resolved separate comment #{existing_comment_id}"
Github.delete("/repos/#{owner_and_repository}/issues/comments/#{existing_comment_id}")
else
puts "No offenses found outside of the diff and no existing separate comment to remove"
end
# Fail the build if there were any offenses
number_of_offenses = files_with_offenses.sum { |file| file.fetch("offenses").length }
number_of_offenses -= offences_outside_diff.sum { |offense| offense[:count] } unless outside_diff_fail
if number_of_offenses > 0
puts ""
puts "#{number_of_offenses} offenses found! Failing the build..."
exit 108
end