mirror of
https://github.com/opf/openproject.git
synced 2026-06-14 03:30:14 +00:00
543e87a67d
When there is an in progress run and a previous successful run, the successful run is being displayed. This gives a false information about the current run being successful. The fix is to display the in progress run instead.
695 lines
19 KiB
Ruby
Executable File
695 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 "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,
|
|
: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.10/lib/bundler/setup -W0 RSPEC_SILENCE_FILTER_ANNOUNCEMENTS=1 /usr/local/bundle/gems/bundler-2.5.10/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)
|
|
warn " Branch: #{report.head_branch.bold}"
|
|
warn " Commit SHA: #{report.head_sha.bold}"
|
|
warn " Commit message: #{report.commit_message.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
|