gitlab_quality-test_tooling 2.9.0 → 2.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.
Files changed (24) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +2 -1
  3. data/Gemfile.lock +9 -1
  4. data/README.md +27 -3
  5. data/exe/failed-test-issues +53 -8
  6. data/exe/feature-readiness-check +61 -0
  7. data/lib/gitlab_quality/test_tooling/feature_readiness/concerns/issue_concern.rb +34 -0
  8. data/lib/gitlab_quality/test_tooling/feature_readiness/concerns/work_item_concern.rb +114 -0
  9. data/lib/gitlab_quality/test_tooling/feature_readiness/operational_readiness_check.rb +82 -0
  10. data/lib/gitlab_quality/test_tooling/gitlab_client/gitlab_graphql_client.rb +54 -0
  11. data/lib/gitlab_quality/test_tooling/gitlab_client/issues_client.rb +32 -0
  12. data/lib/gitlab_quality/test_tooling/gitlab_client/issues_dry_client.rb +3 -3
  13. data/lib/gitlab_quality/test_tooling/gitlab_client/labels_client.rb +13 -0
  14. data/lib/gitlab_quality/test_tooling/gitlab_client/work_items_client.rb +240 -0
  15. data/lib/gitlab_quality/test_tooling/gitlab_client/work_items_dry_client.rb +25 -0
  16. data/lib/gitlab_quality/test_tooling/labels_inference.rb +4 -0
  17. data/lib/gitlab_quality/test_tooling/report/failed_test_issue.rb +6 -6
  18. data/lib/gitlab_quality/test_tooling/report/health_problem_reporter.rb +120 -20
  19. data/lib/gitlab_quality/test_tooling/report/knapsack_report_issue.rb +1 -1
  20. data/lib/gitlab_quality/test_tooling/report/prepare_stage_reports.rb +1 -1
  21. data/lib/gitlab_quality/test_tooling/runtime/env.rb +11 -6
  22. data/lib/gitlab_quality/test_tooling/test_metrics_exporter/support/gcs_tools.rb +18 -5
  23. data/lib/gitlab_quality/test_tooling/version.rb +1 -1
  24. metadata +25 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 737a2e86ac9d3545cf1c5f5b9344fead1e03d12dd4a6fe0f9b7ed44b00f604e6
4
- data.tar.gz: 99cac62fe8e0672f28930be527ad4ef73743ca8d63c5e59734f610e2e3624ca9
3
+ metadata.gz: 548aaf25c91235b88c18601ebdeb3cecf4c6b6caedbaceb6ddec4bdbaaf3519a
4
+ data.tar.gz: 04de3d4ce782f42ab487c9d7872f80236d7115250bd28b4a58e02c83aebc0ec9
5
5
  SHA512:
6
- metadata.gz: dac0023706ab1428abc046d6189e7c6df3ad684e4a7f70fcbed84633505c20a515e399e4050c3427584abceb1008e61908c9aacc5286f9dd74431d04e1423149
7
- data.tar.gz: 876fa1ec99dcba949757b9f9073a4b118c4f98d877c1c2edfd3bcb55850cbf9de48683684de427536d13b21639d7f66aff98a552abe6d80ec1bd582db6385058
6
+ metadata.gz: f5cb5a3f95f291e7f00b7454f9fe211c2f2db662bb08fe47e72c587468dc2658b4ce8019fdaf963de4f5845032863bb71c69d684a5d10702b0efb28caea207eb
7
+ data.tar.gz: 92ef6b0c7360c676294b9f9e7d15e3ce2b0651ceb5b0e2ccc65a328ae0806f87997a2fed1a74ca398427ba6ecf431299e9bf16e952c4cd1c10cd26332de06332
data/.rspec CHANGED
@@ -1,3 +1,4 @@
1
1
  --color
2
- --format documentation
2
+ --order random
3
+ --format progress
3
4
  --require spec_helper
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- gitlab_quality-test_tooling (2.9.0)
4
+ gitlab_quality-test_tooling (2.10.0)
5
5
  activesupport (>= 7.0, < 7.3)
6
6
  amatch (~> 0.4.1)
7
7
  fog-google (~> 1.24, >= 1.24.1)
@@ -187,6 +187,7 @@ GEM
187
187
  http-cookie (~> 1.0)
188
188
  http-form_data (~> 2.2)
189
189
  llhttp-ffi (~> 0.5.0)
190
+ http-accept (1.7.0)
190
191
  http-cookie (1.0.7)
191
192
  domain_name (~> 0.5)
192
193
  http-form_data (2.3.0)
@@ -233,6 +234,7 @@ GEM
233
234
  nenv (0.3.0)
234
235
  net-http (0.4.1)
235
236
  uri
237
+ netrc (0.11.0)
236
238
  nokogiri (1.16.7)
237
239
  mini_portile2 (~> 2.8.2)
238
240
  racc (~> 1.4)
@@ -273,6 +275,11 @@ GEM
273
275
  declarative (< 0.1.0)
274
276
  trailblazer-option (>= 0.1.1, < 0.2.0)
275
277
  uber (< 0.2.0)
278
+ rest-client (2.1.0)
279
+ http-accept (>= 1.7.0, < 2.0)
280
+ http-cookie (>= 1.0.2, < 2.0)
281
+ mime-types (>= 1.16, < 4.0)
282
+ netrc (~> 0.8)
276
283
  retriable (3.1.2)
277
284
  reverse_markdown (2.1.1)
278
285
  nokogiri
@@ -408,6 +415,7 @@ DEPENDENCIES
408
415
  lefthook (~> 1.3)
409
416
  pry-byebug (= 3.10.1)
410
417
  rake (~> 13.0)
418
+ rest-client (~> 2.1.0)
411
419
  rspec (~> 3.12)
412
420
  rspec_junit_formatter (~> 0.6.0)
413
421
  simplecov (~> 0.22)
data/README.md CHANGED
@@ -161,9 +161,17 @@ Usage: exe/knapsack-report-issues [options]
161
161
  ```shell
162
162
  Purpose: Relate test failures to failure issues from RSpec report files (JSON or JUnit XML)
163
163
  Usage: exe/failed-test-issues [options]
164
- -i, --input-files INPUT_FILES RSpec report files (JSON or JUnit XML)
165
- -p, --project PROJECT Can be an integer or a group/project string
166
- -t, --token TOKEN A valid access token with `api` scope and Maintainer permission in PROJECT
164
+ -i, --input-files INPUT_FILES RSpec report files (JSON or JUnit XML) [REQUIRED]
165
+ -p, --project PROJECT Can be an integer or a group/project string [REQUIRED]
166
+ -t, --token TOKEN A valid access token with `api` scope and Maintainer permission in PROJECT [REQUIRED]
167
+ --enable-issue-update [BOOLEAN]
168
+ Enable/disable test health issue creation/update (default: true)
169
+ --enable-gcs [BOOLEAN] Enable/disable the push of JSON data to a Google Cloud Storage (GCS) bucket (default: false)
170
+ --gcs-project-id GCS_PROJECT_ID
171
+ Google Cloud project ID for GCS bucket access
172
+ --gcs-bucket GCS_BUCKET Name of the GCS bucket to store metrics data
173
+ --gcs-credentials GCS_CREDENTIALS
174
+ GCS service account credentials (file path or string)
167
175
  --max-diff-ratio MAX_DIFF_RATO
168
176
  Max stacktrace diff ratio for failure issues detection
169
177
  -r RELATED_ISSUES_FILE, The file path for the related issues
@@ -249,6 +257,22 @@ Usage: exe/update-test-meta [options]
249
257
  -h, --help Show the usage
250
258
  ```
251
259
 
260
+ ### `exe/feature-readiness-check`
261
+
262
+ ```shell
263
+ Purpose: Conditionally create operational readiness checklist and link it to the epic
264
+ Usage: exe/feature-readiness-check [options]
265
+ -p, --project PROJECT Can be an integer or a group/project string
266
+ -g, --group GROUP Can be an integer or a group/project string
267
+ -t, --token TOKEN A valid access token with `api` scope and Maintainer permission in PROJECT
268
+ -l, --search_labels LABELS A comma seperated list of labels to filter the epics on
269
+ -m, --minutes MINUTES Limit the search to issues and epics created with last MINUTES. Optional.
270
+ -b, --blocking Any issues created and linked to the epic will block the epic
271
+ --dry-run Perform a dry-run (don't create branches, commits or MRs)
272
+ -v, --version Show the version
273
+ -h, --help Show the usage
274
+ ```
275
+
252
276
  ## Development
253
277
 
254
278
  ### Initial setup
@@ -9,20 +9,48 @@ require_relative "../lib/gitlab_quality/test_tooling"
9
9
  params = {}
10
10
 
11
11
  options = OptionParser.new do |opts|
12
+ def to_boolean(value, default)
13
+ return default if value.nil?
14
+ return true if value == true || value.to_s.casecmp('true').zero?
15
+
16
+ false
17
+ end
18
+
12
19
  opts.banner = "Usage: #{$PROGRAM_NAME} [options]"
13
20
 
14
- opts.on('-i', '--input-files INPUT_FILES', String, 'RSpec report files (JSON or JUnit XML)') do |input_files|
21
+ opts.on('-i', '--input-files INPUT_FILES', String, 'RSpec report files (JSON or JUnit XML) [REQUIRED]') do |input_files|
15
22
  params[:input_files] = input_files
16
23
  end
17
24
 
18
- opts.on('-p', '--project PROJECT', String, 'Can be an integer or a group/project string') do |project|
25
+ opts.on('-p', '--project PROJECT', String, 'Can be an integer or a group/project string [REQUIRED]') do |project|
19
26
  params[:project] = project
20
27
  end
21
28
 
22
- opts.on('-t', '--token TOKEN', String, 'A valid access token with `api` scope and Maintainer permission in PROJECT') do |token|
29
+ opts.on('-t', '--token TOKEN', String, 'A valid access token with `api` scope and Maintainer permission in PROJECT [REQUIRED]') do |token|
23
30
  params[:token] = token
24
31
  end
25
32
 
33
+ opts.on('--enable-issue-update [BOOLEAN]', "Enable/disable test health issue creation/update (default: true)") do |toggle|
34
+ params[:issue_update_enabled] = to_boolean(toggle, true)
35
+ end
36
+
37
+ # Google Cloud Storage (GCS) options
38
+ opts.on('--enable-gcs [BOOLEAN]', "Enable/disable the push of JSON data to a Google Cloud Storage (GCS) bucket (default: false)") do |toggle|
39
+ params[:gcs_enabled] = to_boolean(toggle, false)
40
+ end
41
+
42
+ opts.on('--gcs-project-id GCS_PROJECT_ID', String, 'Google Cloud project ID for GCS bucket access') do |project_id|
43
+ params[:gcs_project_id] = project_id
44
+ end
45
+
46
+ opts.on('--gcs-bucket GCS_BUCKET', String, 'Name of the GCS bucket to store metrics data') do |bucket|
47
+ params[:gcs_bucket] = bucket
48
+ end
49
+
50
+ opts.on('--gcs-credentials GCS_CREDENTIALS', String, 'GCS service account credentials (file path or string)') do |credentials|
51
+ params[:gcs_credentials] = credentials
52
+ end
53
+
26
54
  opts.on('--max-diff-ratio MAX_DIFF_RATO', Float, 'Max stacktrace diff ratio for failure issues detection') do |max_diff_ratio|
27
55
  params[:max_diff_ratio] = max_diff_ratio
28
56
  end
@@ -56,13 +84,30 @@ options = OptionParser.new do |opts|
56
84
  puts opts
57
85
  exit
58
86
  end
59
-
60
- opts.parse(ARGV)
61
87
  end
62
88
 
63
- if params.any?
89
+ begin
90
+ options.parse!(ARGV)
91
+
92
+ required_options = [:input_files, :project, :token]
93
+ missing_options = required_options.select { |option| params[option].nil? }
94
+
95
+ if params[:gcs_enabled]
96
+ gcs_required_options = [:gcs_project_id, :gcs_bucket, :gcs_credentials]
97
+ missing_gcs_options = gcs_required_options.select { |option| params[option].nil? }
98
+
99
+ missing_options.concat(missing_gcs_options) if missing_gcs_options.any?
100
+ end
101
+
102
+ if missing_options.any?
103
+ warn "Error: Missing required options: #{missing_options.map { |o| "--#{o.to_s.tr('_', '-')}" }.join(', ')}\n"
104
+ warn options
105
+ exit 1
106
+ end
107
+
64
108
  GitlabQuality::TestTooling::Report::FailedTestIssue.new(**params).invoke!
65
- else
66
- puts options
109
+ rescue OptionParser::InvalidOption, OptionParser::MissingArgument => e
110
+ warn "Error: #{e.message}"
111
+ warn options
67
112
  exit 1
68
113
  end
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "optparse"
5
+
6
+ require_relative "../lib/gitlab_quality/test_tooling"
7
+ params = {}
8
+
9
+ options = OptionParser.new do |opts|
10
+ opts.banner = "Usage: #{$PROGRAM_NAME} [options]"
11
+
12
+ opts.on('-p', '--project PROJECT', String, 'Can be an integer or a group/project string') do |project|
13
+ params[:project] = project
14
+ end
15
+ opts.on('-g', '--group GROUP', String, 'Can be an integer or a group/project string') do |group|
16
+ params[:group] = group
17
+ end
18
+
19
+ opts.on('-t', '--token TOKEN', String, 'A valid access token with `api` scope and Maintainer permission in PROJECT') do |token|
20
+ params[:token] = token
21
+ end
22
+
23
+ opts.on('-l', '--search_labels LABELS', String, 'A comma seperated list of labels to filter the epics on') do |search_labels|
24
+ params[:search_labels] = search_labels.split(',')
25
+ end
26
+
27
+ opts.on('-m', '--minutes MINUTES', Integer, 'Limit the search to issues and epics created with last MINUTES. Optional.') do |minutes|
28
+ params[:limit_to_minutes] = minutes
29
+ end
30
+
31
+ opts.on('-b', '--blocking', 'Block the epic by any issues created and linked to it') do
32
+ params[:issue_is_blocking] = true
33
+ end
34
+
35
+ opts.on('--dry-run', "Perform a dry-run (don't create branches, commits or MRs)") do
36
+ params[:dry_run] = true
37
+ end
38
+
39
+ opts.on_tail('-v', '--version', 'Show the version') do
40
+ require_relative "../lib/gitlab_quality/test_tooling/version"
41
+ puts "#{$PROGRAM_NAME} : #{GitlabQuality::TestTooling::VERSION}"
42
+ exit
43
+ end
44
+
45
+ opts.on_tail('-h', '--help', 'Show the usage') do
46
+ puts "Purpose: Conditionally create operational readiness checklist and link it to the epic"
47
+ puts opts
48
+ exit
49
+ end
50
+
51
+ opts.parse(ARGV)
52
+ end
53
+
54
+ raise ArgumentError, "Missing argument(s). Required arguments are: --project, --group, --token" if params.empty? || ([:project, :group, :token] - params.keys).any?
55
+
56
+ if params.any?
57
+ GitlabQuality::TestTooling::FeatureReadiness::OperationalReadinessCheck.new(**params).invoke!
58
+ else
59
+ puts options
60
+ exit 1
61
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GitlabQuality
4
+ module TestTooling
5
+ module FeatureReadiness
6
+ module Concerns
7
+ module IssueConcern
8
+ OPERATIONAL_READINESS_CHECKLIST_LABEL = 'operational-readiness-checklist'
9
+
10
+ def create_operation_readiness_issue(work_item_title, assignee_id, issue_client, repository_files_client)
11
+ operational_readiness_issue = issue_client.create_issue(
12
+ title: "Operational Readiness Checklist for: '#{work_item_title}'",
13
+ description: repository_files_client.file_contents,
14
+ labels: [OPERATIONAL_READINESS_CHECKLIST_LABEL],
15
+ assignee_id: assignee_id
16
+ )
17
+
18
+ puts "\nCreated operational readiness issue: #{operational_readiness_issue.web_url}\n"
19
+
20
+ operational_readiness_issue
21
+ end
22
+
23
+ def has_operational_readiness_issue_linked?(linked_issue_iids, issue_client)
24
+ linked_issues(linked_issue_iids, issue_client).any? { |issue| (issue.labels & [OPERATIONAL_READINESS_CHECKLIST_LABEL]).any? }
25
+ end
26
+
27
+ def linked_issues(linked_issue_iids, issue_client)
28
+ linked_issue_iids.flat_map { |iid| issue_client.find_issues(iid: iid) }
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GitlabQuality
4
+ module TestTooling
5
+ module FeatureReadiness
6
+ module Concerns
7
+ module WorkItemConcern
8
+ OPERATIONAL_READINESS_NOTE_ID = '<!-- OPERATIONAL READINESS PRECHECK COMMENT -->'
9
+ OPERATIONAL_READINESS_TRACKING_LABEL = 'tracking operational readiness'
10
+
11
+ def add_operational_readiness_precheck_comment(work_item, client)
12
+ comment = <<~COMMENT
13
+ #{OPERATIONAL_READINESS_NOTE_ID}
14
+ ## Operational Readiness Pre-Check
15
+
16
+ @#{work_item[:author][:username]} This is an automated comment to help determine if an
17
+ [operational readiness check](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/issue_templates/Operational%20Readiness.md?ref_type=heads)
18
+ is needed for this feature.
19
+
20
+ Please respond with the ✅ emoji on this comment if your feature meets any of the below criteria. If not, please respond with the ❌ emoji.
21
+
22
+ 1. Requires new infrastructure components, or significant changes to existing components that have dependencies on the GitLab application.
23
+ 2. Requires changes to our application architecture that change how the infrastructure scales, GitLab is deployed or how data is processed or stored.
24
+ 3. Adds new services or changes existing services that will factor into the availability of the GitLab application.
25
+ COMMENT
26
+
27
+ add_labels(ids_for_labels([OPERATIONAL_READINESS_TRACKING_LABEL]), work_item[:id], client)
28
+
29
+ discussion = existing_note_containing_text(OPERATIONAL_READINESS_NOTE_ID, work_item, client)
30
+
31
+ return discussion if discussion
32
+
33
+ client.create_discussion(id: work_item[:id], note: comment)
34
+
35
+ puts "\nAdded operational readiness comment to epic work item: #{work_item[:webUrl]}\n"
36
+
37
+ existing_note_containing_text(OPERATIONAL_READINESS_NOTE_ID, work_item, client)
38
+ end
39
+
40
+ def existing_note_containing_text(text, work_item, client)
41
+ work_item = fetch_work_item(work_item[:iid], client)
42
+
43
+ work_item[:widgets]
44
+ .find { |widget| widget.key?(:discussions) }
45
+ .dig(:discussions, :nodes)
46
+ .find { |node| node[:notes][:nodes].any? { |node| node[:body].include?(text) } }
47
+ &.dig(:notes, :nodes, 0)
48
+ end
49
+
50
+ def link_operation_readiness_issue(issue, work_item, link_type, client)
51
+ client.create_linked_items(work_item_id: work_item[:id], item_ids: ["gid://gitlab/Issue/#{issue.id}"], link_type: link_type)
52
+ end
53
+
54
+ def note_has_emoji?(note, emoji_name)
55
+ note&.dig(:awardEmoji, :nodes)&.any? { |node| node[:name] == emoji_name }
56
+ end
57
+
58
+ def post_comment_about_operation_readiness_issue_created(work_item, issue, precheck_comment, client)
59
+ comment_text = <<~COMMENT
60
+ @#{work_item[:author][:username]} Thanks for confirming that your feature requires an operational readiness check.
61
+ Based on your response, an operational readiness check issue has been created and linked to this issue: #{issue.web_url}.
62
+ COMMENT
63
+
64
+ client.create_discussion_note(work_item_id: work_item[:id], discussion_id: precheck_comment.dig(:discussion, :id), text: comment_text)
65
+ end
66
+
67
+ def add_labels(label_ids, work_item_id, client)
68
+ client.add_labels(work_item_id: work_item_id, label_ids: label_gids(label_ids))
69
+ end
70
+
71
+ def fetch_work_item(iid, client)
72
+ client.work_item(workitem_iid: iid)
73
+ end
74
+
75
+ def has_a_child_epic?(epic)
76
+ epic[:widgets]
77
+ .find { |widget| widget.has_key?(:children) }[:children][:nodes]
78
+ .any? { |child| child[:workItemType][:name] == "Epic" }
79
+ end
80
+
81
+ def work_item_author_id(work_item)
82
+ extract_id_from_gid(work_item[:author][:id])
83
+ end
84
+
85
+ def linked_issue_iids(work_item)
86
+ work_item[:widgets].find { |widget| widget.key?(:linkedItems) }
87
+ .dig(:linkedItems, :nodes)
88
+ .map { |node| node.dig(:workItem, :iid) }
89
+ end
90
+
91
+ def label_gids(label_ids = [])
92
+ label_ids.map { |label_id| "gid://gitlab/Label/#{label_id}" }
93
+ end
94
+
95
+ def ids_for_labels(labels = [])
96
+ labels.map { |label| get_id_for_label(label) }
97
+ end
98
+
99
+ def get_id_for_label(label_name)
100
+ labels = label_client.labels(options: { search: label_name })
101
+
102
+ raise "No labels found with name: '#{label_name}'" if labels.empty?
103
+
104
+ labels.first.id
105
+ end
106
+
107
+ def extract_id_from_gid(gid)
108
+ gid.to_s.split('/').last.to_i
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'time'
4
+
5
+ module GitlabQuality
6
+ module TestTooling
7
+ module FeatureReadiness
8
+ class OperationalReadinessCheck
9
+ include Concerns::IssueConcern
10
+ include Concerns::WorkItemConcern
11
+
12
+ BASE_LABELS_FOR_SEARCH = ["feature::addition"].freeze
13
+ OPERATIONAL_READINESS_ISSUE_TEMPLATE_PATH = '.gitlab/issue_templates/Operational Readiness.md'
14
+
15
+ def initialize(token:, project: nil, group: nil, limit_to_minutes: nil, search_labels: [], issue_is_blocking: false, dry_run: false)
16
+ @token = token
17
+ @project = project
18
+ @group = group
19
+ @limit_to_minutes = limit_to_minutes
20
+ @search_labels = search_labels
21
+ @issue_is_blocking = issue_is_blocking
22
+ @dry_run = dry_run
23
+ end
24
+
25
+ def invoke!
26
+ created_after = utc_time_minus_mins(limit_to_minutes)
27
+
28
+ epics = work_items_client.paginated_call(:group_work_items,
29
+ labels: search_labels.concat(BASE_LABELS_FOR_SEARCH), state: 'opened', created_after: created_after, extras: [:work_item_fields])
30
+
31
+ epics.each do |epic|
32
+ process_epic(epic)
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ attr_reader :token, :project, :dry_run, :group, :limit_to_minutes, :search_labels, :issue_is_blocking
39
+
40
+ def issue_client
41
+ @issue_client ||= (dry_run ? GitlabClient::IssuesDryClient : GitlabClient::IssuesClient).new(token: token, project: project)
42
+ end
43
+
44
+ def work_items_client
45
+ @work_items_client ||= (dry_run ? GitlabClient::WorkItemsDryClient : GitlabClient::WorkItemsClient).new(token: token, project: project, group: group)
46
+ end
47
+
48
+ def label_client
49
+ @label_client ||= GitlabClient::LabelsClient.new(token: token, project: project)
50
+ end
51
+
52
+ def repository_files_client
53
+ @repository_files_client ||= GitlabClient::RepositoryFilesClient.new(token: token, project: project, file_path: OPERATIONAL_READINESS_ISSUE_TEMPLATE_PATH, ref: 'master')
54
+ end
55
+
56
+ def utc_time_minus_mins(mins)
57
+ (Time.now - (mins * 60)).utc.iso8601 if mins
58
+ end
59
+
60
+ def process_epic(epic) # rubocop:disable Metrics/AbcSize
61
+ epic = fetch_work_item(epic[:iid], work_items_client)
62
+
63
+ return if has_a_child_epic?(epic)
64
+
65
+ pre_check_comment = add_operational_readiness_precheck_comment(epic, work_items_client)
66
+
67
+ return unless note_has_emoji?(pre_check_comment, 'white_check_mark') && !has_operational_readiness_issue_linked?(linked_issue_iids(epic), issue_client)
68
+
69
+ issue = create_operation_readiness_issue(epic[:title], work_item_author_id(epic), issue_client, repository_files_client)
70
+
71
+ link_operation_readiness_issue(issue, epic, link_type, work_items_client)
72
+
73
+ post_comment_about_operation_readiness_issue_created(epic, issue, pre_check_comment, work_items_client)
74
+ end
75
+
76
+ def link_type
77
+ issue_is_blocking ? 'BLOCKED_BY' : 'RELATED'
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rest-client'
4
+ require 'json'
5
+
6
+ module GitlabQuality
7
+ module TestTooling
8
+ module GitlabClient
9
+ class GitlabGraphqlClient
10
+ def initialize(token:, project:, group:, endpoint: nil)
11
+ @token = token
12
+ @project = project
13
+ @group = group
14
+ @endpoint = endpoint || Runtime::Env.gitlab_graphql_api_base
15
+ end
16
+
17
+ def post(payload)
18
+ payload = { query: payload } if payload.is_a?(String)
19
+ request_args = {
20
+ method: :post,
21
+ url: endpoint,
22
+ payload: payload,
23
+ headers: { 'Authorization' => "Bearer #{token}" },
24
+ verify_ssl: false
25
+ }
26
+ extract_graphql_body(RestClient::Request.execute(request_args))
27
+ rescue StandardError => e
28
+ return_response_or_raise(e)
29
+ end
30
+
31
+ private
32
+
33
+ attr_reader :project, :token, :endpoint, :group
34
+
35
+ def return_response_or_raise(error)
36
+ return error.response if error.respond_to?(:response) && error.response
37
+
38
+ raise error
39
+ end
40
+
41
+ def parse_body(response)
42
+ JSON.parse(response.body, symbolize_names: true)
43
+ end
44
+
45
+ def extract_graphql_body(graphql_response)
46
+ parsed_body = parse_body(graphql_response)
47
+
48
+ data = parsed_body[:data].to_h
49
+ data.values[0].to_h
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -9,6 +9,10 @@ module Gitlab
9
9
  get("/projects/#{url_encode(project)}/members/all/#{id}")
10
10
  end
11
11
 
12
+ def issue_note_award_emoji(project, issue_id, note_id, options = {})
13
+ get("/projects/#{url_encode(project)}/issues/#{issue_id}/notes/#{note_id}/award_emoji", query: options)
14
+ end
15
+
12
16
  def issue_discussions(project, issue_id, options = {})
13
17
  get("/projects/#{url_encode(project)}/issues/#{issue_id}/discussions", query: options)
14
18
  end
@@ -17,6 +21,16 @@ module Gitlab
17
21
  post("/projects/#{url_encode(project)}/issues/#{issue_iid}/discussions", query: options)
18
22
  end
19
23
 
24
+ def create_issue_link(project, issue_iid, target_project_id, target_issue_iid, link_type)
25
+ post("/projects/#{url_encode(project)}/issues/#{issue_iid}/links",
26
+ query: {
27
+ target_project_id: target_project_id,
28
+ target_issue_iid: target_issue_iid,
29
+ link_type: link_type
30
+ }.compact
31
+ )
32
+ end
33
+
20
34
  def add_note_to_issue_discussion_as_thread(project, issue_id, discussion_id, options = {})
21
35
  post("/projects/#{url_encode(project)}/issues/#{issue_id}/discussions/#{discussion_id}/notes", query: options)
22
36
  end
@@ -79,6 +93,18 @@ module GitlabQuality
79
93
  end
80
94
  end
81
95
 
96
+ def create_issue_link(project:, issue_iid:, target_project_id:, target_issue_iid:, link_type: 'is_blocked_by')
97
+ handle_gitlab_client_exceptions do
98
+ client.create_issue_link(project, issue_iid, target_project_id, target_issue_iid, link_type)
99
+ end
100
+ end
101
+
102
+ def issue_links(project:, issue_iid:, options: {})
103
+ handle_gitlab_client_exceptions do
104
+ client.issue_links(project, issue_iid, options)
105
+ end
106
+ end
107
+
82
108
  def edit_issue(iid:, options: {})
83
109
  handle_gitlab_client_exceptions do
84
110
  client.edit_issue(project, iid, options)
@@ -97,6 +123,12 @@ module GitlabQuality
97
123
  end
98
124
  end
99
125
 
126
+ def get_note_award_emojis(issue_iid:, note_id:)
127
+ handle_gitlab_client_exceptions do
128
+ client.issue_note_award_emoji(project, issue_iid, note_id)
129
+ end
130
+ end
131
+
100
132
  def create_issue_discussion(iid:, note:)
101
133
  handle_gitlab_client_exceptions do
102
134
  client.create_issue_discussion(project, iid, body: note)
@@ -20,15 +20,15 @@ module GitlabQuality
20
20
  end
21
21
 
22
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}"
23
+ puts "The following note would have been edited on #{project}##{issue_iid} (note #{note_id}) issue:\n\n #{note}"
24
24
  end
25
25
 
26
26
  def create_issue_discussion(iid:, note:)
27
- puts "The following discussion would have been posted on #{project}##{iid} issue: #{note}"
27
+ puts "The following discussion would have been posted on #{project}##{iid} issue:\n\n #{note}"
28
28
  end
29
29
 
30
30
  def add_note_to_issue_discussion_as_thread(iid:, discussion_id:, note:)
31
- puts "The following discussion note would have been posted on #{project}##{iid} (discussion #{discussion_id}) issue: #{note}"
31
+ puts "The following discussion note would have been posted on #{project}##{iid} (discussion #{discussion_id}) issue:\n\n #{note}"
32
32
  end
33
33
 
34
34
  def upload_file(file_fullpath:)
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GitlabQuality
4
+ module TestTooling
5
+ module GitlabClient
6
+ class LabelsClient < GitlabClient
7
+ def labels(options: {})
8
+ client.labels(project, options)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end