gitlab_quality-test_tooling 1.8.1 → 1.10.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4c8eda4a61584293cfb2f20a4ece0c5afd8963be75c56c5c3a1f71cbbf04a2ed
4
- data.tar.gz: 72ea08e5a2fac3b5d9f0d5dada3bb71c7b655acbd7c03f4533e0bbe951be954e
3
+ metadata.gz: 6aa86323a85f779b89a3d26bd9e2b78e2166d4aaf1fdd18c5b3c26e67edbed1f
4
+ data.tar.gz: 9096461ccb4f8527f0347c4987e405d0ef39ff45aa890468968b773372f854a0
5
5
  SHA512:
6
- metadata.gz: fe64d9f93ff1343c36d1d478f63e9a4614587897b6414ec3a1f4c28fdd90ee1d245134e9350f546373350acf1f1355dd50d31da162a899d096ceb1114029767e
7
- data.tar.gz: d4dd2ba2714e54c766592734481f7ba373692375f4c2011578f1b22da36fae3b8d58e721677e47ba214805d3798737c5ad6dec7711d586a46727029e555fb72e
6
+ metadata.gz: 3c6e475336007851d1fd92e18d3ec622ef1ea505b5a603b344eef1a36139a6be1cafc67c12758ce37b045eacb12812fe52aad58a80829336502b6edff48f17ca
7
+ data.tar.gz: 4047f4eb355bb0247366d838367d2cf9628b3fe35b43ffe75760fc7578cc3bbeaa8ccb77ddc7c84184dbcfe2dbf888c5c679e9a600560d229193a3da055203cf
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- gitlab_quality-test_tooling (1.8.1)
4
+ gitlab_quality-test_tooling (1.10.0)
5
5
  activesupport (>= 6.1, < 7.2)
6
6
  amatch (~> 0.4.1)
7
7
  gitlab (~> 4.19)
data/exe/post-to-slack CHANGED
@@ -8,6 +8,9 @@ require_relative "../lib/gitlab_quality/test_tooling"
8
8
 
9
9
  params = {}
10
10
 
11
+ messages = []
12
+ gitlab_api_token = nil
13
+
11
14
  options = OptionParser.new do |opts|
12
15
  opts.banner = "Usage: #{$PROGRAM_NAME} [options]"
13
16
 
@@ -15,16 +18,34 @@ options = OptionParser.new do |opts|
15
18
  params[:slack_webhook_url] = slack_webhook_url
16
19
  end
17
20
 
21
+ opts.on('-a', '--gitlab-api-token TOKEN', String, 'GitLab API token') do |token|
22
+ gitlab_api_token = token unless token.empty?
23
+ end
24
+
18
25
  opts.on('-c', '--channel CHANNEL', String, 'Slack channel to post the message to') do |channel|
19
26
  params[:channel] = channel
20
27
  end
21
28
 
22
29
  opts.on('-m', '--message MESSAGE', String, 'Post message to Slack') do |message|
23
- params[:message] = message
30
+ messages << message
24
31
  end
25
32
 
26
33
  opts.on('-t', '--include-summary-table FILES', String, 'Add a test summary table based on RSpec report files (JUnit XML)') do |files|
27
- params[:message] += "\n\n#{GitlabQuality::TestTooling::SummaryTable.create(input_files: files)}"
34
+ messages << GitlabQuality::TestTooling::SummaryTable.create(input_files: files)
35
+ end
36
+
37
+ opts.on('-j', '--include-failed-jobs-table', 'Add a list of failed jobs in the pipeline') do
38
+ next puts("Failed jobs table requires api token to be set via --gitlab-api-token option, skipping failed jobs table") unless gitlab_api_token
39
+
40
+ project_id = ENV['CI_PROJECT_ID']&.to_i || (next puts("CI_PROJECT_ID not set, skipping failed jobs table"))
41
+ pipeline_id = ENV['CI_PIPELINE_ID']&.to_i || (next puts("CI_PIPELINE_ID not set, skipping failed jobs table"))
42
+
43
+ jobs = GitlabQuality::TestTooling::GitlabClient::JobsClient.new(token: gitlab_api_token, project: project_id).pipeline_jobs(pipeline_id: pipeline_id, scope: 'failed')
44
+ next if jobs.empty?
45
+
46
+ messages << GitlabQuality::TestTooling::FailedJobsTable.create(jobs: jobs)
47
+ rescue StandardError => e
48
+ puts "Failed to fetch failed jobs. #{e.class}: #{e.message}"
28
49
  end
29
50
 
30
51
  opts.on('-u', '--username USERNAME', String, 'Username to use for the Slack message') do |username|
@@ -50,6 +71,8 @@ options = OptionParser.new do |opts|
50
71
  opts.parse(ARGV)
51
72
  end
52
73
 
74
+ params[:message] = messages.join("\n")
75
+
53
76
  if params.any?
54
77
  GitlabQuality::TestTooling::Slack::PostToSlack.new(**params).invoke!
55
78
  else
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'table_print'
4
+
5
+ module GitlabQuality
6
+ module TestTooling
7
+ module FailedJobsTable
8
+ class << self
9
+ # Create table with formatted list of failed jobs
10
+ #
11
+ # @param [Array<Gitlab::ObjectifiedHash>] jobs
12
+ # @return [String]
13
+ def create(jobs:)
14
+ "```\n#{TablePrint::Printer.table_print(collect_results(jobs))}\n```\n"
15
+ end
16
+
17
+ private
18
+
19
+ # Format list of failed jobs
20
+ #
21
+ # @param [Array<Gitlab::ObjectifiedHash>] jobs
22
+ # @return [Array]
23
+ def collect_results(jobs)
24
+ jobs.sort_by(&:stage).map do |job|
25
+ {
26
+ "Job" => job.name,
27
+ "Stage" => job.stage,
28
+ "Failure Reason" => job.failure_reason
29
+ }
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -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::MergeRequestDryClient : GitlabClient::MergeRequest).new(token: token, project: project,
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}(```)/m
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 ? GitlabIssueDryClient : GitlabIssueClient).new(token: token, project: project)
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?
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GitlabQuality
4
4
  module TestTooling
5
- VERSION = "1.8.1"
5
+ VERSION = "1.10.0"
6
6
  end
7
7
  end
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.8.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: 2023-12-05 00:00:00.000000000 Z
11
+ date: 2024-01-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: climate_control
@@ -383,10 +383,13 @@ files:
383
383
  - exe/update-screenshot-paths
384
384
  - lefthook.yml
385
385
  - lib/gitlab_quality/test_tooling.rb
386
- - lib/gitlab_quality/test_tooling/gitlab_client/merge_request.rb
387
- - lib/gitlab_quality/test_tooling/gitlab_client/merge_request_dry_client.rb
388
- - lib/gitlab_quality/test_tooling/gitlab_issue_client.rb
389
- - lib/gitlab_quality/test_tooling/gitlab_issue_dry_client.rb
386
+ - lib/gitlab_quality/test_tooling/failed_jobs_table.rb
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
390
393
  - lib/gitlab_quality/test_tooling/labels_inference.rb
391
394
  - lib/gitlab_quality/test_tooling/report/concerns/find_set_dri.rb
392
395
  - lib/gitlab_quality/test_tooling/report/concerns/group_and_category_labels.rb
@@ -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