gitlab_quality-test_tooling 2.10.0 → 2.15.3
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 +7 -1
- data/lib/gitlab_quality/test_tooling/gitlab_client/issues_dry_client.rb +1 -1
- 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/concerns/results_reporter.rb +1 -1
- data/lib/gitlab_quality/test_tooling/report/concerns/utils.rb +3 -3
- data/lib/gitlab_quality/test_tooling/report/feature_readiness/report_on_epic.rb +174 -0
- data/lib/gitlab_quality/test_tooling/report/generate_test_session.rb +1 -1
- data/lib/gitlab_quality/test_tooling/report/relate_failure_issue.rb +21 -7
- data/lib/gitlab_quality/test_tooling/runtime/env.rb +1 -1
- data/lib/gitlab_quality/test_tooling/test_meta/processor/add_to_blocking_processor.rb +1 -1
- data/lib/gitlab_quality/test_tooling/test_meta/processor/add_to_quarantine_processor.rb +1 -1
- data/lib/gitlab_quality/test_tooling/test_result/base_test_result.rb +13 -4
- 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: 699557a2175d1f39cb2f4c786b42b0ee286cbf55d6e26ebe7b46e9a6335af56b
|
4
|
+
data.tar.gz: d32b217fe2e6dc6c0d9c0382b9e0eb91a59467908f5dbfa1115e9079bffb52d4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5a79d49a6389fcf4c1e87fd1679bea287c9e641b5b7ec8fde047c932a5e74b89446becdaa26be92273fe985876358b1b1656e847f37bde813c1c752c31150b42
|
7
|
+
data.tar.gz: a8d34a50788287842fd7df3e4eee0f66e3e2e5f994d45ca41eb18336ba1087d3dbaa3537dbc7f03198e6e1ed663269986cfd9f3292d5a9d9e7289473f8db02ea
|
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.15.3)
|
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, < 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
|
|