ci_runner 0.1.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/ci_runner.gemspec +1 -0
- data/lib/ci_runner/check/base.rb +63 -0
- data/lib/ci_runner/check/buildkite.rb +88 -0
- data/lib/ci_runner/check/circle_ci.rb +128 -0
- data/lib/ci_runner/check/concurrent_download.rb +57 -0
- data/lib/ci_runner/check/github.rb +40 -0
- data/lib/ci_runner/check/unsupported.rb +33 -0
- data/lib/ci_runner/cli.rb +116 -24
- data/lib/ci_runner/client/authenticated_buildkite.rb +67 -0
- data/lib/ci_runner/client/base.rb +78 -0
- data/lib/ci_runner/client/buildkite.rb +60 -0
- data/lib/ci_runner/client/circle_ci.rb +58 -0
- data/lib/ci_runner/client/error.rb +27 -0
- data/lib/ci_runner/client/github.rb +88 -0
- data/lib/ci_runner/configuration/user.rb +48 -0
- data/lib/ci_runner/log_downloader.rb +20 -20
- data/lib/ci_runner/runners/base.rb +6 -4
- data/lib/ci_runner/runners/minitest_runner.rb +1 -1
- data/lib/ci_runner/runners/rspec.rb +1 -1
- data/lib/ci_runner/test_failure.rb +3 -0
- data/lib/ci_runner/test_run_finder.rb +81 -18
- data/lib/ci_runner/version.rb +1 -1
- data/lib/ci_runner/version_verifier.rb +53 -0
- data/lib/ci_runner.rb +22 -6
- metadata +17 -4
- data/lib/ci_runner/github_client.rb +0 -105
@@ -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
|