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.
- 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
@@ -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 [
|
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 =
|
26
|
-
|
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 [
|
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 [
|
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
|
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))
|
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 [
|
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
|
91
|
-
if check_run
|
92
|
-
"#{::CLI::UI::Glyph.lookup("v")} #{check_run
|
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
|
-
|
157
|
+
next
|
95
158
|
end
|
96
159
|
end
|
97
160
|
|
data/lib/ci_runner/version.rb
CHANGED
@@ -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,
|
10
|
-
autoload :
|
11
|
-
autoload :
|
12
|
-
autoload :
|
13
|
-
autoload :
|
14
|
-
autoload :
|
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.
|
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-
|
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.
|
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
|