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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d7c32e6b11eb72bf1f453827d3c8c0dd98a8d37788c22000566fac4fe9b0daab
4
- data.tar.gz: 573ea110d2d1bea41f217f135f6dfe57a814ecc261142813e4dbbda17f813e0e
3
+ metadata.gz: 6aa86323a85f779b89a3d26bd9e2b78e2166d4aaf1fdd18c5b3c26e67edbed1f
4
+ data.tar.gz: 9096461ccb4f8527f0347c4987e405d0ef39ff45aa890468968b773372f854a0
5
5
  SHA512:
6
- metadata.gz: b7f5b809cd506c860edba93bc45c932183106da000b2eb14ae7149aa5d7a820502971fd5a26747664be1921334af3b1041a668fb128f8b4c739507ac63f850f2
7
- data.tar.gz: 1a428ce49770515622c810bfaf2e918df2223b7ed23f38958d994de6bef4c591c5fd10efc7be071d1fdf6cde38eacc8598126b612fde62c300a01c0e9608c83a
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.9.0)
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
@@ -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::FailedJobs.new(token: gitlab_api_token, project_id: project_id, pipeline_id: pipeline_id).fetch
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\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::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.9.0"
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.9.0
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-07 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
@@ -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/failed_jobs.rb
388
- - lib/gitlab_quality/test_tooling/gitlab_client/merge_request.rb
389
- - lib/gitlab_quality/test_tooling/gitlab_client/merge_request_dry_client.rb
390
- - lib/gitlab_quality/test_tooling/gitlab_issue_client.rb
391
- - lib/gitlab_quality/test_tooling/gitlab_issue_dry_client.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
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