gitlab_quality-test_tooling 2.10.0 → 2.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +3 -3
  3. data/README.md +22 -5
  4. data/exe/{feature-readiness-check → feature-readiness-checklist} +2 -2
  5. data/exe/feature-readiness-evaluation +62 -0
  6. data/exe/relate-failure-issue +5 -0
  7. data/lib/gitlab_quality/test_tooling/feature_readiness/analyzed_items/analyzed_epic.rb +94 -0
  8. data/lib/gitlab_quality/test_tooling/feature_readiness/analyzed_items/analyzed_issue.rb +92 -0
  9. data/lib/gitlab_quality/test_tooling/feature_readiness/analyzed_items/analyzed_merge_request.rb +139 -0
  10. data/lib/gitlab_quality/test_tooling/feature_readiness/concerns/work_item_concern.rb +26 -12
  11. data/lib/gitlab_quality/test_tooling/feature_readiness/evaluation.rb +82 -0
  12. data/lib/gitlab_quality/test_tooling/feature_readiness/operational_readiness_check.rb +4 -4
  13. data/lib/gitlab_quality/test_tooling/gitlab_client/issues_client.rb +7 -1
  14. data/lib/gitlab_quality/test_tooling/gitlab_client/merge_requests_client.rb +21 -0
  15. data/lib/gitlab_quality/test_tooling/gitlab_client/merge_requests_dry_client.rb +0 -10
  16. data/lib/gitlab_quality/test_tooling/gitlab_client/work_items_client.rb +71 -34
  17. data/lib/gitlab_quality/test_tooling/report/concerns/results_reporter.rb +1 -1
  18. data/lib/gitlab_quality/test_tooling/report/concerns/utils.rb +3 -3
  19. data/lib/gitlab_quality/test_tooling/report/feature_readiness/report_on_epic.rb +174 -0
  20. data/lib/gitlab_quality/test_tooling/report/flaky_test_issue.rb +1 -1
  21. data/lib/gitlab_quality/test_tooling/report/generate_test_session.rb +1 -1
  22. data/lib/gitlab_quality/test_tooling/report/group_issues/error_message_normalizer.rb +49 -0
  23. data/lib/gitlab_quality/test_tooling/report/group_issues/error_pattern_matcher.rb +36 -0
  24. data/lib/gitlab_quality/test_tooling/report/group_issues/failure_processor.rb +73 -0
  25. data/lib/gitlab_quality/test_tooling/report/group_issues/group_results_in_issues.rb +48 -0
  26. data/lib/gitlab_quality/test_tooling/report/group_issues/incident_checker.rb +61 -0
  27. data/lib/gitlab_quality/test_tooling/report/group_issues/issue_base.rb +48 -0
  28. data/lib/gitlab_quality/test_tooling/report/group_issues/issue_creator.rb +44 -0
  29. data/lib/gitlab_quality/test_tooling/report/group_issues/issue_finder.rb +79 -0
  30. data/lib/gitlab_quality/test_tooling/report/group_issues/issue_formatter.rb +83 -0
  31. data/lib/gitlab_quality/test_tooling/report/group_issues/issue_manager.rb +33 -0
  32. data/lib/gitlab_quality/test_tooling/report/group_issues/issue_updater.rb +87 -0
  33. data/lib/gitlab_quality/test_tooling/report/relate_failure_issue.rb +149 -12
  34. data/lib/gitlab_quality/test_tooling/runtime/env.rb +1 -1
  35. data/lib/gitlab_quality/test_tooling/test_meta/processor/add_to_blocking_processor.rb +1 -1
  36. data/lib/gitlab_quality/test_tooling/test_meta/processor/add_to_quarantine_processor.rb +1 -1
  37. data/lib/gitlab_quality/test_tooling/test_meta/test_meta_updater.rb +38 -8
  38. data/lib/gitlab_quality/test_tooling/test_result/base_test_result.rb +17 -4
  39. data/lib/gitlab_quality/test_tooling/version.rb +1 -1
  40. data/lib/gitlab_quality/test_tooling.rb +2 -0
  41. metadata +34 -10
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 548aaf25c91235b88c18601ebdeb3cecf4c6b6caedbaceb6ddec4bdbaaf3519a
4
- data.tar.gz: 04de3d4ce782f42ab487c9d7872f80236d7115250bd28b4a58e02c83aebc0ec9
3
+ metadata.gz: 7db77718644f72fd0a096182fdf76ad10c912002f0770e245ac782a9b238b690
4
+ data.tar.gz: 7b67154f177663fdc2bd19882b128e5eaa3540f20c31c50fddcf73c6661590ff
5
5
  SHA512:
6
- metadata.gz: f5cb5a3f95f291e7f00b7454f9fe211c2f2db662bb08fe47e72c587468dc2658b4ce8019fdaf963de4f5845032863bb71c69d684a5d10702b0efb28caea207eb
7
- data.tar.gz: 92ef6b0c7360c676294b9f9e7d15e3ce2b0651ceb5b0e2ccc65a328ae0806f87997a2fed1a74ca398427ba6ecf431299e9bf16e952c4cd1c10cd26332de06332
6
+ metadata.gz: 0776f0aef2a4076a7a0bfd71c902d57400eea5388982e62583d689038137ed7fe1166368dabfb006999069e32525fd47d752faf92afffa536b9cbe72303c207f
7
+ data.tar.gz: c3145f170b88abebe7e2a6875d94ccca55fccb370972f37c7679479257186a5e2d73eaedb6bb799139c72bfc32ade74f60e1e5cf07aebb5aa7e3bb8621c7a65d
data/Gemfile.lock CHANGED
@@ -1,17 +1,17 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- gitlab_quality-test_tooling (2.10.0)
4
+ gitlab_quality-test_tooling (2.18.0)
5
5
  activesupport (>= 7.0, < 7.3)
6
6
  amatch (~> 0.4.1)
7
7
  fog-google (~> 1.24, >= 1.24.1)
8
- gitlab (>= 4.19, < 6.0)
8
+ gitlab (>= 4.19, < 7.0)
9
9
  http (~> 5.0)
10
10
  influxdb-client (~> 3.1)
11
11
  nokogiri (~> 1.10)
12
12
  parallel (>= 1, < 2)
13
13
  rainbow (>= 3, < 4)
14
- rspec-parameterized (~> 1.0.0)
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-check`
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-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
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
- -b, --blocking Any issues created and linked to the epic will block the epic
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, 'Can be an integer or a group/project string') do |project|
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, 'Can be an integer or a group/project string') do |group|
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
@@ -58,6 +58,10 @@ options = OptionParser.new do |opts|
58
58
  params[:dry_run] = true
59
59
  end
60
60
 
61
+ opts.on("--group-similar", "Enable grouping similar issues") do
62
+ params[:group_similar] = true
63
+ end
64
+
61
65
  opts.on_tail('-v', '--version', 'Show the version') do
62
66
  require_relative "../lib/gitlab_quality/test_tooling/version"
63
67
  puts "#{$PROGRAM_NAME} : #{GitlabQuality::TestTooling::VERSION}"
@@ -66,6 +70,7 @@ options = OptionParser.new do |opts|
66
70
 
67
71
  opts.on_tail('-h', '--help', 'Show the usage') do
68
72
  puts "Purpose: Relate test failures to failure issues from RSpec report files (JSON or JUnit XML)"
73
+ puts ""
69
74
  puts opts
70
75
  exit
71
76
  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
@@ -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, client)
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], client)
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, client)
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
- client.create_discussion(id: work_item[:id], note: comment)
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, client)
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, work_item, client)
41
- work_item = fetch_work_item(work_item[:iid], client)
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