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.
@@ -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,30 @@ 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
+ when "buildkite.com"
133
+ Check::Buildkite.new(repository, commit, *commit_status.values_at("context", "state", "target_url"))
134
+ else
135
+ Check::Unsupported.new(repository, commit, *commit_status.values_at("context", "state"))
136
+ end
137
+ end
138
+
78
139
  # @param [String] run_name The name of the CI check input or chosen by the user.
79
140
  #
80
141
  # @return [String] A error message to display.
@@ -82,16 +143,18 @@ module CIRunner
82
143
  "The CI check '#{run_name}' was successfull. There should be no failing tests to rerun."
83
144
  end
84
145
 
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.
146
+ # @param ci_checks [Array<Check::Base>] The list of CI checks previously by the +fetch_ci_checks+ method.
147
+ # @param run_name [String] run_name The name of the CI check input or chosen by the user.
87
148
  #
88
149
  # @return [String] A error message letting the user know why CI Runner couldn't continue.
89
150
  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"]}"
151
+ possible_checks = ci_checks.filter_map do |check_run|
152
+ if check_run.success?
153
+ "#{::CLI::UI::Glyph.lookup("v")} #{check_run.name}"
154
+ elsif check_run.failed?
155
+ "#{::CLI::UI::Glyph.lookup("x")} #{check_run.name}"
93
156
  else
94
- "#{::CLI::UI::Glyph.lookup("x")} #{check_run["name"]}"
157
+ next
95
158
  end
96
159
  end
97
160
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CIRunner
4
- VERSION = "0.1.0"
4
+ VERSION = "0.3.0"
5
5
  end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module CIRunner
6
+ # Class used to check if a newer version of CI Runner has been released.
7
+ # This is used to inform the user to update its gem.
8
+ #
9
+ # The check only runs every week.
10
+ class VersionVerifier
11
+ SEVEN_DAYS = 86_400 * 7
12
+
13
+ # Check if the user is running the latest version of CI Runner.
14
+ #
15
+ # @return [Boolean]
16
+ def new_ci_runner_version?
17
+ return false unless check?
18
+
19
+ fetch_upstream_version
20
+ FileUtils.touch(last_checked)
21
+
22
+ upstream_version > Gem::Version.new(VERSION)
23
+ end
24
+
25
+ # Makes a request to GitHub to get the latest release on the Edouard-chin/ci_runner repository
26
+ #
27
+ # @return [Gem::Version] An instance of Gem::Version
28
+ def upstream_version
29
+ @upstream_version ||= begin
30
+ release = Client::Github.new(Configuration::User.instance.github_token).latest_release("Edouard-chin/ci_runner")
31
+
32
+ Gem::Version.new(release["tag_name"].sub(/\Av/, ""))
33
+ end
34
+ end
35
+ alias_method :fetch_upstream_version, :upstream_version
36
+
37
+ # Path of a file used to store when we last checked for a release.
38
+ #
39
+ # @return [Pathname]
40
+ def last_checked
41
+ Configuration::User.instance.config_directory.join("last-checked")
42
+ end
43
+
44
+ private
45
+
46
+ # @return [Boolean] Whether we checked for a release in the 7 days.
47
+ def check?
48
+ Time.now > (File.stat(last_checked).mtime + SEVEN_DAYS)
49
+ rescue Errno::ENOENT
50
+ true
51
+ end
52
+ end
53
+ end
data/lib/ci_runner.rb CHANGED
@@ -6,12 +6,28 @@ require_relative "ci_runner/version"
6
6
  module CIRunner
7
7
  Error = Class.new(StandardError)
8
8
 
9
- autoload :CLI, "ci_runner/cli"
10
- autoload :GithubClient, "ci_runner/github_client"
11
- autoload :GitHelper, "ci_runner/git_helper"
12
- autoload :TestRunFinder, "ci_runner/test_run_finder"
13
- autoload :LogDownloader, "ci_runner/log_downloader"
14
- autoload :TestFailure, "ci_runner/test_failure"
9
+ autoload :CLI, "ci_runner/cli"
10
+ autoload :GitHelper, "ci_runner/git_helper"
11
+ autoload :TestRunFinder, "ci_runner/test_run_finder"
12
+ autoload :LogDownloader, "ci_runner/log_downloader"
13
+ autoload :TestFailure, "ci_runner/test_failure"
14
+ autoload :VersionVerifier, "ci_runner/version_verifier"
15
+
16
+ module Check
17
+ autoload :Buildkite, "ci_runner/check/buildkite"
18
+ autoload :Github, "ci_runner/check/github"
19
+ autoload :CircleCI, "ci_runner/check/circle_ci"
20
+ autoload :Unsupported, "ci_runner/check/unsupported"
21
+ autoload :ConcurrentDownload, "ci_runner/check/concurrent_download"
22
+ end
23
+
24
+ module Client
25
+ autoload :Error, "ci_runner/client/error"
26
+ autoload :Github, "ci_runner/client/github"
27
+ autoload :CircleCI, "ci_runner/client/circle_ci"
28
+ autoload :Buildkite, "ci_runner/client/buildkite"
29
+ autoload :AuthenticatedBuildkite, "ci_runner/client/authenticated_buildkite"
30
+ end
15
31
 
16
32
  module Configuration
17
33
  autoload :User, "ci_runner/configuration/user"
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.3.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-10-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: cli-ui
@@ -112,11 +112,22 @@ 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/buildkite.rb
117
+ - lib/ci_runner/check/circle_ci.rb
118
+ - lib/ci_runner/check/concurrent_download.rb
119
+ - lib/ci_runner/check/github.rb
120
+ - lib/ci_runner/check/unsupported.rb
115
121
  - lib/ci_runner/cli.rb
122
+ - lib/ci_runner/client/authenticated_buildkite.rb
123
+ - lib/ci_runner/client/base.rb
124
+ - lib/ci_runner/client/buildkite.rb
125
+ - lib/ci_runner/client/circle_ci.rb
126
+ - lib/ci_runner/client/error.rb
127
+ - lib/ci_runner/client/github.rb
116
128
  - lib/ci_runner/configuration/project.rb
117
129
  - lib/ci_runner/configuration/user.rb
118
130
  - lib/ci_runner/git_helper.rb
119
- - lib/ci_runner/github_client.rb
120
131
  - lib/ci_runner/log_downloader.rb
121
132
  - lib/ci_runner/runners/base.rb
122
133
  - lib/ci_runner/runners/minitest_runner.rb
@@ -124,6 +135,7 @@ files:
124
135
  - lib/ci_runner/test_failure.rb
125
136
  - lib/ci_runner/test_run_finder.rb
126
137
  - lib/ci_runner/version.rb
138
+ - lib/ci_runner/version_verifier.rb
127
139
  - lib/minitest/ci_runner_plugin.rb
128
140
  homepage: https://github.com/Edouard-chin/ci_runner
129
141
  licenses:
@@ -131,6 +143,7 @@ licenses:
131
143
  metadata:
132
144
  homepage_uri: https://github.com/Edouard-chin/ci_runner
133
145
  source_code_uri: https://github.com/Edouard-chin/ci_runner
146
+ changelog_uri: https://github.com/Edouard-chin/ci_runner/blob/main/CHANGELOG.md
134
147
  allowed_push_host: https://rubygems.org
135
148
  rubygems_mfa_required: 'true'
136
149
  post_install_message:
@@ -148,7 +161,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
148
161
  - !ruby/object:Gem::Version
149
162
  version: '0'
150
163
  requirements: []
151
- rubygems_version: 3.3.7
164
+ rubygems_version: 3.3.19
152
165
  signing_key:
153
166
  specification_version: 4
154
167
  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