allure-report-publisher 1.0.0 → 1.2.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 +4 -4
- data/README.md +1 -0
- data/lib/allure_report_publisher/commands/upload.rb +51 -14
- data/lib/allure_report_publisher/lib/helpers/gsutil.rb +141 -0
- data/lib/allure_report_publisher/lib/helpers/helpers.rb +78 -20
- data/lib/allure_report_publisher/lib/helpers/spinner.rb +32 -9
- data/lib/allure_report_publisher/lib/helpers/summary.rb +1 -1
- data/lib/allure_report_publisher/lib/providers/github.rb +8 -1
- data/lib/allure_report_publisher/lib/providers/gitlab.rb +8 -1
- data/lib/allure_report_publisher/lib/report_generator.rb +17 -21
- data/lib/allure_report_publisher/lib/uploaders/_uploader.rb +15 -7
- data/lib/allure_report_publisher/lib/uploaders/gcs.rb +56 -7
- data/lib/allure_report_publisher/lib/uploaders/s3.rb +13 -4
- data/lib/allure_report_publisher/version.rb +1 -1
- data/lib/allure_report_publisher.rb +1 -1
- metadata +32 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1809950b5cb79015f9d4568c28eea62fea2ff4abb581ee762303684aa86859a2
|
4
|
+
data.tar.gz: e22f93a0a820aded1cd74bef9b3836867e6c25a0a454acd57343159ba19c780f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7695a8fab036e58f775d1d6372ddcc054cd791212e278c840fb2642e4c4c29fb35409fe5cf9e30475217be471f564e68c231362305e89f3815bdb31c53f05ec1
|
7
|
+
data.tar.gz: c9a64aaf86756c14c2e735a9632a1024d178ea1b8e9e97a43d65ad2afd05ff4adaa38094a9f12e083a70d145006269defdb1252ef64991e6412913cd47e6ea5e
|
data/README.md
CHANGED
@@ -53,6 +53,7 @@ Options:
|
|
53
53
|
--[no-]copy-latest # Keep copy of latest report at base prefix path, default: false
|
54
54
|
--[no-]color # Force color output
|
55
55
|
--[no-]ignore-missing-results # Ignore missing allure results, default: false
|
56
|
+
--[no-]debug # Print additional debug output, default: false
|
56
57
|
--help, -h # Print this help
|
57
58
|
|
58
59
|
Examples:
|
@@ -55,6 +55,10 @@ module Publisher
|
|
55
55
|
type: :boolean,
|
56
56
|
default: false,
|
57
57
|
desc: "Ignore missing allure results"
|
58
|
+
option :debug,
|
59
|
+
type: :boolean,
|
60
|
+
default: false,
|
61
|
+
desc: "Print additional debug output"
|
58
62
|
|
59
63
|
example [
|
60
64
|
"s3 --results-glob='path/to/allure-results' --bucket=my-bucket",
|
@@ -66,18 +70,15 @@ module Publisher
|
|
66
70
|
@args = args
|
67
71
|
|
68
72
|
validate_args
|
69
|
-
|
70
|
-
|
71
|
-
log("Generating allure report")
|
72
|
-
Spinner.spin("generating") { uploader.generate_report }
|
73
|
+
scan_results_paths
|
73
74
|
|
74
|
-
|
75
|
-
|
76
|
-
uploader.report_urls.each { |k, v| log("#{k}: #{v}", :green) }
|
75
|
+
generate_report
|
76
|
+
upload_report
|
77
77
|
return unless args[:update_pr] && uploader.pr?
|
78
78
|
|
79
|
-
|
80
|
-
|
79
|
+
add_report_urls
|
80
|
+
rescue StandardError => e
|
81
|
+
handle_error(e)
|
81
82
|
end
|
82
83
|
|
83
84
|
private
|
@@ -90,8 +91,8 @@ module Publisher
|
|
90
91
|
def uploader
|
91
92
|
@uploader ||= uploaders(args[:type]).new(
|
92
93
|
summary_type: args[:summary],
|
94
|
+
result_paths: @result_paths,
|
93
95
|
**args.slice(
|
94
|
-
:results_glob,
|
95
96
|
:bucket,
|
96
97
|
:prefix,
|
97
98
|
:copy_latest,
|
@@ -121,18 +122,54 @@ module Publisher
|
|
121
122
|
error("Missing argument --bucket!") unless args[:bucket]
|
122
123
|
end
|
123
124
|
|
124
|
-
#
|
125
|
+
# Scan for allure results paths
|
125
126
|
#
|
126
127
|
# @param [String] results_glob
|
127
128
|
# @return [void]
|
128
|
-
def
|
129
|
+
def scan_results_paths
|
129
130
|
results_glob = args[:results_glob]
|
130
131
|
ignore = args[:ignore_missing_results]
|
131
|
-
|
132
|
+
@result_paths = Dir.glob(results_glob)
|
133
|
+
log_debug("Glob '#{results_glob}' found #{@result_paths.size} paths")
|
134
|
+
return unless @result_paths.empty?
|
132
135
|
|
133
|
-
log("Glob '#{results_glob}' did not match any
|
136
|
+
log("Glob '#{results_glob}' did not match any paths!", ignore ? :yellow : :red)
|
134
137
|
exit(ignore ? 0 : 1)
|
135
138
|
end
|
139
|
+
|
140
|
+
# Generate allure report
|
141
|
+
#
|
142
|
+
# @return [void]
|
143
|
+
def generate_report
|
144
|
+
log("Generating allure report")
|
145
|
+
Spinner.spin("generating", debug: args[:debug]) { uploader.generate_report }
|
146
|
+
end
|
147
|
+
|
148
|
+
# Upload report to cloud storage
|
149
|
+
#
|
150
|
+
# @return [void]
|
151
|
+
def upload_report
|
152
|
+
log("Uploading allure report to #{args[:type]}")
|
153
|
+
Spinner.spin("uploading", debug: args[:debug]) { uploader.upload }
|
154
|
+
uploader.report_urls.each { |k, v| log("#{k}: #{v}", :green) }
|
155
|
+
end
|
156
|
+
|
157
|
+
# Add report results to pr/mr
|
158
|
+
#
|
159
|
+
# @return [void]
|
160
|
+
def add_report_urls
|
161
|
+
log("Adding reports urls")
|
162
|
+
Spinner.spin("updating", exit_on_error: false, debug: args[:debug]) { uploader.add_result_summary }
|
163
|
+
end
|
164
|
+
|
165
|
+
# Handle error during upload command
|
166
|
+
#
|
167
|
+
# @param [StandardError] error
|
168
|
+
# @return [void]
|
169
|
+
def handle_error(error)
|
170
|
+
exit(1) if error.is_a?(Spinner::Failure)
|
171
|
+
error(error)
|
172
|
+
end
|
136
173
|
end
|
137
174
|
end
|
138
175
|
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
require "tempfile"
|
2
|
+
|
3
|
+
module Publisher
|
4
|
+
module Helpers
|
5
|
+
# Helper class for gsutil cli utility
|
6
|
+
#
|
7
|
+
class Gsutil
|
8
|
+
class UnsupportedConfig < StandardError; end
|
9
|
+
class Uninitialised < StandardError; end
|
10
|
+
|
11
|
+
include Helpers
|
12
|
+
|
13
|
+
def self.init
|
14
|
+
new.init!
|
15
|
+
end
|
16
|
+
|
17
|
+
private_class_method :new
|
18
|
+
|
19
|
+
# Initialize gsutil
|
20
|
+
#
|
21
|
+
# @return [Gsutil]
|
22
|
+
def init!
|
23
|
+
log_debug("Setting up gsutil")
|
24
|
+
@valid = execute_shell("which gsutil") && true
|
25
|
+
|
26
|
+
log_debug("Checking google credentials")
|
27
|
+
check_credentials
|
28
|
+
log_debug("Credentials valid, gsutil initialized")
|
29
|
+
self
|
30
|
+
rescue StandardError => e
|
31
|
+
case e
|
32
|
+
when UnsupportedConfig
|
33
|
+
log_debug("credentials not compatible with gsutil! Falling back to google sdk client for batch uploads")
|
34
|
+
when ShellCommandFailure
|
35
|
+
log_debug("gsutil command not found, falling back to gcs client")
|
36
|
+
else
|
37
|
+
log_debug("gsutil init failed: error: #{e}\nbacktrace: #{e.backtrace&.join("\n")}")
|
38
|
+
end
|
39
|
+
|
40
|
+
@valid = false
|
41
|
+
self
|
42
|
+
end
|
43
|
+
|
44
|
+
# Check if gsutil is valid
|
45
|
+
#
|
46
|
+
# @return [Boolean]
|
47
|
+
def valid?
|
48
|
+
@valid
|
49
|
+
end
|
50
|
+
|
51
|
+
# Perform copy operation within a single bucket
|
52
|
+
#
|
53
|
+
# @param [String] source_dir
|
54
|
+
# @param [String] destination_dir
|
55
|
+
# @param [String] bucket
|
56
|
+
# @param [Integer] cache_control
|
57
|
+
# @return [void]
|
58
|
+
def batch_copy(source_dir:, destination_dir:, bucket:, cache_control: 3600)
|
59
|
+
batch_upload(
|
60
|
+
source_dir: "gs://#{bucket}/#{source_dir}",
|
61
|
+
destination_dir: destination_dir,
|
62
|
+
bucket: bucket,
|
63
|
+
cache_control: cache_control
|
64
|
+
)
|
65
|
+
end
|
66
|
+
|
67
|
+
# Perform batch upload operation
|
68
|
+
#
|
69
|
+
# @param [String] source_dir
|
70
|
+
# @param [String] destination_dir
|
71
|
+
# @param [String] bucket
|
72
|
+
# @param [String] cache_control
|
73
|
+
# @return [void]
|
74
|
+
def batch_upload(source_dir:, destination_dir:, bucket:, cache_control: 3600)
|
75
|
+
raise(Uninitialised, "gsutil has not been properly set up!") unless valid?
|
76
|
+
|
77
|
+
action = source_dir.start_with?("gs://") ? "Copying" : "Uploading"
|
78
|
+
destination = "gs://#{bucket}/#{destination_dir}"
|
79
|
+
|
80
|
+
log_debug("#{action} '#{source_dir}' to '#{destination}'")
|
81
|
+
with_credentials do |key_file|
|
82
|
+
execute_shell([
|
83
|
+
base_cmd(key_file),
|
84
|
+
"-h 'Cache-Control:private, max-age=#{cache_control}'",
|
85
|
+
"rsync",
|
86
|
+
"-j json,csv,txt,js,css",
|
87
|
+
"-r #{source_dir} #{destination}"
|
88
|
+
].join(" "))
|
89
|
+
end
|
90
|
+
log_debug("Finished upload successfully")
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
# Execute block with gcs credentials
|
96
|
+
#
|
97
|
+
# @return [void]
|
98
|
+
def with_credentials
|
99
|
+
if json_key[:file]
|
100
|
+
yield(json_key[:key])
|
101
|
+
else
|
102
|
+
Tempfile.create("auth") do |f|
|
103
|
+
f.write(json_key[:key])
|
104
|
+
f.close
|
105
|
+
|
106
|
+
yield(f.path)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Google auth default credentials
|
112
|
+
#
|
113
|
+
# @return [String, Hash]
|
114
|
+
def gcs_credentials
|
115
|
+
@gcs_credentials ||= Google::Cloud::Storage.default_credentials
|
116
|
+
end
|
117
|
+
|
118
|
+
# Google auth json key
|
119
|
+
#
|
120
|
+
# @return [Hash]
|
121
|
+
def json_key
|
122
|
+
@json_key ||= if gcs_credentials.is_a?(Hash)
|
123
|
+
{ file: false, key: gcs_credentials.to_json }
|
124
|
+
elsif gcs_credentials.is_a?(String) && File.exist?(gcs_credentials)
|
125
|
+
{ file: true, key: gcs_credentials.tap { |f| JSON.parse(File.read(f)) } }
|
126
|
+
else
|
127
|
+
raise(UnsupportedConfig, "only google key json credentials are supported for gsutil")
|
128
|
+
end
|
129
|
+
end
|
130
|
+
alias check_credentials json_key
|
131
|
+
|
132
|
+
# Base command
|
133
|
+
#
|
134
|
+
# @param [String] key_file
|
135
|
+
# @return [String]
|
136
|
+
def base_cmd(key_file)
|
137
|
+
"gsutil -o 'Credentials:gs_service_key_file=#{key_file}' -m"
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
@@ -1,28 +1,66 @@
|
|
1
1
|
require "pastel"
|
2
2
|
require "open3"
|
3
|
+
require "logger"
|
4
|
+
require "stringio"
|
3
5
|
|
4
6
|
module Publisher
|
5
7
|
# Helpers
|
6
8
|
#
|
7
9
|
module Helpers
|
8
|
-
|
9
|
-
#
|
10
|
-
# @param [Boolean] force_color
|
11
|
-
# @return [Pastel]
|
12
|
-
def self.pastel(force_color: nil)
|
13
|
-
@pastel ||= Pastel.new(enabled: force_color, eachline: "\n")
|
14
|
-
end
|
10
|
+
class ShellCommandFailure < StandardError; end
|
15
11
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
12
|
+
class << self
|
13
|
+
# Global instance of pastel
|
14
|
+
#
|
15
|
+
# @param [Boolean] force_color
|
16
|
+
# @return [Pastel]
|
17
|
+
def pastel(force_color: nil)
|
18
|
+
@pastel ||= Pastel.new(enabled: force_color, eachline: "\n")
|
19
|
+
end
|
20
|
+
|
21
|
+
# Check allure cli is installed and executable
|
22
|
+
#
|
23
|
+
# @return [void]
|
24
|
+
def allure_cli?
|
25
|
+
execute_shell("which allure")
|
26
|
+
rescue StandardError
|
27
|
+
Helpers.error(
|
28
|
+
"Allure cli is missing! See https://docs.qameta.io/allure/#_installing_a_commandline on how to install it!"
|
29
|
+
)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Check if gsutil is installed and executable
|
33
|
+
#
|
34
|
+
# @return [Boolean]
|
35
|
+
def gsutil?
|
36
|
+
execute_shell("which gsutil") && true
|
37
|
+
rescue StandardError
|
38
|
+
false
|
39
|
+
end
|
40
|
+
|
41
|
+
# Debug logging session output
|
42
|
+
#
|
43
|
+
# @return [StringIO]
|
44
|
+
def debug_io
|
45
|
+
@debug_io ||= StringIO.new
|
46
|
+
end
|
47
|
+
|
48
|
+
# Clear debug log output
|
49
|
+
#
|
50
|
+
# @return [void]
|
51
|
+
def reset_debug_io!
|
52
|
+
@debug_io = nil
|
53
|
+
end
|
22
54
|
|
23
|
-
|
24
|
-
|
25
|
-
|
55
|
+
# Logger instance
|
56
|
+
#
|
57
|
+
# @return [Logger]
|
58
|
+
def logger
|
59
|
+
Logger.new(debug_io).tap do |logger|
|
60
|
+
logger.datetime_format = "%Y-%m-%d %H:%M:%S"
|
61
|
+
logger.formatter = proc { |_severity, time, _progname, msg| "[#{time}] #{msg}\n" }
|
62
|
+
end
|
63
|
+
end
|
26
64
|
end
|
27
65
|
|
28
66
|
# Colorize string
|
@@ -43,6 +81,14 @@ module Publisher
|
|
43
81
|
puts colorize(message, color)
|
44
82
|
end
|
45
83
|
|
84
|
+
# Save debug message to be displayed later
|
85
|
+
#
|
86
|
+
# @param [String] message
|
87
|
+
# @return [void]
|
88
|
+
def log_debug(message)
|
89
|
+
Helpers.logger.info(message)
|
90
|
+
end
|
91
|
+
|
46
92
|
# Print error message and exit
|
47
93
|
#
|
48
94
|
# @param [String] message
|
@@ -64,13 +110,25 @@ module Publisher
|
|
64
110
|
#
|
65
111
|
# @param [String] command
|
66
112
|
# @return [String] output
|
67
|
-
def execute_shell(command)
|
113
|
+
def execute_shell(command, mask: nil)
|
114
|
+
loggable_command = mask ? command.gsub(mask, "***") : command
|
115
|
+
log_debug("Executing command '#{loggable_command}'")
|
68
116
|
out, err, status = Open3.capture3(command)
|
69
|
-
raise("Out:\n#{out}\n\nErr:\n#{err}") unless status.success?
|
70
117
|
|
71
|
-
|
118
|
+
cmd_output = []
|
119
|
+
cmd_output << "Out: #{out}" unless out.empty?
|
120
|
+
cmd_output << "Err: #{err}" unless err.empty?
|
121
|
+
output = cmd_output.join("\n")
|
122
|
+
|
123
|
+
unless status.success?
|
124
|
+
err_msg = "Command '#{loggable_command}' failed!\n#{output}"
|
125
|
+
err_msg = mask ? err_msg.gsub(mask, "***") : err_msg
|
126
|
+
raise(ShellCommandFailure, err_msg)
|
127
|
+
end
|
128
|
+
|
129
|
+
mask ? output.gsub(mask, "***") : output
|
72
130
|
end
|
73
131
|
|
74
|
-
module_function :colorize, :log, :error, :path, :execute_shell
|
132
|
+
module_function :colorize, :log, :log_debug, :error, :path, :execute_shell
|
75
133
|
end
|
76
134
|
end
|
@@ -9,9 +9,12 @@ module Publisher
|
|
9
9
|
class Spinner
|
10
10
|
include Helpers
|
11
11
|
|
12
|
-
|
12
|
+
class Failure < StandardError; end
|
13
|
+
|
14
|
+
def initialize(spinner_message, exit_on_error: true, debug: false)
|
13
15
|
@spinner_message = spinner_message
|
14
16
|
@exit_on_error = exit_on_error
|
17
|
+
@debug = debug
|
15
18
|
end
|
16
19
|
|
17
20
|
# Run code block inside spinner
|
@@ -21,8 +24,8 @@ module Publisher
|
|
21
24
|
# @param [Boolean] exit_on_error
|
22
25
|
# @param [Proc] &block
|
23
26
|
# @return [void]
|
24
|
-
def self.spin(spinner_message, done_message: "done", exit_on_error: true, &block)
|
25
|
-
new(spinner_message, exit_on_error: exit_on_error).spin(done_message, &block)
|
27
|
+
def self.spin(spinner_message, done_message: "done", exit_on_error: true, debug: false, &block)
|
28
|
+
new(spinner_message, exit_on_error: exit_on_error, debug: debug).spin(done_message, &block)
|
26
29
|
end
|
27
30
|
|
28
31
|
# Run code block inside spinner
|
@@ -34,14 +37,32 @@ module Publisher
|
|
34
37
|
yield
|
35
38
|
spinner_success(done_message)
|
36
39
|
rescue StandardError => e
|
37
|
-
spinner_error(e
|
38
|
-
|
40
|
+
spinner_error(e)
|
41
|
+
raise(Failure, e.message) if exit_on_error
|
42
|
+
ensure
|
43
|
+
print_debug
|
44
|
+
Helpers.reset_debug_io!
|
39
45
|
end
|
40
46
|
end
|
41
47
|
|
42
48
|
private
|
43
49
|
|
44
|
-
attr_reader :spinner_message,
|
50
|
+
attr_reader :spinner_message,
|
51
|
+
:exit_on_error,
|
52
|
+
:debug
|
53
|
+
|
54
|
+
# Print debug contents
|
55
|
+
#
|
56
|
+
# @return [void]
|
57
|
+
def print_debug
|
58
|
+
return if !debug || Helpers.debug_io.string.empty?
|
59
|
+
|
60
|
+
puts <<~OUT.strip
|
61
|
+
== DEBUG LOG OUTPUT ==
|
62
|
+
#{Helpers.debug_io.string.strip}
|
63
|
+
== DEBUG LOG OUTPUT ==
|
64
|
+
OUT
|
65
|
+
end
|
45
66
|
|
46
67
|
# Error message color
|
47
68
|
#
|
@@ -96,10 +117,12 @@ module Publisher
|
|
96
117
|
|
97
118
|
# Return spinner error
|
98
119
|
#
|
99
|
-
# @param [
|
120
|
+
# @param [StandardError] error
|
100
121
|
# @return [void]
|
101
|
-
def spinner_error(
|
102
|
-
|
122
|
+
def spinner_error(error)
|
123
|
+
message = ["failed", error.message]
|
124
|
+
message << error.backtrace if debug
|
125
|
+
colored_message = colorize(message.compact.join("\n"), error_color)
|
103
126
|
return spinner.error(colored_message) if tty?
|
104
127
|
|
105
128
|
spinner.stop
|
@@ -70,7 +70,7 @@ module Publisher
|
|
70
70
|
# Short summary table
|
71
71
|
#
|
72
72
|
# @return [Array<String>]
|
73
|
-
def short_summary
|
73
|
+
def short_summary
|
74
74
|
return @short_summary if defined?(@short_summary)
|
75
75
|
|
76
76
|
sum = summary_data.values.each_with_object({
|
@@ -5,6 +5,8 @@ module Publisher
|
|
5
5
|
# Github implementation
|
6
6
|
#
|
7
7
|
class Github < Provider
|
8
|
+
include Helpers
|
9
|
+
|
8
10
|
# Set octokit to autopaginate
|
9
11
|
#
|
10
12
|
Octokit.configure do |config|
|
@@ -60,6 +62,7 @@ module Publisher
|
|
60
62
|
def update_pr_description
|
61
63
|
return File.write(step_summary_file, url_section_builder.comment_body) if actions?
|
62
64
|
|
65
|
+
log_debug("Updating pr description for pr !#{pr_id}")
|
63
66
|
client.update_pull_request(repository, pr_id, body: url_section_builder.updated_pr_description(pr_description))
|
64
67
|
end
|
65
68
|
|
@@ -67,8 +70,12 @@ module Publisher
|
|
67
70
|
#
|
68
71
|
# @return [void]
|
69
72
|
def add_comment
|
70
|
-
|
73
|
+
unless comment
|
74
|
+
log_debug("Creating comment with summary for pr ! #{pr_id}")
|
75
|
+
return client.add_comment(repository, pr_id, url_section_builder.comment_body)
|
76
|
+
end
|
71
77
|
|
78
|
+
log_debug("Updating summary in comment with id #{comment[:id]} in pr !#{pr_id}")
|
72
79
|
client.update_comment(repository, comment[:id], url_section_builder.comment_body(comment[:body]))
|
73
80
|
end
|
74
81
|
|
@@ -5,6 +5,8 @@ module Publisher
|
|
5
5
|
# Gitlab implementation
|
6
6
|
#
|
7
7
|
class Gitlab < Provider
|
8
|
+
include Helpers
|
9
|
+
|
8
10
|
# Get ci run ID without creating instance of ci provider
|
9
11
|
#
|
10
12
|
# @return [String]
|
@@ -48,6 +50,7 @@ module Publisher
|
|
48
50
|
#
|
49
51
|
# @return [void]
|
50
52
|
def update_pr_description
|
53
|
+
log_debug("Updating mr description for mr !#{mr_iid}")
|
51
54
|
client.update_merge_request(
|
52
55
|
project,
|
53
56
|
mr_iid,
|
@@ -59,8 +62,12 @@ module Publisher
|
|
59
62
|
#
|
60
63
|
# @return [void]
|
61
64
|
def add_comment
|
62
|
-
|
65
|
+
unless comment
|
66
|
+
log_debug("Creating comment with summary for mr ! #{mr_iid}")
|
67
|
+
return client.create_merge_request_comment(project, mr_iid, url_section_builder.comment_body)
|
68
|
+
end
|
63
69
|
|
70
|
+
log_debug("Updating summary in comment with id #{comment.id} in mr !#{mr_iid}")
|
64
71
|
client.edit_merge_request_note(project, mr_iid, comment.id, url_section_builder.comment_body(comment.body))
|
65
72
|
end
|
66
73
|
|
@@ -10,14 +10,16 @@ module Publisher
|
|
10
10
|
class ReportGenerator
|
11
11
|
include Helpers
|
12
12
|
|
13
|
-
def initialize(
|
14
|
-
@
|
13
|
+
def initialize(result_paths)
|
14
|
+
@result_paths = result_paths.join(" ")
|
15
15
|
end
|
16
16
|
|
17
17
|
# Generate allure report
|
18
18
|
#
|
19
19
|
# @return [void]
|
20
20
|
def generate
|
21
|
+
create_common_path
|
22
|
+
|
21
23
|
generate_report
|
22
24
|
end
|
23
25
|
|
@@ -25,40 +27,34 @@ module Publisher
|
|
25
27
|
#
|
26
28
|
# @return [String]
|
27
29
|
def common_info_path
|
28
|
-
@common_info_path ||= Dir.mktmpdir("allure-results")
|
30
|
+
@common_info_path ||= Dir.mktmpdir("allure-results").tap do |path|
|
31
|
+
log_debug("Created tmp folder for common data: '#{path}'")
|
32
|
+
end
|
29
33
|
end
|
34
|
+
alias create_common_path common_info_path
|
30
35
|
|
31
36
|
# Allure report directory
|
32
37
|
#
|
33
38
|
# @return [String]
|
34
39
|
def report_path
|
35
|
-
@report_path ||= Dir.
|
40
|
+
@report_path ||= File.join(Dir.tmpdir, "allure-report-#{Time.now.to_i}")
|
36
41
|
end
|
37
42
|
|
38
43
|
private
|
39
44
|
|
40
|
-
|
41
|
-
|
42
|
-
# Return all allure results paths from glob
|
43
|
-
#
|
44
|
-
# @return [String]
|
45
|
-
def result_paths
|
46
|
-
@result_paths ||= begin
|
47
|
-
paths = Dir.glob(results_glob)
|
48
|
-
raise(NoAllureResultsError, "Missing allure results") if paths.empty?
|
49
|
-
|
50
|
-
paths.join(" ")
|
51
|
-
end
|
52
|
-
end
|
45
|
+
# @return [String] result paths string
|
46
|
+
attr_reader :result_paths
|
53
47
|
|
54
48
|
# Generate allure report
|
55
49
|
#
|
56
50
|
# @return [void]
|
57
51
|
def generate_report
|
58
|
-
|
59
|
-
|
60
|
-
)
|
61
|
-
|
52
|
+
log_debug("Generating allure report")
|
53
|
+
cmd = "allure generate --clean --output #{report_path} #{common_info_path} #{result_paths}"
|
54
|
+
out = execute_shell(cmd)
|
55
|
+
log_debug("Generated allure report. #{out}")
|
56
|
+
rescue StandardError => e
|
57
|
+
raise(AllureError, e.message)
|
62
58
|
end
|
63
59
|
end
|
64
60
|
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require "forwardable"
|
2
|
+
require "json"
|
2
3
|
|
3
4
|
module Publisher
|
4
5
|
module Uploaders
|
@@ -22,7 +23,7 @@ module Publisher
|
|
22
23
|
# Uploader instance
|
23
24
|
#
|
24
25
|
# @param [Hash] args
|
25
|
-
# @option args [
|
26
|
+
# @option args [Array] :result_paths
|
26
27
|
# @option args [String] :bucket
|
27
28
|
# @option args [String] :prefix
|
28
29
|
# @option args [Boolean] :update_pr
|
@@ -31,7 +32,7 @@ module Publisher
|
|
31
32
|
# @option args [Boolean] :collapse_summary
|
32
33
|
# @option args [String] :copy_latest
|
33
34
|
def initialize(**args)
|
34
|
-
@
|
35
|
+
@result_paths = args[:result_paths]
|
35
36
|
@bucket_name = args[:bucket]
|
36
37
|
@prefix = args[:prefix]
|
37
38
|
@update_pr = args[:update_pr]
|
@@ -73,6 +74,7 @@ module Publisher
|
|
73
74
|
def add_result_summary
|
74
75
|
return unless update_pr && ci_provider
|
75
76
|
|
77
|
+
log_debug("Adding test result summary")
|
76
78
|
ci_provider.add_result_summary
|
77
79
|
end
|
78
80
|
|
@@ -95,7 +97,7 @@ module Publisher
|
|
95
97
|
|
96
98
|
private
|
97
99
|
|
98
|
-
attr_reader :
|
100
|
+
attr_reader :result_paths,
|
99
101
|
:bucket_name,
|
100
102
|
:prefix,
|
101
103
|
:update_pr,
|
@@ -162,7 +164,7 @@ module Publisher
|
|
162
164
|
#
|
163
165
|
# @return [Publisher::ReportGenerator]
|
164
166
|
def report_generator
|
165
|
-
@report_generator ||= ReportGenerator.new(
|
167
|
+
@report_generator ||= ReportGenerator.new(result_paths)
|
166
168
|
end
|
167
169
|
|
168
170
|
# Report path prefix
|
@@ -214,7 +216,8 @@ module Publisher
|
|
214
216
|
def add_history
|
215
217
|
create_history_dir
|
216
218
|
download_history
|
217
|
-
rescue HistoryNotFoundError
|
219
|
+
rescue HistoryNotFoundError => e
|
220
|
+
log_debug(e.message)
|
218
221
|
nil
|
219
222
|
end
|
220
223
|
|
@@ -224,7 +227,11 @@ module Publisher
|
|
224
227
|
def add_executor_info
|
225
228
|
return unless ci_provider
|
226
229
|
|
227
|
-
|
230
|
+
json_path = "#{common_info_path}/#{EXECUTOR_JSON}"
|
231
|
+
json = ci_provider.executor_info.to_json
|
232
|
+
log_debug("Saving ci executor info")
|
233
|
+
File.write(json_path, json)
|
234
|
+
log_debug("Saved '#{EXECUTOR_JSON}' as '#{json_path}'\n#{JSON.pretty_generate(ci_provider.executor_info)}")
|
228
235
|
end
|
229
236
|
|
230
237
|
# Run upload commands
|
@@ -240,7 +247,8 @@ module Publisher
|
|
240
247
|
#
|
241
248
|
# @return [void]
|
242
249
|
def create_history_dir
|
243
|
-
FileUtils.mkdir_p(path(common_info_path, "history"))
|
250
|
+
path = FileUtils.mkdir_p(path(common_info_path, "history"))
|
251
|
+
log_debug("Created tmp folder for history data: '#{path.first}'")
|
244
252
|
end
|
245
253
|
end
|
246
254
|
end
|
@@ -14,6 +14,13 @@ module Publisher
|
|
14
14
|
@client ||= Google::Cloud::Storage.new
|
15
15
|
end
|
16
16
|
|
17
|
+
# Gsutil class
|
18
|
+
#
|
19
|
+
# @return [Helpers::Gsutil]
|
20
|
+
def gsutil
|
21
|
+
@gsutil ||= Helpers::Gsutil.init
|
22
|
+
end
|
23
|
+
|
17
24
|
# GCS bucket
|
18
25
|
#
|
19
26
|
# @return [Google::Cloud::Storage::Bucket]
|
@@ -39,11 +46,14 @@ module Publisher
|
|
39
46
|
#
|
40
47
|
# @return [void]
|
41
48
|
def download_history
|
49
|
+
log_debug("Downloading previous run history")
|
42
50
|
HISTORY.each do |file_name|
|
43
51
|
file = bucket.file(key(prefix, "history", file_name))
|
44
52
|
raise(HistoryNotFoundError, "Allure history from previous runs not found!") unless file
|
45
53
|
|
46
|
-
|
54
|
+
file_path = path(common_info_path, "history", file_name)
|
55
|
+
file.download(file_path)
|
56
|
+
log_debug("Downloaded '#{file_name}' as '#{file_path}'")
|
47
57
|
end
|
48
58
|
end
|
49
59
|
|
@@ -51,21 +61,28 @@ module Publisher
|
|
51
61
|
#
|
52
62
|
# @return [void]
|
53
63
|
def upload_history
|
54
|
-
|
64
|
+
log_debug("Uploading report history")
|
65
|
+
file_upload(report_files.select { |file| file.fnmatch?("*/history/*") }, prefix)
|
55
66
|
end
|
56
67
|
|
57
68
|
# Upload allure report
|
58
69
|
#
|
59
70
|
# @return [void]
|
60
71
|
def upload_report
|
61
|
-
|
72
|
+
log_debug("Uploading report files")
|
73
|
+
return batch_upload(report_path, full_prefix) if gsutil.valid?
|
74
|
+
|
75
|
+
file_upload(report_files, full_prefix)
|
62
76
|
end
|
63
77
|
|
64
78
|
# Upload copy of latest run
|
65
79
|
#
|
66
80
|
# @return [void]
|
67
81
|
def upload_latest_copy
|
68
|
-
|
82
|
+
log_debug("Uploading report copy as latest report")
|
83
|
+
return batch_copy(full_prefix, prefix, cache_control: 60) if gsutil.valid?
|
84
|
+
|
85
|
+
file_upload(report_files, prefix, cache_control: 60)
|
69
86
|
end
|
70
87
|
|
71
88
|
# Upload files to gcs
|
@@ -73,8 +90,9 @@ module Publisher
|
|
73
90
|
# @param [Array<Pathname>] files
|
74
91
|
# @param [String] key_prefix
|
75
92
|
# @param [Hash] params
|
76
|
-
# @return [
|
77
|
-
def
|
93
|
+
# @return [void]
|
94
|
+
def file_upload(files, key_prefix, cache_control: 3600)
|
95
|
+
threads = 8
|
78
96
|
args = files.map do |file|
|
79
97
|
{
|
80
98
|
file: file.to_s,
|
@@ -83,9 +101,40 @@ module Publisher
|
|
83
101
|
}
|
84
102
|
end
|
85
103
|
|
86
|
-
|
104
|
+
log_debug("Uploading '#{args.size}' files in '#{threads}' threads to bucker '#{bucket_name}'")
|
105
|
+
Parallel.each(args, in_threads: threads) do |obj|
|
87
106
|
bucket.create_file(*obj.slice(:file, :path).values, **obj.slice(:cache_control))
|
88
107
|
end
|
108
|
+
log_debug("Finished upload successfully")
|
109
|
+
end
|
110
|
+
|
111
|
+
# Upload directory recursively
|
112
|
+
#
|
113
|
+
# @param [String] source_dir
|
114
|
+
# @param [String] destination_dir
|
115
|
+
# @return [void]
|
116
|
+
def batch_upload(source_dir, destination_dir, cache_control: 3600)
|
117
|
+
gsutil.batch_upload(
|
118
|
+
source_dir: source_dir,
|
119
|
+
destination_dir: destination_dir,
|
120
|
+
bucket: bucket_name,
|
121
|
+
cache_control: cache_control
|
122
|
+
)
|
123
|
+
end
|
124
|
+
|
125
|
+
# Copy directory within the bucket
|
126
|
+
#
|
127
|
+
# @param [String] source_dir
|
128
|
+
# @param [String] destination_dir
|
129
|
+
# @param [String] cache_control
|
130
|
+
# @return [void]
|
131
|
+
def batch_copy(source_dir, destination_dir, cache_control: 3600)
|
132
|
+
gsutil.batch_copy(
|
133
|
+
source_dir: source_dir,
|
134
|
+
destination_dir: destination_dir,
|
135
|
+
bucket: bucket_name,
|
136
|
+
cache_control: cache_control
|
137
|
+
)
|
89
138
|
end
|
90
139
|
|
91
140
|
# Fabricate key for s3 object
|
@@ -47,12 +47,15 @@ module Publisher
|
|
47
47
|
#
|
48
48
|
# @return [void]
|
49
49
|
def download_history
|
50
|
-
|
50
|
+
log_debug("Downloading previous run history")
|
51
|
+
HISTORY.each do |file_name|
|
52
|
+
file_path = path(common_info_path, "history", file_name)
|
51
53
|
client.get_object(
|
52
|
-
response_target:
|
53
|
-
key: key(prefix, "history",
|
54
|
+
response_target: file_path,
|
55
|
+
key: key(prefix, "history", file_name),
|
54
56
|
bucket: bucket_name
|
55
57
|
)
|
58
|
+
log_debug("Downloaded '#{file_name}' as '#{file_path}'")
|
56
59
|
end
|
57
60
|
rescue Aws::S3::Errors::NoSuchKey
|
58
61
|
raise(HistoryNotFoundError, "Allure history from previous runs not found!")
|
@@ -62,6 +65,7 @@ module Publisher
|
|
62
65
|
#
|
63
66
|
# @return [void]
|
64
67
|
def upload_history
|
68
|
+
log_debug("Uploading report history")
|
65
69
|
upload_to_s3(report_files.select { |file| file.fnmatch?("*/history/*") }, prefix)
|
66
70
|
end
|
67
71
|
|
@@ -69,6 +73,7 @@ module Publisher
|
|
69
73
|
#
|
70
74
|
# @return [void]
|
71
75
|
def upload_report
|
76
|
+
log_debug("Uploading report files")
|
72
77
|
upload_to_s3(report_files, full_prefix)
|
73
78
|
end
|
74
79
|
|
@@ -76,6 +81,7 @@ module Publisher
|
|
76
81
|
#
|
77
82
|
# @return [void]
|
78
83
|
def upload_latest_copy
|
84
|
+
log_debug("Uploading report copy as latest report")
|
79
85
|
upload_to_s3(report_files, prefix, cache_control: 60)
|
80
86
|
end
|
81
87
|
|
@@ -85,6 +91,7 @@ module Publisher
|
|
85
91
|
# @param [String] key_prefix
|
86
92
|
# @return [Array<Hash>]
|
87
93
|
def upload_to_s3(files, key_prefix, cache_control: 3600)
|
94
|
+
threads = 8
|
88
95
|
args = files.map do |file|
|
89
96
|
{
|
90
97
|
body: File.new(file),
|
@@ -95,7 +102,9 @@ module Publisher
|
|
95
102
|
}
|
96
103
|
end
|
97
104
|
|
98
|
-
|
105
|
+
log_debug("Uploading '#{args.size}' files in '#{threads}' threads")
|
106
|
+
Parallel.each(args, in_threads: threads) { |obj| client.put_object(obj) }
|
107
|
+
log_debug("Finished upload successfully")
|
99
108
|
end
|
100
109
|
|
101
110
|
# Fabricate key for s3 object
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: allure-report-publisher
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrejs Cunskis
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-10-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: aws-sdk-s3
|
@@ -19,7 +19,7 @@ dependencies:
|
|
19
19
|
version: 1.93.1
|
20
20
|
- - "<"
|
21
21
|
- !ruby/object:Gem::Version
|
22
|
-
version: 1.
|
22
|
+
version: 1.116.0
|
23
23
|
type: :runtime
|
24
24
|
prerelease: false
|
25
25
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -29,7 +29,7 @@ dependencies:
|
|
29
29
|
version: 1.93.1
|
30
30
|
- - "<"
|
31
31
|
- !ruby/object:Gem::Version
|
32
|
-
version: 1.
|
32
|
+
version: 1.116.0
|
33
33
|
- !ruby/object:Gem::Dependency
|
34
34
|
name: dry-cli
|
35
35
|
requirement: !ruby/object:Gem::Requirement
|
@@ -121,7 +121,7 @@ dependencies:
|
|
121
121
|
version: '4.21'
|
122
122
|
- - "<"
|
123
123
|
- !ruby/object:Gem::Version
|
124
|
-
version: '
|
124
|
+
version: '7.0'
|
125
125
|
type: :runtime
|
126
126
|
prerelease: false
|
127
127
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -131,7 +131,7 @@ dependencies:
|
|
131
131
|
version: '4.21'
|
132
132
|
- - "<"
|
133
133
|
- !ruby/object:Gem::Version
|
134
|
-
version: '
|
134
|
+
version: '7.0'
|
135
135
|
- !ruby/object:Gem::Dependency
|
136
136
|
name: parallel
|
137
137
|
requirement: !ruby/object:Gem::Requirement
|
@@ -202,6 +202,20 @@ dependencies:
|
|
202
202
|
- - "~>"
|
203
203
|
- !ruby/object:Gem::Version
|
204
204
|
version: 0.9.3
|
205
|
+
- !ruby/object:Gem::Dependency
|
206
|
+
name: activesupport
|
207
|
+
requirement: !ruby/object:Gem::Requirement
|
208
|
+
requirements:
|
209
|
+
- - "~>"
|
210
|
+
- !ruby/object:Gem::Version
|
211
|
+
version: '7.0'
|
212
|
+
type: :development
|
213
|
+
prerelease: false
|
214
|
+
version_requirements: !ruby/object:Gem::Requirement
|
215
|
+
requirements:
|
216
|
+
- - "~>"
|
217
|
+
- !ruby/object:Gem::Version
|
218
|
+
version: '7.0'
|
205
219
|
- !ruby/object:Gem::Dependency
|
206
220
|
name: allure-rspec
|
207
221
|
requirement: !ruby/object:Gem::Requirement
|
@@ -231,19 +245,25 @@ dependencies:
|
|
231
245
|
- !ruby/object:Gem::Version
|
232
246
|
version: 1.2.0
|
233
247
|
- !ruby/object:Gem::Dependency
|
234
|
-
name:
|
248
|
+
name: debug
|
235
249
|
requirement: !ruby/object:Gem::Requirement
|
236
250
|
requirements:
|
237
251
|
- - "~>"
|
238
252
|
- !ruby/object:Gem::Version
|
239
|
-
version: '
|
253
|
+
version: '1.0'
|
254
|
+
- - ">="
|
255
|
+
- !ruby/object:Gem::Version
|
256
|
+
version: 1.0.0
|
240
257
|
type: :development
|
241
258
|
prerelease: false
|
242
259
|
version_requirements: !ruby/object:Gem::Requirement
|
243
260
|
requirements:
|
244
261
|
- - "~>"
|
245
262
|
- !ruby/object:Gem::Version
|
246
|
-
version: '
|
263
|
+
version: '1.0'
|
264
|
+
- - ">="
|
265
|
+
- !ruby/object:Gem::Version
|
266
|
+
version: 1.0.0
|
247
267
|
- !ruby/object:Gem::Dependency
|
248
268
|
name: rake
|
249
269
|
requirement: !ruby/object:Gem::Requirement
|
@@ -362,14 +382,14 @@ dependencies:
|
|
362
382
|
requirements:
|
363
383
|
- - "~>"
|
364
384
|
- !ruby/object:Gem::Version
|
365
|
-
version: 0.
|
385
|
+
version: 0.47.0
|
366
386
|
type: :development
|
367
387
|
prerelease: false
|
368
388
|
version_requirements: !ruby/object:Gem::Requirement
|
369
389
|
requirements:
|
370
390
|
- - "~>"
|
371
391
|
- !ruby/object:Gem::Version
|
372
|
-
version: 0.
|
392
|
+
version: 0.47.0
|
373
393
|
description: Upload allure reports to different file storage providers
|
374
394
|
email:
|
375
395
|
- andrejs.cunskis@gmail.com
|
@@ -383,6 +403,7 @@ files:
|
|
383
403
|
- lib/allure_report_publisher.rb
|
384
404
|
- lib/allure_report_publisher/commands/upload.rb
|
385
405
|
- lib/allure_report_publisher/commands/version.rb
|
406
|
+
- lib/allure_report_publisher/lib/helpers/gsutil.rb
|
386
407
|
- lib/allure_report_publisher/lib/helpers/helpers.rb
|
387
408
|
- lib/allure_report_publisher/lib/helpers/spinner.rb
|
388
409
|
- lib/allure_report_publisher/lib/helpers/summary.rb
|