jiminy 0.1.0.pre1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +20 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +0 -0
- data/Gemfile +12 -0
- data/Gemfile.lock +245 -0
- data/LICENSE +21 -0
- data/README.md +173 -0
- data/Rakefile +12 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/example.png +0 -0
- data/exe/jiminy +9 -0
- data/jiminy.gemspec +42 -0
- data/lib/jiminy/cli.rb +137 -0
- data/lib/jiminy/configuration.rb +97 -0
- data/lib/jiminy/github_apiable.rb +17 -0
- data/lib/jiminy/recording/n_plus_one.rb +50 -0
- data/lib/jiminy/recording/prosopite_ext/send_notifications_with_tmp_file.rb +44 -0
- data/lib/jiminy/recording/prosopite_ext/tmp_file_recorder.rb +40 -0
- data/lib/jiminy/recording/rspec.rb +22 -0
- data/lib/jiminy/recording/test_controller_concern.rb +26 -0
- data/lib/jiminy/recording.rb +14 -0
- data/lib/jiminy/reporting/ci_providers/circle_ci/api_request.rb +45 -0
- data/lib/jiminy/reporting/ci_providers/circle_ci/artifact.rb +17 -0
- data/lib/jiminy/reporting/ci_providers/circle_ci/base.rb +46 -0
- data/lib/jiminy/reporting/ci_providers/circle_ci/job.rb +17 -0
- data/lib/jiminy/reporting/ci_providers/circle_ci/pipeline.rb +49 -0
- data/lib/jiminy/reporting/ci_providers/circle_ci/vcs.rb +13 -0
- data/lib/jiminy/reporting/ci_providers/circle_ci/workflow.rb +41 -0
- data/lib/jiminy/reporting/ci_providers/circle_ci.rb +54 -0
- data/lib/jiminy/reporting/ci_providers/github.rb +29 -0
- data/lib/jiminy/reporting/ci_providers/local/artifact.rb +25 -0
- data/lib/jiminy/reporting/ci_providers/local.rb +30 -0
- data/lib/jiminy/reporting/ci_providers/provider_configuration.rb +28 -0
- data/lib/jiminy/reporting/ci_providers.rb +12 -0
- data/lib/jiminy/reporting/n_plus_one.rb +39 -0
- data/lib/jiminy/reporting/reporters/base_reporter.rb +26 -0
- data/lib/jiminy/reporting/reporters/dry_run_reporter.rb +13 -0
- data/lib/jiminy/reporting/reporters/github_reporter.rb +28 -0
- data/lib/jiminy/reporting/reporters.rb +11 -0
- data/lib/jiminy/reporting/yaml_file_comment_presenter.rb +71 -0
- data/lib/jiminy/reporting.rb +32 -0
- data/lib/jiminy/rspec.rb +3 -0
- data/lib/jiminy/setup.rb +10 -0
- data/lib/jiminy/templates/reporting/comment_header.md.erb +5 -0
- data/lib/jiminy/templates/reporting/n_plus_one.md.erb +12 -0
- data/lib/jiminy/version.rb +5 -0
- data/lib/jiminy.rb +13 -0
- metadata +254 -0
data/lib/jiminy/cli.rb
ADDED
@@ -0,0 +1,137 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Jiminy
|
4
|
+
require "thor"
|
5
|
+
require "byebug"
|
6
|
+
class CLI < Thor
|
7
|
+
require "jiminy/reporting/ci_providers/circle_ci"
|
8
|
+
include Jiminy::Reporting::CIProviders
|
9
|
+
|
10
|
+
class WorkflowStillRunningError < StandardError; end
|
11
|
+
private_constant :WorkflowStillRunningError
|
12
|
+
|
13
|
+
MAX_TIMEOUT = 1800 # 1 hour
|
14
|
+
POLL_INTERVAL = 60 # 1 min
|
15
|
+
|
16
|
+
def self.exit_on_failure?
|
17
|
+
false
|
18
|
+
end
|
19
|
+
|
20
|
+
desc "Report results", "Reports the results of tests"
|
21
|
+
method_option :commit, type: :string, aliases: "c", required: true,
|
22
|
+
banner: "3e078f8770743549b722382ec5d412a30b9fdcc5",
|
23
|
+
desc: "The full SHA for the current HEAD commit"
|
24
|
+
method_option :pr_number, type: :numeric, aliases: %w[pr p], required: true,
|
25
|
+
banner: "1",
|
26
|
+
desc: "The GitHub PR number"
|
27
|
+
method_option :dry_run, type: :boolean, default: false, lazy_default: true,
|
28
|
+
desc: "Print to STDOUT instead of leaving a comment on GitHub"
|
29
|
+
method_option :timeout, type: :numeric, aliases: %w[max-timeout], default: MAX_TIMEOUT,
|
30
|
+
desc: "How long to poll CircleCI before timing out (in seconds)"
|
31
|
+
method_option :poll_interval, type: :numeric, aliases: %w[poll-interval], default: POLL_INTERVAL,
|
32
|
+
desc: "How frequently to poll CircleCI (in seconds)"
|
33
|
+
method_option :source, type: :string, default: "circleci",
|
34
|
+
desc: "Where are the results.yml files we should report?"
|
35
|
+
def report
|
36
|
+
self.start_time = Time.now
|
37
|
+
artifact_urls = artifacts.map(&:url)
|
38
|
+
|
39
|
+
Jiminy::Reporting.report!(*artifact_urls,
|
40
|
+
pr_number: options[:pr_number],
|
41
|
+
dry_run: options[:dry_run])
|
42
|
+
|
43
|
+
$stdout.puts "Reported N+1s successfully"
|
44
|
+
exit(0)
|
45
|
+
end
|
46
|
+
|
47
|
+
# rubocop:disable Metrics/BlockLength
|
48
|
+
no_tasks do
|
49
|
+
attr_accessor :start_time
|
50
|
+
|
51
|
+
def poll_interval
|
52
|
+
options[:poll_interval] || POLL_INTERVAL
|
53
|
+
end
|
54
|
+
|
55
|
+
def source
|
56
|
+
options[:source] || :circleci
|
57
|
+
end
|
58
|
+
|
59
|
+
def max_timeout
|
60
|
+
options[:timeout] || MAX_TIMEOUT
|
61
|
+
end
|
62
|
+
|
63
|
+
def timed_out?
|
64
|
+
(Time.now - start_time) > max_timeout
|
65
|
+
end
|
66
|
+
|
67
|
+
def git_revision
|
68
|
+
options[:commit]
|
69
|
+
end
|
70
|
+
|
71
|
+
def pr_number
|
72
|
+
options[:pr_number]
|
73
|
+
end
|
74
|
+
|
75
|
+
def pipeline
|
76
|
+
@_pipeline ||= CircleCI::Pipeline.find_by_revision(git_revision: git_revision, pr_number: pr_number) or
|
77
|
+
abort("No such Pipeline with commit SHA #{git_revision}")
|
78
|
+
end
|
79
|
+
|
80
|
+
def missing_options
|
81
|
+
[].tap do |array|
|
82
|
+
array.concat("--pr-number") unless pr_number
|
83
|
+
array.concat("--commit") unless git_revision
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity
|
88
|
+
def workflow
|
89
|
+
@_workflow ||= begin
|
90
|
+
result = CircleCI::Workflow.find(pipeline_id: pipeline.id, workflow_name: Jiminy.config.ci_workflow_name)
|
91
|
+
if result.nil?
|
92
|
+
abort("Unable to find workflow called '#{Jiminy.config.ci_workflow_name}' in Pipeline #{pipeline.id}")
|
93
|
+
end
|
94
|
+
|
95
|
+
if result.not_run? || result.running?
|
96
|
+
$stdout.puts "Workflow still running..."
|
97
|
+
raise(WorkflowStillRunningError)
|
98
|
+
end
|
99
|
+
abort("Workflow #{result.status}—aborting...") unless result.success?
|
100
|
+
|
101
|
+
result
|
102
|
+
rescue WorkflowStillRunningError
|
103
|
+
sleep(poll_interval)
|
104
|
+
$stdout.puts "Retrying..."
|
105
|
+
retry unless timed_out?
|
106
|
+
|
107
|
+
abort("Process timed out after #{Time.now - start_time} seconds")
|
108
|
+
end
|
109
|
+
end
|
110
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity
|
111
|
+
|
112
|
+
def jobs
|
113
|
+
@_jobs ||= CircleCI::Job.all(workflow_id: workflow.id)
|
114
|
+
end
|
115
|
+
|
116
|
+
def artifacts
|
117
|
+
@_artifacts ||= begin
|
118
|
+
unless respond_to?(:"artifacts_from_#{source}")
|
119
|
+
raise ArgumentError, "Uknown value for source option #{source}"
|
120
|
+
end
|
121
|
+
|
122
|
+
public_send(:"artifacts_from_#{source}")
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def artifacts_from_local
|
127
|
+
Local::Artifact.all
|
128
|
+
end
|
129
|
+
|
130
|
+
def artifacts_from_circle_ci
|
131
|
+
CircleCI::Artifact.all(job_number: jobs.first.job_number)
|
132
|
+
end
|
133
|
+
alias_method :artifacts_from_circleci, :artifacts_from_circle_ci
|
134
|
+
end
|
135
|
+
# rubocop:enable Metrics/BlockLength
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Jiminy
|
4
|
+
class Configuration
|
5
|
+
DEFAULT_CONFIG_READER = -> { instance_variable_get(:"@_#{__callee__}" || raise_missing_required(__callee__)) }
|
6
|
+
DEFAULT_CONFIG_WRITER = ->(value) { instance_variable_set(:"@_#{__callee__}"[0..-2], value) }
|
7
|
+
|
8
|
+
class MissingConfigError < StandardError
|
9
|
+
attr_reader :missing_config_name
|
10
|
+
|
11
|
+
def initialize(missing_config_name)
|
12
|
+
super()
|
13
|
+
@missing_config_name = missing_config_name
|
14
|
+
end
|
15
|
+
|
16
|
+
def message
|
17
|
+
<<~MESSAGE
|
18
|
+
You must provide a configuration value for ##{missing_config_name}
|
19
|
+
|
20
|
+
This probably means you haven't set Jiminy.config.#{missing_config_name}
|
21
|
+
MESSAGE
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class << self
|
26
|
+
def define_config(name, options = {})
|
27
|
+
define_method(:"#{name}=", DEFAULT_CONFIG_WRITER)
|
28
|
+
define_method(name.to_sym, DEFAULT_CONFIG_READER)
|
29
|
+
defined_configs[name].merge!(options)
|
30
|
+
end
|
31
|
+
|
32
|
+
def defined_configs
|
33
|
+
@_defined_configs ||= Hash.new { |hash, key| hash[key] = { default: nil, required: true } }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
define_config :ci_workflow_name, default: "build"
|
38
|
+
|
39
|
+
define_config :circle_ci_api_token
|
40
|
+
|
41
|
+
define_config :github_token
|
42
|
+
|
43
|
+
define_config :ignore_file_path, default: File.join("./jiminy_ignores.yml")
|
44
|
+
|
45
|
+
define_config :project_reponame
|
46
|
+
|
47
|
+
define_config :project_username
|
48
|
+
|
49
|
+
define_config :temp_file_location, default: File.join("./tmp/jiminy/results.yml")
|
50
|
+
|
51
|
+
def initialize
|
52
|
+
apply_defaults!
|
53
|
+
end
|
54
|
+
|
55
|
+
def repo_path
|
56
|
+
[project_username, project_reponame].join("/")
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def defined_configs_with_defaults
|
62
|
+
self.class.defined_configs.select { |_config_name, options| options[:default] }
|
63
|
+
end
|
64
|
+
|
65
|
+
def required_configs
|
66
|
+
self.class.defined_configs.select { |_config_name, options| options[:required] }
|
67
|
+
end
|
68
|
+
|
69
|
+
def apply_defaults!
|
70
|
+
defined_configs_with_defaults.each do |config_name, options|
|
71
|
+
public_send(:"#{config_name}=", options[:default])
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def raise_missing_required(config_name)
|
76
|
+
return unless required_configs.key?(config_name)
|
77
|
+
|
78
|
+
raise MissingConfigError, config_name
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
module ConfigurationMethods
|
83
|
+
def configure(&block)
|
84
|
+
block.call(configuration)
|
85
|
+
end
|
86
|
+
|
87
|
+
def configuration
|
88
|
+
@_configuration ||= Configuration.new
|
89
|
+
end
|
90
|
+
|
91
|
+
alias config configuration
|
92
|
+
|
93
|
+
def configured?
|
94
|
+
!!configuration
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Jiminy
|
4
|
+
module GithubAPIable
|
5
|
+
require "octokit"
|
6
|
+
|
7
|
+
private
|
8
|
+
|
9
|
+
def env_config
|
10
|
+
@_env_config ||= Reporting::CIProviders::Github::Configuration.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def client
|
14
|
+
@_client ||= Octokit::Client.new(access_token: env_config.github_token)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Jiminy
|
4
|
+
module Recording
|
5
|
+
class NPlusOne
|
6
|
+
attr_reader :file, :location
|
7
|
+
|
8
|
+
LOCATION_MATCHER = /(?<file>.+\.rb):
|
9
|
+
(?<line>\d+):in\s`
|
10
|
+
(?:block\sin\s)?
|
11
|
+
(?<method_name>.+)'
|
12
|
+
/x.freeze
|
13
|
+
|
14
|
+
EXAMPLES_COUNT = 3
|
15
|
+
|
16
|
+
def initialize(location:, queries: [])
|
17
|
+
@location = location.to_s.strip
|
18
|
+
@queries = queries
|
19
|
+
match = location.match(LOCATION_MATCHER)
|
20
|
+
@line = match[:line]
|
21
|
+
@method_name = match[:method_name]
|
22
|
+
@file = match[:file]
|
23
|
+
freeze
|
24
|
+
end
|
25
|
+
|
26
|
+
def ==(other)
|
27
|
+
location == other.location
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_h
|
31
|
+
{
|
32
|
+
location => attributes
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
attr_reader :queries, :line, :method_name
|
39
|
+
|
40
|
+
def attributes
|
41
|
+
{
|
42
|
+
"file" => file,
|
43
|
+
"line" => line,
|
44
|
+
"method" => method_name,
|
45
|
+
"examples" => queries.take(EXAMPLES_COUNT)
|
46
|
+
}
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Jiminy
|
4
|
+
module Recording
|
5
|
+
module ProsopiteExt
|
6
|
+
require_relative "tmp_file_recorder"
|
7
|
+
|
8
|
+
module SendNotificationsWithTmpFile
|
9
|
+
def prepare_results_file!
|
10
|
+
TmpFileRecorder.prepare_results_file!
|
11
|
+
end
|
12
|
+
|
13
|
+
def tmp_file=(value)
|
14
|
+
@tmp_file = value
|
15
|
+
end
|
16
|
+
|
17
|
+
def tmp_file
|
18
|
+
!!@tmp_file
|
19
|
+
end
|
20
|
+
|
21
|
+
def send_notifications
|
22
|
+
super
|
23
|
+
|
24
|
+
return unless Prosopite.tmp_file
|
25
|
+
|
26
|
+
# https://github.com/charkost/prosopite/blob/main/lib/prosopite.rb#L157
|
27
|
+
tc[:prosopite_notifications].each do |queries, backtrace|
|
28
|
+
absolute_location = backtrace.detect { |path| path.exclude?(Bundler.bundle_path.to_s) }
|
29
|
+
next unless absolute_location
|
30
|
+
|
31
|
+
relative_location = absolute_location.gsub("#{Rails.root.realpath}/", "")
|
32
|
+
tmp_file_recorder.record(location: relative_location, queries: queries)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def tmp_file_recorder
|
39
|
+
@_tmp_file_recorder ||= TmpFileRecorder.new
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Jiminy
|
4
|
+
module Recording
|
5
|
+
module ProsopiteExt
|
6
|
+
class TmpFileRecorder
|
7
|
+
require_relative "../n_plus_one"
|
8
|
+
|
9
|
+
def record(location:, queries:)
|
10
|
+
yaml_content = File.read(Jiminy.config.temp_file_location)
|
11
|
+
array = YAML.safe_load(yaml_content)
|
12
|
+
n_plus_one = NPlusOne.new(location: location, queries: queries)
|
13
|
+
|
14
|
+
array << n_plus_one.to_h unless location_in_array?(location, array) || filepath_ignored?(n_plus_one.file)
|
15
|
+
File.write(Jiminy.config.temp_file_location, array.to_yaml)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def location_in_array?(location, array)
|
21
|
+
array.detect { |hash| hash.key?(location) }
|
22
|
+
end
|
23
|
+
|
24
|
+
def filepath_ignored?(filepath)
|
25
|
+
ignored_files.include?(filepath)
|
26
|
+
end
|
27
|
+
|
28
|
+
def ignored_files
|
29
|
+
@_ignored_files ||= load_ignored_files
|
30
|
+
end
|
31
|
+
|
32
|
+
def load_ignored_files
|
33
|
+
return [] unless File.exist?(Jiminy.config.ignore_file_path)
|
34
|
+
|
35
|
+
YAML.load_file(Jiminy.config.ignore_file_path) || []
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Jiminy
|
4
|
+
module Recording
|
5
|
+
require "jiminy/recording/prosopite_ext/send_notifications_with_tmp_file"
|
6
|
+
|
7
|
+
module RSpec
|
8
|
+
require "prosopite"
|
9
|
+
require "jiminy/recording/test_controller_concern"
|
10
|
+
|
11
|
+
::Prosopite.singleton_class.prepend ProsopiteExt::SendNotificationsWithTmpFile
|
12
|
+
|
13
|
+
def wrap_rspec_example(example)
|
14
|
+
Prosopite.tmp_file = true
|
15
|
+
ActionController::Base.include(Jiminy::Recording::TestControllerConcern)
|
16
|
+
example.run
|
17
|
+
Prosopite.tmp_file = false
|
18
|
+
end
|
19
|
+
Jiminy.extend(self)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Jiminy
|
4
|
+
module Recording
|
5
|
+
module TestControllerConcern
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
around_action :_test_n_plus_one_detection
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def _test_n_plus_one_detection
|
15
|
+
yield and return if Prosopite.scan?
|
16
|
+
|
17
|
+
begin
|
18
|
+
Prosopite.scan
|
19
|
+
yield
|
20
|
+
ensure
|
21
|
+
Prosopite.finish
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Jiminy
|
4
|
+
module Recording
|
5
|
+
abort("Jiminy::Recording must be run from a Rails app") unless defined?(Rails)
|
6
|
+
|
7
|
+
module_function
|
8
|
+
|
9
|
+
def reset_results_file!
|
10
|
+
Rails.root.join(File.dirname(Jiminy.config.temp_file_location)).mkpath
|
11
|
+
File.write(Jiminy.config.temp_file_location, [].to_yaml)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Jiminy
|
4
|
+
module Reporting
|
5
|
+
module CIProviders
|
6
|
+
module CircleCI
|
7
|
+
class APIRequest
|
8
|
+
API_BASE = "https://circleci.com/api/v2/"
|
9
|
+
|
10
|
+
CIRCLE_TOKEN_HEADER = "Circle-Token"
|
11
|
+
|
12
|
+
def initialize(path)
|
13
|
+
@url = URI.join(API_BASE, path)
|
14
|
+
end
|
15
|
+
|
16
|
+
def perform!
|
17
|
+
puts url
|
18
|
+
response
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
attr_reader :url
|
24
|
+
|
25
|
+
def response
|
26
|
+
@_response ||= http.request(request)
|
27
|
+
end
|
28
|
+
|
29
|
+
def request
|
30
|
+
@_request ||= Net::HTTP::Get.new(url).tap do |req|
|
31
|
+
req[CIRCLE_TOKEN_HEADER] = Jiminy.config.circle_ci_api_token
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def http
|
36
|
+
@_http ||= Net::HTTP.new(url.host, url.port).tap do |http_instance|
|
37
|
+
http_instance.use_ssl = true
|
38
|
+
http_instance.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Jiminy
|
4
|
+
module Reporting
|
5
|
+
module CIProviders
|
6
|
+
module CircleCI
|
7
|
+
class Artifact < Base
|
8
|
+
define_attribute_readers :url
|
9
|
+
|
10
|
+
def self.all(job_number:)
|
11
|
+
fetch_api_resource("project/gh/#{Jiminy.config.repo_path}/#{job_number}/artifacts")
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Jiminy
|
4
|
+
module Reporting
|
5
|
+
module CIProviders
|
6
|
+
module CircleCI
|
7
|
+
require_relative "api_request"
|
8
|
+
|
9
|
+
class Base
|
10
|
+
require "uri"
|
11
|
+
require "net/http"
|
12
|
+
require "json"
|
13
|
+
|
14
|
+
attr_reader :attributes
|
15
|
+
|
16
|
+
def self.define_attribute_readers(*attr_names)
|
17
|
+
attr_names.each do |attr_name|
|
18
|
+
define_method(attr_name, -> { attributes[__callee__.to_s] })
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class << self
|
23
|
+
attr_reader :next_token
|
24
|
+
end
|
25
|
+
|
26
|
+
class << self
|
27
|
+
attr_writer :next_token
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.fetch_api_resource(path)
|
31
|
+
response = APIRequest.new(path).perform!
|
32
|
+
raise "Error response: #{response.body}" unless response.is_a?(Net::HTTPOK)
|
33
|
+
|
34
|
+
json_body = JSON.parse(response.read_body)
|
35
|
+
self.next_token = json_body["next_page_token"]
|
36
|
+
json_body["items"].map { |attributes| new(attributes) }
|
37
|
+
end
|
38
|
+
|
39
|
+
def initialize(attributes = {})
|
40
|
+
@attributes = attributes.transform_keys!(&:to_s)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Jiminy
|
4
|
+
module Reporting
|
5
|
+
module CIProviders
|
6
|
+
module CircleCI
|
7
|
+
class Job < Base
|
8
|
+
define_attribute_readers :job_number
|
9
|
+
|
10
|
+
def self.all(workflow_id:)
|
11
|
+
fetch_api_resource("workflow/#{workflow_id}/job")
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Jiminy
|
4
|
+
module Reporting
|
5
|
+
module CIProviders
|
6
|
+
module CircleCI
|
7
|
+
require_relative "base"
|
8
|
+
require_relative "vcs"
|
9
|
+
|
10
|
+
class Pipeline < Base
|
11
|
+
MAX_PAGE_LOOKUP = 20
|
12
|
+
|
13
|
+
define_attribute_readers :id, :project_slug, :vcs
|
14
|
+
|
15
|
+
def self.find_by_revision(git_revision:, pr_number:)
|
16
|
+
attempt_count = 0
|
17
|
+
matching_pipeline = nil
|
18
|
+
until matching_pipeline || attempt_count >= MAX_PAGE_LOOKUP
|
19
|
+
page_pipelines = fetch_page_from_api(next_token)
|
20
|
+
matching_pipeline = page_pipelines.detect { |p| pipeline_match?(p, git_revision, pr_number) }
|
21
|
+
attempt_count += 1
|
22
|
+
end
|
23
|
+
matching_pipeline
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.fetch_page_from_api(page_token)
|
27
|
+
query = "org-slug=gh/#{Jiminy.config.project_username}&mine=false"
|
28
|
+
query += "&page-token=#{page_token}" if page_token
|
29
|
+
url = "pipeline?#{query}"
|
30
|
+
fetch_api_resource(url)
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.pipeline_match?(pipeline, git_revision, _pr_number)
|
34
|
+
return false unless pipeline.project_slug.to_s.downcase.end_with?(Jiminy.config.repo_path.downcase)
|
35
|
+
|
36
|
+
pipeline.vcs.revision == git_revision
|
37
|
+
|
38
|
+
# TODO: Get PR comparison working too
|
39
|
+
# pipeline.vcs.review_url.end_with?("pull/#{pr_number}")
|
40
|
+
end
|
41
|
+
|
42
|
+
def vcs
|
43
|
+
VCS.new(attributes["vcs"])
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Jiminy
|
4
|
+
module Reporting
|
5
|
+
module CIProviders
|
6
|
+
module CircleCI
|
7
|
+
class Workflow < Base
|
8
|
+
# Link to states: https://circleci.com/docs/2.0/workflows/#states
|
9
|
+
SUCCESS = "success"
|
10
|
+
FAILED = "failed"
|
11
|
+
NOT_RUN = "not run"
|
12
|
+
RUNNING = "running"
|
13
|
+
|
14
|
+
define_attribute_readers :id, :name, :status
|
15
|
+
|
16
|
+
def self.find(pipeline_id:, workflow_name:)
|
17
|
+
url = "pipeline/#{pipeline_id}/workflow"
|
18
|
+
collection = fetch_api_resource(url)
|
19
|
+
collection.detect { |w| w.name.to_s == workflow_name.to_s }
|
20
|
+
end
|
21
|
+
|
22
|
+
def success?
|
23
|
+
status == SUCCESS
|
24
|
+
end
|
25
|
+
|
26
|
+
def running?
|
27
|
+
status == RUNNING
|
28
|
+
end
|
29
|
+
|
30
|
+
def not_run?
|
31
|
+
status == NOT_RUN
|
32
|
+
end
|
33
|
+
|
34
|
+
def failed?
|
35
|
+
status == FAILED
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|