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,119 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../report'
|
4
|
+
|
5
|
+
module CircleCIReporter
|
6
|
+
module Reporters
|
7
|
+
# @abstract Subclass and override {.default_dir}, {.default_html_file_name},
|
8
|
+
# {.default_json_file_name} and {#parse_json} to implement a custom Reporter class.
|
9
|
+
class Base
|
10
|
+
def self.default_dir
|
11
|
+
raise NotImplementedError
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.default_html_file_name
|
15
|
+
raise NotImplementedError
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.default_json_file_name
|
19
|
+
raise NotImplementedError
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(options = {})
|
23
|
+
@options = options
|
24
|
+
end
|
25
|
+
|
26
|
+
# @return [Boolean]
|
27
|
+
def active?
|
28
|
+
File.directory?(File.join(configuration.artifacts_dir, dir))
|
29
|
+
end
|
30
|
+
|
31
|
+
# @param base_build [Build, nil]
|
32
|
+
# @param previous_build [Build, nil]
|
33
|
+
# @return [Report]
|
34
|
+
def report(base_build, previous_build)
|
35
|
+
Report.new(
|
36
|
+
self,
|
37
|
+
create_current_result,
|
38
|
+
base: base_build ? create_build_result(base_build) : nil,
|
39
|
+
previous: previous_build ? create_build_result(previous_build) : nil
|
40
|
+
)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Name of reporter.
|
44
|
+
#
|
45
|
+
# @return [String]
|
46
|
+
def name
|
47
|
+
self.class.name.split('::').last
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
# @return [String]
|
53
|
+
def dir
|
54
|
+
@options[:dir] || self.class.default_dir
|
55
|
+
end
|
56
|
+
|
57
|
+
def html_file_name
|
58
|
+
@options[:html_file_name] || self.class.default_html_file_name
|
59
|
+
end
|
60
|
+
|
61
|
+
def json_file_name
|
62
|
+
@options[:json_file_name] || self.class.default_json_file_name
|
63
|
+
end
|
64
|
+
|
65
|
+
# @param build [Build, nil]
|
66
|
+
# @return [Result, nil]
|
67
|
+
def create_build_result(build)
|
68
|
+
Result.new(build_coverage(build), build_url(build))
|
69
|
+
end
|
70
|
+
|
71
|
+
# @return [AbstractResult]
|
72
|
+
def create_current_result
|
73
|
+
Result.new(current_coverage, current_url)
|
74
|
+
end
|
75
|
+
|
76
|
+
# @return [Float]
|
77
|
+
def current_coverage
|
78
|
+
parse_json(File.read(File.join(configuration.artifacts_dir, dir, json_file_name)))
|
79
|
+
end
|
80
|
+
|
81
|
+
# @return [String]
|
82
|
+
def current_url
|
83
|
+
[
|
84
|
+
'https://circle-artifacts.com/gh',
|
85
|
+
configuration.project,
|
86
|
+
configuration.current_build_number,
|
87
|
+
'artifacts',
|
88
|
+
"0/#{configuration.artifacts_dir}",
|
89
|
+
dir,
|
90
|
+
html_file_name
|
91
|
+
].join('/')
|
92
|
+
end
|
93
|
+
|
94
|
+
# @param build [Build]
|
95
|
+
# @return [Float]
|
96
|
+
def build_coverage(build)
|
97
|
+
artifact = build.find_artifact(json_file_name, node_index: 0) or return Float::NAN
|
98
|
+
parse_json(artifact.body)
|
99
|
+
end
|
100
|
+
|
101
|
+
# @param build [Build]
|
102
|
+
# @return [String]
|
103
|
+
def build_url(build)
|
104
|
+
artifact = build.find_artifact(html_file_name, node_index: 0) or return '#'
|
105
|
+
artifact.url
|
106
|
+
end
|
107
|
+
|
108
|
+
def configuration
|
109
|
+
CircleCIReporter.configuration
|
110
|
+
end
|
111
|
+
|
112
|
+
# @param json [String]
|
113
|
+
# @return [Float]
|
114
|
+
def parse_json(json)
|
115
|
+
raise NotImplementedError
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
require_relative '../result'
|
6
|
+
require_relative './base'
|
7
|
+
|
8
|
+
module CircleCIReporter
|
9
|
+
module Reporters
|
10
|
+
class Flow < Base
|
11
|
+
def self.default_dir
|
12
|
+
'flow-coverage'
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.default_html_file_name
|
16
|
+
'index.html'
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.default_json_file_name
|
20
|
+
'flow-coverage.json'
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
# @param json [String]
|
26
|
+
# @return [Float]
|
27
|
+
def parse_json(json)
|
28
|
+
JSON.parse(json)['percent'].to_f
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../result'
|
4
|
+
|
5
|
+
module CircleCIReporter
|
6
|
+
module Reporters
|
7
|
+
class Link
|
8
|
+
# @attr name [String]
|
9
|
+
# @attr url [String]
|
10
|
+
# @attr base_url [String, nil]
|
11
|
+
# @attr previous_url [String, nil]
|
12
|
+
LinkReport = Struct.new(:name, :url, :base_url, :previous_url) do
|
13
|
+
# @return [String]
|
14
|
+
def to_s
|
15
|
+
links = []
|
16
|
+
links << "[master](#{base_url})" if base_url
|
17
|
+
links << "[previous](#{previous_url})" if previous_url
|
18
|
+
link = links.empty? ? nil : " (#{links.join(', ')})"
|
19
|
+
"[#{name}](#{url})#{link}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# @param path [String]
|
24
|
+
# @param name [String]
|
25
|
+
def initialize(path:, name:)
|
26
|
+
@path = path
|
27
|
+
@name = name
|
28
|
+
end
|
29
|
+
|
30
|
+
# @note Implementation for {Base#active?}
|
31
|
+
def active?
|
32
|
+
File.file?(File.join(configuration.artifacts_dir, path))
|
33
|
+
end
|
34
|
+
|
35
|
+
# @note Override {Base#name}
|
36
|
+
attr_reader :name
|
37
|
+
|
38
|
+
# @param base_build [Build, nil]
|
39
|
+
# @param previous_build [Build, nil]
|
40
|
+
# @return [LinkReport]
|
41
|
+
def report(base_build, previous_build)
|
42
|
+
LinkReport.new(name, url, extract_artifact_url(base_build), extract_artifact_url(previous_build))
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
attr_reader :path
|
48
|
+
|
49
|
+
# @return [String]
|
50
|
+
def url
|
51
|
+
[
|
52
|
+
'https://circle-artifacts.com/gh',
|
53
|
+
configuration.project,
|
54
|
+
configuration.current_build_number,
|
55
|
+
'artifacts',
|
56
|
+
"0/#{configuration.artifacts_dir}",
|
57
|
+
path
|
58
|
+
].join('/')
|
59
|
+
end
|
60
|
+
|
61
|
+
def configuration
|
62
|
+
CircleCIReporter.configuration
|
63
|
+
end
|
64
|
+
|
65
|
+
# @param build [Build, nil]
|
66
|
+
# @return [String, nil]
|
67
|
+
def extract_artifact_url(build)
|
68
|
+
return unless build
|
69
|
+
|
70
|
+
artifact = build.find_artifact(path)
|
71
|
+
artifact ? artifact.url : nil
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
require_relative '../result'
|
6
|
+
require_relative './base'
|
7
|
+
|
8
|
+
module CircleCIReporter
|
9
|
+
module Reporters
|
10
|
+
class RubyCritic < Base
|
11
|
+
DEFAULT_DIR = 'rubycritic'
|
12
|
+
def self.default_dir
|
13
|
+
'rubycritic'
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.default_html_file_name
|
17
|
+
'overview.html'
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.default_json_file_name
|
21
|
+
'report.json'
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
# @param json [String]
|
27
|
+
# @return [Float]
|
28
|
+
def parse_json(json)
|
29
|
+
JSON.parse(json)['score'].to_f
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
require_relative '../result'
|
6
|
+
require_relative './base'
|
7
|
+
|
8
|
+
module CircleCIReporter
|
9
|
+
module Reporters
|
10
|
+
class SimpleCov < Base
|
11
|
+
def self.default_dir
|
12
|
+
'coverage'
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.default_html_file_name
|
16
|
+
'index.html'
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.default_json_file_name
|
20
|
+
'.last_run.json'
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
# @param json [String]
|
26
|
+
# @return [Float]
|
27
|
+
def parse_json(json)
|
28
|
+
JSON.parse(json)['result']['line'].to_f
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CircleCIReporter
|
4
|
+
# @attr coverage [Float]
|
5
|
+
# @attr url [String] URL for coverage index.html
|
6
|
+
Result = Struct.new(:coverage, :url) do
|
7
|
+
# @return [String]
|
8
|
+
def pretty_coverage
|
9
|
+
"#{coverage.round(2)}%"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'vcs/github'
|
4
|
+
|
5
|
+
module CircleCIReporter
|
6
|
+
class Runner
|
7
|
+
# @return [void]
|
8
|
+
def run
|
9
|
+
reports = reporters.map { |reporter| reporter.report(base_build, previous_build) }
|
10
|
+
vcs_client.create_comment(reports.map(&:to_s).join("\n"))
|
11
|
+
end
|
12
|
+
|
13
|
+
# @return [void]
|
14
|
+
def dump
|
15
|
+
puts <<~RUNNER
|
16
|
+
Runner | Value
|
17
|
+
------------------|-----------------------------------------------------------------------------------
|
18
|
+
base_build | #{base_build.inspect}
|
19
|
+
base_build_number | #{base_build_number.inspect}
|
20
|
+
previous_build | #{previous_build.inspect}
|
21
|
+
RUNNER
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
# @return [AbstractVCSClient]
|
27
|
+
def vcs_client
|
28
|
+
case configuration.vcs_type
|
29
|
+
when 'github'
|
30
|
+
VCS::GitHub.new(configuration.vcs_token)
|
31
|
+
else
|
32
|
+
raise NotImplementedError
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# @return [Build, nil]
|
37
|
+
def base_build
|
38
|
+
@base_build ||= client.single_build(base_build_number)
|
39
|
+
end
|
40
|
+
|
41
|
+
# @return [Build, nil]
|
42
|
+
def previous_build
|
43
|
+
@previous_build ||= client.single_build(previous_build_number)
|
44
|
+
end
|
45
|
+
|
46
|
+
# @return [Client]
|
47
|
+
def client
|
48
|
+
CircleCIReporter.client
|
49
|
+
end
|
50
|
+
|
51
|
+
# @return [Configuration]
|
52
|
+
def configuration
|
53
|
+
CircleCIReporter.configuration
|
54
|
+
end
|
55
|
+
|
56
|
+
# @return [String, nil]
|
57
|
+
def base_revision
|
58
|
+
configuration.base_revision
|
59
|
+
end
|
60
|
+
|
61
|
+
# @return [Integer, nil]
|
62
|
+
def previous_build_number
|
63
|
+
configuration.previous_build_number
|
64
|
+
end
|
65
|
+
|
66
|
+
# @return [Array<AbstractReporter>]
|
67
|
+
def reporters
|
68
|
+
configuration.reporters
|
69
|
+
end
|
70
|
+
|
71
|
+
# @return [Integer, nil]
|
72
|
+
def base_build_number
|
73
|
+
return if configuration.base_revision == configuration.current_revision
|
74
|
+
|
75
|
+
@base_build_number ||= client.build_number_by_revision(base_revision, branch: 'master')
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../circleci_reporter'
|
4
|
+
require_relative 'client'
|
5
|
+
require_relative 'configuration'
|
6
|
+
|
7
|
+
module CircleCIReporter
|
8
|
+
# A sandbox isolates the enclosed code into an environment that looks 'new'
|
9
|
+
# meaning globally accessed objects are reset for the duration of the sandbox.
|
10
|
+
#
|
11
|
+
# @note This module is not normally available. You must require
|
12
|
+
# `circleci_reporter/sandbox` to load it.
|
13
|
+
module Sandbox
|
14
|
+
# Execute a provided block with CircleCIReporter global objects(
|
15
|
+
# configuration, client) reset.
|
16
|
+
#
|
17
|
+
# @yield [Configuration]
|
18
|
+
# @return [void]
|
19
|
+
def self.sandboxed
|
20
|
+
orig_config = CircleCIReporter.configuration
|
21
|
+
orig_client = CircleCIReporter.client
|
22
|
+
|
23
|
+
CircleCIReporter.configuration = Configuration.new
|
24
|
+
CircleCIReporter.client = Client.new
|
25
|
+
|
26
|
+
yield CircleCIReporter.configuration
|
27
|
+
ensure
|
28
|
+
CircleCIReporter.configuration = orig_config
|
29
|
+
CircleCIReporter.client = orig_client
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CircleCIReporter
|
4
|
+
module VCS
|
5
|
+
# @abstract Subclass and override {#create_comment} to implement a custom VCS client class.
|
6
|
+
class Base
|
7
|
+
# @param token [String]
|
8
|
+
def initialize(token)
|
9
|
+
@token = token
|
10
|
+
end
|
11
|
+
|
12
|
+
# @param body [String]
|
13
|
+
# @return [void]
|
14
|
+
def create_comment(body)
|
15
|
+
raise NotImplementedError
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
attr_reader :token
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|