ci_runner 0.1.0 → 0.3.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.
@@ -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