circleci_reporter 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.config.reek +23 -0
- data/.gitignore +13 -0
- data/.rspec +2 -0
- data/.rubocop.yml +10 -0
- data/.yardopts +4 -0
- data/CHANGELOG.md +94 -0
- data/Gemfile +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +115 -0
- data/Rakefile +29 -0
- data/bin/rspec +18 -0
- data/bin/rubocop +18 -0
- data/bin/setup +8 -0
- data/bin/yard +18 -0
- data/circle.yml +11 -0
- data/circleci_reporter.gemspec +37 -0
- data/lib/circleci_reporter.rb +45 -0
- data/lib/circleci_reporter/artifact.rb +22 -0
- data/lib/circleci_reporter/build.rb +27 -0
- data/lib/circleci_reporter/client.rb +133 -0
- data/lib/circleci_reporter/configuration.rb +90 -0
- data/lib/circleci_reporter/errors.rb +19 -0
- data/lib/circleci_reporter/rake_task.rb +17 -0
- data/lib/circleci_reporter/report.rb +106 -0
- data/lib/circleci_reporter/reporters/base.rb +119 -0
- data/lib/circleci_reporter/reporters/flow.rb +32 -0
- data/lib/circleci_reporter/reporters/link.rb +75 -0
- data/lib/circleci_reporter/reporters/rubycritic.rb +33 -0
- data/lib/circleci_reporter/reporters/simplecov.rb +32 -0
- data/lib/circleci_reporter/result.rb +12 -0
- data/lib/circleci_reporter/runner.rb +78 -0
- data/lib/circleci_reporter/sandbox.rb +32 -0
- data/lib/circleci_reporter/vcs/base.rb +23 -0
- data/lib/circleci_reporter/vcs/github.rb +39 -0
- data/lib/circleci_reporter/version.rb +13 -0
- metadata +247 -0
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'circleci_reporter/client'
|
4
|
+
require_relative 'circleci_reporter/configuration'
|
5
|
+
require_relative 'circleci_reporter/errors'
|
6
|
+
require_relative 'circleci_reporter/runner'
|
7
|
+
|
8
|
+
require_relative 'circleci_reporter/reporters/flow'
|
9
|
+
require_relative 'circleci_reporter/reporters/link'
|
10
|
+
require_relative 'circleci_reporter/reporters/rubycritic'
|
11
|
+
require_relative 'circleci_reporter/reporters/simplecov'
|
12
|
+
|
13
|
+
module CircleCIReporter
|
14
|
+
class << self
|
15
|
+
# Setters for shared global objects
|
16
|
+
# @api private
|
17
|
+
attr_writer :client, :configuration
|
18
|
+
end
|
19
|
+
|
20
|
+
# @return [Configuration]
|
21
|
+
def self.configuration
|
22
|
+
@configuration ||= Configuration.new
|
23
|
+
end
|
24
|
+
|
25
|
+
# @return [Client]
|
26
|
+
def self.client
|
27
|
+
@client ||= Client.new
|
28
|
+
end
|
29
|
+
|
30
|
+
# Yields the global configuration to a block.
|
31
|
+
#
|
32
|
+
# @yield [Configuration]
|
33
|
+
def self.configure
|
34
|
+
yield configuration if block_given?
|
35
|
+
end
|
36
|
+
|
37
|
+
# @return [void]
|
38
|
+
def self.run
|
39
|
+
configuration.reporters.select!(&:active?)
|
40
|
+
configuration.dump
|
41
|
+
raise NoActiveReporter if configuration.reporters.empty?
|
42
|
+
|
43
|
+
Runner.new.tap(&:dump).run
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CircleCIReporter
|
4
|
+
# Encapsulate a CircleCI artifact
|
5
|
+
#
|
6
|
+
# @attr path [String] abstract path to the artifact in CircleCI container
|
7
|
+
# @attr url [String] URL of the artifact
|
8
|
+
# @attr node_index [Integer] the ID of the artifact's container
|
9
|
+
Artifact = Struct.new(:path, :url, :node_index) do
|
10
|
+
# @param value [String]
|
11
|
+
# @param node_index [Integer, nil]
|
12
|
+
# @return [Boolean]
|
13
|
+
def match?(value, node_index: nil)
|
14
|
+
path.end_with?(value) && (node_index.nil? || self.node_index == node_index)
|
15
|
+
end
|
16
|
+
|
17
|
+
# @return [String] content of the artifact
|
18
|
+
def body
|
19
|
+
@body ||= CircleCIReporter.client.get(url).body
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CircleCIReporter
|
4
|
+
# Encapsulate a CircleCI build
|
5
|
+
#
|
6
|
+
# @attr vcs_revision [String] revision of git
|
7
|
+
# @attr build_number [Integer] the ID of the CircleCI build
|
8
|
+
Build = Struct.new(:vcs_revision, :build_number) do
|
9
|
+
# @param revision [String]
|
10
|
+
# @return [Boolean]
|
11
|
+
def match?(revision)
|
12
|
+
vcs_revision.start_with?(revision)
|
13
|
+
end
|
14
|
+
|
15
|
+
# @return [Array<Artifact>]
|
16
|
+
def artifacts
|
17
|
+
@artifacts ||= CircleCIReporter.client.artifacts(build_number)
|
18
|
+
end
|
19
|
+
|
20
|
+
# @param string [String]
|
21
|
+
# @param node_index [Integer, nil]
|
22
|
+
# @return [Artifact, nil]
|
23
|
+
def find_artifact(string, node_index: nil)
|
24
|
+
artifacts.find { |artifact| artifact.match?(string, node_index: node_index) }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'faraday'
|
4
|
+
|
5
|
+
require_relative 'artifact'
|
6
|
+
require_relative 'build'
|
7
|
+
require_relative 'errors'
|
8
|
+
|
9
|
+
module CircleCIReporter
|
10
|
+
# CircleCI API client
|
11
|
+
class Client
|
12
|
+
CIRCLECI_ENDPOINT = 'https://circleci.com/api/v1.1'
|
13
|
+
|
14
|
+
# Fetch a build data from API and create a {Build} object for it.
|
15
|
+
#
|
16
|
+
# @param build_number [Integer, nil]
|
17
|
+
# @return [Build, nil]
|
18
|
+
# @raise [RequestError]
|
19
|
+
# @see https://circleci.com/docs/api/v1-reference/#build
|
20
|
+
def single_build(build_number)
|
21
|
+
return unless build_number
|
22
|
+
|
23
|
+
resp = get(single_build_url(build_number))
|
24
|
+
body = JSON.parse(resp.body)
|
25
|
+
raise RequestError.new(body['message'], resp) unless resp.success?
|
26
|
+
|
27
|
+
create_build(body)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Retrieve artifacts for the build.
|
31
|
+
#
|
32
|
+
# @param build_number [Integer]
|
33
|
+
# @return [Array<Artifact>]
|
34
|
+
# @raise [RequestError]
|
35
|
+
# @see https://circleci.com/docs/api/v1-reference/#build-artifacts
|
36
|
+
def artifacts(build_number)
|
37
|
+
resp = get(artifacts_url(build_number))
|
38
|
+
body = JSON.parse(resp.body)
|
39
|
+
raise RequestError.new(body['message'], resp) unless resp.success?
|
40
|
+
|
41
|
+
body.map{ |hash| create_artifact(hash) }
|
42
|
+
end
|
43
|
+
|
44
|
+
# Find the latest build number for the given vcs revision.
|
45
|
+
#
|
46
|
+
# @param revision [String]
|
47
|
+
# @param branch [String, nil]
|
48
|
+
# @return [Integer, nil]
|
49
|
+
def build_number_by_revision(revision, branch: nil)
|
50
|
+
build = recent_builds(branch).find { |recent_build| recent_build.match?(revision) }
|
51
|
+
build ? build.build_number : nil
|
52
|
+
end
|
53
|
+
|
54
|
+
# Raw entry point for GET APIs.
|
55
|
+
#
|
56
|
+
# @param url [String]
|
57
|
+
# @param params [Hash]
|
58
|
+
# @return [Faraday::Response]
|
59
|
+
def get(url, params = {})
|
60
|
+
params['circle-token'] = configuration.circleci_token
|
61
|
+
query_string = params.map { |key, value| "#{key}=#{value}" }.join('&')
|
62
|
+
Faraday.get("#{url}?#{query_string}")
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
# @return [Configuration]
|
68
|
+
def configuration
|
69
|
+
CircleCIReporter.configuration
|
70
|
+
end
|
71
|
+
|
72
|
+
# @param build_number [Integer]
|
73
|
+
# @return [String] URL for "Artifacts of a Bulid API"
|
74
|
+
def artifacts_url(build_number)
|
75
|
+
[
|
76
|
+
CIRCLECI_ENDPOINT,
|
77
|
+
'project',
|
78
|
+
configuration.vcs_type,
|
79
|
+
configuration.project,
|
80
|
+
build_number,
|
81
|
+
'artifacts'
|
82
|
+
].join('/')
|
83
|
+
end
|
84
|
+
|
85
|
+
# @param branch [String, nil]
|
86
|
+
# @return [Array<Build>]
|
87
|
+
# @raise [RequestError]
|
88
|
+
def recent_builds(branch)
|
89
|
+
resp = get(recent_builds_url(branch), limit: 100)
|
90
|
+
body = JSON.parse(resp.body)
|
91
|
+
raise RequestError.new(body['message'], resp) unless resp.success?
|
92
|
+
|
93
|
+
body.map { |hash| create_build(hash) }
|
94
|
+
end
|
95
|
+
|
96
|
+
# @param branch [String, nil]
|
97
|
+
# @return [String]
|
98
|
+
def recent_builds_url(branch)
|
99
|
+
elements = [
|
100
|
+
CIRCLECI_ENDPOINT,
|
101
|
+
'project',
|
102
|
+
configuration.vcs_type,
|
103
|
+
configuration.project
|
104
|
+
]
|
105
|
+
elements += ['tree', branch] if branch
|
106
|
+
elements.join('/')
|
107
|
+
end
|
108
|
+
|
109
|
+
# @param build_number [Integer]
|
110
|
+
# @return [String]
|
111
|
+
def single_build_url(build_number)
|
112
|
+
[
|
113
|
+
CIRCLECI_ENDPOINT,
|
114
|
+
'project',
|
115
|
+
configuration.vcs_type,
|
116
|
+
configuration.project,
|
117
|
+
build_number
|
118
|
+
].join('/')
|
119
|
+
end
|
120
|
+
|
121
|
+
# @param hash [Hash]
|
122
|
+
# @return [Artifact]
|
123
|
+
def create_artifact(hash)
|
124
|
+
Artifact.new(hash['path'], hash['url'], hash['node_index'])
|
125
|
+
end
|
126
|
+
|
127
|
+
# @param hash [Hash]
|
128
|
+
# @return [Build]
|
129
|
+
def create_build(hash)
|
130
|
+
Build.new(hash['vcs_revision'], hash['build_num'])
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'reporters/flow'
|
4
|
+
require_relative 'reporters/rubycritic'
|
5
|
+
require_relative 'reporters/simplecov'
|
6
|
+
|
7
|
+
module CircleCIReporter
|
8
|
+
class Configuration
|
9
|
+
DEFAULT_REPORTERS = [
|
10
|
+
Reporters::SimpleCov.new,
|
11
|
+
Reporters::Flow.new,
|
12
|
+
Reporters::RubyCritic.new
|
13
|
+
].freeze
|
14
|
+
DEFAULT_VCS_TYPE = 'github'
|
15
|
+
|
16
|
+
attr_accessor :circleci_token, :vcs_token
|
17
|
+
attr_writer :artifacts_dir, :base_revision, :current_build_number,
|
18
|
+
:current_revision, :previous_build_number, :reporters, :repository_name,
|
19
|
+
:template, :template_safe_mode, :user_name, :vcs_type
|
20
|
+
|
21
|
+
# @return [String]
|
22
|
+
def project
|
23
|
+
"#{user_name}/#{repository_name}"
|
24
|
+
end
|
25
|
+
|
26
|
+
# @return [Array<Reporters::Base>]
|
27
|
+
def reporters
|
28
|
+
@reporters ||= DEFAULT_REPORTERS.dup
|
29
|
+
end
|
30
|
+
|
31
|
+
# @return [String]
|
32
|
+
def vcs_type
|
33
|
+
@vcs_type ||= DEFAULT_VCS_TYPE
|
34
|
+
end
|
35
|
+
|
36
|
+
# @return [String]
|
37
|
+
def artifacts_dir
|
38
|
+
@artifacts_dir ||= ENV['CIRCLE_ARTIFACTS']
|
39
|
+
end
|
40
|
+
|
41
|
+
# @return [String]
|
42
|
+
def base_revision
|
43
|
+
@base_revision ||= `git merge-base origin/master HEAD`.strip
|
44
|
+
end
|
45
|
+
|
46
|
+
# @return [Integer]
|
47
|
+
def current_build_number
|
48
|
+
@current_build_number ||= ENV['CIRCLE_BUILD_NUM']
|
49
|
+
end
|
50
|
+
|
51
|
+
# @return [String]
|
52
|
+
def current_revision
|
53
|
+
@current_revision ||= ENV['CIRCLE_SHA1']
|
54
|
+
end
|
55
|
+
|
56
|
+
# @return [Integer, nil]
|
57
|
+
def previous_build_number
|
58
|
+
@previous_build_number ||= ENV['CIRCLE_PREVIOUS_BUILD_NUM']&.to_i
|
59
|
+
end
|
60
|
+
|
61
|
+
# @return [String]
|
62
|
+
def repository_name
|
63
|
+
@repository_name ||= ENV['CIRCLE_PROJECT_REPONAME']
|
64
|
+
end
|
65
|
+
|
66
|
+
# @return [String]
|
67
|
+
def user_name
|
68
|
+
@user_name ||= ENV['CIRCLE_PROJECT_USERNAME']
|
69
|
+
end
|
70
|
+
|
71
|
+
# @return [void]
|
72
|
+
def dump
|
73
|
+
puts <<~CONFIGURATION
|
74
|
+
Configuration | Value
|
75
|
+
----------------------|----------------------------------------------------------------------------
|
76
|
+
artifacts_dir | #{artifacts_dir.inspect}
|
77
|
+
base_revision | #{base_revision.inspect}
|
78
|
+
circleci_token | #{circleci_token[-4..].rjust(40, '*').inspect if circleci_token}
|
79
|
+
current_build_number | #{current_build_number.inspect}
|
80
|
+
current_revision | #{current_revision.inspect}
|
81
|
+
previous_build_number | #{previous_build_number.inspect}
|
82
|
+
reporters | #{reporters.inspect}
|
83
|
+
repository_name | #{repository_name.inspect}
|
84
|
+
user_name | #{user_name.inspect}
|
85
|
+
vcs_token | #{vcs_token[-4..].rjust(40, '*').inspect if vcs_token}
|
86
|
+
vcs_type | #{vcs_type.inspect}
|
87
|
+
CONFIGURATION
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CircleCIReporter
|
4
|
+
class Error < StandardError; end
|
5
|
+
|
6
|
+
class RequestError < Error
|
7
|
+
attr_reader :resp
|
8
|
+
|
9
|
+
# @param message [String]
|
10
|
+
# @param resp [Faraday::Response]
|
11
|
+
def initialize(message, resp)
|
12
|
+
@message = message
|
13
|
+
@resp = resp
|
14
|
+
super(message)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class NoActiveReporter < Error; end
|
19
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'circleci_reporter'
|
4
|
+
|
5
|
+
namespace :circleci_reporter do
|
6
|
+
desc 'Report test coverage'
|
7
|
+
task :coverage do
|
8
|
+
abort unless ENV['CIRCLECI']
|
9
|
+
|
10
|
+
CircleCIReporter.configure do |config|
|
11
|
+
config.circleci_token = ENV['CIRCLECI_REPORTER_CIRCLECI_TOKEN']
|
12
|
+
config.vcs_token = ENV['CIRCLECI_REPORTER_VCS_TOKEN']
|
13
|
+
end
|
14
|
+
|
15
|
+
CircleCIReporter.run
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CircleCIReporter
|
4
|
+
# Encapsulate a report created by a reporter.
|
5
|
+
#
|
6
|
+
# @see Reporters::Base#report
|
7
|
+
class Report
|
8
|
+
# @param reporter [Reporters::Base] the reporter of the report
|
9
|
+
# @param current [Result]
|
10
|
+
# @param base [Result, nil] result at master branch
|
11
|
+
# @param previous [Result, nil] result at previous build in same branch
|
12
|
+
def initialize(reporter, current, base: nil, previous: nil)
|
13
|
+
@reporter = reporter
|
14
|
+
@current_result = current
|
15
|
+
@base_result = base
|
16
|
+
@previous_result = previous
|
17
|
+
end
|
18
|
+
|
19
|
+
# @return [String]
|
20
|
+
def to_s
|
21
|
+
"#{link}: #{current_result.pretty_coverage}#{emoji}#{progress}"
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
attr_reader :reporter, :current_result, :base_result, :previous_result
|
27
|
+
|
28
|
+
# @return [String]
|
29
|
+
def link
|
30
|
+
"[#{reporter.name}](#{current_result.url})"
|
31
|
+
end
|
32
|
+
|
33
|
+
# @return [String]
|
34
|
+
def emoji
|
35
|
+
if base_diff.nil? || base_diff.nan? || base_diff.round(2).zero?
|
36
|
+
''
|
37
|
+
elsif base_diff.positive?
|
38
|
+
':chart_with_upwards_trend:'
|
39
|
+
else
|
40
|
+
':chart_with_downwards_trend:'
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# @return [String]
|
45
|
+
def progress
|
46
|
+
progresses.empty? ? '' : "(#{progresses.join(', ')})"
|
47
|
+
end
|
48
|
+
|
49
|
+
# @return [Array<String>]
|
50
|
+
def progresses
|
51
|
+
[base_progress, branch_progress].compact
|
52
|
+
end
|
53
|
+
|
54
|
+
# @return [String, nil]
|
55
|
+
def base_progress
|
56
|
+
base_diff ? "[master](#{base_result.url}): #{pretty_base_diff}" : nil
|
57
|
+
end
|
58
|
+
|
59
|
+
# @return [String, nil]
|
60
|
+
def branch_progress
|
61
|
+
branch_diff ? "[previous](#{previous_result.url}): #{pretty_branch_diff}" : nil
|
62
|
+
end
|
63
|
+
|
64
|
+
# @return [String, nil]
|
65
|
+
def pretty_base_diff
|
66
|
+
return unless base_diff
|
67
|
+
|
68
|
+
pretty_diff(base_diff.round(2))
|
69
|
+
end
|
70
|
+
|
71
|
+
# @return [String, nil]
|
72
|
+
def pretty_branch_diff
|
73
|
+
return unless branch_diff
|
74
|
+
|
75
|
+
pretty_diff(branch_diff.round(2))
|
76
|
+
end
|
77
|
+
|
78
|
+
# @return [Float, nil]
|
79
|
+
def base_diff
|
80
|
+
return unless base_result
|
81
|
+
|
82
|
+
current_result.coverage - base_result.coverage
|
83
|
+
end
|
84
|
+
|
85
|
+
# @return [Float, nil]
|
86
|
+
def branch_diff
|
87
|
+
return unless previous_result
|
88
|
+
|
89
|
+
current_result.coverage - previous_result.coverage
|
90
|
+
end
|
91
|
+
|
92
|
+
# @param diff [Float]
|
93
|
+
# @return [String]
|
94
|
+
def pretty_diff(diff)
|
95
|
+
if diff.nan?
|
96
|
+
'NaN'
|
97
|
+
elsif diff.positive?
|
98
|
+
"+#{diff}"
|
99
|
+
elsif diff.negative?
|
100
|
+
diff.to_s
|
101
|
+
else
|
102
|
+
'±0'
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|