gitlab_quality-test_tooling 2.5.0 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cd181fe7622793361cd772d2f949d1a8922f4b0762c2d50ae9f839fdfd69bb86
4
- data.tar.gz: aaa900d8e19eff7ae0016bda52bf6dc3722aa4e4fe288f16c16d6685254c831a
3
+ metadata.gz: bc2b98142fc0891fe937da031f64a665ef016c153263b01b7c9820b284967800
4
+ data.tar.gz: 588d0cec55fb89405d61d6e41528343929d948fe6fb03717edf2e895fe727e5c
5
5
  SHA512:
6
- metadata.gz: 5b5e1a85230726944574ec3c92c9fbe0e8a547776a6a7f7ce07d4db1889abf1a23b35a8b13b55d79eaf90eb3b7cfac4ec47960efa9eedaa509ffa02dd9655077
7
- data.tar.gz: 47f438135b5ac5d4cc2cfc95a30e6a917f6de4c714bd8f42ed74c4d51439af925ac1424bf2105e71c76cd581f5faf287d8d0a415bf4c220df36df2fd85ec6149
6
+ metadata.gz: 2402289c4b5282ad62289b7560b0faadc8cabc675d7e0262b38e6582cee5e2e6a2711ab053dc677d2e32d917c8a29608e8b90b485d977d4fe244c6a7e4c47505
7
+ data.tar.gz: 4bebf905075eb9b8ca2fb707c004d78746d8d8e518a6e10ee7d1085a2e3450b8a33d672a9ef382103deaf996591bd1f7c6b5d86f2249276a7a576a61074acbd7
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- gitlab_quality-test_tooling (2.5.0)
4
+ gitlab_quality-test_tooling (2.7.0)
5
5
  activesupport (>= 7.0, < 7.2)
6
6
  amatch (~> 0.4.1)
7
7
  fog-google (~> 1.24, >= 1.24.1)
@@ -9,10 +9,11 @@ module GitlabQuality
9
9
  RETRY_BACK_OFF_DELAY = 60
10
10
  MAX_RETRY_ATTEMPTS = 3
11
11
 
12
- def initialize(token:, project:, **_kwargs)
12
+ def initialize(token:, project:, endpoint: nil, **_kwargs)
13
13
  @token = token
14
14
  @project = project
15
15
  @retry_backoff = 0
16
+ @endpoint = endpoint
16
17
  end
17
18
 
18
19
  def handle_gitlab_client_exceptions
@@ -76,11 +77,11 @@ module GitlabQuality
76
77
 
77
78
  private
78
79
 
79
- attr_reader :project, :token
80
+ attr_reader :project, :token, :endpoint
80
81
 
81
82
  def client
82
83
  @client ||= Gitlab.client(
83
- endpoint: Runtime::Env.gitlab_api_base,
84
+ endpoint: endpoint || Runtime::Env.gitlab_api_base,
84
85
  private_token: token
85
86
  )
86
87
  end
@@ -122,6 +122,45 @@ module GitlabQuality
122
122
  end
123
123
  end
124
124
 
125
+ def find_pipeline(project, pipeline_id)
126
+ handle_gitlab_client_exceptions do
127
+ client.pipeline(project, pipeline_id)
128
+ end
129
+ end
130
+
131
+ def find_commit(project, sha)
132
+ handle_gitlab_client_exceptions do
133
+ client.commit(project, sha)
134
+ end
135
+ end
136
+
137
+ def find_commit_parent(project, sha)
138
+ handle_gitlab_client_exceptions do
139
+ # In a merged results commit, the first parent is the one from
140
+ # the main branch, and the second parent is from the branch
141
+ # itself (more likely to have caused the issue)
142
+ client.commit(project, sha).parent_ids.last
143
+ end
144
+ end
145
+
146
+ def find_commit_diff(project, sha)
147
+ handle_gitlab_client_exceptions do
148
+ client.commit_diff(project, sha)
149
+ end
150
+ end
151
+
152
+ def find_deployments(project, environment:, status:, order_by: 'id', sort: 'desc')
153
+ handle_gitlab_client_exceptions do
154
+ client.deployments(
155
+ project,
156
+ environment: environment,
157
+ status: status,
158
+ order_by: order_by,
159
+ sort: sort
160
+ )
161
+ end
162
+ end
163
+
125
164
  private
126
165
 
127
166
  attr_reader :token, :project
@@ -113,11 +113,11 @@ module GitlabQuality
113
113
  end
114
114
  end
115
115
 
116
- def initial_reports_section(test)
116
+ def initial_reports_section(test, item_extra_content: nil)
117
117
  <<~REPORTS
118
118
  ### Reports (1)
119
119
 
120
- #{ReportsList.report_list_item(test)}
120
+ #{ReportsList.report_list_item(test, item_extra_content: item_extra_content)}
121
121
  REPORTS
122
122
  end
123
123
 
@@ -26,6 +26,28 @@ module GitlabQuality
26
26
  NEW_ISSUE_LABELS = Set.new(%w[test failure::new priority::2 automation:bot-authored]).freeze
27
27
  SCREENSHOT_IGNORED_ERRORS = ['500 Internal Server Error', 'fabricate_via_api!', 'Error Code 500'].freeze
28
28
 
29
+ # Map commits to security fork (gitlab-org/security/gitlab) for gitlab-org/gitlab since security patches exist
30
+ # there before being released to the public repository
31
+ DIFF_PROJECT_MAPPINGS = {
32
+ 'gitlab-org/gitlab' => 'gitlab-org/security/gitlab',
33
+ 'gitlab-org/customers-gitlab-com' => 'gitlab-org/customers-gitlab-com'
34
+ }.freeze
35
+
36
+ # The project contains record of the deployments we use to determine the commit diff
37
+ OPS_RELEASES_METADATA_PROJECT = 'gitlab-org/release/metadata'
38
+
39
+ # Naming of the environments are different between `gitlab-org/release/metadata` and `gitlab-org/quality`
40
+ # This maps the gitlab-org/quality environment names to the equivalent in `gitlab-org/release/metadata`
41
+ ENVIRONMENT_MAPPING = {
42
+ 'production' => 'gprd',
43
+ 'canary' => 'gprd-cny',
44
+ 'staging' => 'gstg',
45
+ 'staging-canary' => 'gstg-cny',
46
+ 'staging-ref' => 'gstg-ref',
47
+ 'preprod' => 'pre',
48
+ 'release' => 'release'
49
+ }.freeze
50
+
29
51
  MultipleIssuesFound = Class.new(StandardError)
30
52
 
31
53
  def initialize(
@@ -47,7 +69,7 @@ module GitlabQuality
47
69
 
48
70
  private
49
71
 
50
- attr_reader :max_diff_ratio, :system_logs, :base_issue_labels, :exclude_labels_for_search, :metrics_files
72
+ attr_reader :max_diff_ratio, :system_logs, :base_issue_labels, :exclude_labels_for_search, :metrics_files, :ops_gitlab_client
51
73
 
52
74
  def run!
53
75
  puts "Reporting test failures in `#{files.join(',')}` as issues in project `#{project}` via the API at `#{Runtime::Env.gitlab_api_base}`."
@@ -271,14 +293,142 @@ module GitlabQuality
271
293
 
272
294
  def new_issue_description(test)
273
295
  super + [
274
- "\n### Stack trace",
296
+ "\n#{commit_diff_section}",
297
+ "### Stack trace",
275
298
  "```\n#{test.full_stacktrace}\n```",
276
299
  screenshot_section(test),
277
300
  system_log_errors_section(test),
278
- initial_reports_section(test)
301
+ initial_reports_section(test, item_extra_content: screenshot_artifact_url(test))
279
302
  ].compact.join("\n\n")
280
303
  end
281
304
 
305
+ def commit_diff_section
306
+ "### Commit diff\n#{generate_diff_link}"
307
+ end
308
+
309
+ def generate_diff_link
310
+ initialize_gitlab_ops_client
311
+
312
+ if Runtime::Env.ci_pipeline_url.include?('ops.gitlab.net')
313
+ pipeline = ops_gitlab_client.find_pipeline(project, Runtime::Env.ci_pipeline_id.to_i)
314
+ generate_ops_gitlab_diff(pipeline)
315
+ else
316
+ pipeline = gitlab.find_pipeline(project, Runtime::Env.ci_pipeline_id.to_i)
317
+ generate_gitlab_diff(pipeline)
318
+ end
319
+ end
320
+
321
+ def generate_ops_gitlab_diff(pipeline)
322
+ deployment_info = fetch_deployment_info(pipeline)
323
+
324
+ return deployment_info if deployment_info.is_a?(String)
325
+
326
+ source_gitlab_ee_sha = fetch_deployment_gitlab_ee_sha(ops_gitlab_client, deployment_info[:source].sha)
327
+ target_gitlab_ee_sha = fetch_deployment_gitlab_ee_sha(ops_gitlab_client, deployment_info[:target].sha)
328
+
329
+ if source_gitlab_ee_sha == target_gitlab_ee_sha
330
+ "No diff"
331
+ else
332
+ "https://gitlab.com/gitlab-org/security/gitlab/-/compare/#{target_gitlab_ee_sha}...#{source_gitlab_ee_sha}"
333
+ end
334
+ end
335
+
336
+ def fetch_deployment_info(pipeline)
337
+ pipeline_deploy_version = Runtime::Env.ci_pipeline_name.match(/(\d+\.\d+\.\d+)(?:-|$)/)&.captures&.first
338
+ deployments = fetch_deployments(ops_gitlab_client, pipeline)
339
+ found_deployment = find_matching_deployment(pipeline_deploy_version, deployments)
340
+
341
+ return 'No matching deployment found to generate a diff link.' unless found_deployment
342
+
343
+ {
344
+ source: found_deployment[:deployment],
345
+ target: deployments[found_deployment[:index] + 1]
346
+ }
347
+ end
348
+
349
+ def fetch_deployments(client, pipeline)
350
+ ops_environment = pipeline.web_url.match(%r{gitlab-org/quality/([^/-]+(?:-[^/-]+)*?)(?:/-)?/pipelines})[1]
351
+
352
+ raise "Environment '#{ops_environment}' is not supported" unless ENVIRONMENT_MAPPING.key?(ops_environment)
353
+
354
+ environment = ENVIRONMENT_MAPPING[ops_environment]
355
+
356
+ client.find_deployments(
357
+ OPS_RELEASES_METADATA_PROJECT,
358
+ environment: environment,
359
+ status: 'success'
360
+ )
361
+ end
362
+
363
+ def initialize_gitlab_ops_client
364
+ @ops_gitlab_client = GitlabClient::IssuesClient.new(
365
+ endpoint: Runtime::Env.ci_api_v4_url,
366
+ token: Runtime::Env.gitlab_ci_token,
367
+ project: OPS_RELEASES_METADATA_PROJECT
368
+ )
369
+ end
370
+
371
+ def find_matching_deployment(pipeline_deploy_version, deployments)
372
+ return nil unless pipeline_deploy_version
373
+ return nil unless deployments
374
+
375
+ target_timestamp = extract_timestamp(pipeline_deploy_version)
376
+ return nil unless target_timestamp
377
+
378
+ matching_deployment = nil
379
+
380
+ deployments.each_with_index do |deployment, index|
381
+ deployment_version = extract_deployment_version(ops_gitlab_client, deployment)
382
+ next unless deployment_version
383
+
384
+ deployment_timestamp = extract_timestamp(deployment_version)
385
+ next unless deployment_timestamp
386
+
387
+ # Stop searching if the deployment timestamp is older than the target timestamp
388
+ break if deployment_timestamp && deployment_timestamp < target_timestamp
389
+
390
+ if deployment_version == pipeline_deploy_version
391
+ matching_deployment = { deployment: deployment, index: index }
392
+ break
393
+ end
394
+ end
395
+
396
+ matching_deployment
397
+ end
398
+
399
+ def extract_timestamp(version)
400
+ version.match(/\d{12}$/)&.to_s
401
+ end
402
+
403
+ def extract_deployment_version(client, deployment)
404
+ commit = client.find_commit(OPS_RELEASES_METADATA_PROJECT, deployment.sha)
405
+ version_match = commit.message&.match(/Product-Version: ([\d.]+)/)
406
+ version_match&.[](1)
407
+ end
408
+
409
+ def fetch_deployment_gitlab_ee_sha(client, deployment_sha)
410
+ commit_diff = client.find_commit_diff(OPS_RELEASES_METADATA_PROJECT, deployment_sha).first
411
+ diffs_content = commit_diff.diff.lines.select { |line| line.start_with?('+') }.map { |line| line[1..] }.join
412
+
413
+ begin
414
+ JSON.parse(diffs_content).dig('releases', 'gitlab-ee', 'sha')
415
+ rescue JSON::ParserError
416
+ raise "Failed to parse the diffs content"
417
+ end
418
+ end
419
+
420
+ def generate_gitlab_diff(pipeline)
421
+ pipeline_sha = pipeline.sha
422
+ parent_sha = gitlab.find_commit_parent(project, pipeline_sha)
423
+ diff_project = if DIFF_PROJECT_MAPPINGS.key?(project)
424
+ DIFF_PROJECT_MAPPINGS[project]
425
+ else
426
+ raise "Project #{project} is not supported for commit diff links"
427
+ end
428
+
429
+ "https://gitlab.com/#{diff_project}/-/compare/#{parent_sha}...#{pipeline_sha}"
430
+ end
431
+
282
432
  def system_log_errors_section(test)
283
433
  correlation_id = test.failures.first['correlation_id']
284
434
  section = ''
@@ -322,7 +472,7 @@ module GitlabQuality
322
472
  state_event = issue.state == 'closed' ? 'reopen' : nil
323
473
 
324
474
  issue_attrs = {
325
- description: increment_reports(current_reports_content: issue.description, test: test).to_s,
475
+ description: increment_reports(current_reports_content: issue.description, test: test, item_extra_content: screenshot_artifact_url(test)).to_s,
326
476
  labels: up_to_date_labels(test: test, issue: issue)
327
477
  }
328
478
  issue_attrs[:state_event] = state_event if state_event
@@ -386,6 +536,13 @@ module GitlabQuality
386
536
  .filter_map { |e| failure_to_ignore.find { |m| e['message'].include?(m) } }
387
537
  .compact
388
538
  end
539
+
540
+ def screenshot_artifact_url(test)
541
+ ci_job_url = test.ci_job_url
542
+ screenshot_path = test.screenshot_image[%r{qa/.*$}]
543
+
544
+ "|| [Screenshot](#{ci_job_url}/artifacts/file/#{screenshot_path})"
545
+ end
389
546
  end
390
547
  end
391
548
  end
@@ -19,9 +19,11 @@ module GitlabQuality
19
19
  'CI_PROJECT_NAME' => :ci_project_name,
20
20
  'CI_PROJECT_PATH' => :ci_project_path,
21
21
  'CI_PIPELINE_ID' => :ci_pipeline_id,
22
+ 'CI_PIPELINE_NAME' => :ci_pipeline_name,
22
23
  'CI_PIPELINE_URL' => :ci_pipeline_url,
23
24
  'SLACK_QA_CHANNEL' => :slack_qa_channel,
24
- 'DEPLOY_VERSION' => :deploy_version
25
+ 'DEPLOY_VERSION' => :deploy_version,
26
+ 'QA_GITLAB_CI_TOKEN' => :gitlab_ci_token
25
27
  }.freeze
26
28
 
27
29
  ENV_VARIABLES.each do |env_name, method_name|
@@ -11,7 +11,9 @@ module GitlabQuality
11
11
  "unexpected token at 'GitLab is not responding'",
12
12
  "GitLab: Internal API error (502).",
13
13
  "could not be found (502)",
14
- "Error reference number: 502"
14
+ "Error reference number: 502",
15
+ "(502): `GitLab is not responding`",
16
+ "<head><title>502 Bad Gateway</title></head>"
15
17
  ].freeze
16
18
 
17
19
  SHARED_EXAMPLES_CALLERS = %w[include_examples it_behaves_like].freeze
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GitlabQuality
4
4
  module TestTooling
5
- VERSION = "2.5.0"
5
+ VERSION = "2.7.0"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gitlab_quality-test_tooling
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.5.0
4
+ version: 2.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - GitLab Quality
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-01-07 00:00:00.000000000 Z
11
+ date: 2025-02-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: climate_control