mirror of
https://github.com/opf/openproject.git
synced 2026-06-14 03:30:14 +00:00
699 lines
19 KiB
Ruby
Executable File
699 lines
19 KiB
Ruby
Executable File
#!/usr/bin/env ruby
|
|
|
|
require "rubygems"
|
|
require "bundler"
|
|
Bundler.setup(:default, :development)
|
|
|
|
require "colored2"
|
|
require "json"
|
|
require "optparse"
|
|
require "base64"
|
|
require "pathname"
|
|
require "pry"
|
|
require "time"
|
|
require "yaml"
|
|
require "httpx"
|
|
|
|
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
|
|
EXCLUDED_JOB_NAMES = %w[eslint rubocop].freeze
|
|
|
|
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
|
|
|
|
class Options
|
|
DEFAULTS = {
|
|
compact: false,
|
|
display_rerun_info: false,
|
|
display_images: false,
|
|
failed_jobs_logs: [],
|
|
no_cache: false,
|
|
run_id: nil,
|
|
verbose: false
|
|
}.freeze
|
|
|
|
BANNER = <<~BANNER.freeze
|
|
Usage: #{$0} [options] [url]
|
|
|
|
Fetches rspec failures from last completed GitHub actions on current
|
|
branch, and outputs them on standard output, one by line.
|
|
|
|
If given an url, it will fetch the failures from the given url instead of
|
|
the tip of the current branch.
|
|
|
|
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
|
|
|
|
class << self
|
|
def options
|
|
return @options if defined?(@options)
|
|
|
|
@options = DEFAULTS.dup
|
|
parse_options!
|
|
@options
|
|
end
|
|
|
|
def method_missing(name, *)
|
|
if DEFAULTS.key?(name)
|
|
options[name]
|
|
else
|
|
super
|
|
end
|
|
end
|
|
|
|
def respond_to_missing?(method_name, include_private = false)
|
|
DEFAULTS.key?(name) || super
|
|
end
|
|
|
|
def parse_options!
|
|
options.merge!(parse_args)
|
|
options.merge!(parse_url(ARGV.first)) if ARGV.any?
|
|
end
|
|
|
|
def parse_args
|
|
options = {}
|
|
opt_parser = OptionParser.new do |parser|
|
|
parser.banner = BANNER
|
|
|
|
parser.on("-c", "--compact", "Output all failing rspec files on one line") do
|
|
options[:compact] = true
|
|
end
|
|
|
|
parser.on("-d", "--display-rerun-info",
|
|
"Displays rspec rerun instructions like CI with seed") do
|
|
options[:display_rerun_info] = true
|
|
end
|
|
|
|
parser.on("-f PATH", "--failed-job-log PATH",
|
|
"Use given file as failed job log instead of downloading from GitHub. Can be used multiple times.") do |path|
|
|
options[:failed_jobs_logs] << path
|
|
end
|
|
|
|
parser.on("-i", "--images", "Download and display screenshots of failed feature tests") do
|
|
options[:display_images] = true
|
|
end
|
|
|
|
parser.on("-n", "--no-cache", "Do not use cached replies from GitHub API calls") do
|
|
options[:no_cache] = 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
|
|
|
|
parser.on("-v", "--verbose",
|
|
"Print more information, mostly useful for debugging") do
|
|
options[:verbose] = true
|
|
end
|
|
end
|
|
opt_parser.parse!
|
|
options
|
|
end
|
|
|
|
def parse_url(url)
|
|
case url
|
|
when %r{^https://github.com/opf/openproject/actions/runs/(\d+)(?:/job/\d+(?:\?.*)?)?$}
|
|
run_id = $1.to_i
|
|
say_verbose("Extracted run id #{run_id} from #{url}")
|
|
{ run_id: }
|
|
else
|
|
warn "Unrecognized url #{url}"
|
|
exit 1
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def say_verbose(text)
|
|
return unless Options.verbose
|
|
|
|
warn "| #{text}"
|
|
end
|
|
|
|
# Returns current branch
|
|
def current_branch_name
|
|
@current_branch_name ||= `git rev-parse --abbrev-ref HEAD`.strip
|
|
end
|
|
|
|
def github_url(path)
|
|
if path.start_with?("http")
|
|
path
|
|
else
|
|
"#{GITHUB_API_OPENPROJECT_PREFIX}/#{path}"
|
|
end
|
|
end
|
|
|
|
def http
|
|
HTTPX
|
|
.plugin(:follow_redirects)
|
|
.plugin(:basic_auth)
|
|
.basic_auth(ENV.fetch("GITHUB_USERNAME"), ENV.fetch("GITHUB_TOKEN"))
|
|
end
|
|
|
|
def get_http(path)
|
|
url = github_url(path)
|
|
say_verbose("HTTP GET #{url}")
|
|
|
|
response = http.get(url)
|
|
|
|
say_verbose("HTTP Response #{response.status}")
|
|
response.raise_for_status
|
|
response.to_s
|
|
rescue HTTPX::HTTPError => 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(error)
|
|
response = error.response
|
|
response_body = response.json
|
|
|
|
parts = []
|
|
parts << "Failed to perform API request GET #{response.uri}: #{error}"
|
|
parts << " #{response_body['message']}"
|
|
parts << " See #{response_body['documentation_url']}"
|
|
parts += error.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}\/?/o, "") # remove https://.../
|
|
.gsub(/\W/, "_") # transform non alphanum chars
|
|
end
|
|
|
|
def get_jobs(workflow_run)
|
|
workflow_run["jobs_url"]
|
|
cache_key = [
|
|
path_to_cache_key(workflow_run["jobs_url"]),
|
|
workflow_run["updated_at"].delete(":")
|
|
].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)
|
|
if Options.no_cache
|
|
return yield
|
|
end
|
|
|
|
cached_file = RAILS_ROOT.join("tmp/github_pr_errors/#{unique_name}")
|
|
if cached_file.file?
|
|
say_verbose("Reading from cached file #{cached_file}")
|
|
content = cached_file.read
|
|
content.start_with?("---") ? YAML::load(content) : content
|
|
else
|
|
content = yield
|
|
say_verbose("Writing to cached file #{cached_file}")
|
|
cached_file.dirname.mkpath
|
|
cached_file.write(content.is_a?(String) ? content : YAML::dump(content))
|
|
content
|
|
end
|
|
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_in_progress || last_completed 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
|
|
|
|
class Report
|
|
attr_accessor :errors,
|
|
:failures_explanation,
|
|
:head_branch,
|
|
:head_sha,
|
|
:commit_message,
|
|
:run_started_at,
|
|
:merge_branch_sha
|
|
end
|
|
|
|
class Error
|
|
attr_accessor :location, :page_html, :page_screenshot, :tests_group, :loading_error
|
|
end
|
|
|
|
# rubocop:disable Layout/LineLength
|
|
# Looks like this in the job log:
|
|
# Process 28: TEST_ENV_NUMBER=28 RUBYOPT=-I/usr/local/bundle/bundler/gems/turbo_tests-3148ae6c3482/lib -r/usr/local/bundle/gems/bundler-2.5.11/lib/bundler/setup -W0 RSPEC_SILENCE_FILTER_ANNOUNCEMENTS=1 /usr/local/bundle/gems/bundler-2.5.11/exe/bundle exec rspec --seed 52674 --format TurboTests::JsonRowsFormatter --out tmp/test-pipes/subprocess-28 --format ParallelTests::RSpec::RuntimeLogger --out spec/support/turbo_runtime_features.log spec/features/api_docs/index_spec.rb spec/features/custom_fields/reorder_options_spec.rb spec/features/projects/projects_portfolio_spec.rb spec/features/projects/template_spec.rb spec/features/versions/edit_spec.rb spec/features/work_packages/details/markdown/description_editor_spec.rb spec/features/work_packages/table/hierarchy/hierarchy_parent_below_spec.rb spec/features/work_packages/table/inline_create/inline_create_refresh_spec.rb spec/features/work_packages/table/invalid_query_spec.rb spec/features/work_packages/tabs/activity_revisions_spec.rb
|
|
# rubocop:enable Layout/LineLength
|
|
class TestsGroup
|
|
attr_accessor :test_env_number, :seed, :files
|
|
|
|
def initialize
|
|
@files = []
|
|
end
|
|
|
|
def include_error?(error)
|
|
return false if error.location.nil?
|
|
|
|
files.any? { |file| error.location.include?(file) }
|
|
end
|
|
|
|
def inspect
|
|
"#<#{self.class} @test_env_number=#{test_env_number} @seed=#{seed} (#{files.count} files)>"
|
|
end
|
|
end
|
|
|
|
class JobErrorsFinder
|
|
SPEC_FAILURES_PATTERN = %r{^\S+ rspec (\S+) #.+$}
|
|
SPEC_LOADING_ERRORS_PATTERN = %r{^\S+ An error occurred while loading (\S+)\.\r?$}
|
|
SCREENSHOT_PATTERN = /\{"message":"Screenshot captured for failed feature test"[^\n]+$/
|
|
TESTS_GROUP_PATTERN = /Process \d+: TEST_ENV_NUMBER=\d+ [^\n]+$/
|
|
BRANCH_MERGE_PATTERN = /Merge \w{40} into (\w{40})$/
|
|
|
|
attr_reader :failures_explanation, :merge_branch_sha
|
|
|
|
def self.scan_logs(report, logs)
|
|
finder = new
|
|
logs.each do |log|
|
|
finder.scan_log(log)
|
|
end
|
|
report.errors = finder.errors
|
|
report.failures_explanation = finder.failures_explanation
|
|
report.merge_branch_sha = finder.merge_branch_sha
|
|
report
|
|
end
|
|
|
|
def scan_log(log)
|
|
find_failures(log)
|
|
find_failures_explanation(log)
|
|
find_loading_errors(log)
|
|
find_screenshots(log)
|
|
find_tests_groups(log)
|
|
find_merge_branch_info(log)
|
|
end
|
|
|
|
def errors
|
|
@errors.values
|
|
end
|
|
|
|
protected
|
|
|
|
def initialize
|
|
@errors = {}
|
|
end
|
|
|
|
def create_error(location)
|
|
return if location.nil?
|
|
|
|
error = Error.new
|
|
error.location = location
|
|
@errors[location] ||= error
|
|
end
|
|
|
|
def with_matching_error(location: nil, id: nil)
|
|
error = @errors[id] || @errors[location]
|
|
yield error if error && block_given?
|
|
error
|
|
end
|
|
|
|
def find_failures(log)
|
|
log.scan(SPEC_FAILURES_PATTERN)
|
|
.flatten
|
|
.uniq
|
|
.sort
|
|
.each do |rerun_location|
|
|
create_error(rerun_location)
|
|
end
|
|
end
|
|
|
|
def find_failures_explanation(log)
|
|
explanations = []
|
|
log.split("\n").each do |line|
|
|
if line.end_with?("Failures:") .. line.end_with?("Failed examples:")
|
|
explanations << line
|
|
end
|
|
end
|
|
explanations.map! { _1[29..] } # Remove leading timestamp (like "2024-02-05T08:37:54.5175930Z")
|
|
explanations.reject! do |line|
|
|
line == "Failures:" ||
|
|
line == "Failed examples:" ||
|
|
line.include?("gems/rspec-retry-") ||
|
|
line.include?("gems/webmock-")
|
|
end
|
|
@failures_explanation = explanations.join("\n")
|
|
end
|
|
|
|
def find_loading_errors(log)
|
|
log.scan(SPEC_LOADING_ERRORS_PATTERN)
|
|
.flatten
|
|
.uniq
|
|
.sort
|
|
.each do |location|
|
|
error = create_error(location)
|
|
error.loading_error = true
|
|
end
|
|
end
|
|
|
|
def find_screenshots(log)
|
|
log.scan(SCREENSHOT_PATTERN)
|
|
.map { JSON.parse _1 }
|
|
.each do |screenshot_info|
|
|
id = screenshot_info["test_id"]
|
|
location = screenshot_info["test_location"]
|
|
with_matching_error(location:, id:) do |error|
|
|
error.page_html = screenshot_info["html"]
|
|
error.page_screenshot = screenshot_info["image"]
|
|
end
|
|
end
|
|
end
|
|
|
|
def find_tests_groups(log)
|
|
tests_groups = log
|
|
.scan(TESTS_GROUP_PATTERN)
|
|
.flatten
|
|
.map { build_tests_group_from_command(_1) }
|
|
|
|
errors.each do |error|
|
|
error.tests_group = tests_groups.find { _1.include_error?(error) }
|
|
end
|
|
end
|
|
|
|
def find_merge_branch_info(log)
|
|
merge_branch_sha = log.scan(BRANCH_MERGE_PATTERN).flatten.first
|
|
@merge_branch_sha = merge_branch_sha if merge_branch_sha
|
|
end
|
|
|
|
def build_tests_group_from_command(line)
|
|
tests_group = TestsGroup.new
|
|
parts = line.split
|
|
while parts.any?
|
|
case part = parts.shift
|
|
when /^TEST_ENV_NUMBER=/
|
|
tests_group.test_env_number = part.delete_prefix("TEST_ENV_NUMBER=")
|
|
when "--seed"
|
|
tests_group.seed = parts.shift
|
|
when /_spec.rb$/
|
|
tests_group.files << part
|
|
end
|
|
end
|
|
tests_group
|
|
end
|
|
end
|
|
|
|
class Formatter
|
|
def initialize(compact: false)
|
|
@compact = compact
|
|
end
|
|
|
|
def compact?
|
|
@compact
|
|
end
|
|
|
|
def display_workflow_run_info(report, workflow_run)
|
|
report.head_branch = workflow_run["head_branch"]
|
|
report.head_sha = workflow_run["head_sha"]
|
|
report.commit_message = commit_message(workflow_run)
|
|
report.run_started_at = Time.parse(workflow_run["run_started_at"]).utc
|
|
warn " Branch: #{report.head_branch.bold}"
|
|
warn " Commit SHA: #{report.head_sha.bold}"
|
|
warn " Commit message: #{report.commit_message.bold}"
|
|
warn " Run started at: #{report.run_started_at.localtime.to_s.bold}"
|
|
display_pull_request_info(workflow_run)
|
|
end
|
|
|
|
def display_workflow_status(workflow_run)
|
|
warn " #{status_line(workflow_run)}"
|
|
end
|
|
|
|
def display_job_status(job)
|
|
warn " #{status_line(job)}"
|
|
end
|
|
|
|
def display_report(report)
|
|
display_failures_explanation(report.failures_explanation)
|
|
display_checkout_test_commit_information(report) if Options.display_rerun_info
|
|
display_errors(report.errors)
|
|
end
|
|
|
|
def display_failures_explanation(failures_explanation)
|
|
return if failures_explanation.nil? || failures_explanation.empty?
|
|
|
|
warn "Failures explanation:".bold
|
|
warn failures_explanation
|
|
warn ""
|
|
end
|
|
|
|
def display_errors(errors)
|
|
if errors.empty?
|
|
warn "No rspec errors found :-/"
|
|
elsif compact?
|
|
display_errors_compact(errors)
|
|
else
|
|
display_errors_detailed(errors)
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def display_errors_compact(errors)
|
|
puts errors.map { escaped_location(_1) }.join(" ")
|
|
end
|
|
|
|
def display_errors_detailed(errors)
|
|
if Options.display_rerun_info
|
|
errors
|
|
.sort_by { |error| [error.tests_group.test_env_number.to_i, error.location] }
|
|
.group_by(&:tests_group)
|
|
.each do |tests_group, tests_group_errors|
|
|
display_tests_group_info(tests_group)
|
|
tests_group_errors.each { display_error(_1) }
|
|
display_tests_group_rerun_commands(tests_group)
|
|
end
|
|
else
|
|
errors
|
|
.sort_by(&:location)
|
|
.each { display_error(_1) }
|
|
end
|
|
end
|
|
|
|
def display_error(error)
|
|
puts escaped_location(error)
|
|
display_error_attribute("loading error", error.loading_error)
|
|
display_error_attribute("html", error.page_html)
|
|
display_error_screenshot(error.page_screenshot)
|
|
end
|
|
|
|
def display_error_screenshot(screenshot_url)
|
|
return unless screenshot_url
|
|
|
|
display_error_attribute("screenshot", screenshot_url)
|
|
if Options.display_images
|
|
if screenshot_url.start_with?("/")
|
|
# external pull requests do not have access to AWS credentials to upload screenshots
|
|
warn " (cannot display screenshot: it is local file stored on the runner)".yellow
|
|
return
|
|
end
|
|
begin
|
|
Open3.popen2e("imgcat", "--width", "50%", "--url", screenshot_url) do |stdin, stdout_and_stderr, _thread|
|
|
stdin.close
|
|
warn stdout_and_stderr.read
|
|
end
|
|
rescue Errno::ENOENT => e
|
|
warn e
|
|
warn "To display image inline, you need to have a compatible terminal like iTerm2 or Konsole, and the 'imgcat' command installed."
|
|
warn "See https://iterm2.com/documentation-images.html to know more."
|
|
end
|
|
end
|
|
end
|
|
|
|
def display_error_attribute(name, value)
|
|
return unless value
|
|
|
|
warn [
|
|
" ",
|
|
"↳".blue.bold,
|
|
" ",
|
|
name.blue,
|
|
": ",
|
|
value.to_s.blue
|
|
].join
|
|
end
|
|
|
|
def display_checkout_test_commit_information(report)
|
|
if report.merge_branch_sha
|
|
warn <<~INSTRUCTIONS.white.dark
|
|
To be with the exact same source files as CI, use these commands:
|
|
git branch --force repro/head_branch #{report.head_sha}
|
|
git branch --force repro/merge_branch #{report.merge_branch_sha}
|
|
git checkout repro/merge_branch
|
|
git merge --no-commit --no-verify repro/head_branch
|
|
git checkout --detach
|
|
git branch --delete repro/head_branch
|
|
git branch --delete repro/merge_branch
|
|
INSTRUCTIONS
|
|
else
|
|
warn <<~INSTRUCTIONS.white.dark
|
|
To be with the exact same source files as CI, use this command:
|
|
git checkout #{report.head_sha}
|
|
INSTRUCTIONS
|
|
end
|
|
warn # empty line
|
|
end
|
|
|
|
def display_tests_group_info(tests_group)
|
|
return unless tests_group
|
|
|
|
warn "Tests group ##{tests_group.test_env_number}".bold
|
|
end
|
|
|
|
def display_tests_group_rerun_commands(tests_group)
|
|
return unless tests_group
|
|
|
|
warn "To run the tests group in the same order as CI, use this command:".white.dark
|
|
warn "CI=true bundle exec rspec --seed #{tests_group.seed} #{tests_group.files.join(' ')}".white.dark
|
|
warn ""
|
|
end
|
|
|
|
def display_pull_request_info(workflow_run)
|
|
return unless workflow_run["event"] == "pull_request"
|
|
|
|
if 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} "
|
|
else
|
|
warn " Pull Request: not found; perhaps it is already merged or closed?"
|
|
end
|
|
end
|
|
|
|
def commit_message(workflow_run)
|
|
workflow_run["head_commit"]
|
|
.then { |commit| commit["message"] }
|
|
.then { |message| message.split("\n", 2).first }
|
|
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 escaped_location(error)
|
|
"'#{error.location}'"
|
|
end
|
|
end
|
|
|
|
def get_failed_jobs_logs_from_github(report, formatter)
|
|
workflow_run = get_workflow_run(Options.run_id)
|
|
|
|
formatter.display_workflow_run_info(report, workflow_run)
|
|
|
|
formatter.display_workflow_status(workflow_run)
|
|
job_logs = get_jobs(workflow_run)
|
|
.then { |jobs_response| jobs_response["jobs"] }
|
|
.sort_by { _1["name"] }
|
|
.each { |job| formatter.display_job_status(job) }
|
|
.select { _1["conclusion"] == "failure" }
|
|
.reject { EXCLUDED_JOB_NAMES.include?(_1["name"]) }
|
|
.map { |job| get_log(job) }
|
|
|
|
[job_logs, job_logs.any? ? "error" : workflow_run["status"]]
|
|
end
|
|
|
|
def get_failed_jobs_logs_from_args
|
|
Options.failed_jobs_logs.map { |path| File.read(path) }
|
|
end
|
|
|
|
def get_failed_jobs_logs(report, formatter)
|
|
if Options.failed_jobs_logs.any?
|
|
[get_failed_jobs_logs_from_args, failed_jobs_logs.none? ? "completed" : "error"]
|
|
else
|
|
get_failed_jobs_logs_from_github(report, formatter)
|
|
end
|
|
end
|
|
|
|
##########
|
|
|
|
# report stores all found information about the workflow run
|
|
report = Report.new
|
|
formatter = Formatter.new(compact: Options.compact)
|
|
|
|
failed_jobs_logs, status = get_failed_jobs_logs(report, formatter)
|
|
|
|
JobErrorsFinder.scan_logs(report, failed_jobs_logs)
|
|
|
|
case status
|
|
when "completed"
|
|
warn "All jobs successful 🎉"
|
|
when "in_progress"
|
|
# NOOP
|
|
when "error"
|
|
formatter.display_report(report)
|
|
end
|