gitlab_quality-test_tooling 2.10.0 → 2.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile.lock +2 -2
- data/README.md +22 -5
- data/exe/{feature-readiness-check → feature-readiness-checklist} +2 -2
- data/exe/feature-readiness-evaluation +62 -0
- data/lib/gitlab_quality/test_tooling/feature_readiness/analyzed_items/analyzed_epic.rb +94 -0
- data/lib/gitlab_quality/test_tooling/feature_readiness/analyzed_items/analyzed_issue.rb +92 -0
- data/lib/gitlab_quality/test_tooling/feature_readiness/analyzed_items/analyzed_merge_request.rb +139 -0
- data/lib/gitlab_quality/test_tooling/feature_readiness/concerns/work_item_concern.rb +26 -12
- data/lib/gitlab_quality/test_tooling/feature_readiness/evaluation.rb +82 -0
- data/lib/gitlab_quality/test_tooling/feature_readiness/operational_readiness_check.rb +4 -4
- data/lib/gitlab_quality/test_tooling/gitlab_client/issues_client.rb +6 -0
- data/lib/gitlab_quality/test_tooling/gitlab_client/merge_requests_client.rb +21 -0
- data/lib/gitlab_quality/test_tooling/gitlab_client/merge_requests_dry_client.rb +0 -10
- data/lib/gitlab_quality/test_tooling/gitlab_client/work_items_client.rb +71 -34
- data/lib/gitlab_quality/test_tooling/report/feature_readiness/report_on_epic.rb +174 -0
- data/lib/gitlab_quality/test_tooling/report/relate_failure_issue.rb +3 -1
- data/lib/gitlab_quality/test_tooling/version.rb +1 -1
- metadata +21 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ed414366558c053e3eaef1211a9bd431899106af38b4f1f80b8a68334294b633
|
4
|
+
data.tar.gz: fb31640455e0cda1ca5551f30c804c38d03c17d93e17e6f6a267347c7460dd18
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c95d137404f52c79e6679c74327124bf3288b074c582cc3caf90a0125dc1f1b19e2ff08d71053c60f01d53db99e4aa2bacebae69f8f2f5c1e5897dd637b602e2
|
7
|
+
data.tar.gz: ebb57aa657a848b93d4aeb763bcfa9f682b09d2e4f0d79aa17894536e78feaed1769fb843fb739c6d60fa71522db6a7f1210e3b21ae2ef97094ab9d698412c1f
|
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.12.0)
|
5
5
|
activesupport (>= 7.0, < 7.3)
|
6
6
|
amatch (~> 0.4.1)
|
7
7
|
fog-google (~> 1.24, >= 1.24.1)
|
@@ -11,7 +11,7 @@ PATH
|
|
11
11
|
nokogiri (~> 1.10)
|
12
12
|
parallel (>= 1, < 2)
|
13
13
|
rainbow (>= 3, < 4)
|
14
|
-
rspec-parameterized (
|
14
|
+
rspec-parameterized (>= 1.0.0, < 3.0)
|
15
15
|
table_print (= 1.5.7)
|
16
16
|
zeitwerk (>= 2, < 3)
|
17
17
|
|
data/README.md
CHANGED
@@ -257,17 +257,34 @@ Usage: exe/update-test-meta [options]
|
|
257
257
|
-h, --help Show the usage
|
258
258
|
```
|
259
259
|
|
260
|
-
### `exe/feature-readiness-
|
260
|
+
### `exe/feature-readiness-checklist`
|
261
261
|
|
262
262
|
```shell
|
263
263
|
Purpose: Conditionally create operational readiness checklist and link it to the epic
|
264
|
-
Usage: exe/feature-readiness-
|
265
|
-
-p, --project PROJECT
|
266
|
-
-g, --group GROUP
|
264
|
+
Usage: exe/feature-readiness-checklist [options]
|
265
|
+
-p, --project PROJECT Name of the project
|
266
|
+
-g, --group GROUP Name of the group
|
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 Block the epic by any issues created and linked to it
|
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
|
+
|
276
|
+
### `exe/feature-readiness-evaluation`
|
277
|
+
|
278
|
+
```shell
|
279
|
+
Purpose: Evaluate MRs for multiple feature readiness criteria and report the status to the corresponding epic
|
280
|
+
Usage: exe/feature-readiness-evaluation [options]
|
281
|
+
-p, --project PROJECT Name of the project
|
282
|
+
-g, --group GROUP Name of the group
|
267
283
|
-t, --token TOKEN A valid access token with `api` scope and Maintainer permission in PROJECT
|
268
284
|
-l, --search_labels LABELS A comma seperated list of labels to filter the epics on
|
269
285
|
-m, --minutes MINUTES Limit the search to issues and epics created with last MINUTES. Optional.
|
270
|
-
-
|
286
|
+
-e, --epic-iid EPIC_IID evaluate an epic with iid
|
287
|
+
-o, --operational-readinessr Perform the operational readiness checklist automation
|
271
288
|
--dry-run Perform a dry-run (don't create branches, commits or MRs)
|
272
289
|
-v, --version Show the version
|
273
290
|
-h, --help Show the usage
|
@@ -9,10 +9,10 @@ params = {}
|
|
9
9
|
options = OptionParser.new do |opts|
|
10
10
|
opts.banner = "Usage: #{$PROGRAM_NAME} [options]"
|
11
11
|
|
12
|
-
opts.on('-p', '--project PROJECT', String, '
|
12
|
+
opts.on('-p', '--project PROJECT', String, 'Name of the project') do |project|
|
13
13
|
params[:project] = project
|
14
14
|
end
|
15
|
-
opts.on('-g', '--group GROUP', String, '
|
15
|
+
opts.on('-g', '--group GROUP', String, 'Name of the group') do |group|
|
16
16
|
params[:group] = group
|
17
17
|
end
|
18
18
|
|
@@ -0,0 +1,62 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "optparse"
|
5
|
+
require 'active_support/core_ext/hash'
|
6
|
+
|
7
|
+
require_relative "../lib/gitlab_quality/test_tooling"
|
8
|
+
params = {}
|
9
|
+
|
10
|
+
options = OptionParser.new do |opts|
|
11
|
+
opts.banner = "Usage: #{$PROGRAM_NAME} [options]"
|
12
|
+
|
13
|
+
opts.on('-p', '--project PROJECT', String, 'Name of the project') do |project|
|
14
|
+
params[:project] = project
|
15
|
+
end
|
16
|
+
opts.on('-g', '--group GROUP', String, 'Name of the group') do |group|
|
17
|
+
params[:group] = group
|
18
|
+
end
|
19
|
+
|
20
|
+
opts.on('-t', '--token TOKEN', String, 'A valid access token with `api` scope and Maintainer permission in PROJECT') do |token|
|
21
|
+
params[:token] = token
|
22
|
+
end
|
23
|
+
|
24
|
+
opts.on('-l', '--search_labels LABELS', String, 'A comma seperated list of labels to filter the epics on') do |search_labels|
|
25
|
+
params[:search_labels] = search_labels.split(',')
|
26
|
+
end
|
27
|
+
|
28
|
+
opts.on('-m', '--minutes MINUTES', Integer, 'Limit the search to issues and epics created with last MINUTES. Optional.') do |minutes|
|
29
|
+
params[:limit_to_minutes] = minutes
|
30
|
+
end
|
31
|
+
|
32
|
+
opts.on('-ei', '--epic-iid EPIC_IID', String, 'evaluate an epic with iid') do |epic_iid|
|
33
|
+
params[:epic_iid] = epic_iid
|
34
|
+
end
|
35
|
+
|
36
|
+
opts.on('--dry-run', "Perform a dry-run (don't create branches, commits or MRs)") do
|
37
|
+
params[:dry_run] = true
|
38
|
+
end
|
39
|
+
|
40
|
+
opts.on_tail('-v', '--version', 'Show the version') do
|
41
|
+
require_relative "../lib/gitlab_quality/test_tooling/version"
|
42
|
+
puts "#{$PROGRAM_NAME} : #{GitlabQuality::TestTooling::VERSION}"
|
43
|
+
exit
|
44
|
+
end
|
45
|
+
|
46
|
+
opts.on_tail('-h', '--help', 'Show the usage') do
|
47
|
+
puts "Purpose: Evaluate MRs for multiple feature readiness criteria and report the status to the corresponding epic"
|
48
|
+
puts opts
|
49
|
+
exit
|
50
|
+
end
|
51
|
+
|
52
|
+
opts.parse(ARGV)
|
53
|
+
end
|
54
|
+
|
55
|
+
raise ArgumentError, "Missing argument(s). Required arguments are: --project, --group, --token" if params.empty? || ([:project, :group, :token] - params.keys).any?
|
56
|
+
|
57
|
+
if params.any?
|
58
|
+
GitlabQuality::TestTooling::FeatureReadiness::Evaluation.new(**params).invoke!
|
59
|
+
else
|
60
|
+
puts options
|
61
|
+
exit 1
|
62
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GitlabQuality
|
4
|
+
module TestTooling
|
5
|
+
module FeatureReadiness
|
6
|
+
module AnalyzedItems
|
7
|
+
class AnalyzedEpic
|
8
|
+
include Concerns::WorkItemConcern
|
9
|
+
|
10
|
+
def initialize(epic:, token:, project:, group:, dry_run:)
|
11
|
+
@token = token
|
12
|
+
@project = project
|
13
|
+
@group = group
|
14
|
+
@dry_run = dry_run
|
15
|
+
@epic = epic
|
16
|
+
@analyzed_issues = []
|
17
|
+
end
|
18
|
+
|
19
|
+
def analyze
|
20
|
+
issue_iids = get_issue_iids(epic, project)
|
21
|
+
return if issue_iids.none?
|
22
|
+
|
23
|
+
open_issues = fetch_open_issues(issue_iids)
|
24
|
+
|
25
|
+
puts "\nProcessing epic: #{epic[:webUrl]} with #{open_issues.count} open feature addition issues."
|
26
|
+
|
27
|
+
process_issues(open_issues)
|
28
|
+
end
|
29
|
+
|
30
|
+
def process_issues(issues)
|
31
|
+
@analyzed_issues.concat(
|
32
|
+
issues.map do |issue|
|
33
|
+
AnalyzedIssue.new(issue: issue, token: token, project: project, group: group, dry_run: dry_run)
|
34
|
+
.tap(&:analyze)
|
35
|
+
.result
|
36
|
+
end
|
37
|
+
)
|
38
|
+
end
|
39
|
+
|
40
|
+
def result
|
41
|
+
{
|
42
|
+
epic_iid: epic[:iid],
|
43
|
+
epic_id: epic[:id],
|
44
|
+
epic_web_url: epic[:webUrl],
|
45
|
+
issues: analyzed_issues,
|
46
|
+
doc_mrs: doc_mrs,
|
47
|
+
feature_spec_mrs: feature_spec_mrs,
|
48
|
+
e2e_spec_mrs: e2e_spec_mrs,
|
49
|
+
feature_flag_mrs: feature_flag_mrs
|
50
|
+
}
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
attr_reader :epic, :token, :project, :dry_run, :group, :analyzed_issues
|
56
|
+
|
57
|
+
def work_items_client
|
58
|
+
@work_items_client ||= (dry_run ? GitlabClient::WorkItemsDryClient : GitlabClient::WorkItemsClient).new(token: token, project: project, group: group)
|
59
|
+
end
|
60
|
+
|
61
|
+
def issue_client
|
62
|
+
@issue_client ||= (dry_run ? GitlabClient::IssuesDryClient : GitlabClient::IssuesClient).new(token: token, project: project)
|
63
|
+
end
|
64
|
+
|
65
|
+
def fetch_open_issues(issue_iids)
|
66
|
+
issue_client.find_issues(
|
67
|
+
options: {
|
68
|
+
iids: issue_iids,
|
69
|
+
labels: Evaluation::BASE_LABELS_FOR_SEARCH,
|
70
|
+
state: 'opened'
|
71
|
+
}
|
72
|
+
)
|
73
|
+
end
|
74
|
+
|
75
|
+
def doc_mrs
|
76
|
+
analyzed_issues.flat_map { |issue| issue[:doc_mrs].any? ? issue[:doc_mrs] : [] }
|
77
|
+
end
|
78
|
+
|
79
|
+
def feature_spec_mrs
|
80
|
+
analyzed_issues.flat_map { |issue| issue[:feature_spec_mrs].any? ? issue[:feature_spec_mrs] : [] }
|
81
|
+
end
|
82
|
+
|
83
|
+
def e2e_spec_mrs
|
84
|
+
analyzed_issues.flat_map { |issue| issue[:e2e_spec_mrs].any? ? issue[:e2e_spec_mrs] : [] }
|
85
|
+
end
|
86
|
+
|
87
|
+
def feature_flag_mrs
|
88
|
+
analyzed_issues.flat_map { |issue| issue[:feature_flag_mrs].any? ? issue[:feature_flag_mrs] : [] }
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GitlabQuality
|
4
|
+
module TestTooling
|
5
|
+
module FeatureReadiness
|
6
|
+
module AnalyzedItems
|
7
|
+
class AnalyzedIssue
|
8
|
+
def initialize(issue:, token:, project:, group:, dry_run:)
|
9
|
+
@issue = issue
|
10
|
+
@token = token
|
11
|
+
@project = project
|
12
|
+
@group = group
|
13
|
+
@dry_run = dry_run
|
14
|
+
@analyzed_merge_requests = []
|
15
|
+
end
|
16
|
+
|
17
|
+
def analyze
|
18
|
+
contributing_mrs = contributing_mrs_for_issue(issue)
|
19
|
+
return if contributing_mrs.empty?
|
20
|
+
|
21
|
+
@analyzed_merge_requests.concat(
|
22
|
+
contributing_mrs
|
23
|
+
.reject { |mr| skip_merge_request?(mr, issue) }
|
24
|
+
.map do |merge_request|
|
25
|
+
AnalyzedMergeRequest.new(merge_request: merge_request, token: token, project: project, group: group, dry_run: dry_run)
|
26
|
+
.tap(&:analyze)
|
27
|
+
.result
|
28
|
+
end
|
29
|
+
)
|
30
|
+
end
|
31
|
+
|
32
|
+
def result
|
33
|
+
{
|
34
|
+
issue_iid: issue.iid,
|
35
|
+
issue_web_url: issue.web_url,
|
36
|
+
merge_requests: analyzed_merge_requests,
|
37
|
+
doc_mrs: doc_mrs,
|
38
|
+
feature_spec_mrs: feature_spec_mrs,
|
39
|
+
e2e_spec_mrs: e2e_spec_mrs,
|
40
|
+
feature_flag_mrs: feature_flag_mrs
|
41
|
+
}
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
attr_reader :issue, :token, :project, :group, :dry_run, :analyzed_merge_requests
|
47
|
+
|
48
|
+
def contributing_mrs_for_issue(issue)
|
49
|
+
related_mrs = issue_client.related_merge_requests(iid: issue.iid)
|
50
|
+
|
51
|
+
return [] if related_mrs.empty?
|
52
|
+
|
53
|
+
related_mrs.select do |mr|
|
54
|
+
mr.description&.include?("##{issue.iid}") || mr.description&.include?(issue.web_url)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def skip_merge_request?(merge_request, issue)
|
59
|
+
if (merge_request.target_project_id != issue.project_id) || (merge_request.draft == true)
|
60
|
+
puts "Skipping: #{merge_request.web_url} #{merge_request.draft == true ? 'as it is a draft' : 'due to different project id'}"
|
61
|
+
return true
|
62
|
+
end
|
63
|
+
|
64
|
+
false
|
65
|
+
end
|
66
|
+
|
67
|
+
def issue_client
|
68
|
+
@issue_client ||= (dry_run ? GitlabClient::IssuesDryClient : GitlabClient::IssuesClient).new(token: token, project: project)
|
69
|
+
end
|
70
|
+
|
71
|
+
def doc_mrs
|
72
|
+
analyzed_merge_requests
|
73
|
+
.select { |mr| mr[:has_docs] }
|
74
|
+
.map.with_index(1) { |mr, index| { "MR#{index}" => mr[:merge_request_web_url] } }
|
75
|
+
end
|
76
|
+
|
77
|
+
def feature_spec_mrs
|
78
|
+
analyzed_merge_requests.select { |mr| mr[:has_feature_specs] }.map.with_index(1) { |mr, index| { "MR#{index}" => mr[:merge_request_web_url] } }
|
79
|
+
end
|
80
|
+
|
81
|
+
def e2e_spec_mrs
|
82
|
+
analyzed_merge_requests.select { |mr| mr[:has_e2e_specs] }.map.with_index(1) { |mr, index| { "MR#{index}" => mr[:merge_request_web_url] } }
|
83
|
+
end
|
84
|
+
|
85
|
+
def feature_flag_mrs
|
86
|
+
analyzed_merge_requests.select { |mr| mr[:added_feature_flag] }.map.with_index(1) { |mr, index| { "MR#{index}" => mr[:merge_request_web_url] } }
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
data/lib/gitlab_quality/test_tooling/feature_readiness/analyzed_items/analyzed_merge_request.rb
ADDED
@@ -0,0 +1,139 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GitlabQuality
|
4
|
+
module TestTooling
|
5
|
+
module FeatureReadiness
|
6
|
+
module AnalyzedItems
|
7
|
+
class AnalyzedMergeRequest
|
8
|
+
CODE_DIRECTORIES = %w[app lib config keeps scripts db].freeze
|
9
|
+
ADDITIONS_THRESHOLD = 5
|
10
|
+
CODE_FILES_EXT = "rb|js|vue"
|
11
|
+
SPEC_FILES_EXT = "rb|js"
|
12
|
+
DOC_FILES_EXT = "tmpl|yaml|yml|md"
|
13
|
+
|
14
|
+
def initialize(merge_request:, token:, project:, group:, dry_run:)
|
15
|
+
@merge_request = merge_request
|
16
|
+
@token = token
|
17
|
+
@project = project
|
18
|
+
@group = group
|
19
|
+
@dry_run = dry_run
|
20
|
+
end
|
21
|
+
|
22
|
+
def analyze
|
23
|
+
@files_with_missing_specs ||= fetch_files_with_missing_specs
|
24
|
+
end
|
25
|
+
|
26
|
+
def result
|
27
|
+
{
|
28
|
+
merge_request_iid: merge_request.iid,
|
29
|
+
merge_request_web_url: merge_request.web_url,
|
30
|
+
files_with_missing_specs: files_with_missing_specs,
|
31
|
+
has_docs: has_docs?,
|
32
|
+
has_feature_specs: has_feature_specs?,
|
33
|
+
has_e2e_specs: has_e2e_specs?,
|
34
|
+
added_feature_flag: added_feature_flag?
|
35
|
+
}
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
attr_reader :merge_request, :token, :project, :group, :dry_run, :files_with_missing_specs
|
41
|
+
|
42
|
+
def merge_request_client
|
43
|
+
@merge_request_client ||= (dry_run ? GitlabClient::MergeRequestsDryClient : GitlabClient::MergeRequestsClient).new(token: token, project: project)
|
44
|
+
end
|
45
|
+
|
46
|
+
def has_docs?
|
47
|
+
doc_diffs.any?
|
48
|
+
end
|
49
|
+
|
50
|
+
def has_feature_specs?
|
51
|
+
spec_diffs('features/').map { |diff| has_significant_addition?(diff) }.any?
|
52
|
+
end
|
53
|
+
|
54
|
+
def added_feature_flag?
|
55
|
+
filter_diffs(%r{^(ee/)?config/feature_flags/.*\.yml$}).any?(&:new_file)
|
56
|
+
end
|
57
|
+
|
58
|
+
def has_e2e_specs?
|
59
|
+
qa_spec_diffs.map { |diff| has_significant_addition?(diff) }.any?
|
60
|
+
end
|
61
|
+
|
62
|
+
def fetch_files_with_missing_specs
|
63
|
+
code_paths_with_missing_specs = []
|
64
|
+
|
65
|
+
CODE_DIRECTORIES.each do |dir|
|
66
|
+
code_diffs = diffs_for_dir(dir, CODE_FILES_EXT)
|
67
|
+
sp_diffs = spec_diffs
|
68
|
+
code_paths_with_missing_specs << collect_paths_with_missing_specs(code_diffs, sp_diffs)
|
69
|
+
end
|
70
|
+
|
71
|
+
code_paths_with_missing_specs.flatten
|
72
|
+
end
|
73
|
+
|
74
|
+
def collect_paths_with_missing_specs(code_diffs, sp_diffs)
|
75
|
+
result_array = []
|
76
|
+
code_diffs.each do |code_diff|
|
77
|
+
result_array << code_diff.new_path if has_significant_addition?(code_diff) && !skip_file?(code_diff.new_path) && !has_matching_spec_for_code?(code_diff, sp_diffs)
|
78
|
+
end
|
79
|
+
|
80
|
+
result_array
|
81
|
+
end
|
82
|
+
|
83
|
+
def has_matching_spec_for_code?(code_diff, sp_diffs)
|
84
|
+
spec_file_names = get_spec_file_names(code_diff.new_path)
|
85
|
+
sp_diffs.any? { |diff| ends_with_names?(diff.new_path, spec_file_names) }
|
86
|
+
end
|
87
|
+
|
88
|
+
def diffs
|
89
|
+
@diffs ||= merge_request_client.merge_request_diffs(merge_request_iid: merge_request.iid)
|
90
|
+
end
|
91
|
+
|
92
|
+
def has_significant_addition?(diff_obj)
|
93
|
+
content_lines = diff_obj.diff.split("\n")
|
94
|
+
|
95
|
+
additions = content_lines.count { |line| line.start_with?("+") }
|
96
|
+
subtractions = content_lines.count { |line| line.start_with?("-") }
|
97
|
+
|
98
|
+
# Return true if additions are above threshold and there are equal or more additions than subtractions
|
99
|
+
additions >= ADDITIONS_THRESHOLD && additions >= subtractions
|
100
|
+
end
|
101
|
+
|
102
|
+
def filter_diffs(pattern)
|
103
|
+
diffs.select { |diff| diff.new_path =~ pattern }
|
104
|
+
end
|
105
|
+
|
106
|
+
def diffs_for_dir(dir, ext)
|
107
|
+
filter_diffs(%r{^(ee/#{Regexp.escape(dir)}|#{Regexp.escape(dir)})/.*\.(#{ext})$})
|
108
|
+
end
|
109
|
+
|
110
|
+
def spec_diffs(spec_sub_dir = '')
|
111
|
+
filter_diffs(%r{^(spec|ee/spec)/#{spec_sub_dir}.+_spec\.(#{SPEC_FILES_EXT})$})
|
112
|
+
end
|
113
|
+
|
114
|
+
def qa_spec_diffs
|
115
|
+
filter_diffs(%r{^qa/qa/specs/features/.+(_spec|_shared_examples|_shared_context)\.rb$})
|
116
|
+
end
|
117
|
+
|
118
|
+
def doc_diffs
|
119
|
+
filter_diffs(%r{^doc/.+\.(#{DOC_FILES_EXT})$}o)
|
120
|
+
end
|
121
|
+
|
122
|
+
def get_spec_file_names(path)
|
123
|
+
filename = File.basename(path, ".*")
|
124
|
+
|
125
|
+
%W[#{filename}_spec #{filename}_shared_examples]
|
126
|
+
end
|
127
|
+
|
128
|
+
def ends_with_names?(path, target_names)
|
129
|
+
[File.basename(path, File.extname(path))].intersect?(target_names)
|
130
|
+
end
|
131
|
+
|
132
|
+
def skip_file?(path)
|
133
|
+
path.end_with?('index.js')
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
@@ -8,7 +8,7 @@ module GitlabQuality
|
|
8
8
|
OPERATIONAL_READINESS_NOTE_ID = '<!-- OPERATIONAL READINESS PRECHECK COMMENT -->'
|
9
9
|
OPERATIONAL_READINESS_TRACKING_LABEL = 'tracking operational readiness'
|
10
10
|
|
11
|
-
def add_operational_readiness_precheck_comment(work_item,
|
11
|
+
def add_operational_readiness_precheck_comment(work_item, work_items_client, label_client)
|
12
12
|
comment = <<~COMMENT
|
13
13
|
#{OPERATIONAL_READINESS_NOTE_ID}
|
14
14
|
## Operational Readiness Pre-Check
|
@@ -24,21 +24,21 @@ module GitlabQuality
|
|
24
24
|
3. Adds new services or changes existing services that will factor into the availability of the GitLab application.
|
25
25
|
COMMENT
|
26
26
|
|
27
|
-
add_labels(ids_for_labels([OPERATIONAL_READINESS_TRACKING_LABEL]), work_item[:id],
|
27
|
+
add_labels(ids_for_labels([OPERATIONAL_READINESS_TRACKING_LABEL], label_client), work_item[:id], work_items_client)
|
28
28
|
|
29
|
-
discussion = existing_note_containing_text(OPERATIONAL_READINESS_NOTE_ID, work_item,
|
29
|
+
discussion = existing_note_containing_text(OPERATIONAL_READINESS_NOTE_ID, work_item[:iid], work_items_client)
|
30
30
|
|
31
31
|
return discussion if discussion
|
32
32
|
|
33
|
-
|
33
|
+
work_items_client.create_discussion(id: work_item[:id], note: comment)
|
34
34
|
|
35
35
|
puts "\nAdded operational readiness comment to epic work item: #{work_item[:webUrl]}\n"
|
36
36
|
|
37
|
-
existing_note_containing_text(OPERATIONAL_READINESS_NOTE_ID, work_item,
|
37
|
+
existing_note_containing_text(OPERATIONAL_READINESS_NOTE_ID, work_item[:iid], work_items_client)
|
38
38
|
end
|
39
39
|
|
40
|
-
def existing_note_containing_text(text,
|
41
|
-
work_item = fetch_work_item(
|
40
|
+
def existing_note_containing_text(text, work_item_iid, client)
|
41
|
+
work_item = fetch_work_item(work_item_iid, client, [:notes])
|
42
42
|
|
43
43
|
work_item[:widgets]
|
44
44
|
.find { |widget| widget.key?(:discussions) }
|
@@ -64,12 +64,26 @@ module GitlabQuality
|
|
64
64
|
client.create_discussion_note(work_item_id: work_item[:id], discussion_id: precheck_comment.dig(:discussion, :id), text: comment_text)
|
65
65
|
end
|
66
66
|
|
67
|
+
def get_labels(work_item)
|
68
|
+
labels_node = work_item[:widgets]&.find { |widget| widget.key?(:labels) }
|
69
|
+
labels_node && labels_node[:labels][:nodes].map { |label| label[:title] }
|
70
|
+
end
|
71
|
+
|
72
|
+
def get_issue_iids(work_item, project)
|
73
|
+
childern_node = work_item[:widgets]&.find { |widget| widget.key?(:children) }
|
74
|
+
childern_node && childern_node[:children][:nodes].filter_map { |issue| issue[:iid] if issue[:workItemType][:name] == "Issue" && issue[:project][:fullPath] == project }
|
75
|
+
end
|
76
|
+
|
77
|
+
def has_label?(work_item, label)
|
78
|
+
get_labels(work_item).include?(label)
|
79
|
+
end
|
80
|
+
|
67
81
|
def add_labels(label_ids, work_item_id, client)
|
68
82
|
client.add_labels(work_item_id: work_item_id, label_ids: label_gids(label_ids))
|
69
83
|
end
|
70
84
|
|
71
|
-
def fetch_work_item(iid, client)
|
72
|
-
client.work_item(workitem_iid: iid)
|
85
|
+
def fetch_work_item(iid, client, widgets = [])
|
86
|
+
client.work_item(workitem_iid: iid, widgets: widgets)
|
73
87
|
end
|
74
88
|
|
75
89
|
def has_a_child_epic?(epic)
|
@@ -92,11 +106,11 @@ module GitlabQuality
|
|
92
106
|
label_ids.map { |label_id| "gid://gitlab/Label/#{label_id}" }
|
93
107
|
end
|
94
108
|
|
95
|
-
def ids_for_labels(labels
|
96
|
-
labels.map { |label| get_id_for_label(label) }
|
109
|
+
def ids_for_labels(labels, label_client)
|
110
|
+
labels.map { |label| get_id_for_label(label, label_client) }
|
97
111
|
end
|
98
112
|
|
99
|
-
def get_id_for_label(label_name)
|
113
|
+
def get_id_for_label(label_name, label_client)
|
100
114
|
labels = label_client.labels(options: { search: label_name })
|
101
115
|
|
102
116
|
raise "No labels found with name: '#{label_name}'" if labels.empty?
|
@@ -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 Evaluation
|
9
|
+
include Concerns::IssueConcern
|
10
|
+
include Concerns::WorkItemConcern
|
11
|
+
|
12
|
+
BASE_LABELS_FOR_SEARCH = ['feature::addition'].freeze
|
13
|
+
FEATURE_READINESS_TRACKING_LABEL = 'tracking feature readiness'
|
14
|
+
|
15
|
+
def initialize(token:, project: nil, group: nil, limit_to_minutes: nil, epic_iid: nil, search_labels: [], dry_run: false)
|
16
|
+
@token = token
|
17
|
+
@project = "#{group}/#{project}"
|
18
|
+
@group = group
|
19
|
+
@limit_to_minutes = limit_to_minutes
|
20
|
+
@epic_iid = epic_iid
|
21
|
+
@search_labels = search_labels
|
22
|
+
@dry_run = dry_run
|
23
|
+
@analyzed_epics = []
|
24
|
+
end
|
25
|
+
|
26
|
+
def invoke!
|
27
|
+
created_after = utc_time_minus_mins(limit_to_minutes)
|
28
|
+
|
29
|
+
epics = fetch_epics(created_after)
|
30
|
+
|
31
|
+
epics.compact.each do |epic|
|
32
|
+
@analyzed_epics << process_epic(epic)
|
33
|
+
rescue StandardError => e
|
34
|
+
puts "ERROR processing epic #{epic[:epic_web_url]} due to: #{e}"
|
35
|
+
end
|
36
|
+
|
37
|
+
report_epics(analyzed_epics)
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
attr_reader :token, :project, :dry_run, :group, :limit_to_minutes, :epic_iid, :search_labels, :analyzed_epics
|
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 fetch_epics(created_after)
|
49
|
+
return [fetch_work_item(epic_iid, work_items_client, [:hierarchy, :labels])] if epic_iid
|
50
|
+
|
51
|
+
work_items_client.paginated_call(:group_work_items,
|
52
|
+
labels: search_labels.concat(BASE_LABELS_FOR_SEARCH).uniq, state: 'opened', created_after: created_after)
|
53
|
+
end
|
54
|
+
|
55
|
+
def label_client
|
56
|
+
@label_client ||= GitlabClient::LabelsClient.new(token: token, project: project)
|
57
|
+
end
|
58
|
+
|
59
|
+
def process_epic(epic)
|
60
|
+
epic = fetch_work_item(epic[:iid], work_items_client, [:hierarchy, :labels])
|
61
|
+
|
62
|
+
AnalyzedItems::AnalyzedEpic.new(epic: epic, token: token, project: project, group: group, dry_run: dry_run)
|
63
|
+
.tap(&:analyze).result
|
64
|
+
end
|
65
|
+
|
66
|
+
def utc_time_minus_mins(mins)
|
67
|
+
(Time.now - (mins * 60)).utc.iso8601 if mins
|
68
|
+
end
|
69
|
+
|
70
|
+
def report_epics(epics)
|
71
|
+
epics.each do |epic|
|
72
|
+
add_labels(ids_for_labels([FEATURE_READINESS_TRACKING_LABEL], label_client), epic[:epic_id], work_items_client)
|
73
|
+
|
74
|
+
Report::FeatureReadiness::ReportOnEpic.report(epic, work_items_client)
|
75
|
+
rescue StandardError => e
|
76
|
+
puts "ERROR reporting epic #{epic[:epic_web_url]} due to: #{e}"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -14,7 +14,7 @@ module GitlabQuality
|
|
14
14
|
|
15
15
|
def initialize(token:, project: nil, group: nil, limit_to_minutes: nil, search_labels: [], issue_is_blocking: false, dry_run: false)
|
16
16
|
@token = token
|
17
|
-
@project = project
|
17
|
+
@project = "#{group}/#{project}"
|
18
18
|
@group = group
|
19
19
|
@limit_to_minutes = limit_to_minutes
|
20
20
|
@search_labels = search_labels
|
@@ -26,7 +26,7 @@ module GitlabQuality
|
|
26
26
|
created_after = utc_time_minus_mins(limit_to_minutes)
|
27
27
|
|
28
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])
|
29
|
+
labels: search_labels.concat(BASE_LABELS_FOR_SEARCH).uniq, state: 'opened', created_after: created_after, extras: [:work_item_fields])
|
30
30
|
|
31
31
|
epics.each do |epic|
|
32
32
|
process_epic(epic)
|
@@ -58,11 +58,11 @@ module GitlabQuality
|
|
58
58
|
end
|
59
59
|
|
60
60
|
def process_epic(epic) # rubocop:disable Metrics/AbcSize
|
61
|
-
epic = fetch_work_item(epic[:iid], work_items_client)
|
61
|
+
epic = fetch_work_item(epic[:iid], work_items_client, [:notes, :linked_items, :labels, :hierarchy])
|
62
62
|
|
63
63
|
return if has_a_child_epic?(epic)
|
64
64
|
|
65
|
-
pre_check_comment = add_operational_readiness_precheck_comment(epic, work_items_client)
|
65
|
+
pre_check_comment = add_operational_readiness_precheck_comment(epic, work_items_client, label_client)
|
66
66
|
|
67
67
|
return unless note_has_emoji?(pre_check_comment, 'white_check_mark') && !has_operational_readiness_issue_linked?(linked_issue_iids(epic), issue_client)
|
68
68
|
|
@@ -72,6 +72,12 @@ module GitlabQuality
|
|
72
72
|
end
|
73
73
|
end
|
74
74
|
|
75
|
+
def related_merge_requests(iid:)
|
76
|
+
handle_gitlab_client_exceptions do
|
77
|
+
client.related_merge_requests(project, iid).auto_paginate
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
75
81
|
def find_issue_discussions(iid:)
|
76
82
|
handle_gitlab_client_exceptions do
|
77
83
|
client.issue_discussions(project, iid, order_by: 'created_at', sort: 'asc').auto_paginate
|
@@ -1,5 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'gitlab'
|
4
|
+
|
5
|
+
module Gitlab
|
6
|
+
# Monkey patch the Gitlab client to allow passing query options
|
7
|
+
class Client
|
8
|
+
def merge_request_diffs(project, merge_request_iid, options = {})
|
9
|
+
get("/projects/#{url_encode(project)}/merge_requests/#{merge_request_iid}/diffs", query: options).auto_paginate
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
3
14
|
module GitlabQuality
|
4
15
|
module TestTooling
|
5
16
|
module GitlabClient
|
@@ -10,6 +21,12 @@ module GitlabQuality
|
|
10
21
|
end
|
11
22
|
end
|
12
23
|
|
24
|
+
def merge_request_diffs(merge_request_iid:)
|
25
|
+
handle_gitlab_client_exceptions do
|
26
|
+
client.merge_request_diffs(project, merge_request_iid, per_page: 100)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
13
30
|
def create_merge_request(title:, source_branch:, target_branch:, description:, labels:, assignee_id: nil, reviewer_ids: [])
|
14
31
|
attrs = {
|
15
32
|
source_branch: source_branch,
|
@@ -33,6 +50,10 @@ module GitlabQuality
|
|
33
50
|
merge_request
|
34
51
|
end
|
35
52
|
|
53
|
+
def merge_request(id:, options: {})
|
54
|
+
client.merge_request(project, id, options)
|
55
|
+
end
|
56
|
+
|
36
57
|
def find(iid: nil, options: {}, &select)
|
37
58
|
select ||= :itself
|
38
59
|
|
@@ -4,16 +4,6 @@ module GitlabQuality
|
|
4
4
|
module TestTooling
|
5
5
|
module GitlabClient
|
6
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
7
|
def find_note(body:, merge_request_iid:)
|
18
8
|
puts "Find note for #{merge_request_iid} with body: #{body} for mr_iid: #{merge_request_iid}"
|
19
9
|
end
|
@@ -3,15 +3,14 @@
|
|
3
3
|
module GitlabQuality
|
4
4
|
module TestTooling
|
5
5
|
module GitlabClient
|
6
|
-
# The GitLab client is used for API access: https://github.com/NARKOZ/gitlab
|
7
6
|
class WorkItemsClient < GitlabGraphqlClient
|
8
|
-
def work_item(workitem_iid:
|
7
|
+
def work_item(workitem_iid:, widgets: [:notes, :linked_items, :labels, :hierarchy])
|
9
8
|
query = <<~GQL
|
10
9
|
query {
|
11
10
|
namespace(fullPath: "#{group}") {
|
12
11
|
workItem(iid: "#{workitem_iid}") {
|
13
12
|
#{work_item_fields}
|
14
|
-
#{work_item_widgets}
|
13
|
+
#{work_item_widgets(widgets)}
|
15
14
|
}
|
16
15
|
}
|
17
16
|
}
|
@@ -19,7 +18,7 @@ module GitlabQuality
|
|
19
18
|
post(query)[:workItem]
|
20
19
|
end
|
21
20
|
|
22
|
-
def group_work_items(labels: [], cursor: '', state: 'opened', created_after: nil, extras: [])
|
21
|
+
def group_work_items(labels: [], cursor: '', state: 'opened', created_after: nil, extras: [:work_item_fields])
|
23
22
|
query = <<~GQL
|
24
23
|
query {
|
25
24
|
group(fullPath: "#{group}") {
|
@@ -73,6 +72,18 @@ module GitlabQuality
|
|
73
72
|
post(query)
|
74
73
|
end
|
75
74
|
|
75
|
+
def update_note(note_id:, body:)
|
76
|
+
query = <<~GQL
|
77
|
+
mutation UpdateNote {
|
78
|
+
updateNote(input: { body: "#{body}", id: "#{note_id}" }) {
|
79
|
+
clientMutationId
|
80
|
+
errors
|
81
|
+
}
|
82
|
+
}
|
83
|
+
GQL
|
84
|
+
post(query)
|
85
|
+
end
|
86
|
+
|
76
87
|
def create_linked_items(work_item_id:, item_ids:, link_type:)
|
77
88
|
query = <<~GQL
|
78
89
|
mutation WorkItemAddLinkedItems {
|
@@ -160,50 +171,76 @@ module GitlabQuality
|
|
160
171
|
GQL
|
161
172
|
end
|
162
173
|
|
163
|
-
def
|
174
|
+
def work_item_widget_notes
|
164
175
|
<<~GQL
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
#{note_fields}
|
172
|
-
}
|
176
|
+
... on WorkItemWidgetNotes {
|
177
|
+
discussions(filter: ONLY_COMMENTS) {
|
178
|
+
nodes {
|
179
|
+
notes {
|
180
|
+
nodes {
|
181
|
+
#{note_fields}
|
173
182
|
}
|
174
183
|
}
|
175
|
-
}
|
176
|
-
}
|
177
|
-
... on WorkItemWidgetLinkedItems {
|
178
|
-
linkedItems {
|
179
|
-
nodes {
|
180
|
-
linkType
|
181
|
-
workItem {
|
182
|
-
#{work_item_fields}
|
183
|
-
}
|
184
184
|
}
|
185
|
-
}
|
186
185
|
}
|
186
|
+
}
|
187
|
+
GQL
|
188
|
+
end
|
187
189
|
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
190
|
+
def work_item_widget_linked_items
|
191
|
+
<<~GQL
|
192
|
+
... on WorkItemWidgetLinkedItems {
|
193
|
+
linkedItems {
|
194
|
+
nodes {
|
195
|
+
linkType
|
196
|
+
workItem {
|
197
|
+
#{work_item_fields}
|
198
|
+
}
|
199
|
+
}
|
193
200
|
}
|
201
|
+
}
|
202
|
+
GQL
|
203
|
+
end
|
204
|
+
|
205
|
+
def work_item_widget_labels
|
206
|
+
<<~GQL
|
207
|
+
... on WorkItemWidgetLabels{
|
208
|
+
labels{
|
209
|
+
nodes{
|
210
|
+
title
|
211
|
+
}
|
194
212
|
}
|
213
|
+
}
|
214
|
+
GQL
|
215
|
+
end
|
195
216
|
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
217
|
+
def work_item_widget_hierarchy
|
218
|
+
<<~GQL
|
219
|
+
... on WorkItemWidgetHierarchy {
|
220
|
+
children {
|
221
|
+
nodes{
|
222
|
+
#{work_item_fields}
|
223
|
+
}
|
202
224
|
}
|
203
225
|
}
|
204
226
|
GQL
|
205
227
|
end
|
206
228
|
|
229
|
+
def work_item_widgets(widgets = [])
|
230
|
+
<<~GQL
|
231
|
+
widgets(onlyTypes: [#{types_for_widgets(widgets)}]) {
|
232
|
+
#{work_item_widget_notes if widgets.include?(:notes)}
|
233
|
+
#{work_item_widget_linked_items if widgets.include?(:linked_items)}
|
234
|
+
#{work_item_widget_labels if widgets.include?(:labels)}
|
235
|
+
#{work_item_widget_hierarchy if widgets.include?(:hierarchy)}
|
236
|
+
}
|
237
|
+
GQL
|
238
|
+
end
|
239
|
+
|
240
|
+
def types_for_widgets(widgets = [])
|
241
|
+
widgets.map(&:upcase).join(', ')
|
242
|
+
end
|
243
|
+
|
207
244
|
# https://docs.gitlab.com/api/graphql/reference/#note
|
208
245
|
def note_fields
|
209
246
|
<<~GQL
|
@@ -0,0 +1,174 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pp'
|
4
|
+
require 'stringio'
|
5
|
+
|
6
|
+
module GitlabQuality
|
7
|
+
module TestTooling
|
8
|
+
module Report
|
9
|
+
module FeatureReadiness
|
10
|
+
class ReportOnEpic
|
11
|
+
FEATURE_READINESS_REPORT_COMMENT_ID = '<!-- FEATURE READINESS REPORT COMMENT -->'
|
12
|
+
|
13
|
+
class << self
|
14
|
+
include GitlabQuality::TestTooling::FeatureReadiness::Concerns::WorkItemConcern
|
15
|
+
|
16
|
+
def report(analyzed_epic, work_item_client)
|
17
|
+
must_haves_report_rows = generate_report_rows(analyzed_epic, :must_haves)
|
18
|
+
should_haves_report_rows = generate_report_rows(analyzed_epic, :should_haves)
|
19
|
+
|
20
|
+
existing_note = existing_note_containing_text(FEATURE_READINESS_REPORT_COMMENT_ID, analyzed_epic[:epic_iid], work_item_client)
|
21
|
+
|
22
|
+
if existing_note
|
23
|
+
work_item_client.update_note(note_id: existing_note[:id],
|
24
|
+
body: comment({ must_haves: must_haves_report_rows, should_haves: should_haves_report_rows }, analyzed_epic).tr('"', "'"))
|
25
|
+
else
|
26
|
+
work_item_client.create_discussion(id: analyzed_epic[:epic_id],
|
27
|
+
note: comment({ must_haves: must_haves_report_rows, should_haves: should_haves_report_rows }, analyzed_epic).tr('"', "'"))
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def generate_report_rows(epic, type)
|
34
|
+
status_checks = check_statuses(epic)
|
35
|
+
create_rows(epic, type, status_checks)
|
36
|
+
end
|
37
|
+
|
38
|
+
def create_rows(epic, type, status_checks)
|
39
|
+
if type == :must_haves
|
40
|
+
[
|
41
|
+
create_documentation_row(epic, status_checks),
|
42
|
+
create_feature_flag_row(epic, status_checks),
|
43
|
+
create_unit_tests_coverage_row(status_checks)
|
44
|
+
|
45
|
+
]
|
46
|
+
else
|
47
|
+
[
|
48
|
+
create_feature_tests_row(epic, status_checks),
|
49
|
+
create_e2e_tests_row(epic, status_checks)
|
50
|
+
]
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def create_documentation_row(epic, status_checks)
|
55
|
+
["Documentation added?", status_icon(status_checks[:has_docs]),
|
56
|
+
prepend_text('Added in:', format_links(epic[:doc_mrs]))]
|
57
|
+
end
|
58
|
+
|
59
|
+
def create_feature_flag_row(epic, status_checks)
|
60
|
+
["Feature Flag added?", status_icon(status_checks[:feature_flag_added]),
|
61
|
+
prepend_text('Added in:', format_links(epic[:feature_flag_mrs]))]
|
62
|
+
end
|
63
|
+
|
64
|
+
def create_feature_tests_row(epic, status_checks)
|
65
|
+
["Feature tests added?", status_icon(status_checks[:has_feature_specs]),
|
66
|
+
format_links(epic[:feature_spec_mrs])]
|
67
|
+
end
|
68
|
+
|
69
|
+
def create_e2e_tests_row(epic, status_checks)
|
70
|
+
["End-to-end tests added?", status_icon(status_checks[:has_e2e_specs]),
|
71
|
+
format_links(epic[:e2e_spec_mrs])]
|
72
|
+
end
|
73
|
+
|
74
|
+
def create_unit_tests_coverage_row(status_checks)
|
75
|
+
["Unit tests coverage complete?", status_icon(status_checks[:has_complete_unit_tests]),
|
76
|
+
prepend_text('Coverage missing for:', format_links(status_checks[:missing_specs]))]
|
77
|
+
end
|
78
|
+
|
79
|
+
def prepend_text(prepend_text, text)
|
80
|
+
return "#{prepend_text} #{text}" unless text.empty?
|
81
|
+
|
82
|
+
text
|
83
|
+
end
|
84
|
+
|
85
|
+
def check_statuses(epic)
|
86
|
+
{
|
87
|
+
has_docs: epic[:doc_mrs].any?,
|
88
|
+
feature_flag_added: epic[:feature_flag_mrs].any?,
|
89
|
+
has_feature_specs: epic[:feature_spec_mrs].any?,
|
90
|
+
has_e2e_specs: epic[:e2e_spec_mrs].any?,
|
91
|
+
missing_specs: missing_spec_mrs(epic),
|
92
|
+
has_complete_unit_tests: missing_spec_mrs(epic).empty?
|
93
|
+
}
|
94
|
+
end
|
95
|
+
|
96
|
+
def comment(rows, epic)
|
97
|
+
# Generate markdown table
|
98
|
+
must_haves_table_rows = rows[:must_haves].map do |description, status, links|
|
99
|
+
"| #{description} | #{status} | #{links} |"
|
100
|
+
end.join("\n")
|
101
|
+
|
102
|
+
should_haves_table_rows = rows[:should_haves].map do |description, status, links|
|
103
|
+
"| #{description} | #{status} | #{links} |"
|
104
|
+
end.join("\n")
|
105
|
+
|
106
|
+
<<~COMMENT
|
107
|
+
#{FEATURE_READINESS_REPORT_COMMENT_ID}
|
108
|
+
|
109
|
+
# :vertical_traffic_light: Feature Readiness Evaluation Report
|
110
|
+
|
111
|
+
### :octagonal_sign: Must haves
|
112
|
+
|
113
|
+
| Evaluation | Result | Notes |
|
114
|
+
|------------|--------|-------|
|
115
|
+
#{must_haves_table_rows}
|
116
|
+
|
117
|
+
### :warning: Should haves
|
118
|
+
|
119
|
+
| Evaluation | Result | Notes |
|
120
|
+
|------------|--------|-------|
|
121
|
+
#{should_haves_table_rows}
|
122
|
+
|
123
|
+
#{data(epic)}
|
124
|
+
|
125
|
+
---
|
126
|
+
|
127
|
+
_Please note that this automation is under testing. Please add any feedback on [this issue](https://gitlab.com/gitlab-org/quality/quality-engineering/team-tasks/-/issues/3587)._
|
128
|
+
|
129
|
+
COMMENT
|
130
|
+
end
|
131
|
+
|
132
|
+
def status_icon(condition)
|
133
|
+
condition ? ':white_check_mark:' : ':x:'
|
134
|
+
end
|
135
|
+
|
136
|
+
def format_links(data)
|
137
|
+
return '' if data.empty?
|
138
|
+
|
139
|
+
data.map do |item|
|
140
|
+
item.map { |key, url| "[#{key}](#{url})" }.first
|
141
|
+
end.join(", ")
|
142
|
+
end
|
143
|
+
|
144
|
+
def missing_spec_mrs(epic)
|
145
|
+
epic[:issues].flat_map do |issue|
|
146
|
+
issue[:merge_requests].flat_map do |mr|
|
147
|
+
next [] unless mr[:files_with_missing_specs]&.any?
|
148
|
+
|
149
|
+
mr[:files_with_missing_specs].map do |file|
|
150
|
+
{ file => mr[:merge_request_web_url] }
|
151
|
+
end
|
152
|
+
end.compact
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def data(epic)
|
157
|
+
output = StringIO.new
|
158
|
+
PP.pp(epic, output)
|
159
|
+
<<~DATA
|
160
|
+
<details><summary>Expand for data</summary>
|
161
|
+
|
162
|
+
```ruby
|
163
|
+
#{output.string}
|
164
|
+
```
|
165
|
+
|
166
|
+
</details>
|
167
|
+
DATA
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
@@ -318,7 +318,7 @@ module GitlabQuality
|
|
318
318
|
initialize_gitlab_ops_client
|
319
319
|
|
320
320
|
if Runtime::Env.ci_pipeline_url.include?('ops.gitlab.net')
|
321
|
-
pipeline = ops_gitlab_client.find_pipeline(
|
321
|
+
pipeline = ops_gitlab_client.find_pipeline(Runtime::Env.ci_project_path, Runtime::Env.ci_pipeline_id.to_i)
|
322
322
|
generate_ops_gitlab_diff(pipeline)
|
323
323
|
else
|
324
324
|
pipeline = gitlab.find_pipeline(project, Runtime::Env.ci_pipeline_id.to_i)
|
@@ -342,6 +342,8 @@ module GitlabQuality
|
|
342
342
|
end
|
343
343
|
|
344
344
|
def fetch_deployment_info(pipeline)
|
345
|
+
return 'No pipeline name set.' unless Runtime::Env.ci_pipeline_name
|
346
|
+
|
345
347
|
pipeline_deploy_version = Runtime::Env.ci_pipeline_name.match(/(\d+\.\d+\.\d+)(?:-|$)/)&.captures&.first
|
346
348
|
deployments = fetch_deployments(ops_gitlab_client, pipeline)
|
347
349
|
found_deployment = find_matching_deployment(pipeline_deploy_version, deployments)
|
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: 2.
|
4
|
+
version: 2.12.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: 2025-
|
11
|
+
date: 2025-05-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: climate_control
|
@@ -380,16 +380,22 @@ dependencies:
|
|
380
380
|
name: rspec-parameterized
|
381
381
|
requirement: !ruby/object:Gem::Requirement
|
382
382
|
requirements:
|
383
|
-
- - "
|
383
|
+
- - ">="
|
384
|
+
- !ruby/object:Gem::Version
|
385
|
+
version: '1.0'
|
386
|
+
- - "<"
|
384
387
|
- !ruby/object:Gem::Version
|
385
|
-
version:
|
388
|
+
version: '3.0'
|
386
389
|
type: :runtime
|
387
390
|
prerelease: false
|
388
391
|
version_requirements: !ruby/object:Gem::Requirement
|
389
392
|
requirements:
|
390
|
-
- - "
|
393
|
+
- - ">="
|
394
|
+
- !ruby/object:Gem::Version
|
395
|
+
version: '1.0'
|
396
|
+
- - "<"
|
391
397
|
- !ruby/object:Gem::Version
|
392
|
-
version:
|
398
|
+
version: '3.0'
|
393
399
|
- !ruby/object:Gem::Dependency
|
394
400
|
name: table_print
|
395
401
|
requirement: !ruby/object:Gem::Requirement
|
@@ -431,7 +437,8 @@ executables:
|
|
431
437
|
- detect-infrastructure-failures
|
432
438
|
- existing-test-health-issue
|
433
439
|
- failed-test-issues
|
434
|
-
- feature-readiness-
|
440
|
+
- feature-readiness-checklist
|
441
|
+
- feature-readiness-evaluation
|
435
442
|
- flaky-test-issues
|
436
443
|
- generate-test-session
|
437
444
|
- knapsack-report-issues
|
@@ -463,7 +470,8 @@ files:
|
|
463
470
|
- exe/detect-infrastructure-failures
|
464
471
|
- exe/existing-test-health-issue
|
465
472
|
- exe/failed-test-issues
|
466
|
-
- exe/feature-readiness-
|
473
|
+
- exe/feature-readiness-checklist
|
474
|
+
- exe/feature-readiness-evaluation
|
467
475
|
- exe/flaky-test-issues
|
468
476
|
- exe/generate-test-session
|
469
477
|
- exe/knapsack-report-issues
|
@@ -479,8 +487,12 @@ files:
|
|
479
487
|
- lib/gitlab_quality/test_tooling.rb
|
480
488
|
- lib/gitlab_quality/test_tooling/concerns/find_set_dri.rb
|
481
489
|
- lib/gitlab_quality/test_tooling/failed_jobs_table.rb
|
490
|
+
- lib/gitlab_quality/test_tooling/feature_readiness/analyzed_items/analyzed_epic.rb
|
491
|
+
- lib/gitlab_quality/test_tooling/feature_readiness/analyzed_items/analyzed_issue.rb
|
492
|
+
- lib/gitlab_quality/test_tooling/feature_readiness/analyzed_items/analyzed_merge_request.rb
|
482
493
|
- lib/gitlab_quality/test_tooling/feature_readiness/concerns/issue_concern.rb
|
483
494
|
- lib/gitlab_quality/test_tooling/feature_readiness/concerns/work_item_concern.rb
|
495
|
+
- lib/gitlab_quality/test_tooling/feature_readiness/evaluation.rb
|
484
496
|
- lib/gitlab_quality/test_tooling/feature_readiness/operational_readiness_check.rb
|
485
497
|
- lib/gitlab_quality/test_tooling/gitlab_client/branches_client.rb
|
486
498
|
- lib/gitlab_quality/test_tooling/gitlab_client/branches_dry_client.rb
|
@@ -507,6 +519,7 @@ files:
|
|
507
519
|
- lib/gitlab_quality/test_tooling/report/concerns/results_reporter.rb
|
508
520
|
- lib/gitlab_quality/test_tooling/report/concerns/utils.rb
|
509
521
|
- lib/gitlab_quality/test_tooling/report/failed_test_issue.rb
|
522
|
+
- lib/gitlab_quality/test_tooling/report/feature_readiness/report_on_epic.rb
|
510
523
|
- lib/gitlab_quality/test_tooling/report/flaky_test_issue.rb
|
511
524
|
- lib/gitlab_quality/test_tooling/report/generate_test_session.rb
|
512
525
|
- lib/gitlab_quality/test_tooling/report/health_problem_reporter.rb
|