ci_runner 0.1.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/ci_runner.gemspec +1 -0
- data/lib/ci_runner/check/base.rb +63 -0
- data/lib/ci_runner/check/buildkite.rb +88 -0
- data/lib/ci_runner/check/circle_ci.rb +128 -0
- data/lib/ci_runner/check/concurrent_download.rb +57 -0
- data/lib/ci_runner/check/github.rb +40 -0
- data/lib/ci_runner/check/unsupported.rb +33 -0
- data/lib/ci_runner/cli.rb +116 -24
- data/lib/ci_runner/client/authenticated_buildkite.rb +67 -0
- data/lib/ci_runner/client/base.rb +78 -0
- data/lib/ci_runner/client/buildkite.rb +60 -0
- data/lib/ci_runner/client/circle_ci.rb +58 -0
- data/lib/ci_runner/client/error.rb +27 -0
- data/lib/ci_runner/client/github.rb +88 -0
- data/lib/ci_runner/configuration/user.rb +48 -0
- data/lib/ci_runner/log_downloader.rb +20 -20
- data/lib/ci_runner/runners/base.rb +6 -4
- data/lib/ci_runner/runners/minitest_runner.rb +1 -1
- data/lib/ci_runner/runners/rspec.rb +1 -1
- data/lib/ci_runner/test_failure.rb +3 -0
- data/lib/ci_runner/test_run_finder.rb +81 -18
- data/lib/ci_runner/version.rb +1 -1
- data/lib/ci_runner/version_verifier.rb +53 -0
- data/lib/ci_runner.rb +22 -6
- metadata +17 -4
- data/lib/ci_runner/github_client.rb +0 -105
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f7834106a8db1f0770e5184dcf31fbfc892a92890b52f4709ab900ccd560f1b2
|
4
|
+
data.tar.gz: 69c9c0533ff5a52a1f97cb1f4374e012f56004a06a584b2b60ab80e4c9897517
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 67d81327e046862ccb85ead5688aac13d9818d8ffc1487ef92001fba07ff6c6795dee7a537566e410c3c864ed0ee8e3712668310412cae9bbab3eddaeff52114
|
7
|
+
data.tar.gz: ac943446f8cf3ebd2a65c5d35e021aebcfee0cca6c5e693bafe7f4ea41b673bc669a3eabe1914eed70a98c646dc12d51dba12d14a066e6b51538a9bfc37318d8
|
data/ci_runner.gemspec
CHANGED
@@ -24,6 +24,7 @@ Gem::Specification.new do |spec|
|
|
24
24
|
|
25
25
|
spec.metadata["homepage_uri"] = spec.homepage
|
26
26
|
spec.metadata["source_code_uri"] = "https://github.com/Edouard-chin/ci_runner"
|
27
|
+
spec.metadata["changelog_uri"] = "https://github.com/Edouard-chin/ci_runner/blob/main/CHANGELOG.md"
|
27
28
|
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
28
29
|
spec.metadata["rubygems_mfa_required"] = "true"
|
29
30
|
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CIRunner
|
4
|
+
module Check
|
5
|
+
# Base class for a CI check.
|
6
|
+
#
|
7
|
+
# @see https://docs.github.com/en/rest/checks/runs#get-a-check-run
|
8
|
+
# @see https://docs.github.com/en/rest/commits/statuses#list-commit-statuses-for-a-reference
|
9
|
+
class Base
|
10
|
+
# @return [String] The full repository name, including the owner (i.e. rails/rails)
|
11
|
+
attr_reader :repository
|
12
|
+
|
13
|
+
# @return [String] The Git commit that has been pushed to GitHub and for which we'll retrieve the CI checks.
|
14
|
+
attr_reader :commit
|
15
|
+
|
16
|
+
# @return [String] The name of that check. Should be whatever you had set in your CI configuration cile
|
17
|
+
attr_reader :name
|
18
|
+
|
19
|
+
# @return [String] The status from the GitHub API for this check. Can be a lot of different values.
|
20
|
+
# See the GitHub API.
|
21
|
+
attr_reader :status
|
22
|
+
|
23
|
+
# @param repository (See #repository)
|
24
|
+
# @param commit (See #commit)
|
25
|
+
# @param name (See #name)
|
26
|
+
# @param status (See #status)
|
27
|
+
def initialize(repository, commit, name, status)
|
28
|
+
@repository = repository
|
29
|
+
@commit = commit
|
30
|
+
@name = name
|
31
|
+
@status = status
|
32
|
+
end
|
33
|
+
|
34
|
+
# Subclass have to implement this to download the log(s) output for the build.
|
35
|
+
#
|
36
|
+
# @raise [NotImplementedError]
|
37
|
+
#
|
38
|
+
# @return [IO]
|
39
|
+
def download_log
|
40
|
+
raise(NotImplementedError, "Subclass responsability")
|
41
|
+
end
|
42
|
+
|
43
|
+
# Used to tell the user which CI provider we are downloading the log output from.
|
44
|
+
#
|
45
|
+
# @return [String]
|
46
|
+
def provider
|
47
|
+
raise(NotImplementedError, "Subclass responsability")
|
48
|
+
end
|
49
|
+
|
50
|
+
# @return [Boolean]
|
51
|
+
def success?
|
52
|
+
@status == "success"
|
53
|
+
end
|
54
|
+
|
55
|
+
# @return [Boolean]
|
56
|
+
#
|
57
|
+
# @see https://docs.github.com/en/rest/commits/statuses#get-the-combined-status-for-a-specific-reference
|
58
|
+
def failed?
|
59
|
+
["error", "failure"].include?(status)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base"
|
4
|
+
require "uri"
|
5
|
+
|
6
|
+
module CIRunner
|
7
|
+
module Check
|
8
|
+
# Check class used when a project is configured to run its CI using Buildkite.
|
9
|
+
class Buildkite < Base
|
10
|
+
include ConcurrentDownload
|
11
|
+
|
12
|
+
attr_reader :url # :private:
|
13
|
+
|
14
|
+
# @param args (See Base#initialize)
|
15
|
+
# @param url [String] The html URL pointing to the Buildkite build.
|
16
|
+
def initialize(*args, url)
|
17
|
+
super(*args)
|
18
|
+
|
19
|
+
@url = url
|
20
|
+
end
|
21
|
+
|
22
|
+
# Used to tell the user which CI provider we are downloading the log output from.
|
23
|
+
#
|
24
|
+
# @return [String]
|
25
|
+
def provider
|
26
|
+
"Buildkite"
|
27
|
+
end
|
28
|
+
|
29
|
+
# Download the CI logs for this Buildkite build.
|
30
|
+
#
|
31
|
+
# The Buildkite API scopes tokens per organizations (token generated for org A can't access
|
32
|
+
# resource on org B, even for public resources). This means that for opensource projects using
|
33
|
+
# Buildkite, users that are not members of the buildkite org normally can't use CI Runner.
|
34
|
+
#
|
35
|
+
# To bypass this problem, for builds that are public, CI Runner uses a different API.
|
36
|
+
# For private build, CI runner will check if the user had stored a Buildkite token in its config.
|
37
|
+
#
|
38
|
+
# @return [Tempfile]
|
39
|
+
def download_log
|
40
|
+
uri = URI(url)
|
41
|
+
_, org, pipeline, _, build = uri.path.split("/")
|
42
|
+
@client = Client::Buildkite.new
|
43
|
+
|
44
|
+
unless @client.public_build?(org, pipeline, build)
|
45
|
+
token = retrieve_token_from_config(org, url)
|
46
|
+
@client = Client::AuthenticatedBuildkite.new(token)
|
47
|
+
end
|
48
|
+
|
49
|
+
@client.job_logs(org, pipeline, build).each do |log_url|
|
50
|
+
@queue << log_url
|
51
|
+
end
|
52
|
+
|
53
|
+
process_queue
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
# @param url [String]
|
59
|
+
#
|
60
|
+
# @return [void]
|
61
|
+
def process(url)
|
62
|
+
@client.reset!
|
63
|
+
response = @client.download_log(url)
|
64
|
+
|
65
|
+
@tempfile.write(response.read)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Retrieve a Buildkite token from the user confg.
|
69
|
+
#
|
70
|
+
# @param organization [String] The organization that owns this buildkite build.
|
71
|
+
# @param url [String] The FQDN pointing to the buildkite build.
|
72
|
+
#
|
73
|
+
# @return [String] The token
|
74
|
+
#
|
75
|
+
# @raise [Error] If no token for that organization exists in the config.
|
76
|
+
def retrieve_token_from_config(organization, url)
|
77
|
+
token = Configuration::User.instance.buildkite_token(organization.downcase)
|
78
|
+
|
79
|
+
token || raise(Error, <<~EOM)
|
80
|
+
Can't get the log output from the Buildkite build #{url} because it requires authentication.
|
81
|
+
|
82
|
+
Please store a Buildkite token scoped to the organization #{organization} and retry.
|
83
|
+
See {{command:ci_runner help buildkite_token}}
|
84
|
+
EOM
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base"
|
4
|
+
require "uri"
|
5
|
+
require "open-uri"
|
6
|
+
require "json"
|
7
|
+
|
8
|
+
module CIRunner
|
9
|
+
module Check
|
10
|
+
# A Step object represents a CircleCI step.
|
11
|
+
# This Struct has +eql?+ and +hash+ implemented in order to check if two steps are the same and remove
|
12
|
+
# the duplicates.
|
13
|
+
#
|
14
|
+
# Two steps are considered the same if their names are equal and both are successful.
|
15
|
+
# The reason this is implemented like this is to avoid downloading too many of the same logfiles.
|
16
|
+
#
|
17
|
+
# Project on CircleCI can be configured to run in parallel, the number of steps and therefore log output
|
18
|
+
# we have to download increases exponentially.
|
19
|
+
#
|
20
|
+
# As an example, imagine this CircleCI configuration:
|
21
|
+
#
|
22
|
+
# 'Minitest':
|
23
|
+
# executor: ruby/default
|
24
|
+
# parallelism: 16
|
25
|
+
# steps:
|
26
|
+
# - setup-ruby
|
27
|
+
# - bundle install
|
28
|
+
# - bin/rails test
|
29
|
+
#
|
30
|
+
# CircleCI will create 48 steps (and 48 log download link). Downloading those 48 log, don't make sense
|
31
|
+
# since they will be all similar. Unless they failed, in which case we download the log for that step.
|
32
|
+
#
|
33
|
+
# @see https://circleci.com/docs/configuration-reference#steps
|
34
|
+
Step = Struct.new(:name, :output_url, :failed) do
|
35
|
+
# Used in conjuction with +hash+ for unique comparison.
|
36
|
+
#
|
37
|
+
# @param other [Object]
|
38
|
+
#
|
39
|
+
# @return [Boolean]
|
40
|
+
def eql?(other)
|
41
|
+
return false if failed || other.failed
|
42
|
+
|
43
|
+
name == other.name
|
44
|
+
end
|
45
|
+
|
46
|
+
# Used for unique comparison.
|
47
|
+
#
|
48
|
+
# @return [String]
|
49
|
+
def hash
|
50
|
+
[self.class, name, failed].hash
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Check class used when a project is configured to run its CI using CircleCI.
|
55
|
+
class CircleCI < Base
|
56
|
+
include ConcurrentDownload
|
57
|
+
|
58
|
+
attr_reader :url # :private:
|
59
|
+
|
60
|
+
# @param args (See Base#initialize)
|
61
|
+
# @param url [String] The html URL pointing to the CircleCI build.
|
62
|
+
def initialize(*args, url)
|
63
|
+
super(*args)
|
64
|
+
|
65
|
+
@url = url
|
66
|
+
end
|
67
|
+
|
68
|
+
# Used to tell the user which CI provider we are downloading the log output from.
|
69
|
+
#
|
70
|
+
# @return [String]
|
71
|
+
def provider
|
72
|
+
"CircleCI"
|
73
|
+
end
|
74
|
+
|
75
|
+
# Download the CI logs for this CI build.
|
76
|
+
#
|
77
|
+
# CircleCI doesn't have an API to download a single log file for the whole build. Instead, we have
|
78
|
+
# to download a log output for each steps. Depending on the number of steps configured on a project, and
|
79
|
+
# whether it uses parallelism, the number of log files to download might be quite important.
|
80
|
+
#
|
81
|
+
# The log for each steps are small in size, so downloading them in parallel to make things much faster.
|
82
|
+
#
|
83
|
+
# @return [Tempfile]
|
84
|
+
def download_log
|
85
|
+
client = Client::CircleCI.new(Configuration::User.instance.circle_ci_token)
|
86
|
+
job = client.job(repository, build_number)
|
87
|
+
steps = []
|
88
|
+
|
89
|
+
job["steps"].each do |step|
|
90
|
+
step["actions"].each do |parallel|
|
91
|
+
next unless parallel["has_output"]
|
92
|
+
|
93
|
+
steps << Step.new(*parallel.values_at("name", "output_url", "failed"))
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
steps.uniq!
|
98
|
+
|
99
|
+
steps.each do |step|
|
100
|
+
@queue << step
|
101
|
+
end
|
102
|
+
|
103
|
+
process_queue
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
|
108
|
+
# @param step [Step]
|
109
|
+
#
|
110
|
+
# @return [void]
|
111
|
+
def process(step)
|
112
|
+
response = URI.open(step.output_url)
|
113
|
+
parsed_response = JSON.parse(response.read)
|
114
|
+
log_output = parsed_response.map! { |res| res["message"] }.join
|
115
|
+
|
116
|
+
@tempfile.write(log_output)
|
117
|
+
end
|
118
|
+
|
119
|
+
# The URL on the commit status will look something like: https://circleci.com/gh/owner/repo/1234?query_string.
|
120
|
+
# We want the `1234` which is the builder number.
|
121
|
+
#
|
122
|
+
# @return [Integer]
|
123
|
+
def build_number
|
124
|
+
URI(@url.to_s).path.split("/").last
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "tempfile"
|
4
|
+
|
5
|
+
module CIRunner
|
6
|
+
module Check
|
7
|
+
# Module used to dowload multiple logfiles in parallel.
|
8
|
+
#
|
9
|
+
# Some CI providers doesn't have an API to download a single log file for the whole
|
10
|
+
# build, and instead one log file is produced per step. CI Runner needs to download
|
11
|
+
# the logfile of all steps in the build in order to rerun all test that failed.
|
12
|
+
module ConcurrentDownload
|
13
|
+
def initialize(...)
|
14
|
+
@queue = Queue.new
|
15
|
+
@tempfile = Tempfile.new
|
16
|
+
|
17
|
+
super(...)
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
# Implement a queuing system in order to download log files in parallel.
|
23
|
+
#
|
24
|
+
# @return [void]
|
25
|
+
def process_queue
|
26
|
+
max_threads = 6
|
27
|
+
threads = []
|
28
|
+
|
29
|
+
max_threads.times do
|
30
|
+
threads << Thread.new do
|
31
|
+
while (element = dequeue)
|
32
|
+
process(element)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
threads.each(&:join)
|
38
|
+
|
39
|
+
@tempfile.tap(&:flush)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Process item in the queue.
|
43
|
+
def process
|
44
|
+
raise(NotImplementedError)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Dequeue a CircleCI Step from the queue.
|
48
|
+
#
|
49
|
+
# @return [Step, nil]
|
50
|
+
def dequeue
|
51
|
+
@queue.pop(true)
|
52
|
+
rescue ThreadError
|
53
|
+
nil
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base"
|
4
|
+
|
5
|
+
module CIRunner
|
6
|
+
module Check
|
7
|
+
# Check class used when a project is configured to run its CI using GitHub actions.
|
8
|
+
#
|
9
|
+
# @see https://docs.github.com/en/rest/actions/workflow-jobs
|
10
|
+
class Github < Base
|
11
|
+
attr_reader :id # :private:
|
12
|
+
|
13
|
+
# @param args (See Base#initialize)
|
14
|
+
# @param id [Integer] The ID of this check.
|
15
|
+
def initialize(*args, id)
|
16
|
+
super(*args)
|
17
|
+
|
18
|
+
@id = id
|
19
|
+
end
|
20
|
+
|
21
|
+
# Used to tell the user which CI provider we are downloading the log output from.
|
22
|
+
#
|
23
|
+
# @return [String]
|
24
|
+
def provider
|
25
|
+
"GitHub"
|
26
|
+
end
|
27
|
+
|
28
|
+
# Download the log output for thig GitHub build.
|
29
|
+
#
|
30
|
+
# @return (See Client::Github#download_log)
|
31
|
+
#
|
32
|
+
# @see https://docs.github.com/en/rest/actions/workflow-jobs#download-job-logs-for-a-workflow-run
|
33
|
+
def download_log
|
34
|
+
github_client = Client::Github.new(Configuration::User.instance.github_token)
|
35
|
+
|
36
|
+
github_client.download_log(@repository, @id)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base"
|
4
|
+
|
5
|
+
module CIRunner
|
6
|
+
module Check
|
7
|
+
# Check class used for any CI provider not (yet) supported by CIRunner.
|
8
|
+
#
|
9
|
+
# When running the `ci_runner` CLI, those will be selectable but CI runner will bail out
|
10
|
+
# if they get selected. Not sure if its a good idea :shrug:.
|
11
|
+
class Unsupported < Base
|
12
|
+
# @return [String]
|
13
|
+
def name
|
14
|
+
"#{@name} (Unsupported by CI Runner)"
|
15
|
+
end
|
16
|
+
|
17
|
+
# @return [String]
|
18
|
+
def provider
|
19
|
+
""
|
20
|
+
end
|
21
|
+
|
22
|
+
# @raise [Error]
|
23
|
+
def download_log
|
24
|
+
raise(Error, <<~EOM)
|
25
|
+
Aw, snap! This CI is not supported by CI Runner.
|
26
|
+
Please open an Issue on GitHub to let me know you are interested:
|
27
|
+
|
28
|
+
{{info:https://github.com/Edouard-chin/ci_runner/issues/new}}
|
29
|
+
EOM
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/lib/ci_runner/cli.rb
CHANGED
@@ -35,6 +35,7 @@ module CIRunner
|
|
35
35
|
def rerun
|
36
36
|
::CLI::UI::StdoutRouter.enable
|
37
37
|
|
38
|
+
check_for_new_version
|
38
39
|
runner = nil
|
39
40
|
|
40
41
|
::CLI::UI.frame("Preparing CI Runner") do
|
@@ -47,14 +48,12 @@ module CIRunner
|
|
47
48
|
run_name = options[:run_name] || ask_for_name(ci_checks)
|
48
49
|
check_run = TestRunFinder.find(ci_checks, run_name)
|
49
50
|
|
50
|
-
ci_log = fetch_ci_log(
|
51
|
-
runner = TestRunFinder.detect_runner(ci_log)
|
51
|
+
ci_log = fetch_ci_log(check_run)
|
52
|
+
runner = TestRunFinder.detect_runner(ci_log.read)
|
52
53
|
runner.parse!
|
53
54
|
|
54
|
-
if runner.failures.count
|
55
|
-
|
56
|
-
end
|
57
|
-
rescue GithubClient::Error, Error => e
|
55
|
+
no_failure_error(ci_log, runner) if runner.failures.count.zero?
|
56
|
+
rescue Client::Error, Error => e
|
58
57
|
::CLI::UI.puts("\n{{red:#{e.message}}}", frame_color: :red)
|
59
58
|
|
60
59
|
exit(false)
|
@@ -71,13 +70,13 @@ module CIRunner
|
|
71
70
|
Save a personal access GitHub token in the ~/.ci_runner/config.yml file.
|
72
71
|
The GitHub token is required to fetch CI checks and download logs from repositories.
|
73
72
|
|
74
|
-
You can get a token from GitHub by following this link: https://github.com/settings/tokens/new?description=CI+Runner&scopes=repo
|
73
|
+
You can get a token from GitHub by following this link: https://github.com/settings/tokens/new?description=CI+Runner&scopes=repo
|
75
74
|
EOM
|
76
75
|
def github_token(token)
|
77
76
|
::CLI::UI::StdoutRouter.enable
|
78
77
|
|
79
78
|
::CLI::UI.frame("Saving GitHub Token") do
|
80
|
-
user =
|
79
|
+
user = Client::Github.new(token).me
|
81
80
|
Configuration::User.instance.save_github_token(token)
|
82
81
|
|
83
82
|
::CLI::UI.puts(<<~EOM)
|
@@ -85,22 +84,97 @@ module CIRunner
|
|
85
84
|
|
86
85
|
{{info:The token has been saved in this file: #{Configuration::User.instance.config_file}}}
|
87
86
|
EOM
|
88
|
-
rescue
|
87
|
+
rescue Client::Error => e
|
89
88
|
::CLI::UI.puts("{{red:\nYour token doesn't seem to be valid. The response from GitHub was: #{e.message}}}")
|
90
89
|
|
91
90
|
exit(false)
|
92
91
|
end
|
93
92
|
end
|
94
93
|
|
94
|
+
desc "circle_ci_token TOKEN", "Save a Circle CI token in your config."
|
95
|
+
long_desc <<~EOM
|
96
|
+
Save a personal access Circle CI token in the ~/.ci_runner/config.yml file.
|
97
|
+
If one of your project uses Circle CI as its CI provider and the project is set to private,
|
98
|
+
CI Runner won't be able to fetch the logs unless you provide a token.
|
99
|
+
|
100
|
+
You can get a token from Circle CI by following this link: https://app.circleci.com/settings/user/tokens
|
101
|
+
EOM
|
102
|
+
def circle_ci_token(token)
|
103
|
+
::CLI::UI::StdoutRouter.enable
|
104
|
+
|
105
|
+
::CLI::UI.frame("Saving CircleCI Token") do
|
106
|
+
user = Client::CircleCI.new(token).me
|
107
|
+
Configuration::User.instance.save_circle_ci_token(token)
|
108
|
+
|
109
|
+
::CLI::UI.puts(<<~EOM)
|
110
|
+
Hello {{warning:#{user["login"]}}}! {{success:Your token is valid!}}
|
111
|
+
|
112
|
+
{{info:The token has been saved in this file: #{Configuration::User.instance.config_file}}}
|
113
|
+
EOM
|
114
|
+
rescue Client::Error => e
|
115
|
+
::CLI::UI.puts("{{red:\nYour token doesn't seem to be valid. The response from Circle CI was: #{e.message}}}")
|
116
|
+
|
117
|
+
exit(false)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
desc "buildkite_token TOKEN ORGANIZATION", "Save a Buildkite token in your config."
|
122
|
+
long_desc <<~EOM
|
123
|
+
Save a personal access Buildkite token in the ~/.ci_runner/config.yml file.
|
124
|
+
Storing a Buildkite token is required to retrieve log from private Buildkite builds.
|
125
|
+
|
126
|
+
The ORGANIZATION, should be the name of the organization the token has access to.
|
127
|
+
|
128
|
+
You can get a token from Buildkite by following this link: https://buildkite.com/user/api-access-tokens/new?description=CI%20Runner&scopes[]=read_builds&scopes[]=read_build_logs
|
129
|
+
EOM
|
130
|
+
def buildkite_token(token, organization)
|
131
|
+
::CLI::UI::StdoutRouter.enable
|
132
|
+
|
133
|
+
required_scopes = ["read_builds", "read_build_logs"]
|
134
|
+
token_scopes = Client::AuthenticatedBuildkite.new(token).access_token["scopes"]
|
135
|
+
missing_scopes = required_scopes - token_scopes
|
136
|
+
|
137
|
+
if missing_scopes.empty?
|
138
|
+
Configuration::User.instance.save_buildkite_token(token, organization)
|
139
|
+
|
140
|
+
::CLI::UI.puts(<<~EOM)
|
141
|
+
{{success:Your token is valid!}}
|
142
|
+
|
143
|
+
{{info:The token has been saved in this file: #{Configuration::User.instance.config_file}}}
|
144
|
+
EOM
|
145
|
+
else
|
146
|
+
::CLI::UI.puts("{{red:\nYour token is missing required scope(s): #{missing_scopes.join(",")}")
|
147
|
+
end
|
148
|
+
rescue Client::Error => e
|
149
|
+
::CLI::UI.puts("{{red:\nYour token doesn't seem to be valid. The response from Buildkite was: #{e.message}}}")
|
150
|
+
|
151
|
+
exit(false)
|
152
|
+
end
|
153
|
+
|
95
154
|
private
|
96
155
|
|
156
|
+
# Inform the user of a possible new CI Runner version.
|
157
|
+
#
|
158
|
+
# @return [void]
|
159
|
+
def check_for_new_version
|
160
|
+
version_verifier = VersionVerifier.new
|
161
|
+
return unless version_verifier.new_ci_runner_version?
|
162
|
+
|
163
|
+
::CLI::UI.puts(<<~EOM)
|
164
|
+
{{info:A newer version of CI Runner is available (#{version_verifier.upstream_version}).}}
|
165
|
+
{{info:You can update CI Runner by running}} {{command:gem update ci_runner}}
|
166
|
+
EOM
|
167
|
+
rescue StandardError
|
168
|
+
nil
|
169
|
+
end
|
170
|
+
|
97
171
|
# Retrieve all the GitHub CI checks for a given commit. Will be used to interactively prompt
|
98
172
|
# the user which one to rerun.
|
99
173
|
#
|
100
174
|
# @param repository [String] The full repository name including the owner (rails/rails).
|
101
175
|
# @param commit [String] A Git commit that has been pushed to GitHub and for which CI failed.
|
102
176
|
#
|
103
|
-
# @return [
|
177
|
+
# @return [Array<Check::Base>] Array filled with Check::Base subclasses.
|
104
178
|
#
|
105
179
|
# @raise [SystemExit] Early exit the process if the CI checks can't be retrieved.
|
106
180
|
#
|
@@ -120,19 +194,17 @@ module CIRunner
|
|
120
194
|
# Download and cache the log for the GitHub check. Downloading the log allows CI Runner to parse it and detect
|
121
195
|
# which test failed in order to run uniquely those on the user machine.
|
122
196
|
#
|
123
|
-
# @param
|
124
|
-
# @param commit [String] A Git commit that has been pushed to GitHub and for which CI failed.
|
125
|
-
# @param check_run [Hash] The GitHub Check that failed. See #fetch_ci_checks.
|
197
|
+
# @param check_run [Check::Base] The GitHub Check that failed. See #fetch_ci_checks.
|
126
198
|
#
|
127
199
|
# @return [String] The content of the CI log.
|
128
200
|
#
|
129
201
|
# @raise [SystemExit] Early exit the process if the CI checks can't be retrieved.
|
130
202
|
#
|
131
203
|
# @see https://docs.github.com/en/rest/actions/workflow-jobs#download-job-logs-for-a-workflow-run
|
132
|
-
def fetch_ci_log(
|
133
|
-
log = LogDownloader.new(
|
134
|
-
puts(<<~EOM)
|
135
|
-
Couldn't fetch the CI log. The
|
204
|
+
def fetch_ci_log(check_run)
|
205
|
+
log = LogDownloader.new(check_run).fetch do |error|
|
206
|
+
::CLI::UI.puts(<<~EOM)
|
207
|
+
Couldn't fetch the CI log. The error was:
|
136
208
|
|
137
209
|
#{error.message}
|
138
210
|
EOM
|
@@ -140,27 +212,26 @@ module CIRunner
|
|
140
212
|
exit(false)
|
141
213
|
end
|
142
214
|
|
143
|
-
log
|
215
|
+
log
|
144
216
|
end
|
145
217
|
|
146
218
|
# Interatively ask the user which CI check to rerun in the case a commit has multiple failing checks.
|
147
219
|
# This method only runs if the user has not passed the '--run-name' flag to ci_runner.
|
148
220
|
# Will automatically select a check in the case where there is only one failing check.
|
149
221
|
#
|
150
|
-
# @param ci_checks [
|
222
|
+
# @param ci_checks [Array<Check::Base>] (See #fetch_ci_checks)
|
151
223
|
#
|
152
|
-
# @return [
|
224
|
+
# @return [Check::Base] A single Check, the one that the user selected.
|
153
225
|
#
|
154
226
|
# @raise [CIRunner::Error] In case all the CI checks on this commit were successfull. In such case
|
155
227
|
# there is no need to proceed as there should be no failing tests to rerun.
|
156
228
|
def ask_for_name(ci_checks)
|
157
|
-
|
158
|
-
failed_runs = check_runs.reject { |check_run| check_run["conclusion"] == "success" }
|
229
|
+
failed_runs = ci_checks.select(&:failed?)
|
159
230
|
|
160
231
|
if failed_runs.count == 0
|
161
232
|
raise(Error, "No CI checks failed on this commit.")
|
162
233
|
elsif failed_runs.count == 1
|
163
|
-
check_run = failed_runs.first
|
234
|
+
check_run = failed_runs.first.name
|
164
235
|
|
165
236
|
::CLI::UI.puts(<<~EOM)
|
166
237
|
{{warning:Automatically selected the CI check #{check_run} because it's the only one failing.}}
|
@@ -170,9 +241,30 @@ module CIRunner
|
|
170
241
|
else
|
171
242
|
::CLI::UI.ask(
|
172
243
|
"Multiple CI checks failed for this commit. Please choose the one you wish to re-run.",
|
173
|
-
options: failed_runs.map
|
244
|
+
options: failed_runs.map(&:name),
|
174
245
|
)
|
175
246
|
end
|
176
247
|
end
|
248
|
+
|
249
|
+
# Raise an error in the case where CI Runner can't detect test failures from the logs.
|
250
|
+
# Can happen for a couple of reasons, outlined in the error message below.
|
251
|
+
#
|
252
|
+
# @param ci_log [Pathname]
|
253
|
+
# @param runner [Runners::Minitest, Runners::RSpec]
|
254
|
+
#
|
255
|
+
# @raise [Error]
|
256
|
+
def no_failure_error(ci_log, runner)
|
257
|
+
raise(Error, <<~EOM)
|
258
|
+
Couldn't detect any {{warning:#{runner.name}}} test failures from the log output. This can be either because:
|
259
|
+
|
260
|
+
{{warning:- The selected CI is not running #{runner.name} tests.}}
|
261
|
+
{{warning:- CIRunner default set of regexes failed to match the failures.}}
|
262
|
+
|
263
|
+
If your application is using custom reporters, you'll need to configure CI Runner.
|
264
|
+
{{info:Have a look at the wiki to know how: https://github.com/Edouard-chin/ci_runner/wiki}}
|
265
|
+
|
266
|
+
The CI log output has been downloaded to {{underline:#{ci_log}}} if you need to inspect it.
|
267
|
+
EOM
|
268
|
+
end
|
177
269
|
end
|
178
270
|
end
|