gitlab_quality-test_tooling 2.6.0 → 2.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: 279a7eab459c664263a3506753c09d78111784af48af599cab26ed8928f54e2a
4
- data.tar.gz: '06388341bfbb2c868ae25569af2597666f64cdb7e980465078a78778081ca107'
3
+ metadata.gz: 8547ed9407707737f7d776f263543f3681e19706d7705591968f2e0ec348ed88
4
+ data.tar.gz: 84f1fb8007ce5c5dd96a1782cfa7b75207d456df2b9c163ddde5900f3efe6ad2
5
5
  SHA512:
6
- metadata.gz: cd37eca775b6899df0e476d11499ee491a8ea4278d88bf8f9584feb3cb8d1245da3319aa534ed4743c06ca1bd7da7a4eefcf619aa26688eb895bf3168d58eb9d
7
- data.tar.gz: 58f7c549202b39a20d6d5b5fc117e9017a044ee2e6bff63f07b82dda9650ed819f43407f20a5363d638b986fb8860d69c2452ea68bdf7fe5b7e33e70abb281b4
6
+ metadata.gz: 43e26da079892ae22a44025b4c96a076b65499e1ab1a5574da130132e9c70358ed12ae9ba123bb645b757b8e177bb59c11fd036f5e8d9d14e22620c5c0616958
7
+ data.tar.gz: f236ba27bc98161951cc4d98e4ff22321eeee476b6e305c0b67e2d58b09f6329a0361c9d834c20ff3efa86b41f5763733eb278c755fffa1e5fa49d25c556c1a9
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- gitlab_quality-test_tooling (2.6.0)
4
+ gitlab_quality-test_tooling (2.8.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
@@ -128,6 +128,12 @@ module GitlabQuality
128
128
  end
129
129
  end
130
130
 
131
+ def find_commit(project, sha)
132
+ handle_gitlab_client_exceptions do
133
+ client.commit(project, sha)
134
+ end
135
+ end
136
+
131
137
  def find_commit_parent(project, sha)
132
138
  handle_gitlab_client_exceptions do
133
139
  # In a merged results commit, the first parent is the one from
@@ -137,6 +143,24 @@ module GitlabQuality
137
143
  end
138
144
  end
139
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
+
140
164
  private
141
165
 
142
166
  attr_reader :token, :project
@@ -5,20 +5,14 @@ module GitlabQuality
5
5
  module Report
6
6
  module Concerns
7
7
  module GroupAndCategoryLabels
8
- def labels_inference
9
- @labels_inference ||= GitlabQuality::TestTooling::LabelsInference.new
10
- end
11
-
12
- def new_issue_labels(test)
13
- debug_line = ' => [DEBUG] '
14
- debug_line += "product_group: #{test&.product_group}; " if test.respond_to?(:product_group)
15
- debug_line += "feature_category: #{test&.feature_category}" if test.respond_to?(:feature_category)
16
- puts debug_line
8
+ def group_and_category_labels_for_test(test)
9
+ labels_inference = GitlabQuality::TestTooling::LabelsInference.new
10
+ new_labels = Set.new
17
11
 
18
- new_labels = self.class::NEW_ISSUE_LABELS
19
12
  new_labels += labels_inference.infer_labels_from_product_group(test.product_group) if test.respond_to?(:product_group)
20
13
  new_labels += labels_inference.infer_labels_from_feature_category(test.feature_category) if test.respond_to?(:feature_category)
21
- up_to_date_labels(test: test, new_labels: new_labels)
14
+
15
+ new_labels
22
16
  end
23
17
  end
24
18
  end
@@ -15,6 +15,10 @@ module GitlabQuality
15
15
  "#{title[...MAX_TITLE_LENGTH - 3]}..."
16
16
  end
17
17
 
18
+ def label_names_to_label_quick_action(label_names)
19
+ %(/label #{label_names.map { |label| %(~"#{label}") }.join(' ')})
20
+ end
21
+
18
22
  def new_issue_title(test)
19
23
  "[Test] #{partial_file_path(test.file)} | #{search_safe(test.name)}".strip
20
24
  end
@@ -57,7 +57,7 @@ module GitlabQuality
57
57
  current_reports_note = find_failure_discussion_note(issue: issue, test: test, reports_discussion: reports_discussion)
58
58
 
59
59
  new_reports_list = new_reports_list(current_reports_note: current_reports_note, test: test)
60
- note_body = new_note_body(
60
+ note_body = append_quick_actions_to_note(
61
61
  new_reports_list: new_reports_list,
62
62
  related_issues: related_issues,
63
63
  options: {
@@ -105,6 +105,14 @@ module GitlabQuality
105
105
  IDENTITY_LABELS
106
106
  end
107
107
 
108
+ def new_issue_labels(test)
109
+ up_to_date_labels(test: test, new_labels: NEW_ISSUE_LABELS + group_and_category_labels_for_test(test))
110
+ end
111
+
112
+ def up_to_date_labels(test:, issue: nil, new_labels: Set.new)
113
+ (base_issue_labels + super).to_a
114
+ end
115
+
108
116
  def report_section_header
109
117
  REPORT_SECTION_HEADER
110
118
  end
@@ -139,10 +147,6 @@ module GitlabQuality
139
147
  quick_actions.join("\n")
140
148
  end
141
149
 
142
- def up_to_date_labels(test:, issue: nil, new_labels: Set.new)
143
- (base_issue_labels + super).to_a
144
- end
145
-
146
150
  def find_failure_discussion_note(issue:, test:, reports_discussion:)
147
151
  return unless reports_discussion
148
152
 
@@ -44,6 +44,14 @@ module GitlabQuality
44
44
  IDENTITY_LABELS
45
45
  end
46
46
 
47
+ def new_issue_labels(test)
48
+ up_to_date_labels(test: test, new_labels: NEW_ISSUE_LABELS)
49
+ end
50
+
51
+ def up_to_date_labels(test:, issue: nil, new_labels: Set.new)
52
+ (base_issue_labels + super).to_a
53
+ end
54
+
47
55
  def report_section_header
48
56
  REPORT_SECTION_HEADER
49
57
  end
@@ -52,22 +60,22 @@ module GitlabQuality
52
60
  REPORTS_DOCUMENTATION
53
61
  end
54
62
 
55
- def health_problem_status_label_quick_action(reports_list, **)
63
+ def health_problem_status_label_quick_action(reports_list, options: {})
56
64
  case reports_list.reports_count
57
65
  when 399..Float::INFINITY
58
- '/label ~"flakiness::1"'
66
+ label_names = Set.new(['flakiness::1'])
67
+ label_names += group_and_category_labels_for_test(options[:test]) if options.key?(:test)
68
+ label_names_to_label_quick_action(label_names)
59
69
  when 37..398
60
- '/label ~"flakiness::2"'
70
+ label_names = Set.new(['flakiness::2'])
71
+ label_names += group_and_category_labels_for_test(options[:test]) if options.key?(:test)
72
+ label_names_to_label_quick_action(label_names)
61
73
  when 13..36
62
74
  '/label ~"flakiness::3"'
63
75
  else
64
76
  '/label ~"flakiness::4"'
65
77
  end
66
78
  end
67
-
68
- def up_to_date_labels(test:, issue: nil, new_labels: Set.new)
69
- (base_issue_labels + super).to_a
70
- end
71
79
  end
72
80
  end
73
81
  end
@@ -45,6 +45,10 @@ module GitlabQuality
45
45
  []
46
46
  end
47
47
 
48
+ def new_issue_labels(_test)
49
+ []
50
+ end
51
+
48
52
  def search_labels
49
53
  BASE_SEARCH_LABELS
50
54
  end
@@ -57,10 +61,6 @@ module GitlabQuality
57
61
  ''
58
62
  end
59
63
 
60
- def health_problem_status_label_quick_action(*)
61
- ''
62
- end
63
-
64
64
  def item_extra_content(_test)
65
65
  found_label
66
66
  end
@@ -115,7 +115,13 @@ module GitlabQuality
115
115
  current_reports_note = existing_reports_note(issue_iid: issue.iid)
116
116
 
117
117
  new_reports_list = new_reports_list(current_reports_note: current_reports_note, test: test)
118
- note_body = new_note_body(new_reports_list: new_reports_list, related_issues: related_issues)
118
+ note_body = append_quick_actions_to_note(
119
+ new_reports_list: new_reports_list,
120
+ related_issues: related_issues,
121
+ options: {
122
+ test: test
123
+ }
124
+ )
119
125
 
120
126
  if current_reports_note
121
127
  gitlab.edit_issue_note(
@@ -138,7 +144,7 @@ module GitlabQuality
138
144
  )
139
145
  end
140
146
 
141
- def new_note_body(new_reports_list:, related_issues:, options: {})
147
+ def append_quick_actions_to_note(new_reports_list:, related_issues:, options: {})
142
148
  report = new_reports_list
143
149
 
144
150
  quick_actions = [
@@ -164,10 +170,15 @@ module GitlabQuality
164
170
  end
165
171
  end
166
172
 
173
+ # Defined in subclasses
174
+ def health_problem_status_label_quick_action(*)
175
+ ''
176
+ end
177
+
167
178
  def identity_labels_quick_action
168
179
  return if identity_labels.empty?
169
180
 
170
- %(/label #{identity_labels.map { |label| %(~"#{label}") }.join(' ')})
181
+ label_names_to_label_quick_action(identity_labels)
171
182
  end
172
183
 
173
184
  def relate_issues_quick_actions(issues)
@@ -39,6 +39,10 @@ module GitlabQuality
39
39
  search_and_create_issue
40
40
  end
41
41
 
42
+ def new_issue_labels(test)
43
+ up_to_date_labels(test: test, new_labels: NEW_ISSUE_LABELS + group_and_category_labels_for_test(test))
44
+ end
45
+
42
46
  def search_and_create_issue
43
47
  filtered_report = KnapsackReports::SpecRunTimeReport.new(
44
48
  token: token,
@@ -33,6 +33,21 @@ module GitlabQuality
33
33
  'gitlab-org/customers-gitlab-com' => 'gitlab-org/customers-gitlab-com'
34
34
  }.freeze
35
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
+
36
51
  MultipleIssuesFound = Class.new(StandardError)
37
52
 
38
53
  def initialize(
@@ -54,7 +69,7 @@ module GitlabQuality
54
69
 
55
70
  private
56
71
 
57
- 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
58
73
 
59
74
  def run!
60
75
  puts "Reporting test failures in `#{files.join(',')}` as issues in project `#{project}` via the API at `#{Runtime::Env.gitlab_api_base}`."
@@ -65,6 +80,14 @@ module GitlabQuality
65
80
  end
66
81
  end
67
82
 
83
+ def new_issue_labels(test)
84
+ up_to_date_labels(test: test, new_labels: NEW_ISSUE_LABELS + group_and_category_labels_for_test(test))
85
+ end
86
+
87
+ def up_to_date_labels(test:, issue: nil, new_labels: Set.new)
88
+ (Set.new(base_issue_labels) + (super << pipeline_name_label)).to_a
89
+ end
90
+
68
91
  def test_metric_collections
69
92
  @test_metric_collections ||= Dir.glob(metrics_files).map do |path|
70
93
  TestMetrics::JsonTestMetricCollection.new(path)
@@ -278,7 +301,7 @@ module GitlabQuality
278
301
 
279
302
  def new_issue_description(test)
280
303
  super + [
281
- "\n#{compare_against_previous_commit}",
304
+ "\n#{commit_diff_section}",
282
305
  "### Stack trace",
283
306
  "```\n#{test.full_stacktrace}\n```",
284
307
  screenshot_section(test),
@@ -287,8 +310,123 @@ module GitlabQuality
287
310
  ].compact.join("\n\n")
288
311
  end
289
312
 
290
- def compare_against_previous_commit
291
- pipeline_sha = gitlab.find_pipeline(project, Runtime::Env.ci_pipeline_id.to_i).sha
313
+ def commit_diff_section
314
+ "### Commit diff\n#{generate_diff_link}"
315
+ end
316
+
317
+ def generate_diff_link
318
+ initialize_gitlab_ops_client
319
+
320
+ if Runtime::Env.ci_pipeline_url.include?('ops.gitlab.net')
321
+ pipeline = ops_gitlab_client.find_pipeline(project, Runtime::Env.ci_pipeline_id.to_i)
322
+ generate_ops_gitlab_diff(pipeline)
323
+ else
324
+ pipeline = gitlab.find_pipeline(project, Runtime::Env.ci_pipeline_id.to_i)
325
+ generate_gitlab_diff(pipeline)
326
+ end
327
+ end
328
+
329
+ def generate_ops_gitlab_diff(pipeline)
330
+ deployment_info = fetch_deployment_info(pipeline)
331
+
332
+ return deployment_info if deployment_info.is_a?(String)
333
+
334
+ source_gitlab_ee_sha = fetch_deployment_gitlab_ee_sha(ops_gitlab_client, deployment_info[:source].sha)
335
+ target_gitlab_ee_sha = fetch_deployment_gitlab_ee_sha(ops_gitlab_client, deployment_info[:target].sha)
336
+
337
+ if source_gitlab_ee_sha == target_gitlab_ee_sha
338
+ "No diff"
339
+ else
340
+ "https://gitlab.com/gitlab-org/security/gitlab/-/compare/#{target_gitlab_ee_sha}...#{source_gitlab_ee_sha}"
341
+ end
342
+ end
343
+
344
+ def fetch_deployment_info(pipeline)
345
+ pipeline_deploy_version = Runtime::Env.ci_pipeline_name.match(/(\d+\.\d+\.\d+)(?:-|$)/)&.captures&.first
346
+ deployments = fetch_deployments(ops_gitlab_client, pipeline)
347
+ found_deployment = find_matching_deployment(pipeline_deploy_version, deployments)
348
+
349
+ return 'No matching deployment found to generate a diff link.' unless found_deployment
350
+
351
+ {
352
+ source: found_deployment[:deployment],
353
+ target: deployments[found_deployment[:index] + 1]
354
+ }
355
+ end
356
+
357
+ def fetch_deployments(client, pipeline)
358
+ ops_environment = pipeline.web_url.match(%r{gitlab-org/quality/([^/-]+(?:-[^/-]+)*?)(?:/-)?/pipelines})[1]
359
+
360
+ raise "Environment '#{ops_environment}' is not supported" unless ENVIRONMENT_MAPPING.key?(ops_environment)
361
+
362
+ environment = ENVIRONMENT_MAPPING[ops_environment]
363
+
364
+ client.find_deployments(
365
+ OPS_RELEASES_METADATA_PROJECT,
366
+ environment: environment,
367
+ status: 'success'
368
+ )
369
+ end
370
+
371
+ def initialize_gitlab_ops_client
372
+ @ops_gitlab_client = GitlabClient::IssuesClient.new(
373
+ endpoint: Runtime::Env.ci_api_v4_url,
374
+ token: Runtime::Env.gitlab_ci_token,
375
+ project: OPS_RELEASES_METADATA_PROJECT
376
+ )
377
+ end
378
+
379
+ def find_matching_deployment(pipeline_deploy_version, deployments)
380
+ return nil unless pipeline_deploy_version
381
+ return nil unless deployments
382
+
383
+ target_timestamp = extract_timestamp(pipeline_deploy_version)
384
+ return nil unless target_timestamp
385
+
386
+ matching_deployment = nil
387
+
388
+ deployments.each_with_index do |deployment, index|
389
+ deployment_version = extract_deployment_version(ops_gitlab_client, deployment)
390
+ next unless deployment_version
391
+
392
+ deployment_timestamp = extract_timestamp(deployment_version)
393
+ next unless deployment_timestamp
394
+
395
+ # Stop searching if the deployment timestamp is older than the target timestamp
396
+ break if deployment_timestamp && deployment_timestamp < target_timestamp
397
+
398
+ if deployment_version == pipeline_deploy_version
399
+ matching_deployment = { deployment: deployment, index: index }
400
+ break
401
+ end
402
+ end
403
+
404
+ matching_deployment
405
+ end
406
+
407
+ def extract_timestamp(version)
408
+ version.match(/\d{12}$/)&.to_s
409
+ end
410
+
411
+ def extract_deployment_version(client, deployment)
412
+ commit = client.find_commit(OPS_RELEASES_METADATA_PROJECT, deployment.sha)
413
+ version_match = commit.message&.match(/Product-Version: ([\d.]+)/)
414
+ version_match&.[](1)
415
+ end
416
+
417
+ def fetch_deployment_gitlab_ee_sha(client, deployment_sha)
418
+ commit_diff = client.find_commit_diff(OPS_RELEASES_METADATA_PROJECT, deployment_sha).first
419
+ diffs_content = commit_diff.diff.lines.select { |line| line.start_with?('+') }.map { |line| line[1..] }.join
420
+
421
+ begin
422
+ JSON.parse(diffs_content).dig('releases', 'gitlab-ee', 'sha')
423
+ rescue JSON::ParserError
424
+ raise "Failed to parse the diffs content"
425
+ end
426
+ end
427
+
428
+ def generate_gitlab_diff(pipeline)
429
+ pipeline_sha = pipeline.sha
292
430
  parent_sha = gitlab.find_commit_parent(project, pipeline_sha)
293
431
  diff_project = if DIFF_PROJECT_MAPPINGS.key?(project)
294
432
  DIFF_PROJECT_MAPPINGS[project]
@@ -296,7 +434,7 @@ module GitlabQuality
296
434
  raise "Project #{project} is not supported for commit diff links"
297
435
  end
298
436
 
299
- "### Commit diff\nhttps://gitlab.com/#{diff_project}/-/compare/#{parent_sha}...#{pipeline_sha}"
437
+ "https://gitlab.com/#{diff_project}/-/compare/#{parent_sha}...#{pipeline_sha}"
300
438
  end
301
439
 
302
440
  def system_log_errors_section(test)
@@ -318,10 +456,6 @@ module GitlabQuality
318
456
  section
319
457
  end
320
458
 
321
- def up_to_date_labels(test:, issue: nil, new_labels: Set.new)
322
- (Set.new(base_issue_labels) + (super << pipeline_name_label)).to_a
323
- end
324
-
325
459
  def new_issue_assignee_id(test)
326
460
  return unless test.product_group?
327
461
 
@@ -144,6 +144,9 @@ module GitlabQuality
144
144
  gitlab.edit_issue(iid: issue.iid, options: { labels: labels.to_a })
145
145
  end
146
146
 
147
+ # Infer labels from the test, and optionally from the issue and new_labels in arguments
148
+ #
149
+ # Called when we're updating a test health issue with a new test report.
147
150
  def up_to_date_labels(test:, issue: nil, new_labels: Set.new)
148
151
  labels = issue_labels(issue)
149
152
  labels |= new_labels.to_set
@@ -34,6 +34,10 @@ module GitlabQuality
34
34
  IDENTITY_LABELS
35
35
  end
36
36
 
37
+ def new_issue_labels(test)
38
+ up_to_date_labels(test: test, new_labels: NEW_ISSUE_LABELS)
39
+ end
40
+
37
41
  def report_section_header
38
42
  REPORT_SECTION_HEADER
39
43
  end
@@ -42,12 +46,16 @@ module GitlabQuality
42
46
  REPORTS_DOCUMENTATION
43
47
  end
44
48
 
45
- def health_problem_status_label_quick_action(reports_list, **)
49
+ def health_problem_status_label_quick_action(reports_list, options: {})
46
50
  case reports_list.reports_count
47
51
  when 6099..Float::INFINITY
48
- '/label ~"slowness::1"'
52
+ label_names = Set.new(['slowness::1'])
53
+ label_names += group_and_category_labels_for_test(options[:test]) if options.key?(:test)
54
+ label_names_to_label_quick_action(label_names)
49
55
  when 2177..6098
50
- '/label ~"slowness::2"'
56
+ label_names = Set.new(['slowness::2'])
57
+ label_names += group_and_category_labels_for_test(options[:test]) if options.key?(:test)
58
+ label_names_to_label_quick_action(label_names)
51
59
  when 521..2176
52
60
  '/label ~"slowness::3"'
53
61
  else
@@ -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.6.0"
5
+ VERSION = "2.8.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.6.0
4
+ version: 2.8.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-24 00:00:00.000000000 Z
11
+ date: 2025-02-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: climate_control