ci_runner 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 25a11b4505aff5153e388723711643c4aae5f3a7b64b53b3265e0bd7198ec381
4
- data.tar.gz: 858492f60b87472cf41da22c5808a2371659d56712ef77dad98590cb98063ecf
3
+ metadata.gz: fb83fde059747332cee33b4a0d313d2eaefb735ec3fe0e291141e8a18a535fa3
4
+ data.tar.gz: 6dd4960bce217bbebba6a22b6e1863d0baaf7190c5835c1fc63de9d0cc86e94e
5
5
  SHA512:
6
- metadata.gz: 51158e9a4b9cd2188f5161320dc140e076009f7c4550206b957ffac03e9b5859533dc99defc644f05aa126713f8d52cf1d2260a4691ba74cba746f779ec3c945
7
- data.tar.gz: 5db9352b2360cec4f5f8b23ce4cf378fcef79c3cc1e8b2f1cb4543fb307a2fc8db1116fea023b766d262d768582dd9f44bb1c48434c2c6171e697962398f0543
6
+ metadata.gz: f1e747da3d2101445f77645cc9f4d37f1f36b53702a85b735aecac605576b0d060037b56a54fcd3269c65a4313ebc3a3b00cab05d4acfbfd0785f3138911d51e
7
+ data.tar.gz: 65a90464a386601f2bd6caaaa61ec6ea6188bd65688ddf9e32db0ee7006aff914bc3d3104271d5a4607f0252a1936514ee301b16bb3a708e1d32b7a12bc75c9d
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,61 @@
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
+ def failed?
57
+ @status == "failure"
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+ require "uri"
5
+ require "open-uri"
6
+ require "json"
7
+ require "tempfile"
8
+
9
+ module CIRunner
10
+ module Check
11
+ # A Step object represents a CircleCI step.
12
+ # This Struct has +eql?+ and +hash+ implemented in order to check if two steps are the same and remove
13
+ # the duplicates.
14
+ #
15
+ # Two steps are considered the same if their names are equal and both are successful.
16
+ # The reason this is implemented like this is to avoid downloading too many of the same logfiles.
17
+ #
18
+ # Project on CircleCI can be configured to run in parallel, the number of steps and therefore log output
19
+ # we have to download increases exponentially.
20
+ #
21
+ # As an example, imagine this CircleCI configuration:
22
+ #
23
+ # 'Minitest':
24
+ # executor: ruby/default
25
+ # parallelism: 16
26
+ # steps:
27
+ # - setup-ruby
28
+ # - bundle install
29
+ # - bin/rails test
30
+ #
31
+ # CircleCI will create 48 steps (and 48 log download link). Downloading those 48 log, don't make sense
32
+ # since they will be all similar. Unless they failed, in which case we download the log for that step.
33
+ #
34
+ # @see https://circleci.com/docs/configuration-reference#steps
35
+ Step = Struct.new(:name, :output_url, :failed) do
36
+ # Used in conjuction with +hash+ for unique comparison.
37
+ #
38
+ # @param other [Object]
39
+ #
40
+ # @return [Boolean]
41
+ def eql?(other)
42
+ return false if failed || other.failed
43
+
44
+ name == other.name
45
+ end
46
+
47
+ # Used for unique comparison.
48
+ #
49
+ # @return [String]
50
+ def hash
51
+ [self.class, name, failed].hash
52
+ end
53
+ end
54
+
55
+ # Check class used when a project is configured to run its CI using CircleCI.
56
+ class CircleCI < Base
57
+ attr_reader :url # :private:
58
+
59
+ # @param args (See Base#initialize)
60
+ # @param url [String] The html URL pointing to the CircleCI build.
61
+ def initialize(*args, url)
62
+ super(*args)
63
+
64
+ @url = url
65
+ @queue = Queue.new
66
+ @tempfile = Tempfile.new
67
+ end
68
+
69
+ # Used to tell the user which CI provider we are downloading the log output from.
70
+ #
71
+ # @return [String]
72
+ def provider
73
+ "CircleCI"
74
+ end
75
+
76
+ # Download the CI logs for this CI build.
77
+ #
78
+ # CircleCI doesn't have an API to download a single log file for the whole build. Instead, we have
79
+ # to download a log output for each steps. Depending on the number of steps configured on a project, and
80
+ # whether it uses parallelism, the number of log files to download might be quite important.
81
+ #
82
+ # The log for each steps are small in size, so downloading them in parallel to make things much faster.
83
+ #
84
+ # @return [Tempfile]
85
+ def download_log
86
+ client = Client::CircleCI.new(Configuration::User.instance.circle_ci_token)
87
+ job = client.job(repository, build_number)
88
+ steps = []
89
+
90
+ job["steps"].each do |step|
91
+ step["actions"].each do |parallel|
92
+ next unless parallel["has_output"]
93
+
94
+ steps << Step.new(*parallel.values_at("name", "output_url", "failed"))
95
+ end
96
+ end
97
+
98
+ steps.uniq!
99
+
100
+ steps.each do |step|
101
+ @queue << step
102
+ end
103
+
104
+ process_queue
105
+
106
+ @tempfile.tap(&:flush)
107
+ end
108
+
109
+ # @return [Boolean]
110
+ #
111
+ # @see https://docs.github.com/en/rest/commits/statuses#get-the-combined-status-for-a-specific-reference
112
+ def failed?
113
+ ["error", "failure"].include?(status)
114
+ end
115
+
116
+ private
117
+
118
+ # Implement a queuing system in order to download log files in parallel.
119
+ #
120
+ # @return [void]
121
+ def process_queue
122
+ max_threads = 6
123
+ threads = []
124
+
125
+ max_threads.times do
126
+ threads << Thread.new do
127
+ while (element = dequeue)
128
+ process(element)
129
+ end
130
+ end
131
+ end
132
+
133
+ threads.each(&:join)
134
+ end
135
+
136
+ # @param step [Step]
137
+ #
138
+ # @return [void]
139
+ def process(step)
140
+ response = URI.open(step.output_url)
141
+ parsed_response = JSON.parse(response.read)
142
+ log_output = parsed_response.map! { |res| res["message"] }.join
143
+
144
+ @tempfile.write(log_output)
145
+ end
146
+
147
+ # Dequeue a CircleCI Step from the queue.
148
+ #
149
+ # @return [Step, nil]
150
+ def dequeue
151
+ @queue.pop(true)
152
+ rescue ThreadError
153
+ nil
154
+ end
155
+
156
+ # The URL on the commit status will look something like: https://circleci.com/gh/owner/repo/1234?query_string.
157
+ # We want the `1234` which is the builder number.
158
+ #
159
+ # @return [Integer]
160
+ def build_number
161
+ URI(@url.to_s).path.split("/").last
162
+ end
163
+ end
164
+ end
165
+ 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
@@ -47,14 +47,12 @@ module CIRunner
47
47
  run_name = options[:run_name] || ask_for_name(ci_checks)
48
48
  check_run = TestRunFinder.find(ci_checks, run_name)
49
49
 
50
- ci_log = fetch_ci_log(repository, commit, check_run)
51
- runner = TestRunFinder.detect_runner(ci_log)
50
+ ci_log = fetch_ci_log(check_run)
51
+ runner = TestRunFinder.detect_runner(ci_log.read)
52
52
  runner.parse!
53
53
 
54
- if runner.failures.count == 0
55
- # Error
56
- end
57
- rescue GithubClient::Error, Error => e
54
+ no_failure_error(ci_log, runner) if runner.failures.count.zero?
55
+ rescue Client::Error, Error => e
58
56
  ::CLI::UI.puts("\n{{red:#{e.message}}}", frame_color: :red)
59
57
 
60
58
  exit(false)
@@ -71,13 +69,13 @@ module CIRunner
71
69
  Save a personal access GitHub token in the ~/.ci_runner/config.yml file.
72
70
  The GitHub token is required to fetch CI checks and download logs from repositories.
73
71
 
74
- You can get a token from GitHub by following this link: https://github.com/settings/tokens/new?description=CI+Runner&scopes=repo # rubocop:disable Layout/LineLength
72
+ You can get a token from GitHub by following this link: https://github.com/settings/tokens/new?description=CI+Runner&scopes=repo
75
73
  EOM
76
74
  def github_token(token)
77
75
  ::CLI::UI::StdoutRouter.enable
78
76
 
79
77
  ::CLI::UI.frame("Saving GitHub Token") do
80
- user = GithubClient.new(token).me
78
+ user = Client::Github.new(token).me
81
79
  Configuration::User.instance.save_github_token(token)
82
80
 
83
81
  ::CLI::UI.puts(<<~EOM)
@@ -85,13 +83,40 @@ module CIRunner
85
83
 
86
84
  {{info:The token has been saved in this file: #{Configuration::User.instance.config_file}}}
87
85
  EOM
88
- rescue GithubClient::Error => e
86
+ rescue Client::Error => e
89
87
  ::CLI::UI.puts("{{red:\nYour token doesn't seem to be valid. The response from GitHub was: #{e.message}}}")
90
88
 
91
89
  exit(false)
92
90
  end
93
91
  end
94
92
 
93
+ desc "circle_ci_token TOKEN", "Save a Circle CI token in your config."
94
+ long_desc <<~EOM
95
+ Save a personal access Circle CI token in the ~/.ci_runner/config.yml file.
96
+ If one of your project uses Circle CI as its CI provider and the project is set to private,
97
+ CI Runner won't be able to fetch the logs unless you provide a token.
98
+
99
+ You can get a token from Circle CI by following this link: https://app.circleci.com/settings/user/tokens
100
+ EOM
101
+ def circle_ci_token(token)
102
+ ::CLI::UI::StdoutRouter.enable
103
+
104
+ ::CLI::UI.frame("Saving CircleCI Token") do
105
+ user = Client::CircleCI.new(token).me
106
+ Configuration::User.instance.save_circle_ci_token(token)
107
+
108
+ ::CLI::UI.puts(<<~EOM)
109
+ Hello {{warning:#{user["login"]}}}! {{success:Your token is valid!}}
110
+
111
+ {{info:The token has been saved in this file: #{Configuration::User.instance.config_file}}}
112
+ EOM
113
+ rescue Client::Error => e
114
+ ::CLI::UI.puts("{{red:\nYour token doesn't seem to be valid. The response from Circle CI was: #{e.message}}}")
115
+
116
+ exit(false)
117
+ end
118
+ end
119
+
95
120
  private
96
121
 
97
122
  # Retrieve all the GitHub CI checks for a given commit. Will be used to interactively prompt
@@ -100,7 +125,7 @@ module CIRunner
100
125
  # @param repository [String] The full repository name including the owner (rails/rails).
101
126
  # @param commit [String] A Git commit that has been pushed to GitHub and for which CI failed.
102
127
  #
103
- # @return [Hash] See the GitHub documentation.
128
+ # @return [Array<Check::Base>] Array filled with Check::Base subclasses.
104
129
  #
105
130
  # @raise [SystemExit] Early exit the process if the CI checks can't be retrieved.
106
131
  #
@@ -120,19 +145,17 @@ module CIRunner
120
145
  # Download and cache the log for the GitHub check. Downloading the log allows CI Runner to parse it and detect
121
146
  # which test failed in order to run uniquely those on the user machine.
122
147
  #
123
- # @param repository [String] The full repository name including the owner (rails/rails).
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.
148
+ # @param check_run [Check::Base] The GitHub Check that failed. See #fetch_ci_checks.
126
149
  #
127
150
  # @return [String] The content of the CI log.
128
151
  #
129
152
  # @raise [SystemExit] Early exit the process if the CI checks can't be retrieved.
130
153
  #
131
154
  # @see https://docs.github.com/en/rest/actions/workflow-jobs#download-job-logs-for-a-workflow-run
132
- def fetch_ci_log(repository, commit, check_run)
133
- log = LogDownloader.new(commit, repository, check_run).fetch do |error|
134
- puts(<<~EOM)
135
- Couldn't fetch the CI log. The response from GitHub was:
155
+ def fetch_ci_log(check_run)
156
+ log = LogDownloader.new(check_run).fetch do |error|
157
+ ::CLI::UI.puts(<<~EOM)
158
+ Couldn't fetch the CI log. The error was:
136
159
 
137
160
  #{error.message}
138
161
  EOM
@@ -140,27 +163,26 @@ module CIRunner
140
163
  exit(false)
141
164
  end
142
165
 
143
- log.read
166
+ log
144
167
  end
145
168
 
146
169
  # Interatively ask the user which CI check to rerun in the case a commit has multiple failing checks.
147
170
  # This method only runs if the user has not passed the '--run-name' flag to ci_runner.
148
171
  # Will automatically select a check in the case where there is only one failing check.
149
172
  #
150
- # @param ci_checks [Hash] (See #fetch_ci_checks)
173
+ # @param ci_checks [Array<Check::Base>] (See #fetch_ci_checks)
151
174
  #
152
- # @return [Hash] A single Check Run, the one that the user selected.
175
+ # @return [Check::Base] A single Check, the one that the user selected.
153
176
  #
154
177
  # @raise [CIRunner::Error] In case all the CI checks on this commit were successfull. In such case
155
178
  # there is no need to proceed as there should be no failing tests to rerun.
156
179
  def ask_for_name(ci_checks)
157
- check_runs = ci_checks["check_runs"]
158
- failed_runs = check_runs.reject { |check_run| check_run["conclusion"] == "success" }
180
+ failed_runs = ci_checks.select(&:failed?)
159
181
 
160
182
  if failed_runs.count == 0
161
183
  raise(Error, "No CI checks failed on this commit.")
162
184
  elsif failed_runs.count == 1
163
- check_run = failed_runs.first["name"]
185
+ check_run = failed_runs.first.name
164
186
 
165
187
  ::CLI::UI.puts(<<~EOM)
166
188
  {{warning:Automatically selected the CI check #{check_run} because it's the only one failing.}}
@@ -170,9 +192,30 @@ module CIRunner
170
192
  else
171
193
  ::CLI::UI.ask(
172
194
  "Multiple CI checks failed for this commit. Please choose the one you wish to re-run.",
173
- options: failed_runs.map { |check_run| check_run["name"] },
195
+ options: failed_runs.map(&:name),
174
196
  )
175
197
  end
176
198
  end
199
+
200
+ # Raise an error in the case where CI Runner can't detect test failures from the logs.
201
+ # Can happen for a couple of reasons, outlined in the error message below.
202
+ #
203
+ # @param ci_log [Pathname]
204
+ # @param runner [Runners::Minitest, Runners::RSpec]
205
+ #
206
+ # @raise [Error]
207
+ def no_failure_error(ci_log, runner)
208
+ raise(Error, <<~EOM)
209
+ Couldn't detect any {{warning:#{runner.name}}} test failures from the log output. This can be either because:
210
+
211
+ {{warning:- The selected CI is not running #{runner.name} tests.}}
212
+ {{warning:- CIRunner default set of regexes failed to match the failures.}}
213
+
214
+ If your application is using custom reporters, you'll need to configure CI Runner.
215
+ {{info:Have a look at the wiki to know how: https://github.com/Edouard-chin/ci_runner/wiki}}
216
+
217
+ The CI log output has been downloaded to {{underline:#{ci_log}}} if you need to inspect it.
218
+ EOM
219
+ end
177
220
  end
178
221
  end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "net/http"
5
+ require "openssl"
6
+
7
+ module CIRunner
8
+ module Client
9
+ class Base
10
+ # @return [Net::HTTP] An instance of Net:HTTP configured to make requests to the GitHub API endpoint.
11
+ def self.default_client
12
+ Net::HTTP.new(self::API_ENDPOINT, 443).tap do |http|
13
+ http.use_ssl = true
14
+ http.read_timeout = 3
15
+ http.write_timeout = 3
16
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
17
+ end
18
+ end
19
+
20
+ # @param access_token [String] The access token with "repo" scope.
21
+ # @param client [Net::HTTP]
22
+ def initialize(access_token = nil, client = self.class.default_client)
23
+ @access_token = access_token
24
+ @client = client
25
+ end
26
+
27
+ private
28
+
29
+ # Add authentication before making the request.
30
+ #
31
+ # @param request [Net::HTTPRequest] A subclass of Net::HTTPRequest.
32
+ #
33
+ # @return [void]
34
+ def authentication(request)
35
+ raise(NotImplementedError, "Subclass responsability")
36
+ end
37
+
38
+ # Perform an authenticated GET request.
39
+ #
40
+ # @param path [String] The resource to access.
41
+ #
42
+ # @return (See #request)
43
+ def get(path)
44
+ request(Net::HTTP::Get, path)
45
+ end
46
+
47
+ # Perform an authenticated request.
48
+ #
49
+ # @param verb_class [Net::HTTPRequest] A subclass of Net::HTTPRequest.
50
+ # @param path [String] The resource to access.
51
+ #
52
+ # @return [Hash, String] A decoded JSON response or a String pointing to the Location redirection.
53
+ def request(verb_class, path)
54
+ req = verb_class.new(path)
55
+ req["Accept"] = "application/json"
56
+ authentication(req)
57
+
58
+ response = @client.request(req)
59
+
60
+ case response.code.to_i
61
+ when 200..204
62
+ response.content_type == "application/json" ? JSON.parse(response.body) : response.body
63
+ when 302
64
+ response["Location"]
65
+ else
66
+ raise(Error.new(response.code, response.body, self.class.name.split("::").last))
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module CIRunner
6
+ module Client
7
+ class CircleCI < Base
8
+ API_ENDPOINT = "circleci.com"
9
+
10
+ # Make an API request to get the authenticated user. Used to verify if the access token
11
+ # the user has stored in its config is valid.
12
+ #
13
+ # @return [Hash] See Circle CI documentation.
14
+ #
15
+ # @see https://circleci.com/docs/api/v1/index.html#user
16
+ def me
17
+ get("/api/v1.1/me")
18
+ end
19
+
20
+ # @param repository [String] The full repository name including the owner (rails/rails).
21
+ # @param build_number [Integer] The CircleCI build number.
22
+ #
23
+ # @see https://circleci.com/docs/api/v1/index.html#single-job
24
+ def job(repository, build_number)
25
+ get("/api/v1.1/project/github/#{repository}/#{build_number}")
26
+ rescue Error => e
27
+ reraise_with_reason(e)
28
+ end
29
+
30
+ private
31
+
32
+ # Add authentication before making the request.
33
+ #
34
+ # @param request [Net::HTTPRequest] A subclass of Net::HTTPRequest.
35
+ #
36
+ # @return [void]
37
+ def authentication(request)
38
+ request.basic_auth(@access_token, "") if @access_token
39
+ end
40
+
41
+ # @param error [Client::Error]
42
+ #
43
+ # @raise [Client::Error] A better error message in case of a 404.
44
+ def reraise_with_reason(error)
45
+ if @access_token.nil? && error.error_code == 404
46
+ raise(error, <<~EOM.rstrip)
47
+ 404 while trying to fetch the CircleCI build.
48
+
49
+ {{warning:Please save a CircleCI token in your configuration.}}
50
+ {{command:ci_runner help circle_ci_token}}
51
+ EOM
52
+ else
53
+ raise(error)
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CIRunner
4
+ module Client
5
+ class Error < StandardError
6
+ attr_reader :error_code
7
+
8
+ # @param error_code [String] The HTTP status code.
9
+ # @param error_body [String] The response from the provider.
10
+ # @param provider [String] The name of the CI provider.
11
+ # @param message [String, nil]
12
+ def initialize(error_code, error_body, provider, message = nil)
13
+ @error_code = error_code.to_i
14
+
15
+ if message
16
+ super(message)
17
+ else
18
+ super(<<~EOM.rstrip)
19
+ Error while making a request to #{provider}. Code: #{error_code}
20
+
21
+ The response was: #{error_body}
22
+ EOM
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+ require "open-uri"
5
+
6
+ module CIRunner
7
+ module Client
8
+ # A simple client to interact the GitHub API.
9
+ #
10
+ # @example Using the client
11
+ # Github.new("access_token").me
12
+ class Github < Base
13
+ API_ENDPOINT = "api.github.com"
14
+
15
+ # Make an API request to get the authenticated user. Used to verify if the access token
16
+ # the user has stored in its config is valid.
17
+ #
18
+ # @return [Hash] See GitHub documentation.
19
+ #
20
+ # @see https://docs.github.com/en/rest/users/users#get-the-authenticated-user
21
+ def me
22
+ get("/user")
23
+ end
24
+
25
+ # Makes an API request to get the CI checks for the +commit+.
26
+ #
27
+ # @param repository [String] The full repository name, including the owner (rails/rails)
28
+ # @param commit [String] The Git commit that has been pushed to GitHub.
29
+ #
30
+ # @return [Hash] See GitHub documentation.
31
+ #
32
+ # @see https://docs.github.com/en/rest/checks/runs#list-check-runs-for-a-git-reference
33
+ def check_runs(repository, commit)
34
+ get("/repos/#{repository}/commits/#{commit}/check-runs")
35
+ end
36
+
37
+ # Makes an API request to get the Commit statuses for the +commit+.
38
+ #
39
+ # @param repository [String] The full repository name, including the owner (rails/rails)
40
+ # @param commit [String] The Git commit that has been pushed to GitHub.
41
+ #
42
+ # @return [Hash] See GitHub documentation.
43
+ #
44
+ # @see https://docs.github.com/en/rest/commits/statuses#list-commit-statuses-for-a-reference
45
+ def commit_statuses(repository, commit)
46
+ get("/repos/#{repository}/commits/#{commit}/statuses")
47
+ end
48
+
49
+ # Makes two requests to get the CI log for a check run.
50
+ # The first request returns a 302 containing a Location header poiting to a short lived url to download the log.
51
+ # The second request is to actually download the log.
52
+ #
53
+ # @param repository [String] The full repository name, including the owner (rails/rails)
54
+ # @param check_run_id [Integer] The GitHub ID of the check run.
55
+ #
56
+ # @return [Tempfile, IO] Depending on the size of the response. Quirk of URI.open.
57
+ #
58
+ # @see https://docs.github.com/en/rest/actions/workflow-jobs#download-job-logs-for-a-workflow-run
59
+ def download_log(repository, check_run_id)
60
+ download_url = get("/repos/#{repository}/actions/jobs/#{check_run_id}/logs")
61
+
62
+ URI.open(download_url)
63
+ end
64
+
65
+ private
66
+
67
+ # Add authentication before making the request.
68
+ #
69
+ # @param request [Net::HTTPRequest] A subclass of Net::HTTPRequest.
70
+ #
71
+ # @return [void]
72
+ def authentication(request)
73
+ request.basic_auth("user", @access_token)
74
+ end
75
+ end
76
+ end
77
+ end
@@ -39,6 +39,13 @@ module CIRunner
39
39
  @yaml_config.dig("github", "token")
40
40
  end
41
41
 
42
+ # Retrieve the stored CircleCI access token of the user.
43
+ #
44
+ # @return [String, nil] Depending if the user ran the `ci_runner circle_ci_token TOKEN` command.
45
+ def circle_ci_token
46
+ @yaml_config.dig("circle_ci", "token")
47
+ end
48
+
42
49
  # Write the GitHub token to the user configuration file
43
50
  #
44
51
  # @param token [String] A valid GitHub access token.
@@ -50,6 +57,17 @@ module CIRunner
50
57
  save!(@yaml_config)
51
58
  end
52
59
 
60
+ # Write the Circle CI token to the user configuration file
61
+ #
62
+ # @param token [String] A valid Circle CI access token.
63
+ #
64
+ # @return [void]
65
+ def save_circle_ci_token(token)
66
+ @yaml_config["circle_ci"] = { "token" => token }
67
+
68
+ save!(@yaml_config)
69
+ end
70
+
53
71
  # @return [Pathname] The path of the configuration file.
54
72
  #
55
73
  # @example
@@ -8,44 +8,44 @@ module CIRunner
8
8
  # A PORO to help download and cache a GitHub CI log.
9
9
  #
10
10
  # @example Using the service
11
- # log_dl = LogDownloader.new("commit_sha", "catanacorp/catana", { "id" => 1, "name" => "Ruby Test 3.1.2" })
11
+ # log_dl = LogDownloader.new(
12
+ # CICheck::GitHub.new(
13
+ # "catanacorp/catana",
14
+ # "commit_sha",
15
+ # "Tests Ruby 2.7",
16
+ # "failed",
17
+ # 12345,
18
+ # )
19
+ # )
12
20
  # log_file = log_dl.fetch
13
21
  # puts log_file # => File
14
- #
15
- # @see https://docs.github.com/en/rest/actions/workflow-jobs#download-job-logs-for-a-workflow-run
16
22
  class LogDownloader
17
- # @param commit [String] A Git commit. Used to compute the file name we are going to cache.
18
- # @param repository [String] The repository full name, including the owner (i.e. rails/rails).
19
- # @param check_run [Hash] A GitHub CI check for which we want to download the log.
20
- def initialize(commit, repository, check_run)
21
- @commit = commit
22
- @repository = repository
23
+ # @param check_run [Check::Base] A Base::Check subclass for which we want to download the log.
24
+ def initialize(check_run)
23
25
  @check_run = check_run
24
26
  end
25
27
 
26
- # Download the CI logs from GitHub or retrieve it from disk in case we previously downloaded it.
28
+ # Ask the +@check_run+ to download the log from its CI or retrieve it from disk in case we previously downloaded it.
27
29
  #
28
30
  # @param block [Proc, Lambda] A proc that gets called if fetching the logs from GitHub fails. Allows the CLI to
29
31
  # prematurely exit while cleaning up the CLI::UI frame.
30
32
  #
31
- # @return [File] A file ready to be read.
33
+ # @return [Pathname] The path to the log file.
32
34
  def fetch(&block)
33
35
  return cached_log if cached_log
34
36
 
35
- github_client = GithubClient.new(Configuration::User.instance.github_token)
36
37
  error = nil
37
38
 
38
- ::CLI::UI.spinner("Downloading CI logs from GitHub", auto_debrief: false) do
39
- logfile = github_client.download_log(@repository, @check_run["id"])
40
-
41
- cache_log(logfile)
42
- rescue GithubClient::Error => e
39
+ ::CLI::UI.spinner("Downloading CI logs from #{@check_run.provider}", auto_debrief: false) do
40
+ cache_log(@check_run.download_log)
41
+ rescue Client::Error, Error => e
43
42
  error = e
44
43
 
45
44
  ::CLI::UI::Spinner::TASK_FAILED
46
45
  end
47
46
 
48
47
  block.call(error) if error
48
+
49
49
  cached_log
50
50
  end
51
51
 
@@ -74,14 +74,14 @@ module CIRunner
74
74
  # @example Given a repository "rails/rails". A CI check called "Ruby 3.0". A commit "abcdef".
75
75
  # puts computed_filed_path # ==> /var/tmpdir/T/.../rails/rails/log-abcdef-Ruby 3.0
76
76
  def computed_file_path
77
- normalized_run_name = @check_run["name"].tr("/", "_")
77
+ normalized_run_name = @check_run.name.tr("/", "_")
78
78
 
79
- log_folder.join("log-#{@commit[0..12]}-#{normalized_run_name}.log")
79
+ log_folder.join("log-#{@check_run.commit[0..12]}-#{normalized_run_name}.log")
80
80
  end
81
81
 
82
82
  # @return [Pathname]
83
83
  def log_folder
84
- Pathname(Dir.tmpdir).join(@repository)
84
+ Pathname(Dir.tmpdir).join(@check_run.repository)
85
85
  end
86
86
 
87
87
  # @return [Pathname, false] Depending if the log has been downloaded before.
@@ -43,13 +43,15 @@ module CIRunner
43
43
  # @return [void]
44
44
  def parse!
45
45
  @ci_log.each_line do |line|
46
- case line
46
+ line_no_ansi_color = line.gsub(/\e\[\d+m/, "")
47
+
48
+ case line_no_ansi_color
47
49
  when seed_regex
48
50
  @seed = first_matching_group(Regexp.last_match)
49
51
  when ruby_detection_regex
50
52
  @ruby_version = first_matching_group(Regexp.last_match)
51
53
 
52
- @buffer << line if buffering?
54
+ @buffer << line_no_ansi_color if buffering?
53
55
  when gemfile_detection_regex
54
56
  @gemfile = first_matching_group(Regexp.last_match)
55
57
  when buffer_detection_regex
@@ -58,9 +60,9 @@ module CIRunner
58
60
  @buffer.clear
59
61
  end
60
62
 
61
- @buffer << line
63
+ @buffer << line_no_ansi_color
62
64
  else
63
- @buffer << line if buffering?
65
+ @buffer << line_no_ansi_color if buffering?
64
66
  end
65
67
  end
66
68
 
@@ -40,7 +40,7 @@ module CIRunner
40
40
  def self.match?(ci_log)
41
41
  default_reporter = %r{(Finished in) \d+\.\d{6}s, \d+\.\d{4} runs/s, \d+\.\d{4} assertions/s\.}
42
42
 
43
- Regexp.union(default_reporter, SEED_REGEX, "minitest").match?(ci_log)
43
+ Regexp.union(default_reporter, SEED_REGEX).match?(ci_log)
44
44
  end
45
45
 
46
46
  # @return [String] See Runners::Base#report
@@ -15,7 +15,7 @@ module CIRunner
15
15
  command = /bundle exec rspec/
16
16
  summary = /Failed examples:/
17
17
 
18
- Regexp.union(command, summary, /rspec/i).match?(log)
18
+ Regexp.union(command, summary).match?(log)
19
19
  end
20
20
 
21
21
  # @return [String] See Runners::Base#report
@@ -2,6 +2,8 @@
2
2
 
3
3
  module CIRunner
4
4
  module TestRunFinder
5
+ GITHUB_ACTION = "github-actions"
6
+
5
7
  extend self
6
8
 
7
9
  # Makes a request to GitHub to retrieve the checks for a commit. Display a nice UI with
@@ -12,18 +14,16 @@ module CIRunner
12
14
  # @param block [Proc, Lambda] A proc that will be called in case we can't retrieve the CI Checks.
13
15
  # This allows the CLI to prematurely exit and let the CLI::UI closes its frame.
14
16
  #
15
- # @return [Hash] See GitHub documentation
16
- #
17
- # @see https://docs.github.com/en/rest/checks/runs#list-check-runs-for-a-git-reference
17
+ # @return [Array<Check::Base>] Array filled with Check::Base subclasses.
18
18
  def fetch_ci_checks(repository, commit, &block)
19
- github_client = GithubClient.new(Configuration::User.instance.github_token)
20
- ci_checks = {}
21
19
  error = nil
22
-
20
+ ci_checks = []
23
21
  title = "Fetching failed CI checks from GitHub for commit {{info:#{commit[..12]}}}"
22
+
24
23
  ::CLI::UI.spinner(title, auto_debrief: false) do
25
- ci_checks = github_client.check_runs(repository, commit)
26
- rescue GithubClient::Error => e
24
+ ci_checks = github_ci(repository, commit)
25
+ ci_checks += other_ci(repository, commit)
26
+ rescue Client::Error, StandardError => e
27
27
  error = e
28
28
 
29
29
  ::CLI::UI::Spinner::TASK_FAILED
@@ -34,21 +34,58 @@ module CIRunner
34
34
  ci_checks
35
35
  end
36
36
 
37
+ # Download the GitHub checks. This is used in case a project uses GitHub itself as its CI provider.
38
+ #
39
+ # @param repository [String] The full repository name, including the owner (i.e. rails/rails)
40
+ # @param commit [String] The Git commit that has been pushed to GitHub and for which we'll retrieve the CI checks.
41
+ #
42
+ # @return [Array<Check::Github>]
43
+ #
44
+ # @see https://docs.github.com/en/rest/checks/runs#list-check-runs-for-a-git-reference
45
+ def github_ci(repository, commit)
46
+ github_client = Client::Github.new(Configuration::User.instance.github_token)
47
+ ci_checks = github_client.check_runs(repository, commit)["check_runs"]
48
+
49
+ ci_checks.filter_map do |check_run|
50
+ next unless check_run.dig("app", "slug") == GITHUB_ACTION
51
+
52
+ Check::Github.new(repository, commit, *check_run.values_at("name", "conclusion", "id"))
53
+ end
54
+ end
55
+
56
+ # Download the Commit Statuses for this commit. Some CI provider (like GitHub or Buildkite), doesn't use
57
+ # the GitHub Check API, but instead this API.
58
+ #
59
+ # @param repository [String] The full repository name, including the owner (i.e. rails/rails)
60
+ # @param commit [String] The Git commit that has been pushed to GitHub and for which we'll retrieve the CI checks.
61
+ #
62
+ # @return [Array<Check::CircleCI, Check::Unsupported>]
63
+ #
64
+ # @see https://docs.github.com/en/rest/checks/runs#get-a-check-run
65
+ def other_ci(repository, commit)
66
+ github_client = Client::Github.new(Configuration::User.instance.github_token)
67
+ commit_statuses = github_client.commit_statuses(repository, commit)
68
+
69
+ commit_statuses.map do |commit_status|
70
+ check_class_from_url(commit_status, repository, commit)
71
+ end.compact
72
+ end
73
+
37
74
  # Find the CI check the user requested from the list of upstream checks.
38
75
  # This method is useful only when the user passes the `--run-name` flag to `ci-runner`. This makes
39
76
  # sure the CI check actually exists.
40
77
  #
41
- # @param ci_checks [Hash] The response from the previous +fetch_ci_checks+ request.
78
+ # @param ci_checks [Array<Check::Base>] A list of CI checks.
42
79
  # @param run_name [String] The name of the CI run that the user would like to retry on its machine.
43
80
  #
44
- # @return [Hash] A single check run from the list of +ci_checks+
81
+ # @return [Check::Base] A single check run from the list of +ci_checks+
45
82
  #
46
83
  # @raise [Error] If no CI checks with the given +run_name+ could be found.
47
84
  # @raise [Error] If the CI check was successfull. No point to continue as there should be no tests to rerun.
48
85
  def find(ci_checks, run_name)
49
- check_run = ci_checks["check_runs"].find { |check_run| check_run["name"] == run_name }
86
+ check_run = ci_checks.find { |check| check.name == run_name }
50
87
  raise(Error, no_check_message(ci_checks, run_name)) if check_run.nil?
51
- raise(Error, check_succeed(run_name)) if check_run["conclusion"] == "success"
88
+ raise(Error, check_succeed(run_name)) unless check_run.failed?
52
89
 
53
90
  check_run
54
91
  end
@@ -75,6 +112,28 @@ module CIRunner
75
112
 
76
113
  private
77
114
 
115
+ # Infer the CI Runner Check class based on the URL pointing to the CI provider's page.
116
+ #
117
+ # @param commit_status [Hash] A single commit status previously retrieved from the GitHub API.
118
+ # @param repository [String] The full repository name, including the owner (i.e. rails/rails)
119
+ # @param commit [String] The Git commit that has been pushed to GitHub and for which we'll retrieve the CI checks.
120
+ #
121
+ # @return [Check::CircleCI, Check::Unsupported] Depending if we could recognize the URL on the commit status
122
+ # pointing to the CI provider.
123
+ def check_class_from_url(commit_status, repository, commit)
124
+ target_url = commit_status["target_url"]
125
+ return unless target_url
126
+
127
+ uri = URI(target_url)
128
+
129
+ case uri.host
130
+ when "circleci.com"
131
+ Check::CircleCI.new(repository, commit, *commit_status.values_at("context", "state", "target_url"))
132
+ else
133
+ Check::Unsupported.new(repository, commit, *commit_status.values_at("context", "state"))
134
+ end
135
+ end
136
+
78
137
  # @param [String] run_name The name of the CI check input or chosen by the user.
79
138
  #
80
139
  # @return [String] A error message to display.
@@ -82,16 +141,18 @@ module CIRunner
82
141
  "The CI check '#{run_name}' was successfull. There should be no failing tests to rerun."
83
142
  end
84
143
 
85
- # @param [Hash] ci_checks The list of CI checks previously by the +fetch_ci_checks+ method.
86
- # @param [String] run_name The name of the CI check input or chosen by the user.
144
+ # @param ci_checks [Array<Check::Base>] The list of CI checks previously by the +fetch_ci_checks+ method.
145
+ # @param run_name [String] run_name The name of the CI check input or chosen by the user.
87
146
  #
88
147
  # @return [String] A error message letting the user know why CI Runner couldn't continue.
89
148
  def no_check_message(ci_checks, run_name)
90
- possible_checks = ci_checks["check_runs"].map do |check_run|
91
- if check_run["conclusion"] == "success"
92
- "#{::CLI::UI::Glyph.lookup("v")} #{check_run["name"]}"
149
+ possible_checks = ci_checks.filter_map do |check_run|
150
+ if check_run.success?
151
+ "#{::CLI::UI::Glyph.lookup("v")} #{check_run.name}"
152
+ elsif check_run.failed?
153
+ "#{::CLI::UI::Glyph.lookup("x")} #{check_run.name}"
93
154
  else
94
- "#{::CLI::UI::Glyph.lookup("x")} #{check_run["name"]}"
155
+ next
95
156
  end
96
157
  end
97
158
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CIRunner
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/ci_runner.rb CHANGED
@@ -7,12 +7,23 @@ module CIRunner
7
7
  Error = Class.new(StandardError)
8
8
 
9
9
  autoload :CLI, "ci_runner/cli"
10
- autoload :GithubClient, "ci_runner/github_client"
11
10
  autoload :GitHelper, "ci_runner/git_helper"
12
11
  autoload :TestRunFinder, "ci_runner/test_run_finder"
13
12
  autoload :LogDownloader, "ci_runner/log_downloader"
14
13
  autoload :TestFailure, "ci_runner/test_failure"
15
14
 
15
+ module Check
16
+ autoload :Github, "ci_runner/check/github"
17
+ autoload :CircleCI, "ci_runner/check/circle_ci"
18
+ autoload :Unsupported, "ci_runner/check/unsupported"
19
+ end
20
+
21
+ module Client
22
+ autoload :Error, "ci_runner/client/error"
23
+ autoload :Github, "ci_runner/client/github"
24
+ autoload :CircleCI, "ci_runner/client/circle_ci"
25
+ end
26
+
16
27
  module Configuration
17
28
  autoload :User, "ci_runner/configuration/user"
18
29
  autoload :Project, "ci_runner/configuration/project"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ci_runner
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Edouard Chin
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-08-02 00:00:00.000000000 Z
11
+ date: 2022-08-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: cli-ui
@@ -112,11 +112,18 @@ files:
112
112
  - ci_runner.gemspec
113
113
  - exe/ci_runner
114
114
  - lib/ci_runner.rb
115
+ - lib/ci_runner/check/base.rb
116
+ - lib/ci_runner/check/circle_ci.rb
117
+ - lib/ci_runner/check/github.rb
118
+ - lib/ci_runner/check/unsupported.rb
115
119
  - lib/ci_runner/cli.rb
120
+ - lib/ci_runner/client/base.rb
121
+ - lib/ci_runner/client/circle_ci.rb
122
+ - lib/ci_runner/client/error.rb
123
+ - lib/ci_runner/client/github.rb
116
124
  - lib/ci_runner/configuration/project.rb
117
125
  - lib/ci_runner/configuration/user.rb
118
126
  - lib/ci_runner/git_helper.rb
119
- - lib/ci_runner/github_client.rb
120
127
  - lib/ci_runner/log_downloader.rb
121
128
  - lib/ci_runner/runners/base.rb
122
129
  - lib/ci_runner/runners/minitest_runner.rb
@@ -131,6 +138,7 @@ licenses:
131
138
  metadata:
132
139
  homepage_uri: https://github.com/Edouard-chin/ci_runner
133
140
  source_code_uri: https://github.com/Edouard-chin/ci_runner
141
+ changelog_uri: https://github.com/Edouard-chin/ci_runner/blob/main/CHANGELOG.md
134
142
  allowed_push_host: https://rubygems.org
135
143
  rubygems_mfa_required: 'true'
136
144
  post_install_message:
@@ -148,7 +156,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
148
156
  - !ruby/object:Gem::Version
149
157
  version: '0'
150
158
  requirements: []
151
- rubygems_version: 3.3.7
159
+ rubygems_version: 3.3.19
152
160
  signing_key:
153
161
  specification_version: 4
154
162
  summary: Re-run failing tests from CI on your local machine without copy/pasting.
@@ -1,105 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "net/http"
4
- require "openssl"
5
- require "json"
6
- require "open-uri"
7
-
8
- module CIRunner
9
- # A simple client to interact the GitHub API.
10
- #
11
- # @example Using the client
12
- # GithubClient.new("access_token").me
13
- class GithubClient
14
- Error = Class.new(StandardError)
15
-
16
- # @return [Net::HTTP] An instance of Net:HTTP configured to make requests to the GitHub API endpoint.
17
- def self.default_client
18
- Net::HTTP.new("api.github.com", 443).tap do |http|
19
- http.use_ssl = true
20
- http.read_timeout = 3
21
- http.write_timeout = 3
22
- http.verify_mode = OpenSSL::SSL::VERIFY_PEER
23
- end
24
- end
25
-
26
- # @param access_token [String] The access token with "repo" scope.
27
- # @param client [Net::HTTP]
28
- def initialize(access_token, client = self.class.default_client)
29
- @access_token = access_token
30
- @client = client
31
- end
32
-
33
- # Make an API request to get the authenticated user. Used to verify if the access token
34
- # the user has stored in its config is valid.
35
- #
36
- # @return [Hash] See GitHub documentation.
37
- #
38
- # @see https://docs.github.com/en/rest/users/users#get-the-authenticated-user
39
- def me
40
- get("/user")
41
- end
42
-
43
- # Makes an API request to get the CI checks for the +commit+.
44
- #
45
- # @param repository [String] The full repository name, including the owner (rails/rails)
46
- # @param commit [String] The Git commit that has been pushed to GitHub.
47
- #
48
- # @return [Hash] See GitHub documentation.
49
- #
50
- # @see https://docs.github.com/en/rest/checks/runs#list-check-runs-for-a-git-reference
51
- def check_runs(repository, commit)
52
- get("/repos/#{repository}/commits/#{commit}/check-runs")
53
- end
54
-
55
- # Makes two requests to get the CI log for a check run.
56
- # The first request returns a 302 containing a Location header poiting to a short lived url to download the log.
57
- # The second request is to actually download the log.
58
- #
59
- # @param repository [String] The full repository name, including the owner (rails/rails)
60
- # @param check_run_id [Integer] The GitHub ID of the check run.
61
- #
62
- # @return [Tempfile, IO] Depending on the size of the response. Quirk of URI.open.
63
- #
64
- # @see https://docs.github.com/en/rest/actions/workflow-jobs#download-job-logs-for-a-workflow-run
65
- def download_log(repository, check_run_id)
66
- download_url = get("/repos/#{repository}/actions/jobs/#{check_run_id}/logs")
67
-
68
- URI.open(download_url) # rubocop:disable Security/Open
69
- end
70
-
71
- private
72
-
73
- # Perform an authenticated GET request.
74
- #
75
- # @param path [String] The resource to access.
76
- #
77
- # @return (See #request)
78
- def get(path)
79
- request(Net::HTTP::Get, path)
80
- end
81
-
82
- # Perform an authenticated request.
83
- #
84
- # @param verb_class [Net::HTTPRequest] A subclass of Net::HTTPRequest.
85
- # @param path [String] The resource to access.
86
- #
87
- # @return [Hash, String] A decoded JSON response or a String pointing to the Location redirection.
88
- def request(verb_class, path)
89
- req = verb_class.new(path)
90
- req["Accept"] = "application/vnd.github+json"
91
- req.basic_auth("user", @access_token)
92
-
93
- response = @client.request(req)
94
-
95
- case response.code.to_i
96
- when 200..204
97
- response.content_type == "application/json" ? JSON.parse(response.body) : response.body
98
- when 302
99
- response["Location"]
100
- else
101
- raise(Error, "GitHub response: Status: #{response.code}. Body:\n\n#{response.body}")
102
- end
103
- end
104
- end
105
- end