circleci_reporter 1.0.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 +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
|