OpenProject is the leading open source project management software.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
openproject/script/github_pr_errors

261 lines
6.7 KiB

#!/usr/bin/env ruby
require 'rubygems'
require 'bundler'
Bundler.setup(:default, :development)
require 'colored2'
require 'json'
require 'optparse'
require 'pathname'
require 'pry'
require 'rest-client'
require 'yaml'
GITHUB_API_OPENPROJECT_PREFIX = 'https://api.github.com/repos/opf/openproject'.freeze
GITHUB_HTML_OPENPROJECT_PREFIX = 'https://github.com/opf/openproject'.freeze
RAILS_ROOT = Pathname.new(__dir__).dirname
SPEC_PATTERN = %r{^\S+ (?:rspec (\S+) #.+|An error occurred while loading (\S+)\.\r?)$}
if !ENV['GITHUB_USERNAME']
raise "Missing GITHUB_USERNAME env"
elsif !ENV['GITHUB_TOKEN']
raise "Missing GITHUB_TOKEN env, go to https://github.com/settings/tokens and create one with 'repo' access"
end
def parse_options
options = {}
opt_parser = OptionParser.new do |parser|
parser.banner = <<~BANNER
Usage: #{$0} [options]
Fetches rspec failures from last completed GitHub actions on current
branch, and outputs them on standard output, one by line.
Information is printed on standard error to preserve standard output.
Use this script with xargs to run failing specs locally:
#{$0} | xargs --no-run-if-empty bundle exec rspec
Options:
BANNER
parser.on("-c", "--compact", "Output all failing rspec files on one line") do
options[:compact] = true
end
parser.on("-r RUN_ID", "--run-id RUN_ID", Integer,
"The workflow run id to use (in github url: actions/runs/{id})") do |value|
options[:run_id] = value
end
parser.on("-h", "--help", "Prints this help") do
puts parser
exit
end
end
opt_parser.parse!
options
end
# Returns current branch
def current_branch_name
@current_branch_name ||= `git rev-parse --abbrev-ref HEAD`.strip
end
def get_http(path)
url =
if path.start_with?('http')
path
else
"#{GITHUB_API_OPENPROJECT_PREFIX}/#{path}"
end
response = RestClient::Request.new(
method: :get,
url:,
user: ENV.fetch('GITHUB_USERNAME'),
password: ENV.fetch('GITHUB_TOKEN')
).execute
response.to_str
rescue RestClient::ExceptionWithResponse => e
warn error_details(e)
exit(1)
rescue StandardError => e
warn "Failed to perform API request GET #{url}: #{e}"
exit 1
end
def error_details(rest_client_exception_with_response)
response = rest_client_exception_with_response.response
error = JSON.parse(response.body)
parts = []
parts << "Failed to perform API request #{response.request.method.upcase} #{response.request.url}: #{rest_client_exception_with_response}"
parts << " #{error['message']}"
parts << " See #{error['documentation_url']}"
parts += rest_client_exception_with_response.backtrace.map { " #{_1}" }
parts.join("\n")
end
def get_json(path)
JSON.parse(get_http(path))
end
def path_to_cache_key(path)
path
.gsub(/\?.*$/, '') # remove query parameter
.gsub(/^#{GITHUB_API_OPENPROJECT_PREFIX}\/?/, '') # remove https://.../
.gsub(/\W/, '_') # transform non alphanum chars
end
def commit_message(workflow_run)
workflow_run['head_commit']
.then { |commit| commit["message"] }
.then { |message| message.split("\n", 2).first }
end
def get_jobs(workflow_run)
workflow_run['jobs_url']
cache_key = [
path_to_cache_key(workflow_run['jobs_url']),
workflow_run['updated_at'].gsub(':', '')
].join('_')
cached(cache_key) { get_json(workflow_run['jobs_url']) }
end
def get_log(job)
cached("job_#{job['id']}.log") do
get_http("actions/jobs/#{job['id']}/logs")
end
end
def cached(unique_name)
cached_file = RAILS_ROOT.join("tmp/github_pr_errors/#{unique_name}")
if cached_file.file?
content = cached_file.read
content.start_with?("---") ? YAML::load(content) : content
else
content = yield
cached_file.dirname.mkpath
cached_file.write(content.is_a?(String) ? content : YAML::dump(content))
content
end
end
def status_icon(job)
case job['status']
when "queued", "in_progress"
"".yellow
else
case job['conclusion']
when "success"
"".green
when "failure"
"".red
else
"-"
end
end
end
def status_url(job)
return if job['conclusion'] == "success"
job['html_url'].white.dark
end
def status_line(job)
[
"#{status_icon(job)} #{job['name']}: #{job['conclusion'] || job['status']}",
status_url(job)
].compact.join(" ")
end
def last_with_status(workflow_runs, status)
workflow_runs
.select { |entry| entry['status'] == status }
.max_by { |entry| entry['run_number'] }
end
def get_last_workflow_run(branch_name)
test_workflow_runs =
get_json("actions/runs?branch=#{CGI.escape(branch_name)}")
.then { |response| response['workflow_runs'] }
.select { |entry| entry['name'] == 'Test suite' }
last_completed = last_with_status(test_workflow_runs, 'completed')
last_in_progress = last_with_status(test_workflow_runs, 'in_progress')
last_completed || last_in_progress or raise "No workflow run found for branch #{branch_name}"
end
def get_workflow_run(run_id)
if run_id
warn "Looking for the workflow run with id #{run_id.to_s.bold}"
get_json("actions/runs/#{CGI.escape(run_id.to_s)}")
else
warn "Looking for the last 'Test suite' workflow run in current branch #{current_branch_name.bold}"
get_last_workflow_run(current_branch_name)
end
end
def display_pull_request_info(workflow_run)
return unless workflow_run['event'] == 'pull_request'
pr = workflow_run['pull_requests'].first
pr_number = "##{pr['number']}"
pr_html_url = "#{GITHUB_HTML_OPENPROJECT_PREFIX}/pull/#{pr['number']}"
pr_display_title = "#{workflow_run['display_title']} #{pr_number.white.dark} #{pr_html_url.white.dark}"
warn " Pull Request: #{pr_display_title} "
end
def display_workflow_run_info(workflow_run)
warn " Branch: #{workflow_run['head_branch'].bold}"
warn " Commit SHA: #{workflow_run['head_sha'].bold}"
warn " Commit message: #{commit_message(workflow_run).bold}"
display_pull_request_info(workflow_run)
end
##########
options = parse_options
workflow_run = get_workflow_run(options[:run_id])
display_workflow_run_info(workflow_run)
errors = []
is_successful = true
warn " #{status_line(workflow_run)}"
get_jobs(workflow_run)
.then { |jobs_response| jobs_response['jobs'] }
.sort_by { _1['name'] }
.each { |job| warn " #{status_line(job)}" }
.select { _1['conclusion'] == 'failure' }
.reject { _1['name'] == 'rubocop' }
.each do |job|
is_successful = false
get_log(job)
.scan(SPEC_PATTERN)
.flatten
.compact
.uniq
.sort
.each do |match|
errors << match
end
end
if is_successful
warn "All jobs successful 🎉"
elsif errors.empty?
warn "No rspec errors found :-/"
else
errors = errors.map { "'#{_1}'" }
errors = errors.join(" ") if options[:compact]
puts errors
end