gitlab-qa 6.7.0 → 6.8.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1615e54144d0b669245a15b6a5874efc1d00157fe859dbc6ff2e723380e9da10
4
- data.tar.gz: f3ccd0528987c838c793d1bbc2f742833584756bf7a87c58c29b60fa2f7b6ac3
3
+ metadata.gz: daf050b7d871f8eefdc1fa68b052608c11ee186b927fe89accfceca15291ed45
4
+ data.tar.gz: 4510e98db8e41c11a1585070a39cd19769384917cff551195942955974dcd78d
5
5
  SHA512:
6
- metadata.gz: 47fce0af7a769ccb9dca14c87f3d1f169495324f753f3db60bb8aa10fc6e05a4612f0bc1704e884dca3d7349bd6d0972d5ca77421bba71f60fd03258e24bc52b
7
- data.tar.gz: f1d65176693a0091989392eec19425a34d2cbc1ee92cb428178ee22ca438b19e7246fd1306bfdaaa73f5d531a6c01ad5a6b2ee1324ca72ef0ba9b255ae4782f3
6
+ metadata.gz: 3a3a6dc22aac1a77fe46f1379a43b489d08cc771b38ea629f9ff3a8f24fd9886f767ed3b3ca8aa18bfeabadc9019aefe2933261c969167e6c8e95f7f75e2c6a1
7
+ data.tar.gz: c612e3b42691a28864bcf703fdf89cbbf57f75817341f867d1e8486a25adcbfcf262f3506157841d734813e699e628620dd524af22b4f8da17c689a96c6116cf
@@ -2,6 +2,7 @@ stages:
2
2
  - check
3
3
  - release
4
4
  - test
5
+ - report
5
6
  - notify
6
7
 
7
8
  default:
@@ -48,6 +49,10 @@ variables:
48
49
  QA_CAN_TEST_GIT_PROTOCOL_V2: "true"
49
50
  QA_CAN_TEST_PRAEFECT: "false"
50
51
  QA_TESTCASES_REPORTING_PROJECT: "gitlab-org/quality/testcases"
52
+ QA_FAILURES_REPORTING_PROJECT: "gitlab-org/gitlab"
53
+ # The --dry-run or --max-diff-ratio option can be set to modify the behavior of `exe/gitlab-qa-report --relate-failure-issue` without releasing a new gem version.
54
+ QA_FAILURES_REPORTER_OPTIONS: "--dry-run"
55
+ QA_TESTCASE_SESSIONS_PROJECT: "gitlab-org/quality/testcase-sessions"
51
56
 
52
57
  .check-base:
53
58
  stage: check
@@ -95,6 +100,7 @@ release:
95
100
  - exe/gitlab-qa-report --update-screenshot-path "gitlab-qa-run-*/**/rspec-*.xml"
96
101
  - export GITLAB_QA_ACCESS_TOKEN="$GITLAB_QA_PRODUCTION_ACCESS_TOKEN"
97
102
  - if [ "$TOP_UPSTREAM_SOURCE_REF" == "master" ] || [[ "$TOP_UPSTREAM_SOURCE_JOB" == https://ops.gitlab.net* ]]; then exe/gitlab-qa-report --report-in-issues "gitlab-qa-run-*/**/rspec-*.json" --project "$QA_TESTCASES_REPORTING_PROJECT" || true; fi
103
+ - if [ "$TOP_UPSTREAM_SOURCE_REF" == "master" ] || [[ "$TOP_UPSTREAM_SOURCE_JOB" == https://ops.gitlab.net* ]]; then exe/gitlab-qa-report --relate-failure-issue "gitlab-qa-run-*/**/rspec-*.json" --project "$QA_FAILURES_REPORTING_PROJECT" $QA_FAILURES_REPORTER_OPTIONS || true; fi
98
104
  - exit $test_run_exit_code
99
105
 
100
106
  .ce-qa:
@@ -333,6 +339,7 @@ ce:update:
333
339
  - exe/gitlab-qa Test::Omnibus::Update ${RELEASE:=CE} ${RELEASE:=CE} -- $RSPEC_REPORT_OPTS || test_run_exit_code=$?
334
340
  - export GITLAB_QA_ACCESS_TOKEN="$GITLAB_QA_PRODUCTION_ACCESS_TOKEN"
335
341
  - if [ "$TOP_UPSTREAM_SOURCE_REF" == "master" ] || [[ "$TOP_UPSTREAM_SOURCE_JOB" == https://ops.gitlab.net* ]]; then exe/gitlab-qa-report --report-in-issues "gitlab-qa-run-*/**/rspec-*.json" --project "$QA_TESTCASES_REPORTING_PROJECT" || true; fi
342
+ - if [ "$TOP_UPSTREAM_SOURCE_REF" == "master" ] || [[ "$TOP_UPSTREAM_SOURCE_JOB" == https://ops.gitlab.net* ]]; then exe/gitlab-qa-report --relate-failure-issue "gitlab-qa-run-*/**/rspec-*.json" --project "$QA_FAILURES_REPORTING_PROJECT" $QA_FAILURES_REPORTER_OPTIONS || true; fi
336
343
  - exit $test_run_exit_code
337
344
  extends:
338
345
  - .test
@@ -347,6 +354,7 @@ ce:update-quarantine:
347
354
  - exe/gitlab-qa Test::Omnibus::Update ${RELEASE:=CE} ${RELEASE:=CE} -- --tag quarantine --tag ~orchestrated $RSPEC_REPORT_OPTS || test_run_exit_code=$?
348
355
  - export GITLAB_QA_ACCESS_TOKEN="$GITLAB_QA_PRODUCTION_ACCESS_TOKEN"
349
356
  - if [ "$TOP_UPSTREAM_SOURCE_REF" == "master" ] || [[ "$TOP_UPSTREAM_SOURCE_JOB" == https://ops.gitlab.net* ]]; then exe/gitlab-qa-report --report-in-issues "gitlab-qa-run-*/**/rspec-*.json" --project "$QA_TESTCASES_REPORTING_PROJECT" || true; fi
357
+ - if [ "$TOP_UPSTREAM_SOURCE_REF" == "master" ] || [[ "$TOP_UPSTREAM_SOURCE_JOB" == https://ops.gitlab.net* ]]; then exe/gitlab-qa-report --relate-failure-issue "gitlab-qa-run-*/**/rspec-*.json" --project "$QA_FAILURES_REPORTING_PROJECT" $QA_FAILURES_REPORTER_OPTIONS || true; fi
350
358
  - exit $test_run_exit_code
351
359
  extends:
352
360
  - .test
@@ -360,6 +368,7 @@ ee:update:
360
368
  - exe/gitlab-qa Test::Omnibus::Update ${RELEASE:=EE} ${RELEASE:=EE} -- $RSPEC_REPORT_OPTS || test_run_exit_code=$?
361
369
  - export GITLAB_QA_ACCESS_TOKEN="$GITLAB_QA_PRODUCTION_ACCESS_TOKEN"
362
370
  - if [ "$TOP_UPSTREAM_SOURCE_REF" == "master" ] || [[ "$TOP_UPSTREAM_SOURCE_JOB" == https://ops.gitlab.net* ]]; then exe/gitlab-qa-report --report-in-issues "gitlab-qa-run-*/**/rspec-*.json" --project "$QA_TESTCASES_REPORTING_PROJECT" || true; fi
371
+ - if [ "$TOP_UPSTREAM_SOURCE_REF" == "master" ] || [[ "$TOP_UPSTREAM_SOURCE_JOB" == https://ops.gitlab.net* ]]; then exe/gitlab-qa-report --relate-failure-issue "gitlab-qa-run-*/**/rspec-*.json" --project "$QA_FAILURES_REPORTING_PROJECT" $QA_FAILURES_REPORTER_OPTIONS || true; fi
363
372
  - exit $test_run_exit_code
364
373
  extends:
365
374
  - .test
@@ -374,6 +383,7 @@ ee:update-quarantine:
374
383
  - exe/gitlab-qa Test::Omnibus::Update ${RELEASE:=EE} ${RELEASE:=EE} -- --tag quarantine --tag ~orchestrated $RSPEC_REPORT_OPTS || test_run_exit_code=$?
375
384
  - export GITLAB_QA_ACCESS_TOKEN="$GITLAB_QA_PRODUCTION_ACCESS_TOKEN"
376
385
  - if [ "$TOP_UPSTREAM_SOURCE_REF" == "master" ] || [[ "$TOP_UPSTREAM_SOURCE_JOB" == https://ops.gitlab.net* ]]; then exe/gitlab-qa-report --report-in-issues "gitlab-qa-run-*/**/rspec-*.json" --project "$QA_TESTCASES_REPORTING_PROJECT" || true; fi
386
+ - if [ "$TOP_UPSTREAM_SOURCE_REF" == "master" ] || [[ "$TOP_UPSTREAM_SOURCE_JOB" == https://ops.gitlab.net* ]]; then exe/gitlab-qa-report --relate-failure-issue "gitlab-qa-run-*/**/rspec-*.json" --project "$QA_FAILURES_REPORTING_PROJECT" $QA_FAILURES_REPORTER_OPTIONS || true; fi
377
387
  - exit $test_run_exit_code
378
388
  extends:
379
389
  - .test
@@ -941,6 +951,22 @@ staging:
941
951
  - .only-qa
942
952
  allow_failure: true
943
953
 
954
+ generate_test_session:
955
+ stage: report
956
+ rules:
957
+ - if: '$TOP_UPSTREAM_SOURCE_JOB && $TOP_UPSTREAM_SOURCE_REF == "master"'
958
+ when: always
959
+ - if: '$TOP_UPSTREAM_SOURCE_JOB =~ /\Ahttps:\/\/ops.gitlab.net\//'
960
+ when: always
961
+ artifacts:
962
+ when: always
963
+ expire_in: 10d
964
+ paths:
965
+ - REPORT_ISSUE_URL
966
+ script:
967
+ - export GITLAB_QA_ACCESS_TOKEN="$GITLAB_QA_PRODUCTION_ACCESS_TOKEN"
968
+ - exe/gitlab-qa-report --generate-test-session "gitlab-qa-run-*/**/rspec-*.json" --project "$QA_TESTCASE_SESSIONS_PROJECT"
969
+
944
970
  .notify_upstream_commit:
945
971
  stage: notify
946
972
  image: ruby:2.6
@@ -966,7 +992,7 @@ notify_upstream_commit:failure:
966
992
  .notify_slack:
967
993
  image: alpine
968
994
  stage: notify
969
- dependencies: []
995
+ dependencies: ['generate_test_session']
970
996
  cache: {}
971
997
  before_script:
972
998
  - apk update && apk add git curl bash
@@ -985,4 +1011,4 @@ notify_slack:
985
1011
  - echo "RELEASE is ${RELEASE}"
986
1012
  - echo "CI_PIPELINE_URL is $CI_PIPELINE_URL"
987
1013
  - echo "TOP_UPSTREAM_SOURCE_JOB is $TOP_UPSTREAM_SOURCE_JOB"
988
- - bin/slack $NOTIFY_CHANNEL "☠️ QA against $RELEASE failed! ☠️ See $CI_PIPELINE_URL (triggered from $TOP_UPSTREAM_SOURCE_JOB)" ci_failing
1014
+ - 'bin/slack $NOTIFY_CHANNEL "☠️ QA against $RELEASE failed! ☠️ See the test session report: $(cat REPORT_ISSUE_URL), and pipeline: $CI_PIPELINE_URL (triggered from $TOP_UPSTREAM_SOURCE_JOB)" ci_failing'
@@ -99,12 +99,15 @@ module Gitlab
99
99
 
100
100
  module Report
101
101
  autoload :GitlabIssueClient, 'gitlab/qa/report/gitlab_issue_client'
102
+ autoload :GitlabIssueDryClient, 'gitlab/qa/report/gitlab_issue_dry_client'
102
103
  autoload :BaseTestResults, 'gitlab/qa/report/base_test_results'
104
+ autoload :RelateFailureIssue, 'gitlab/qa/report/relate_failure_issue'
103
105
  autoload :JsonTestResults, 'gitlab/qa/report/json_test_results'
104
106
  autoload :JUnitTestResults, 'gitlab/qa/report/junit_test_results'
105
107
  autoload :PrepareStageReports, 'gitlab/qa/report/prepare_stage_reports'
106
108
  autoload :ReportAsIssue, 'gitlab/qa/report/report_as_issue'
107
109
  autoload :ResultsInIssues, 'gitlab/qa/report/results_in_issues'
110
+ autoload :GenerateTestSession, 'gitlab/qa/report/generate_test_session'
108
111
  autoload :SummaryTable, 'gitlab/qa/report/summary_table'
109
112
  autoload :TestResult, 'gitlab/qa/report/test_result'
110
113
  autoload :UpdateScreenshotPath, 'gitlab/qa/report/update_screenshot_path'
@@ -6,8 +6,11 @@ module Gitlab
6
6
  class BaseTestResults
7
7
  include Enumerable
8
8
 
9
+ attr_reader :path
10
+
9
11
  def initialize(path)
10
- @results = parse(path)
12
+ @path = path
13
+ @results = parse
11
14
  @testcases = process
12
15
  end
13
16
 
@@ -15,7 +18,7 @@ module Gitlab
15
18
  testcases.each(&block)
16
19
  end
17
20
 
18
- def write(path)
21
+ def write
19
22
  raise NotImplementedError
20
23
  end
21
24
 
@@ -23,7 +26,7 @@ module Gitlab
23
26
 
24
27
  attr_reader :results, :testcases
25
28
 
26
- def parse(path)
29
+ def parse
27
30
  raise NotImplementedError
28
31
  end
29
32
 
@@ -0,0 +1,203 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gitlab
4
+ module QA
5
+ module Report
6
+ class GenerateTestSession < ReportAsIssue
7
+ private
8
+
9
+ def run!
10
+ puts "Generating test results in `#{files.join(',')}` as issues in project `#{project}` via the API at `#{Runtime::Env.gitlab_api_base}`."
11
+
12
+ tests = Dir.glob(files).flat_map do |path|
13
+ puts "Loading tests in #{path}"
14
+
15
+ Report::JsonTestResults.new(path).to_a
16
+ end
17
+
18
+ issue = gitlab.create_issue(
19
+ title: "Test session report | #{Runtime::Env.deploy_environment}",
20
+ description: generate_description(tests),
21
+ labels: ['Quality', 'QA', 'triage report']
22
+ )
23
+
24
+ File.write('REPORT_ISSUE_URL', issue.web_url)
25
+ end
26
+
27
+ def generate_description(tests)
28
+ <<~MARKDOWN
29
+ ## Session summary
30
+
31
+ * Deploy version: #{Runtime::Env.deploy_version}
32
+ * Pipeline: [#{Runtime::Env.ci_pipeline_id}](#{Runtime::Env.ci_pipeline_url})
33
+ #{generate_summary(tests: tests)}
34
+
35
+ #{generate_stages_listing(tests)}
36
+
37
+ ## Release QA issue
38
+
39
+ * #{Runtime::Env.qa_issue_url}
40
+ MARKDOWN
41
+ end
42
+
43
+ def generate_summary(tests:, tests_by_status: nil)
44
+ tests_by_status ||= tests.group_by(&:status)
45
+ total = tests.size
46
+ passed = tests_by_status['passed']&.size || 0
47
+ failed = tests_by_status['failed']&.size || 0
48
+ others = total - passed - failed
49
+
50
+ <<~MARKDOWN.chomp
51
+ * Total #{total} tests
52
+ * Passed #{passed} tests
53
+ * Failed #{failed} tests
54
+ * #{others} other tests (usually skipped)
55
+ MARKDOWN
56
+ end
57
+
58
+ def generate_stages_listing(tests)
59
+ generate_tests_by_stage(tests).map do |stage, tests_for_stage|
60
+ tests_by_status = tests_for_stage.group_by(&:status)
61
+
62
+ <<~MARKDOWN.chomp
63
+ ### #{stage&.capitalize || 'Unknown'}
64
+
65
+ #{generate_summary(
66
+ tests: tests_for_stage, tests_by_status: tests_by_status)}
67
+
68
+ #{generate_testcase_listing_by_status(
69
+ tests: tests_for_stage, tests_by_status: tests_by_status)}
70
+ MARKDOWN
71
+ end.join("\n\n")
72
+ end
73
+
74
+ def generate_tests_by_stage(tests)
75
+ # https://about.gitlab.com/handbook/product/product-categories/#devops-stages
76
+ ordering = %w[
77
+ manage
78
+ plan
79
+ create
80
+ verify
81
+ package
82
+ release
83
+ configure
84
+ monitor
85
+ secure
86
+ defend
87
+ growth
88
+ fulfillment
89
+ enablement
90
+ ]
91
+
92
+ tests.sort_by do |test|
93
+ ordering.index(test.stage) || ordering.size
94
+ end.group_by(&:stage)
95
+ end
96
+
97
+ def generate_testcase_listing_by_status(tests:, tests_by_status:)
98
+ failed_tests = tests_by_status['failed']
99
+ passed_tests = tests_by_status['passed']
100
+ other_tests = tests.reject do |test|
101
+ test.status == 'failed' || test.status == 'passed'
102
+ end
103
+
104
+ [
105
+ (failed_listings(failed_tests) if failed_tests),
106
+ (passed_listings(passed_tests) if passed_tests),
107
+ (other_listings(other_tests) if other_tests.any?)
108
+ ].compact.join("\n\n")
109
+ end
110
+
111
+ def failed_listings(failed_tests)
112
+ generate_testcase_listing(failed_tests)
113
+ end
114
+
115
+ def passed_listings(passed_tests)
116
+ <<~MARKDOWN.chomp
117
+ <details><summary>Passed tests:</summary>
118
+
119
+ #{generate_testcase_listing(passed_tests)}
120
+
121
+ </details>
122
+ MARKDOWN
123
+ end
124
+
125
+ def other_listings(other_tests)
126
+ <<~MARKDOWN.chomp
127
+ <details><summary>Other tests:</summary>
128
+
129
+ #{generate_testcase_listing(other_tests)}
130
+
131
+ </details>
132
+ MARKDOWN
133
+ end
134
+
135
+ def generate_testcase_listing(tests)
136
+ body = tests.group_by(&:testcase).map do |testcase, tests_with_same_testcase|
137
+ tests_with_same_testcase.sort_by!(&:name)
138
+ [
139
+ generate_test_text(testcase, tests_with_same_testcase),
140
+ generate_test_job(tests_with_same_testcase),
141
+ generate_test_status(tests_with_same_testcase),
142
+ generate_test_actions(tests_with_same_testcase)
143
+ ].join(' | ')
144
+ end.join("\n")
145
+
146
+ <<~MARKDOWN.chomp
147
+ | Test | Job | Status | Action |
148
+ | - | - | - | - |
149
+ #{body}
150
+ MARKDOWN
151
+ end
152
+
153
+ def generate_test_text(testcase, tests_with_same_testcase)
154
+ text = tests_with_same_testcase.map(&:name).uniq.join(', ')
155
+
156
+ if testcase
157
+ "[#{text}](#{testcase})"
158
+ else
159
+ text
160
+ end
161
+ end
162
+
163
+ def generate_test_job(tests_with_same_testcase)
164
+ tests_with_same_testcase.map(&:ci_job_url).uniq.map do |ci_job_url|
165
+ ci_job_id = ci_job_url[/\d+\z/]
166
+
167
+ "[#{ci_job_id}](#{ci_job_url})"
168
+ end.join(', ')
169
+ end
170
+
171
+ def generate_test_status(tests_with_same_testcase)
172
+ tests_with_same_testcase.map(&:status).uniq.map do |status|
173
+ %(~"#{status}")
174
+ end.join(', ')
175
+ end
176
+
177
+ def generate_test_actions(tests_with_same_testcase)
178
+ # All failed tests would be grouped together, meaning that
179
+ # if one failed, all the tests here would be failed too.
180
+ # So this check is safe. Same applies to 'passed'.
181
+ # But all other status might be mixing together,
182
+ # we cannot assume other statuses.
183
+ if tests_with_same_testcase.first.status == 'failed'
184
+ tests_having_failure_issue =
185
+ tests_with_same_testcase.select(&:failure_issue)
186
+
187
+ if tests_having_failure_issue.any?
188
+ items = tests_having_failure_issue.map do |test|
189
+ "<li>[ ] [failure issue](#{test.failure_issue})</li>"
190
+ end.join(' ')
191
+
192
+ "<ul>#{items}</ul>"
193
+ else
194
+ '<ul><li>[ ] failure issue exists or was created</li></ul>'
195
+ end
196
+ else
197
+ '-'
198
+ end
199
+ end
200
+ end
201
+ end
202
+ end
203
+ end
@@ -45,7 +45,7 @@ module Gitlab
45
45
  abort_not_permitted
46
46
  end
47
47
 
48
- def find_issues(iid:, options: {}, &select)
48
+ def find_issues(iid: nil, options: {}, &select)
49
49
  select ||= :itself
50
50
 
51
51
  handle_gitlab_client_exceptions do
@@ -57,15 +57,17 @@ module Gitlab
57
57
  end
58
58
  end
59
59
 
60
+ def find_issue_discussions(iid:)
61
+ handle_gitlab_client_exceptions do
62
+ Gitlab.issue_discussions(project, iid, order_by: 'created_at', sort: 'asc')
63
+ end
64
+ end
65
+
60
66
  def create_issue(title:, description:, labels:)
61
- puts "Creating issue..."
67
+ attrs = { description: description, labels: labels }
62
68
 
63
69
  handle_gitlab_client_exceptions do
64
- Gitlab.create_issue(
65
- project,
66
- title,
67
- { description: description, labels: labels }
68
- )
70
+ Gitlab.create_issue(project, title, attrs)
69
71
  end
70
72
  end
71
73
 
@@ -75,6 +77,18 @@ module Gitlab
75
77
  end
76
78
  end
77
79
 
80
+ def create_issue_note(iid:, note:)
81
+ handle_gitlab_client_exceptions do
82
+ Gitlab.create_issue_note(project, iid, note)
83
+ end
84
+ end
85
+
86
+ def add_note_to_issue_discussion_as_thread(iid:, discussion_id:, body:)
87
+ handle_gitlab_client_exceptions do
88
+ Gitlab.add_note_to_issue_discussion_as_thread(project, iid, discussion_id, body: body)
89
+ end
90
+ end
91
+
78
92
  def handle_gitlab_client_exceptions
79
93
  yield
80
94
  rescue Gitlab::Error::NotFound
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gitlab
4
+ module QA
5
+ module Report
6
+ class GitlabIssueDryClient < GitlabIssueClient
7
+ def create_issue(title:, description:, labels:)
8
+ attrs = { description: description, labels: labels }
9
+
10
+ puts "The following issue would have been created:"
11
+ puts "project: #{project}, title: #{title}, attrs: #{attrs}"
12
+ end
13
+
14
+ def edit_issue(iid:, options: {})
15
+ puts "The #{project}##{iid} issue would have been updated with: #{options}"
16
+ end
17
+
18
+ def create_issue_note(iid:, note:)
19
+ puts "The following note would have been posted on #{project}##{iid} issue: #{note}"
20
+ end
21
+
22
+ def add_note_to_issue_discussion_as_thread(iid:, discussion_id:, body:)
23
+ puts "The following discussion note would have been posted on #{project}##{iid} (discussion #{discussion_id}) issue: #{body}"
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -6,7 +6,7 @@ module Gitlab
6
6
  module QA
7
7
  module Report
8
8
  class JsonTestResults < BaseTestResults
9
- def write(path)
9
+ def write
10
10
  json = results.merge('examples' => testcases.map(&:report))
11
11
 
12
12
  File.write(path, JSON.pretty_generate(json))
@@ -14,7 +14,7 @@ module Gitlab
14
14
 
15
15
  private
16
16
 
17
- def parse(path)
17
+ def parse
18
18
  JSON.parse(File.read(path))
19
19
  end
20
20
 
@@ -6,13 +6,13 @@ module Gitlab
6
6
  module QA
7
7
  module Report
8
8
  class JUnitTestResults < BaseTestResults
9
- def write(path)
9
+ def write
10
10
  # Ignore it for now
11
11
  end
12
12
 
13
13
  private
14
14
 
15
- def parse(path)
15
+ def parse
16
16
  Nokogiri::XML.parse(File.read(path))
17
17
  end
18
18
 
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'nokogiri'
4
+ require 'active_support/core_ext/enumerable'
5
+ require 'rubygems/text'
6
+
7
+ module Gitlab
8
+ module QA
9
+ module Report
10
+ # Uses the API to create or update GitLab issues with the results of tests from RSpec report files.
11
+ class RelateFailureIssue < ReportAsIssue
12
+ DEFAULT_MAX_DIFF_RATIO_FOR_DETECTION = 0.05
13
+ STACKTRACE_REGEX = %r{### Stack trace\s*(```)\s*.*(Failure/Error: .+)(\1)}m.freeze
14
+ NEW_ISSUE_LABELS = Set.new(%w[QA Quality test failure::investigating priority::2]).freeze
15
+
16
+ MultipleIssuesFound = Class.new(StandardError)
17
+
18
+ def initialize(max_diff_ratio: DEFAULT_MAX_DIFF_RATIO_FOR_DETECTION, **kwargs)
19
+ super
20
+ @max_diff_ratio = max_diff_ratio.to_f
21
+ end
22
+
23
+ private
24
+
25
+ attr_reader :max_diff_ratio
26
+
27
+ def run!
28
+ puts "Reporting test failures in `#{files.join(',')}` as issues in project `#{project}` via the API at `#{Runtime::Env.gitlab_api_base}`."
29
+
30
+ test_results_per_file do |test_results|
31
+ puts "=> Reporting tests in #{test_results.path}"
32
+
33
+ test_results.each do |test|
34
+ next if test.failures.empty?
35
+
36
+ relate_test_to_issue(test)
37
+ end
38
+
39
+ test_results.write
40
+ end
41
+ end
42
+
43
+ def relate_test_to_issue(test)
44
+ puts " => Searching issues for test '#{test.name}'..."
45
+
46
+ begin
47
+ issue = find_or_create_issue(test)
48
+ return unless issue
49
+
50
+ post_failed_job_note(issue, test)
51
+ puts " => Marked #{issue.web_url} as related to #{test.testcase}."
52
+ rescue MultipleIssuesFound => e
53
+ warn(e.message)
54
+ end
55
+ end
56
+
57
+ def find_or_create_issue(test)
58
+ issue, diff_ratio = find_failure_issue(test)
59
+
60
+ if issue
61
+ puts " => Found issue #{issue.web_url} for test '#{test.name}' with a diff ratio of #{(diff_ratio * 100).round(2)}%."
62
+ else
63
+ issue = create_issue(test)
64
+ puts " => Created new issue: #{issue.web_url} for test '#{test.name}'." if issue
65
+ end
66
+
67
+ issue
68
+ end
69
+
70
+ def failure_issues(test)
71
+ gitlab.find_issues(options: { state: 'opened', labels: 'QA' }).select do |issue|
72
+ issue_title = issue.title.strip
73
+ issue_title.include?(test.name) || issue_title.include?(partial_file_path(test.file))
74
+ end
75
+ end
76
+
77
+ def find_relevant_failure_issues(test) # rubocop:disable Metrics/AbcSize
78
+ ld = Class.new.extend(Gem::Text).method(:levenshtein_distance)
79
+ first_test_failure_stacktrace = test.failures.first['message_lines'].join("\n")
80
+
81
+ # Search with the `search` param returns 500 errors, so we filter by ~QA and then filter further in Ruby
82
+ failure_issues(test).each_with_object({}) do |issue, memo|
83
+ relevant_issue_stacktrace = find_issue_stacktrace(issue)
84
+ unless relevant_issue_stacktrace
85
+ puts " => [DEBUG] Found issue #{issue.web_url} but stacktrace doesn't match."
86
+ next
87
+ end
88
+
89
+ distance = ld.call(first_test_failure_stacktrace, relevant_issue_stacktrace)
90
+ diff_ratio = (distance.to_f / first_test_failure_stacktrace.size).round(3)
91
+ if diff_ratio <= max_diff_ratio
92
+ memo[issue] = diff_ratio
93
+ else
94
+ puts " => [DEBUG] Found issue #{issue.web_url} but stacktrace is too different (#{(diff_ratio * 100).round(2)}%)."
95
+ end
96
+ end
97
+ end
98
+
99
+ def find_issue_stacktrace(issue)
100
+ issue_stacktrace_match = issue.description.match(STACKTRACE_REGEX)
101
+
102
+ if issue_stacktrace_match
103
+ issue_stacktrace_match[2].gsub(/^#.*$/, '').strip
104
+ else
105
+ puts "\n => Stacktrace couldn't be found for #{issue.web_url}:\n\n#{issue.description}\n\n----------------------------------\n"
106
+ end
107
+ end
108
+
109
+ def find_failure_issue(test)
110
+ relevant_issues = find_relevant_failure_issues(test)
111
+
112
+ return nil if relevant_issues.empty?
113
+
114
+ best_matching_issue, smaller_diff_ratio = relevant_issues.min_by { |_, diff_ratio| diff_ratio }
115
+
116
+ unless relevant_issues.values.count(smaller_diff_ratio) == 1 # rubocop:disable Style/IfUnlessModifier
117
+ raise(MultipleIssuesFound, %(Too many issues found for test '#{test.name}' (`#{test.file}`)!))
118
+ end
119
+
120
+ test.failure_issue ||= best_matching_issue.web_url
121
+
122
+ [best_matching_issue, smaller_diff_ratio]
123
+ end
124
+
125
+ def new_issue_description(test)
126
+ super + "\n\n### Stack trace\n\n```\n#{test.failures.first['message_lines'].join("\n")}\n```"
127
+ end
128
+
129
+ def new_issue_labels(test)
130
+ NEW_ISSUE_LABELS + up_to_date_labels(test: test)
131
+ end
132
+
133
+ def post_failed_job_note(issue, test)
134
+ gitlab.create_issue_note(iid: issue.iid, note: "/relate #{test.testcase}")
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
@@ -1,13 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'set'
4
+
3
5
  module Gitlab
4
6
  module QA
5
7
  module Report
6
8
  class ReportAsIssue
7
- def initialize(token:, input_files:, project: nil)
8
- @gitlab = GitlabIssueClient.new(token: token, project: project)
9
- @files = Array(input_files)
9
+ MAX_TITLE_LENGTH = 255
10
+
11
+ def initialize(token:, input_files:, project: nil, dry_run: false, **kwargs)
10
12
  @project = project
13
+ @gitlab = (dry_run ? GitlabIssueDryClient : GitlabIssueClient).new(token: token, project: project)
14
+ @files = Array(input_files)
11
15
  end
12
16
 
13
17
  def invoke!
@@ -24,6 +28,14 @@ module Gitlab
24
28
  raise NotImplementedError
25
29
  end
26
30
 
31
+ def new_issue_description(test)
32
+ "### Full description\n\n#{search_safe(test.name)}\n\n### File path\n\n#{test.file}"
33
+ end
34
+
35
+ def new_issue_labels(test)
36
+ []
37
+ end
38
+
27
39
  def validate_input!
28
40
  assert_project!
29
41
  assert_input_files!(files)
@@ -41,6 +53,92 @@ module Gitlab
41
53
 
42
54
  abort "Please provide valid JUnit report files. No files were found matching `#{files.join(',')}`"
43
55
  end
56
+
57
+ def test_results_per_file
58
+ Dir.glob(files).each do |path|
59
+ extension = File.extname(path)
60
+
61
+ test_results =
62
+ case extension
63
+ when '.json'
64
+ Report::JsonTestResults.new(path)
65
+ when '.xml'
66
+ Report::JUnitTestResults.new(path)
67
+ else
68
+ raise "Unknown extension #{extension}"
69
+ end
70
+
71
+ yield test_results
72
+ end
73
+ end
74
+
75
+ def create_issue(test)
76
+ gitlab.create_issue(
77
+ title: title_from_test(test),
78
+ description: new_issue_description(test),
79
+ labels: new_issue_labels(test)
80
+ )
81
+ end
82
+
83
+ def issue_labels(issue)
84
+ issue&.labels&.to_set || Set.new
85
+ end
86
+
87
+ def update_labels(issue, test)
88
+ new_labels = up_to_date_labels(test: test, issue: issue)
89
+
90
+ return if issue_labels(issue) == new_labels
91
+
92
+ gitlab.edit_issue(iid: issue.iid, options: { labels: new_labels.to_a })
93
+ end
94
+
95
+ def up_to_date_labels(test:, issue: nil)
96
+ labels = issue_labels(issue)
97
+ labels << "Enterprise Edition" if ee_test?(test)
98
+ quarantine_job? ? labels << "quarantine" : labels.delete("quarantine")
99
+
100
+ labels
101
+ end
102
+
103
+ def ee_test?(test)
104
+ test.file =~ %r{features/ee/(api|browser_ui)}
105
+ end
106
+
107
+ def quarantine_job?
108
+ Runtime::Env.ci_job_name&.include?('quarantine')
109
+ end
110
+
111
+ def partial_file_path(path)
112
+ path.match(/((api|browser_ui).*)/i)[1]
113
+ end
114
+
115
+ def title_from_test(test)
116
+ title = "#{partial_file_path(test.file)} | #{search_safe(test.name)}".strip
117
+
118
+ return title unless title.length > MAX_TITLE_LENGTH
119
+
120
+ "#{title[0...MAX_TITLE_LENGTH - 3]}..."
121
+ end
122
+
123
+ def search_safe(value)
124
+ value.delete('"')
125
+ end
126
+
127
+ def pipeline
128
+ # Gets the name of the pipeline the test was run in, to be used as the key of a scoped label
129
+ #
130
+ # Tests can be run in several pipelines:
131
+ # gitlab-qa, nightly, master, staging, canary, production, preprod, and MRs
132
+ #
133
+ # Some of those run in their own project, so CI_PROJECT_NAME is the name we need. Those are:
134
+ # nightly, staging, canary, production, and preprod
135
+ #
136
+ # MR, master, and gitlab-qa tests run in gitlab-qa, but we only want to report tests run on master
137
+ # because the other pipelines will be monitored by the author of the MR that triggered them.
138
+ # So we assume that we're reporting a master pipeline if the project name is 'gitlab-qa'.
139
+
140
+ @pipeline ||= Runtime::Env.pipeline_from_project_name
141
+ end
44
142
  end
45
143
  end
46
144
  end
@@ -8,31 +8,19 @@ module Gitlab
8
8
  module Report
9
9
  # Uses the API to create or update GitLab issues with the results of tests from RSpec report files.
10
10
  class ResultsInIssues < ReportAsIssue
11
- MAX_TITLE_LENGTH = 255
12
-
13
11
  private
14
12
 
15
13
  def run!
16
14
  puts "Reporting test results in `#{files.join(',')}` as issues in project `#{project}` via the API at `#{Runtime::Env.gitlab_api_base}`."
17
15
 
18
- Dir.glob(files).each do |path|
19
- puts "Reporting tests in #{path}"
20
- extension = File.extname(path)
21
-
22
- case extension
23
- when '.json'
24
- test_results = Report::JsonTestResults.new(path)
25
- when '.xml'
26
- test_results = Report::JUnitTestResults.new(path)
27
- else
28
- raise "Unknown extension #{extension}"
29
- end
16
+ test_results_per_file do |test_results|
17
+ puts "Reporting tests in #{test_results.path}"
30
18
 
31
19
  test_results.each do |test|
32
20
  report_test(test)
33
21
  end
34
22
 
35
- test_results.write(path)
23
+ test_results.write
36
24
  end
37
25
  end
38
26
 
@@ -72,12 +60,14 @@ module Gitlab
72
60
  issues.first
73
61
  end
74
62
 
75
- def create_issue(test)
76
- gitlab.create_issue(
77
- title: title_from_test(test),
78
- description: "### Full description\n\n#{search_safe(test.name)}\n\n### File path\n\n#{test.file}",
79
- labels: 'status::automated'
80
- )
63
+ def new_issue_labels(test)
64
+ %w[status::automated]
65
+ end
66
+
67
+ def up_to_date_labels(test:, issue: nil)
68
+ labels = super
69
+ labels.delete_if { |label| label.start_with?("#{pipeline}::") }
70
+ labels << (test.failures.empty? ? "#{pipeline}::passed" : "#{pipeline}::failed")
81
71
  end
82
72
 
83
73
  def iid_from_testcase_url(url)
@@ -85,23 +75,7 @@ module Gitlab
85
75
  end
86
76
 
87
77
  def search_term(test)
88
- %("#{test.file}" "#{search_safe(test.name)}")
89
- end
90
-
91
- def title_from_test(test)
92
- title = "#{partial_file_path(test.file)} | #{search_safe(test.name)}".strip
93
-
94
- return title unless title.length > MAX_TITLE_LENGTH
95
-
96
- "#{title[0...MAX_TITLE_LENGTH - 3]}..."
97
- end
98
-
99
- def partial_file_path(path)
100
- path.match(/((api|browser_ui).*)/i)[1]
101
- end
102
-
103
- def search_safe(value)
104
- value.delete('"')
78
+ %("#{partial_file_path(test.file)}" "#{search_safe(test.name)}")
105
79
  end
106
80
 
107
81
  def note_status(issue, test)
@@ -109,13 +83,11 @@ module Gitlab
109
83
 
110
84
  note = note_content(test)
111
85
 
112
- gitlab.handle_gitlab_client_exceptions do
113
- Gitlab.issue_discussions(project, issue.iid, order_by: 'created_at', sort: 'asc').each do |discussion|
114
- return add_note_to_discussion(issue.iid, discussion.id) if new_note_matches_discussion?(note, discussion)
115
- end
116
-
117
- Gitlab.create_issue_note(project, issue.iid, note)
86
+ gitlab.find_issue_discussions(iid: issue.iid).each do |discussion|
87
+ return gitlab.add_note_to_issue_discussion_as_thread(iid: issue.iid, discussion_id: discussion.id, body: failure_summary) if new_note_matches_discussion?(note, discussion)
118
88
  end
89
+
90
+ gitlab.create_issue_note(iid: issue.iid, note: note)
119
91
  end
120
92
 
121
93
  def note_content(test)
@@ -143,10 +115,6 @@ module Gitlab
143
115
  summary.join(' ')
144
116
  end
145
117
 
146
- def quarantine_job?
147
- Runtime::Env.ci_job_name&.include?('quarantine')
148
- end
149
-
150
118
  def new_note_matches_discussion?(note, discussion)
151
119
  note_error = error_and_stack_trace(note)
152
120
  discussion_error = error_and_stack_trace(discussion.notes.first['body'])
@@ -163,42 +131,6 @@ module Gitlab
163
131
 
164
132
  result
165
133
  end
166
-
167
- def add_note_to_discussion(issue_iid, discussion_id)
168
- gitlab.handle_gitlab_client_exceptions do
169
- Gitlab.add_note_to_issue_discussion_as_thread(project, issue_iid, discussion_id, body: failure_summary)
170
- end
171
- end
172
-
173
- def update_labels(issue, test)
174
- labels = issue.labels
175
- labels.delete_if { |label| label.start_with?("#{pipeline}::") }
176
- labels << (test.failures.empty? ? "#{pipeline}::passed" : "#{pipeline}::failed")
177
- labels << "Enterprise Edition" if ee_test?(test)
178
- quarantine_job? ? labels << "quarantine" : labels.delete("quarantine")
179
-
180
- gitlab.edit_issue(iid: issue.iid, options: { labels: labels })
181
- end
182
-
183
- def ee_test?(test)
184
- test.file =~ %r{features/ee/(api|browser_ui)}
185
- end
186
-
187
- def pipeline
188
- # Gets the name of the pipeline the test was run in, to be used as the key of a scoped label
189
- #
190
- # Tests can be run in several pipelines:
191
- # gitlab-qa, nightly, master, staging, canary, production, preprod, and MRs
192
- #
193
- # Some of those run in their own project, so CI_PROJECT_NAME is the name we need. Those are:
194
- # nightly, staging, canary, production, and preprod
195
- #
196
- # MR, master, and gitlab-qa tests run in gitlab-qa, but we only want to report tests run on master
197
- # because the other pipelines will be monitored by the author of the MR that triggered them.
198
- # So we assume that we're reporting a master pipeline if the project name is 'gitlab-qa'.
199
-
200
- Runtime::Env.pipeline_from_project_name
201
- end
202
134
  end
203
135
  end
204
136
  end
@@ -19,6 +19,10 @@ module Gitlab
19
19
  self.failures = failures_from_exceptions
20
20
  end
21
21
 
22
+ def stage
23
+ @stage ||= file[%r{(?:api|browser_ui)/(?:(?:\d+_)?(\w+))}, 1]
24
+ end
25
+
22
26
  def name
23
27
  raise NotImplementedError
24
28
  end
@@ -46,8 +50,16 @@ module Gitlab
46
50
  report['file_path']
47
51
  end
48
52
 
53
+ def status
54
+ report['status']
55
+ end
56
+
57
+ def ci_job_url
58
+ report['ci_job_url']
59
+ end
60
+
49
61
  def skipped
50
- report['status'] == 'pending'
62
+ status == 'pending'
51
63
  end
52
64
 
53
65
  def testcase
@@ -58,6 +70,14 @@ module Gitlab
58
70
  report['testcase'] = new_testcase
59
71
  end
60
72
 
73
+ def failure_issue
74
+ report['failure_issue']
75
+ end
76
+
77
+ def failure_issue=(new_failure_issue)
78
+ report['failure_issue'] = new_failure_issue
79
+ end
80
+
61
81
  private
62
82
 
63
83
  # rubocop:disable Metrics/AbcSize
@@ -71,6 +91,7 @@ module Gitlab
71
91
 
72
92
  {
73
93
  'message' => "#{exception['class']}: #{exception['message']}",
94
+ 'message_lines' => exception['message_lines'],
74
95
  'stacktrace' => "#{exception['message_lines'].join("\n")}\n#{exception['backtrace'].slice(0..spec_file_first_index).join("\n")}"
75
96
  }
76
97
  end
@@ -4,6 +4,7 @@ module Gitlab
4
4
  module QA
5
5
  class Reporter
6
6
  # rubocop:disable Metrics/AbcSize
7
+ # rubocop:disable Metrics/PerceivedComplexity
7
8
  def self.invoke(args)
8
9
  report_options = {}
9
10
  slack_options = {}
@@ -21,11 +22,25 @@ module Gitlab
21
22
  report_options[:input_files] = files if files
22
23
  end
23
24
 
24
- opts.on('-p', '--project PROJECT_ID', String, 'A valid project ID. Can be an integer or a group/project string. Required by --report-in-issues') do |value|
25
+ opts.on('--relate-failure-issue FILES', String, 'Relate test failures to failure issues from RSpec JSON files') do |files|
26
+ report_options[:relate_failure_issue] = true
27
+ report_options[:input_files] = files if files
28
+ end
29
+
30
+ opts.on('--max-diff-ratio DIFF_RATO', Float, 'Max stacktrace diff ratio for QA failure issues detection. Used by with --relate-failure-issue') do |value|
31
+ report_options[:max_diff_ratio] = value
32
+ end
33
+
34
+ opts.on('-p', '--project PROJECT_ID', String, 'A valid project ID. Can be an integer or a group/project string. Required by --report-in-issues and --relate-failure-issue') do |value|
25
35
  report_options[:project] = value
26
36
  end
27
37
 
28
- opts.on('-t', '--token ACCESS_TOKEN', String, 'A valid access token. Used by --report-in-issues') do |value|
38
+ opts.on('--generate-test-session FILES', String, 'Generate test session report') do |files|
39
+ report_options[:generate_test_session] = true
40
+ report_options[:input_files] = files if files
41
+ end
42
+
43
+ opts.on('-t', '--token ACCESS_TOKEN', String, 'A valid access token. Required by --report-in-issues and --relate-failure-issue') do |value|
29
44
  report_options[:token] = value
30
45
  end
31
46
 
@@ -45,6 +60,10 @@ module Gitlab
45
60
  report_options[:files] = files
46
61
  end
47
62
 
63
+ opts.on('--dry-run', "Perform a dry-run (don't create or update issues)") do |files|
64
+ report_options[:dry_run] = true
65
+ end
66
+
48
67
  opts.on_tail('-v', '--version', 'Show the version') do
49
68
  require 'gitlab/qa/version'
50
69
  puts "#{$PROGRAM_NAME} : #{VERSION}"
@@ -63,10 +82,18 @@ module Gitlab
63
82
  if report_options.delete(:prepare_stage_reports)
64
83
  Gitlab::QA::Report::PrepareStageReports.new(**report_options).invoke!
65
84
 
85
+ elsif report_options.delete(:relate_failure_issue)
86
+ report_options[:token] = Runtime::TokenFinder.find_token!(report_options[:token])
87
+ Gitlab::QA::Report::RelateFailureIssue.new(**report_options).invoke!
88
+
66
89
  elsif report_options.delete(:report_in_issues)
67
90
  report_options[:token] = Runtime::TokenFinder.find_token!(report_options[:token])
68
91
  Gitlab::QA::Report::ResultsInIssues.new(**report_options).invoke!
69
92
 
93
+ elsif report_options.delete(:generate_test_session)
94
+ report_options[:token] = Runtime::TokenFinder.find_token!(report_options[:token])
95
+ Gitlab::QA::Report::GenerateTestSession.new(**report_options).invoke!
96
+
70
97
  elsif slack_options.delete(:post_to_slack)
71
98
  Gitlab::QA::Slack::PostToSlack.new(**slack_options).invoke!
72
99
 
@@ -79,6 +106,7 @@ module Gitlab
79
106
  exit 1
80
107
  end
81
108
  end
109
+ # rubocop:enable Metrics/PerceivedComplexity
82
110
  # rubocop:enable Metrics/AbcSize
83
111
  end
84
112
  end
@@ -62,7 +62,6 @@ module Gitlab
62
62
  'KNAPSACK_TEST_FILE_PATTERN' => :knapsack_test_file_pattern,
63
63
  'KNAPSACK_TEST_DIR' => :knapsack_test_dir,
64
64
  'CI' => :ci,
65
- 'CI_JOB_ID' => :ci_job_id,
66
65
  'CI_JOB_URL' => :ci_job_url,
67
66
  'CI_RUNNER_ID' => :ci_runner_id,
68
67
  'CI_SERVER_HOST' => :ci_server_host,
@@ -91,7 +90,10 @@ module Gitlab
91
90
  attr_writer(method_name)
92
91
 
93
92
  define_method(method_name) do
94
- ENV[env_name] || instance_variable_get("@#{method_name}")
93
+ ENV[env_name] ||
94
+ if instance_variable_defined?("@#{method_name}")
95
+ instance_variable_get("@#{method_name}")
96
+ end
95
97
  end
96
98
  end
97
99
 
@@ -123,12 +125,28 @@ module Gitlab
123
125
  ENV['CI_PIPELINE_SOURCE']
124
126
  end
125
127
 
128
+ def ci_pipeline_url
129
+ ENV['CI_PIPELINE_URL']
130
+ end
131
+
132
+ def ci_pipeline_id
133
+ ENV['CI_PIPELINE_ID']
134
+ end
135
+
126
136
  def ci_project_name
127
137
  ENV['CI_PROJECT_NAME']
128
138
  end
129
139
 
130
140
  def pipeline_from_project_name
131
- ci_project_name.to_s.start_with?('gitlab-qa') ? 'master' : ci_project_name
141
+ if ci_project_name.to_s.start_with?('gitlab-qa')
142
+ if ENV['TOP_UPSTREAM_SOURCE_JOB'].to_s.start_with?('https://ops.gitlab.net')
143
+ 'staging-orchestrated'
144
+ else
145
+ 'master'
146
+ end
147
+ else
148
+ ci_project_name
149
+ end
132
150
  end
133
151
 
134
152
  def run_id
@@ -147,6 +165,14 @@ module Gitlab
147
165
  ENV['GITLAB_QA_CONTAINER_REGISTRY_ACCESS_TOKEN']
148
166
  end
149
167
 
168
+ def qa_issue_url
169
+ ENV['GITLAB_QA_ISSUE_URL']
170
+ end
171
+
172
+ def deploy_environment
173
+ ENV['DEPLOY_ENVIRONMENT'] || pipeline_from_project_name
174
+ end
175
+
150
176
  def host_artifacts_dir
151
177
  @host_artifacts_dir ||= File.join(ENV['QA_ARTIFACTS_DIR'] || '/tmp/gitlab-qa', Runtime::Env.run_id)
152
178
  end
@@ -1,5 +1,5 @@
1
1
  module Gitlab
2
2
  module QA
3
- VERSION = '6.7.0'.freeze
3
+ VERSION = '6.8.0'.freeze
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gitlab-qa
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.7.0
4
+ version: 6.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Grzegorz Bizon
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-11-02 00:00:00.000000000 Z
11
+ date: 2020-11-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: climate_control
@@ -259,10 +259,13 @@ files:
259
259
  - lib/gitlab/qa/docker/volumes.rb
260
260
  - lib/gitlab/qa/release.rb
261
261
  - lib/gitlab/qa/report/base_test_results.rb
262
+ - lib/gitlab/qa/report/generate_test_session.rb
262
263
  - lib/gitlab/qa/report/gitlab_issue_client.rb
264
+ - lib/gitlab/qa/report/gitlab_issue_dry_client.rb
263
265
  - lib/gitlab/qa/report/json_test_results.rb
264
266
  - lib/gitlab/qa/report/junit_test_results.rb
265
267
  - lib/gitlab/qa/report/prepare_stage_reports.rb
268
+ - lib/gitlab/qa/report/relate_failure_issue.rb
266
269
  - lib/gitlab/qa/report/report_as_issue.rb
267
270
  - lib/gitlab/qa/report/results_in_issues.rb
268
271
  - lib/gitlab/qa/report/summary_table.rb