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.
- checksums.yaml +4 -4
- data/.rspec +2 -1
- data/Gemfile.lock +9 -1
- data/README.md +27 -3
- data/exe/failed-test-issues +53 -8
- data/exe/feature-readiness-check +61 -0
- data/lib/gitlab_quality/test_tooling/feature_readiness/concerns/issue_concern.rb +34 -0
- data/lib/gitlab_quality/test_tooling/feature_readiness/concerns/work_item_concern.rb +114 -0
- data/lib/gitlab_quality/test_tooling/feature_readiness/operational_readiness_check.rb +82 -0
- data/lib/gitlab_quality/test_tooling/gitlab_client/gitlab_graphql_client.rb +54 -0
- data/lib/gitlab_quality/test_tooling/gitlab_client/issues_client.rb +32 -0
- data/lib/gitlab_quality/test_tooling/gitlab_client/issues_dry_client.rb +3 -3
- data/lib/gitlab_quality/test_tooling/gitlab_client/labels_client.rb +13 -0
- data/lib/gitlab_quality/test_tooling/gitlab_client/work_items_client.rb +240 -0
- data/lib/gitlab_quality/test_tooling/gitlab_client/work_items_dry_client.rb +25 -0
- data/lib/gitlab_quality/test_tooling/labels_inference.rb +4 -0
- data/lib/gitlab_quality/test_tooling/report/failed_test_issue.rb +6 -6
- data/lib/gitlab_quality/test_tooling/report/health_problem_reporter.rb +120 -20
- data/lib/gitlab_quality/test_tooling/report/knapsack_report_issue.rb +1 -1
- data/lib/gitlab_quality/test_tooling/report/prepare_stage_reports.rb +1 -1
- data/lib/gitlab_quality/test_tooling/runtime/env.rb +11 -6
- data/lib/gitlab_quality/test_tooling/test_metrics_exporter/support/gcs_tools.rb +18 -5
- data/lib/gitlab_quality/test_tooling/version.rb +1 -1
- metadata +25 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 548aaf25c91235b88c18601ebdeb3cecf4c6b6caedbaceb6ddec4bdbaaf3519a
|
4
|
+
data.tar.gz: 04de3d4ce782f42ab487c9d7872f80236d7115250bd28b4a58e02c83aebc0ec9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f5cb5a3f95f291e7f00b7454f9fe211c2f2db662bb08fe47e72c587468dc2658b4ce8019fdaf963de4f5845032863bb71c69d684a5d10702b0efb28caea207eb
|
7
|
+
data.tar.gz: 92ef6b0c7360c676294b9f9e7d15e3ce2b0651ceb5b0e2ccc65a328ae0806f87997a2fed1a74ca398427ba6ecf431299e9bf16e952c4cd1c10cd26332de06332
|
data/.rspec
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
gitlab_quality-test_tooling (2.
|
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
|
data/exe/failed-test-issues
CHANGED
@@ -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
|
-
|
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
|
-
|
66
|
-
|
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
|
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
|
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
|
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:)
|