gitlab_quality-test_tooling 1.9.0 → 1.10.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/Gemfile.lock +1 -1
- data/exe/post-to-slack +2 -2
- data/lib/gitlab_quality/test_tooling/gitlab_client/gitlab_client.rb +91 -0
- data/lib/gitlab_quality/test_tooling/gitlab_client/issues_client.rb +153 -0
- data/lib/gitlab_quality/test_tooling/gitlab_client/issues_dry_client.rb +36 -0
- data/lib/gitlab_quality/test_tooling/gitlab_client/jobs_client.rb +15 -0
- data/lib/gitlab_quality/test_tooling/gitlab_client/merge_requests_client.rb +85 -0
- data/lib/gitlab_quality/test_tooling/gitlab_client/merge_requests_dry_client.rb +36 -0
- data/lib/gitlab_quality/test_tooling/report/merge_request_slow_tests_report.rb +6 -7
- data/lib/gitlab_quality/test_tooling/report/relate_failure_issue.rb +1 -1
- data/lib/gitlab_quality/test_tooling/report/report_as_issue.rb +1 -1
- data/lib/gitlab_quality/test_tooling/version.rb +1 -1
- metadata +8 -7
- data/lib/gitlab_quality/test_tooling/gitlab_client/failed_jobs.rb +0 -32
- data/lib/gitlab_quality/test_tooling/gitlab_client/merge_request.rb +0 -52
- data/lib/gitlab_quality/test_tooling/gitlab_client/merge_request_dry_client.rb +0 -31
- data/lib/gitlab_quality/test_tooling/gitlab_issue_client.rb +0 -226
- data/lib/gitlab_quality/test_tooling/gitlab_issue_dry_client.rb +0 -34
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6aa86323a85f779b89a3d26bd9e2b78e2166d4aaf1fdd18c5b3c26e67edbed1f
|
4
|
+
data.tar.gz: 9096461ccb4f8527f0347c4987e405d0ef39ff45aa890468968b773372f854a0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3c6e475336007851d1fd92e18d3ec622ef1ea505b5a603b344eef1a36139a6be1cafc67c12758ce37b045eacb12812fe52aad58a80829336502b6edff48f17ca
|
7
|
+
data.tar.gz: 4047f4eb355bb0247366d838367d2cf9628b3fe35b43ffe75760fc7578cc3bbeaa8ccb77ddc7c84184dbcfe2dbf888c5c679e9a600560d229193a3da055203cf
|
data/Gemfile.lock
CHANGED
data/exe/post-to-slack
CHANGED
@@ -40,7 +40,7 @@ options = OptionParser.new do |opts|
|
|
40
40
|
project_id = ENV['CI_PROJECT_ID']&.to_i || (next puts("CI_PROJECT_ID not set, skipping failed jobs table"))
|
41
41
|
pipeline_id = ENV['CI_PIPELINE_ID']&.to_i || (next puts("CI_PIPELINE_ID not set, skipping failed jobs table"))
|
42
42
|
|
43
|
-
jobs = GitlabQuality::TestTooling::GitlabClient::
|
43
|
+
jobs = GitlabQuality::TestTooling::GitlabClient::JobsClient.new(token: gitlab_api_token, project: project_id).pipeline_jobs(pipeline_id: pipeline_id, scope: 'failed')
|
44
44
|
next if jobs.empty?
|
45
45
|
|
46
46
|
messages << GitlabQuality::TestTooling::FailedJobsTable.create(jobs: jobs)
|
@@ -71,7 +71,7 @@ options = OptionParser.new do |opts|
|
|
71
71
|
opts.parse(ARGV)
|
72
72
|
end
|
73
73
|
|
74
|
-
params[:message] = messages.join("\n
|
74
|
+
params[:message] = messages.join("\n")
|
75
75
|
|
76
76
|
if params.any?
|
77
77
|
GitlabQuality::TestTooling::Slack::PostToSlack.new(**params).invoke!
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'gitlab'
|
4
|
+
|
5
|
+
module GitlabQuality
|
6
|
+
module TestTooling
|
7
|
+
module GitlabClient
|
8
|
+
class GitlabClient
|
9
|
+
RETRY_BACK_OFF_DELAY = 60
|
10
|
+
MAX_RETRY_ATTEMPTS = 3
|
11
|
+
|
12
|
+
def initialize(token:, project:, **_kwargs)
|
13
|
+
@token = token
|
14
|
+
@project = project
|
15
|
+
@retry_backoff = 0
|
16
|
+
end
|
17
|
+
|
18
|
+
def handle_gitlab_client_exceptions # rubocop:disable Metrics/AbcSize
|
19
|
+
yield
|
20
|
+
rescue Gitlab::Error::NotFound
|
21
|
+
# This error could be raised in assert_user_permission!
|
22
|
+
# If so, we want it to terminate at that point
|
23
|
+
raise
|
24
|
+
rescue SystemCallError, OpenSSL::SSL::SSLError, Net::OpenTimeout, Net::ReadTimeout,
|
25
|
+
Gitlab::Error::InternalServerError, Gitlab::Error::Parsing => e
|
26
|
+
@retry_backoff += RETRY_BACK_OFF_DELAY
|
27
|
+
|
28
|
+
raise if @retry_backoff > RETRY_BACK_OFF_DELAY * MAX_RETRY_ATTEMPTS
|
29
|
+
|
30
|
+
warn_exception(e)
|
31
|
+
warn("Sleeping for #{@retry_backoff} seconds before retrying...")
|
32
|
+
sleep @retry_backoff
|
33
|
+
|
34
|
+
retry
|
35
|
+
rescue StandardError => e
|
36
|
+
pipeline = Runtime::Env.pipeline_from_project_name
|
37
|
+
channel = case pipeline
|
38
|
+
when "canary"
|
39
|
+
"qa-production"
|
40
|
+
when "staging-canary"
|
41
|
+
"qa-staging"
|
42
|
+
else
|
43
|
+
"qa-#{pipeline}"
|
44
|
+
end
|
45
|
+
error_msg = warn_exception(e)
|
46
|
+
|
47
|
+
return unless Runtime::Env.ci_commit_ref_name == Runtime::Env.default_branch
|
48
|
+
|
49
|
+
slack_options = {
|
50
|
+
slack_webhook_url: ENV.fetch('CI_SLACK_WEBHOOK_URL', nil),
|
51
|
+
channel: channel,
|
52
|
+
username: "GitLab Quality Test Tooling",
|
53
|
+
icon_emoji: ':ci_failing:',
|
54
|
+
message: <<~MSG
|
55
|
+
An unexpected error occurred while reporting test results in issues.
|
56
|
+
The error occurred in job: #{Runtime::Env.ci_job_url}
|
57
|
+
`#{error_msg}`
|
58
|
+
MSG
|
59
|
+
}
|
60
|
+
puts "Posting Slack message to channel: #{channel}"
|
61
|
+
|
62
|
+
GitlabQuality::TestTooling::Slack::PostToSlack.new(**slack_options).invoke!
|
63
|
+
end
|
64
|
+
|
65
|
+
def ignore_gitlab_client_exceptions
|
66
|
+
yield
|
67
|
+
rescue StandardError, SystemCallError, OpenSSL::SSL::SSLError, Net::OpenTimeout, Net::ReadTimeout,
|
68
|
+
Gitlab::Error::Error => e
|
69
|
+
puts "Ignoring the following error: #{e}"
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
attr_reader :project, :token
|
75
|
+
|
76
|
+
def client
|
77
|
+
@client ||= Gitlab.client(
|
78
|
+
endpoint: Runtime::Env.gitlab_api_base,
|
79
|
+
private_token: token
|
80
|
+
)
|
81
|
+
end
|
82
|
+
|
83
|
+
def warn_exception(error)
|
84
|
+
error_msg = "#{error.class.name} #{error.message}"
|
85
|
+
warn(error_msg)
|
86
|
+
error_msg
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'gitlab'
|
4
|
+
|
5
|
+
module Gitlab
|
6
|
+
# Monkey patch the Gitlab client to use the correct API path and add required methods
|
7
|
+
class Client
|
8
|
+
def team_member(project, id)
|
9
|
+
get("/projects/#{url_encode(project)}/members/all/#{id}")
|
10
|
+
end
|
11
|
+
|
12
|
+
def issue_discussions(project, issue_id, options = {})
|
13
|
+
get("/projects/#{url_encode(project)}/issues/#{issue_id}/discussions", query: options)
|
14
|
+
end
|
15
|
+
|
16
|
+
def add_note_to_issue_discussion_as_thread(project, issue_id, discussion_id, options = {})
|
17
|
+
post("/projects/#{url_encode(project)}/issues/#{issue_id}/discussions/#{discussion_id}/notes", query: options)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
module GitlabQuality
|
23
|
+
module TestTooling
|
24
|
+
module GitlabClient
|
25
|
+
# The GitLab client is used for API access: https://github.com/NARKOZ/gitlab
|
26
|
+
class IssuesClient < GitlabClient
|
27
|
+
REPORTER_ACCESS_LEVEL = 20
|
28
|
+
|
29
|
+
def assert_user_permission!
|
30
|
+
handle_gitlab_client_exceptions do
|
31
|
+
member = client.team_member(project, user.id)
|
32
|
+
|
33
|
+
abort_not_permitted(member.access_level) if member.access_level < REPORTER_ACCESS_LEVEL
|
34
|
+
end
|
35
|
+
rescue Gitlab::Error::NotFound
|
36
|
+
abort_member_not_found(user)
|
37
|
+
end
|
38
|
+
|
39
|
+
def find_issues(iid: nil, options: {}, &select)
|
40
|
+
select ||= :itself
|
41
|
+
|
42
|
+
handle_gitlab_client_exceptions do
|
43
|
+
break [client.issue(project, iid)].select(&select) if iid
|
44
|
+
|
45
|
+
client.issues(project, options)
|
46
|
+
.auto_paginate
|
47
|
+
.select(&select)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def find_issues_by_hash(test_hash, &select)
|
52
|
+
select ||= :itself
|
53
|
+
|
54
|
+
handle_gitlab_client_exceptions do
|
55
|
+
client.search_in_project(project, 'issues', test_hash)
|
56
|
+
.auto_paginate
|
57
|
+
.select(&select)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def find_issue_discussions(iid:)
|
62
|
+
handle_gitlab_client_exceptions do
|
63
|
+
client.issue_discussions(project, iid, order_by: 'created_at', sort: 'asc').auto_paginate
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def create_issue(title:, description:, labels:, issue_type: 'issue', assignee_id: nil, due_date: nil, confidential: false)
|
68
|
+
attrs = {
|
69
|
+
issue_type: issue_type,
|
70
|
+
description: description,
|
71
|
+
labels: labels,
|
72
|
+
assignee_id: assignee_id,
|
73
|
+
due_date: due_date,
|
74
|
+
confidential: confidential
|
75
|
+
}.compact
|
76
|
+
|
77
|
+
handle_gitlab_client_exceptions do
|
78
|
+
client.create_issue(project, title, attrs)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def edit_issue(iid:, options: {})
|
83
|
+
handle_gitlab_client_exceptions do
|
84
|
+
client.edit_issue(project, iid, options)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def find_issue_notes(iid:)
|
89
|
+
handle_gitlab_client_exceptions do
|
90
|
+
client.issue_notes(project, iid, order_by: 'created_at', sort: 'asc')&.auto_paginate
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def create_issue_note(iid:, note:)
|
95
|
+
handle_gitlab_client_exceptions do
|
96
|
+
client.create_issue_note(project, iid, note)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def edit_issue_note(issue_iid:, note_id:, note:)
|
101
|
+
handle_gitlab_client_exceptions do
|
102
|
+
client.edit_issue_note(project, issue_iid, note_id, note)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def add_note_to_issue_discussion_as_thread(iid:, discussion_id:, body:)
|
107
|
+
handle_gitlab_client_exceptions do
|
108
|
+
client.add_note_to_issue_discussion_as_thread(project, iid, discussion_id, body: body)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def find_user_id(username:)
|
113
|
+
handle_gitlab_client_exceptions do
|
114
|
+
user = client.users(username: username)&.first
|
115
|
+
user['id'] unless user.nil?
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def upload_file(file_fullpath:)
|
120
|
+
ignore_gitlab_client_exceptions do
|
121
|
+
client.upload_file(project, file_fullpath)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
private
|
126
|
+
|
127
|
+
attr_reader :token, :project
|
128
|
+
|
129
|
+
def user
|
130
|
+
return @user if defined?(@user)
|
131
|
+
|
132
|
+
@user ||= begin
|
133
|
+
client.user
|
134
|
+
rescue Gitlab::Error::NotFound
|
135
|
+
abort_user_not_found
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def abort_not_permitted(access_level)
|
140
|
+
abort "#{user.username} must have at least Reporter access to the project '#{project}' to use this feature. Current access level: #{access_level}"
|
141
|
+
end
|
142
|
+
|
143
|
+
def abort_user_not_found
|
144
|
+
abort "User not found for given token."
|
145
|
+
end
|
146
|
+
|
147
|
+
def abort_member_not_found(user)
|
148
|
+
abort "#{user.username} must be a member of the '#{project}' project."
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GitlabQuality
|
4
|
+
module TestTooling
|
5
|
+
module GitlabClient
|
6
|
+
class IssuesDryClient < IssuesClient
|
7
|
+
def create_issue(title:, description:, labels:, issue_type: 'issue', confidential: false)
|
8
|
+
attrs = { description: description, labels: labels, confidential: confidential }
|
9
|
+
|
10
|
+
puts "The following #{issue_type} would have been created:"
|
11
|
+
puts "project: #{project}, title: #{title}, attrs: #{attrs}"
|
12
|
+
end
|
13
|
+
|
14
|
+
def edit_issue(iid:, options: {})
|
15
|
+
puts "The #{project}##{iid} issue would have been updated with: #{options}"
|
16
|
+
end
|
17
|
+
|
18
|
+
def create_issue_note(iid:, note:)
|
19
|
+
puts "The following note would have been posted on #{project}##{iid} issue: #{note}"
|
20
|
+
end
|
21
|
+
|
22
|
+
def edit_issue_note(issue_iid:, note_id:, note:)
|
23
|
+
puts "The following note would have been edited on #{project}##{issue_iid} (note #{note_id}) issue: #{note}"
|
24
|
+
end
|
25
|
+
|
26
|
+
def add_note_to_issue_discussion_as_thread(iid:, discussion_id:, body:)
|
27
|
+
puts "The following discussion note would have been posted on #{project}##{iid} (discussion #{discussion_id}) issue: #{body}"
|
28
|
+
end
|
29
|
+
|
30
|
+
def upload_file(file_fullpath:)
|
31
|
+
puts "The following file would have been uploaded: #{file_fullpath}"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'gitlab'
|
4
|
+
|
5
|
+
module GitlabQuality
|
6
|
+
module TestTooling
|
7
|
+
module GitlabClient
|
8
|
+
class JobsClient < GitlabClient
|
9
|
+
def pipeline_jobs(pipeline_id:, scope:)
|
10
|
+
client.pipeline_jobs(project, pipeline_id, scope: scope)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'gitlab'
|
4
|
+
|
5
|
+
module GitlabQuality
|
6
|
+
module TestTooling
|
7
|
+
module GitlabClient
|
8
|
+
class MergeRequestsClient < GitlabClient
|
9
|
+
def find_merge_request_changes(merge_request_iid:)
|
10
|
+
client.merge_request_changes(project, merge_request_iid)
|
11
|
+
end
|
12
|
+
|
13
|
+
def create_merge_request(title:, source_branch:, target_branch:, description:, labels:)
|
14
|
+
merge_request = handle_gitlab_client_exceptions do
|
15
|
+
client.create_merge_request(project,
|
16
|
+
title,
|
17
|
+
source_branch: source_branch,
|
18
|
+
target_branch: target_branch,
|
19
|
+
description: description,
|
20
|
+
labels: labels,
|
21
|
+
squash: true,
|
22
|
+
remove_source_branch: true)
|
23
|
+
end
|
24
|
+
|
25
|
+
Runtime::Logger.debug("Created merge request #{merge_request['iid']} (#{merge_request['web_url']})") if merge_request
|
26
|
+
end
|
27
|
+
|
28
|
+
def find(iid: nil, options: {}, &select)
|
29
|
+
select ||= :itself
|
30
|
+
|
31
|
+
if iid
|
32
|
+
find_merge_request(iid, &select)
|
33
|
+
else
|
34
|
+
find_merge_requests(options, &select)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def merge_request_changed_files(merge_request_iid:)
|
39
|
+
find_merge_request_changes(merge_request_iid: merge_request_iid)["changes"].map do |change|
|
40
|
+
change["new_path"]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def find_note(body:, merge_request_iid:)
|
45
|
+
client.merge_request_notes(project, merge_request_iid, per_page: 100).auto_paginate.find do |mr_note|
|
46
|
+
mr_note['body'] =~ /#{body}/
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def create_note(note:, merge_request_iid:)
|
51
|
+
client.create_merge_request_note(project, merge_request_iid, note)
|
52
|
+
end
|
53
|
+
|
54
|
+
def update_note(id:, note:, merge_request_iid:)
|
55
|
+
client.edit_merge_request_note(project, merge_request_iid, id, note)
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
attr_reader :project, :token, :merge_request_iid
|
61
|
+
|
62
|
+
def find_merge_request(iid, &select)
|
63
|
+
handle_gitlab_client_exceptions do
|
64
|
+
[client.merge_requests(project, iid)].select(&select)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def find_merge_requests(options, &select)
|
69
|
+
handle_gitlab_client_exceptions do
|
70
|
+
client.merge_requests(project, options)
|
71
|
+
.auto_paginate
|
72
|
+
.select(&select)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def client
|
77
|
+
@client ||= Gitlab.client(
|
78
|
+
endpoint: Runtime::Env.gitlab_api_base,
|
79
|
+
private_token: token
|
80
|
+
)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GitlabQuality
|
4
|
+
module TestTooling
|
5
|
+
module GitlabClient
|
6
|
+
class MergeRequestsDryClient < MergeRequestsClient
|
7
|
+
def find_merge_request_changes(merge_request_iid:)
|
8
|
+
puts "Finding changes for merge_request_id #{merge_request_iid}"
|
9
|
+
puts "project: #{project}"
|
10
|
+
end
|
11
|
+
|
12
|
+
def merge_request_changed_files(merge_request_iid:)
|
13
|
+
puts "Changed files for #{merge_request_iid}"
|
14
|
+
[]
|
15
|
+
end
|
16
|
+
|
17
|
+
def find_note(body:, merge_request_iid:)
|
18
|
+
puts "Find note for #{merge_request_iid} with body: #{body} for mr_iid: #{merge_request_iid}"
|
19
|
+
end
|
20
|
+
|
21
|
+
def create_note(note:, merge_request_iid:)
|
22
|
+
puts "The following note would have been created with body: #{note} for mr_iid: #{merge_request_iid}"
|
23
|
+
end
|
24
|
+
|
25
|
+
def update_note(id:, note:, merge_request_iid:)
|
26
|
+
puts "The following note would have been updated id: #{id} with body: #{note} for mr_iid: #{merge_request_iid}"
|
27
|
+
end
|
28
|
+
|
29
|
+
def create_merge_request(title:, source_branch:, target_branch:, description:, labels:)
|
30
|
+
puts "A merge request would be created with title: #{title} " \
|
31
|
+
"source_branch: #{source_branch} target_branch: #{target_branch} description: #{description} labels: #{labels}"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -10,8 +10,7 @@ module GitlabQuality
|
|
10
10
|
|
11
11
|
def initialize(token:, input_files:, merge_request_iid:, project: nil, dry_run: false, **_kwargs)
|
12
12
|
@project = project
|
13
|
-
@gitlab_merge_request = (dry_run ? GitlabClient::
|
14
|
-
merge_request_iid: merge_request_iid)
|
13
|
+
@gitlab_merge_request = (dry_run ? GitlabClient::MergeRequestsDryClient : GitlabClient::MergeRequestsClient).new(token: token, project: project)
|
15
14
|
@files = Array(input_files)
|
16
15
|
@merge_request_iid = merge_request_iid
|
17
16
|
@slow_tests = []
|
@@ -57,7 +56,7 @@ module GitlabQuality
|
|
57
56
|
end
|
58
57
|
|
59
58
|
def merge_request_changed_files
|
60
|
-
@merge_request_changed_files ||= gitlab_merge_request.merge_request_changed_files
|
59
|
+
@merge_request_changed_files ||= gitlab_merge_request.merge_request_changed_files(merge_request_iid: merge_request_iid)
|
61
60
|
end
|
62
61
|
|
63
62
|
def find_slow_tests(test_results)
|
@@ -119,21 +118,21 @@ module GitlabQuality
|
|
119
118
|
"#{gitlab_note}\n\n</details>"
|
120
119
|
end
|
121
120
|
|
122
|
-
def upsert_mr_note(slow_tests)
|
123
|
-
existing_note = gitlab_merge_request.find_note(body: SLOW_TEST_MESSAGE)
|
121
|
+
def upsert_mr_note(slow_tests) # rubocop:disable Metrics/AbcSize
|
122
|
+
existing_note = gitlab_merge_request.find_note(body: SLOW_TEST_MESSAGE, merge_request_iid: merge_request_iid)
|
124
123
|
|
125
124
|
if existing_note
|
126
125
|
puts "Update note for merge request: #{merge_request_iid}"
|
127
126
|
|
128
127
|
up_to_date_note = add_slow_test_rows(existing_note.body, slow_tests)
|
129
128
|
|
130
|
-
gitlab_merge_request.update_note(id: existing_note['id'], note: up_to_date_note) if existing_note.body != up_to_date_note
|
129
|
+
gitlab_merge_request.update_note(id: existing_note['id'], note: up_to_date_note, merge_request_iid: merge_request_iid) if existing_note.body != up_to_date_note
|
131
130
|
else
|
132
131
|
up_to_date_note = build_note(slow_tests)
|
133
132
|
|
134
133
|
puts "Create note for merge request: #{merge_request_iid}"
|
135
134
|
|
136
|
-
gitlab_merge_request.create_note(note: up_to_date_note)
|
135
|
+
gitlab_merge_request.create_note(note: up_to_date_note, merge_request_iid: merge_request_iid)
|
137
136
|
end
|
138
137
|
end
|
139
138
|
|
@@ -25,7 +25,7 @@ module GitlabQuality
|
|
25
25
|
SYSTEMIC_EXCEPTIONS_THRESHOLD = 10
|
26
26
|
SPAM_THRESHOLD_FOR_FAILURE_ISSUES = 3
|
27
27
|
FAILURE_STACKTRACE_REGEX = %r{(?:(?:.*Failure/Error:(?<stacktrace>.+))|(?<stacktrace>.+))}m
|
28
|
-
ISSUE_STACKTRACE_REGEX = /### Stack trace\s*(```)#{FAILURE_STACKTRACE_REGEX}(```)
|
28
|
+
ISSUE_STACKTRACE_REGEX = /### Stack trace\s*(```)#{FAILURE_STACKTRACE_REGEX}(```)\n*\n###/m
|
29
29
|
|
30
30
|
NEW_ISSUE_LABELS = Set.new(%w[test failure::new priority::2]).freeze
|
31
31
|
IGNORED_FAILURES = [
|
@@ -10,7 +10,7 @@ module GitlabQuality
|
|
10
10
|
|
11
11
|
def initialize(token:, input_files:, related_issues_file: nil, project: nil, confidential: false, dry_run: false, **_kwargs)
|
12
12
|
@project = project
|
13
|
-
@gitlab = (dry_run ?
|
13
|
+
@gitlab = (dry_run ? GitlabClient::IssuesDryClient : GitlabClient::IssuesClient).new(token: token, project: project)
|
14
14
|
@files = Array(input_files)
|
15
15
|
@confidential = confidential
|
16
16
|
@issue_logger = IssueLogger.new(file_path: related_issues_file) unless related_issues_file.nil?
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: gitlab_quality-test_tooling
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.10.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- GitLab Quality
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-01-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: climate_control
|
@@ -384,11 +384,12 @@ files:
|
|
384
384
|
- lefthook.yml
|
385
385
|
- lib/gitlab_quality/test_tooling.rb
|
386
386
|
- lib/gitlab_quality/test_tooling/failed_jobs_table.rb
|
387
|
-
- lib/gitlab_quality/test_tooling/gitlab_client/
|
388
|
-
- lib/gitlab_quality/test_tooling/gitlab_client/
|
389
|
-
- lib/gitlab_quality/test_tooling/gitlab_client/
|
390
|
-
- lib/gitlab_quality/test_tooling/
|
391
|
-
- lib/gitlab_quality/test_tooling/
|
387
|
+
- lib/gitlab_quality/test_tooling/gitlab_client/gitlab_client.rb
|
388
|
+
- lib/gitlab_quality/test_tooling/gitlab_client/issues_client.rb
|
389
|
+
- lib/gitlab_quality/test_tooling/gitlab_client/issues_dry_client.rb
|
390
|
+
- lib/gitlab_quality/test_tooling/gitlab_client/jobs_client.rb
|
391
|
+
- lib/gitlab_quality/test_tooling/gitlab_client/merge_requests_client.rb
|
392
|
+
- lib/gitlab_quality/test_tooling/gitlab_client/merge_requests_dry_client.rb
|
392
393
|
- lib/gitlab_quality/test_tooling/labels_inference.rb
|
393
394
|
- lib/gitlab_quality/test_tooling/report/concerns/find_set_dri.rb
|
394
395
|
- lib/gitlab_quality/test_tooling/report/concerns/group_and_category_labels.rb
|
@@ -1,32 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'gitlab'
|
4
|
-
|
5
|
-
module GitlabQuality
|
6
|
-
module TestTooling
|
7
|
-
module GitlabClient
|
8
|
-
class FailedJobs
|
9
|
-
def initialize(token:, project_id:, pipeline_id:)
|
10
|
-
@token = token
|
11
|
-
@project_id = project_id
|
12
|
-
@pipeline_id = pipeline_id
|
13
|
-
end
|
14
|
-
|
15
|
-
def fetch
|
16
|
-
client.pipeline_jobs(project_id, pipeline_id, scope: 'failed')
|
17
|
-
end
|
18
|
-
|
19
|
-
private
|
20
|
-
|
21
|
-
attr_reader :token, :project_id, :pipeline_id
|
22
|
-
|
23
|
-
def client
|
24
|
-
@client ||= Gitlab.client(
|
25
|
-
endpoint: Runtime::Env.gitlab_api_base,
|
26
|
-
private_token: token
|
27
|
-
)
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|
@@ -1,52 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'gitlab'
|
4
|
-
|
5
|
-
module GitlabQuality
|
6
|
-
module TestTooling
|
7
|
-
module GitlabClient
|
8
|
-
class MergeRequest
|
9
|
-
def initialize(token:, project:, merge_request_iid:)
|
10
|
-
@token = token
|
11
|
-
@project = project
|
12
|
-
@merge_request_iid = merge_request_iid
|
13
|
-
end
|
14
|
-
|
15
|
-
def find_merge_request
|
16
|
-
client.merge_request_changes(project, merge_request_iid)
|
17
|
-
end
|
18
|
-
|
19
|
-
def merge_request_changed_files
|
20
|
-
find_merge_request["changes"].map do |change|
|
21
|
-
change["new_path"]
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
|
-
def find_note(body:)
|
26
|
-
client.merge_request_notes(project, merge_request_iid, per_page: 100).auto_paginate.find do |mr_note|
|
27
|
-
mr_note['body'] =~ /#{body}/
|
28
|
-
end
|
29
|
-
end
|
30
|
-
|
31
|
-
def create_note(note:)
|
32
|
-
client.create_merge_request_note(project, merge_request_iid, note)
|
33
|
-
end
|
34
|
-
|
35
|
-
def update_note(id:, note:)
|
36
|
-
client.edit_merge_request_note(project, merge_request_iid, id, note)
|
37
|
-
end
|
38
|
-
|
39
|
-
private
|
40
|
-
|
41
|
-
attr_reader :project, :token, :merge_request_iid
|
42
|
-
|
43
|
-
def client
|
44
|
-
@client ||= Gitlab.client(
|
45
|
-
endpoint: Runtime::Env.gitlab_api_base,
|
46
|
-
private_token: token
|
47
|
-
)
|
48
|
-
end
|
49
|
-
end
|
50
|
-
end
|
51
|
-
end
|
52
|
-
end
|
@@ -1,31 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module GitlabQuality
|
4
|
-
module TestTooling
|
5
|
-
module GitlabClient
|
6
|
-
class MergeRequestDryClient < MergeRequest
|
7
|
-
def find_merge_request
|
8
|
-
puts "Finding merge_request_id #{merge_request_iid}"
|
9
|
-
puts "project: #{project}"
|
10
|
-
end
|
11
|
-
|
12
|
-
def merge_request_changed_files
|
13
|
-
puts "Changed files for #{merge_request_iid}"
|
14
|
-
[]
|
15
|
-
end
|
16
|
-
|
17
|
-
def find_note(body:)
|
18
|
-
puts "Find note for #{merge_request_iid} with body: #{body}"
|
19
|
-
end
|
20
|
-
|
21
|
-
def create_note(note:)
|
22
|
-
puts "The following note would have been created with body: #{note}"
|
23
|
-
end
|
24
|
-
|
25
|
-
def update_note(id:, note:)
|
26
|
-
puts "The following note would have been update id: #{id} with body: #{note}"
|
27
|
-
end
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
@@ -1,226 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'gitlab'
|
4
|
-
|
5
|
-
module Gitlab
|
6
|
-
# Monkey patch the Gitlab client to use the correct API path and add required methods
|
7
|
-
class Client
|
8
|
-
def team_member(project, id)
|
9
|
-
get("/projects/#{url_encode(project)}/members/all/#{id}")
|
10
|
-
end
|
11
|
-
|
12
|
-
def issue_discussions(project, issue_id, options = {})
|
13
|
-
get("/projects/#{url_encode(project)}/issues/#{issue_id}/discussions", query: options)
|
14
|
-
end
|
15
|
-
|
16
|
-
def add_note_to_issue_discussion_as_thread(project, issue_id, discussion_id, options = {})
|
17
|
-
post("/projects/#{url_encode(project)}/issues/#{issue_id}/discussions/#{discussion_id}/notes", query: options)
|
18
|
-
end
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
module GitlabQuality
|
23
|
-
module TestTooling
|
24
|
-
# The GitLab client is used for API access: https://github.com/NARKOZ/gitlab
|
25
|
-
class GitlabIssueClient
|
26
|
-
REPORTER_ACCESS_LEVEL = 20
|
27
|
-
RETRY_BACK_OFF_DELAY = 60
|
28
|
-
MAX_RETRY_ATTEMPTS = 3
|
29
|
-
|
30
|
-
def initialize(token:, project:)
|
31
|
-
@token = token
|
32
|
-
@project = project
|
33
|
-
@retry_backoff = 0
|
34
|
-
end
|
35
|
-
|
36
|
-
def assert_user_permission!
|
37
|
-
handle_gitlab_client_exceptions do
|
38
|
-
member = client.team_member(project, user.id)
|
39
|
-
|
40
|
-
abort_not_permitted(member.access_level) if member.access_level < REPORTER_ACCESS_LEVEL
|
41
|
-
end
|
42
|
-
rescue Gitlab::Error::NotFound
|
43
|
-
abort_member_not_found(user)
|
44
|
-
end
|
45
|
-
|
46
|
-
def find_issues(iid: nil, options: {}, &select)
|
47
|
-
select ||= :itself
|
48
|
-
|
49
|
-
handle_gitlab_client_exceptions do
|
50
|
-
break [client.issue(project, iid)].select(&select) if iid
|
51
|
-
|
52
|
-
client.issues(project, options)
|
53
|
-
.auto_paginate
|
54
|
-
.select(&select)
|
55
|
-
end
|
56
|
-
end
|
57
|
-
|
58
|
-
def find_issues_by_hash(test_hash, &select)
|
59
|
-
select ||= :itself
|
60
|
-
|
61
|
-
handle_gitlab_client_exceptions do
|
62
|
-
client.search_in_project(project, 'issues', test_hash)
|
63
|
-
.auto_paginate
|
64
|
-
.select(&select)
|
65
|
-
end
|
66
|
-
end
|
67
|
-
|
68
|
-
def find_issue_discussions(iid:)
|
69
|
-
handle_gitlab_client_exceptions do
|
70
|
-
client.issue_discussions(project, iid, order_by: 'created_at', sort: 'asc').auto_paginate
|
71
|
-
end
|
72
|
-
end
|
73
|
-
|
74
|
-
def create_issue(title:, description:, labels:, issue_type: 'issue', assignee_id: nil, due_date: nil, confidential: false)
|
75
|
-
attrs = {
|
76
|
-
issue_type: issue_type,
|
77
|
-
description: description,
|
78
|
-
labels: labels,
|
79
|
-
assignee_id: assignee_id,
|
80
|
-
due_date: due_date,
|
81
|
-
confidential: confidential
|
82
|
-
}.compact
|
83
|
-
|
84
|
-
handle_gitlab_client_exceptions do
|
85
|
-
client.create_issue(project, title, attrs)
|
86
|
-
end
|
87
|
-
end
|
88
|
-
|
89
|
-
def edit_issue(iid:, options: {})
|
90
|
-
handle_gitlab_client_exceptions do
|
91
|
-
client.edit_issue(project, iid, options)
|
92
|
-
end
|
93
|
-
end
|
94
|
-
|
95
|
-
def find_issue_notes(iid:)
|
96
|
-
handle_gitlab_client_exceptions do
|
97
|
-
client.issue_notes(project, iid, order_by: 'created_at', sort: 'asc')&.auto_paginate
|
98
|
-
end
|
99
|
-
end
|
100
|
-
|
101
|
-
def create_issue_note(iid:, note:)
|
102
|
-
handle_gitlab_client_exceptions do
|
103
|
-
client.create_issue_note(project, iid, note)
|
104
|
-
end
|
105
|
-
end
|
106
|
-
|
107
|
-
def edit_issue_note(issue_iid:, note_id:, note:)
|
108
|
-
handle_gitlab_client_exceptions do
|
109
|
-
client.edit_issue_note(project, issue_iid, note_id, note)
|
110
|
-
end
|
111
|
-
end
|
112
|
-
|
113
|
-
def add_note_to_issue_discussion_as_thread(iid:, discussion_id:, body:)
|
114
|
-
handle_gitlab_client_exceptions do
|
115
|
-
client.add_note_to_issue_discussion_as_thread(project, iid, discussion_id, body: body)
|
116
|
-
end
|
117
|
-
end
|
118
|
-
|
119
|
-
def find_user_id(username:)
|
120
|
-
handle_gitlab_client_exceptions do
|
121
|
-
user = client.users(username: username)&.first
|
122
|
-
user['id'] unless user.nil?
|
123
|
-
end
|
124
|
-
end
|
125
|
-
|
126
|
-
def upload_file(file_fullpath:)
|
127
|
-
ignore_gitlab_client_exceptions do
|
128
|
-
client.upload_file(project, file_fullpath)
|
129
|
-
end
|
130
|
-
end
|
131
|
-
|
132
|
-
def ignore_gitlab_client_exceptions
|
133
|
-
yield
|
134
|
-
rescue StandardError, SystemCallError, OpenSSL::SSL::SSLError, Net::OpenTimeout, Net::ReadTimeout,
|
135
|
-
Gitlab::Error::Error => e
|
136
|
-
puts "Ignoring the following error: #{e}"
|
137
|
-
end
|
138
|
-
|
139
|
-
def handle_gitlab_client_exceptions # rubocop:disable Metrics/AbcSize
|
140
|
-
yield
|
141
|
-
rescue Gitlab::Error::NotFound
|
142
|
-
# This error could be raised in assert_user_permission!
|
143
|
-
# If so, we want it to terminate at that point
|
144
|
-
raise
|
145
|
-
rescue SystemCallError, OpenSSL::SSL::SSLError, Net::OpenTimeout, Net::ReadTimeout,
|
146
|
-
Gitlab::Error::InternalServerError, Gitlab::Error::Parsing => e
|
147
|
-
@retry_backoff += RETRY_BACK_OFF_DELAY
|
148
|
-
|
149
|
-
raise if @retry_backoff > RETRY_BACK_OFF_DELAY * MAX_RETRY_ATTEMPTS
|
150
|
-
|
151
|
-
warn_exception(e)
|
152
|
-
warn("Sleeping for #{@retry_backoff} seconds before retrying...")
|
153
|
-
sleep @retry_backoff
|
154
|
-
|
155
|
-
retry
|
156
|
-
rescue StandardError => e
|
157
|
-
pipeline = Runtime::Env.pipeline_from_project_name
|
158
|
-
channel = case pipeline
|
159
|
-
when "canary"
|
160
|
-
"qa-production"
|
161
|
-
when "staging-canary"
|
162
|
-
"qa-staging"
|
163
|
-
else
|
164
|
-
"qa-#{pipeline}"
|
165
|
-
end
|
166
|
-
error_msg = warn_exception(e)
|
167
|
-
|
168
|
-
return unless Runtime::Env.ci_commit_ref_name == Runtime::Env.default_branch
|
169
|
-
|
170
|
-
slack_options = {
|
171
|
-
slack_webhook_url: ENV.fetch('CI_SLACK_WEBHOOK_URL', nil),
|
172
|
-
channel: channel,
|
173
|
-
username: "GitLab Quality Test Tooling",
|
174
|
-
icon_emoji: ':ci_failing:',
|
175
|
-
message: <<~MSG
|
176
|
-
An unexpected error occurred while reporting test results in issues.
|
177
|
-
The error occurred in job: #{Runtime::Env.ci_job_url}
|
178
|
-
`#{error_msg}`
|
179
|
-
MSG
|
180
|
-
}
|
181
|
-
puts "Posting Slack message to channel: #{channel}"
|
182
|
-
|
183
|
-
GitlabQuality::TestTooling::Slack::PostToSlack.new(**slack_options).invoke!
|
184
|
-
end
|
185
|
-
|
186
|
-
private
|
187
|
-
|
188
|
-
attr_reader :token, :project
|
189
|
-
|
190
|
-
def client
|
191
|
-
@client ||= Gitlab.client(
|
192
|
-
endpoint: Runtime::Env.gitlab_api_base,
|
193
|
-
private_token: token
|
194
|
-
)
|
195
|
-
end
|
196
|
-
|
197
|
-
def user
|
198
|
-
return @user if defined?(@user)
|
199
|
-
|
200
|
-
@user ||= begin
|
201
|
-
client.user
|
202
|
-
rescue Gitlab::Error::NotFound
|
203
|
-
abort_user_not_found
|
204
|
-
end
|
205
|
-
end
|
206
|
-
|
207
|
-
def abort_not_permitted(access_level)
|
208
|
-
abort "#{user.username} must have at least Reporter access to the project '#{project}' to use this feature. Current access level: #{access_level}"
|
209
|
-
end
|
210
|
-
|
211
|
-
def abort_user_not_found
|
212
|
-
abort "User not found for given token."
|
213
|
-
end
|
214
|
-
|
215
|
-
def abort_member_not_found(user)
|
216
|
-
abort "#{user.username} must be a member of the '#{project}' project."
|
217
|
-
end
|
218
|
-
|
219
|
-
def warn_exception(error)
|
220
|
-
error_msg = "#{error.class.name} #{error.message}"
|
221
|
-
warn(error_msg)
|
222
|
-
error_msg
|
223
|
-
end
|
224
|
-
end
|
225
|
-
end
|
226
|
-
end
|
@@ -1,34 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module GitlabQuality
|
4
|
-
module TestTooling
|
5
|
-
class GitlabIssueDryClient < GitlabIssueClient
|
6
|
-
def create_issue(title:, description:, labels:, issue_type: 'issue', confidential: false)
|
7
|
-
attrs = { description: description, labels: labels, confidential: confidential }
|
8
|
-
|
9
|
-
puts "The following #{issue_type} would have been created:"
|
10
|
-
puts "project: #{project}, title: #{title}, attrs: #{attrs}"
|
11
|
-
end
|
12
|
-
|
13
|
-
def edit_issue(iid:, options: {})
|
14
|
-
puts "The #{project}##{iid} issue would have been updated with: #{options}"
|
15
|
-
end
|
16
|
-
|
17
|
-
def create_issue_note(iid:, note:)
|
18
|
-
puts "The following note would have been posted on #{project}##{iid} issue: #{note}"
|
19
|
-
end
|
20
|
-
|
21
|
-
def edit_issue_note(issue_iid:, note_id:, note:)
|
22
|
-
puts "The following note would have been edited on #{project}##{issue_iid} (note #{note_id}) issue: #{note}"
|
23
|
-
end
|
24
|
-
|
25
|
-
def add_note_to_issue_discussion_as_thread(iid:, discussion_id:, body:)
|
26
|
-
puts "The following discussion note would have been posted on #{project}##{iid} (discussion #{discussion_id}) issue: #{body}"
|
27
|
-
end
|
28
|
-
|
29
|
-
def upload_file(file_fullpath:)
|
30
|
-
puts "The following file would have been uploaded: #{file_fullpath}"
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|