datadog-ci 1.15.0 → 1.17.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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +39 -2
  3. data/lib/datadog/ci/async_writer.rb +112 -0
  4. data/lib/datadog/ci/codeowners/rule.rb +5 -0
  5. data/lib/datadog/ci/configuration/components.rb +50 -9
  6. data/lib/datadog/ci/configuration/settings.rb +17 -0
  7. data/lib/datadog/ci/contrib/activesupport/configuration/settings.rb +25 -0
  8. data/lib/datadog/ci/contrib/activesupport/ext.rb +14 -0
  9. data/lib/datadog/ci/contrib/activesupport/integration.rb +43 -0
  10. data/lib/datadog/ci/contrib/activesupport/logs_formatter.rb +41 -0
  11. data/lib/datadog/ci/contrib/activesupport/patcher.rb +50 -0
  12. data/lib/datadog/ci/contrib/knapsack/patcher.rb +1 -3
  13. data/lib/datadog/ci/contrib/knapsack/runner.rb +2 -0
  14. data/lib/datadog/ci/contrib/lograge/configuration/settings.rb +25 -0
  15. data/lib/datadog/ci/contrib/lograge/ext.rb +14 -0
  16. data/lib/datadog/ci/contrib/lograge/integration.rb +43 -0
  17. data/lib/datadog/ci/contrib/lograge/log_subscriber.rb +41 -0
  18. data/lib/datadog/ci/contrib/lograge/patcher.rb +32 -0
  19. data/lib/datadog/ci/contrib/minitest/runner.rb +1 -0
  20. data/lib/datadog/ci/contrib/minitest/test.rb +7 -2
  21. data/lib/datadog/ci/contrib/parallel_tests/patcher.rb +1 -4
  22. data/lib/datadog/ci/contrib/patcher.rb +4 -0
  23. data/lib/datadog/ci/contrib/rspec/helpers.rb +1 -3
  24. data/lib/datadog/ci/contrib/semantic_logger/configuration/settings.rb +25 -0
  25. data/lib/datadog/ci/contrib/semantic_logger/ext.rb +14 -0
  26. data/lib/datadog/ci/contrib/semantic_logger/integration.rb +42 -0
  27. data/lib/datadog/ci/contrib/semantic_logger/logger.rb +32 -0
  28. data/lib/datadog/ci/contrib/semantic_logger/patcher.rb +32 -0
  29. data/lib/datadog/ci/ext/environment/extractor.rb +4 -6
  30. data/lib/datadog/ci/ext/environment/providers/appveyor.rb +5 -0
  31. data/lib/datadog/ci/ext/environment/providers/base.rb +7 -2
  32. data/lib/datadog/ci/ext/environment/providers/bitbucket.rb +6 -0
  33. data/lib/datadog/ci/ext/environment/providers/bitrise.rb +7 -1
  34. data/lib/datadog/ci/ext/environment/providers/buddy.rb +5 -0
  35. data/lib/datadog/ci/ext/environment/providers/github_actions.rb +37 -18
  36. data/lib/datadog/ci/ext/environment/providers/gitlab.rb +13 -1
  37. data/lib/datadog/ci/ext/environment/providers/user_defined_tags.rb +12 -0
  38. data/lib/datadog/ci/ext/git.rb +3 -0
  39. data/lib/datadog/ci/ext/settings.rb +3 -0
  40. data/lib/datadog/ci/ext/telemetry.rb +4 -0
  41. data/lib/datadog/ci/ext/test.rb +7 -3
  42. data/lib/datadog/ci/ext/transport.rb +3 -0
  43. data/lib/datadog/ci/git/local_repository.rb +238 -4
  44. data/lib/datadog/ci/git/tree_uploader.rb +10 -3
  45. data/lib/datadog/ci/impacted_tests_detection/component.rb +83 -0
  46. data/lib/datadog/ci/impacted_tests_detection/telemetry.rb +16 -0
  47. data/lib/datadog/ci/logs/component.rb +46 -0
  48. data/lib/datadog/ci/logs/transport.rb +73 -0
  49. data/lib/datadog/ci/remote/component.rb +6 -1
  50. data/lib/datadog/ci/remote/library_settings.rb +8 -0
  51. data/lib/datadog/ci/span.rb +7 -0
  52. data/lib/datadog/ci/test.rb +16 -0
  53. data/lib/datadog/ci/test_management/tests_properties.rb +2 -1
  54. data/lib/datadog/ci/test_retries/component.rb +8 -17
  55. data/lib/datadog/ci/test_retries/driver/{retry_new.rb → retry_flake_detection.rb} +2 -2
  56. data/lib/datadog/ci/test_retries/strategy/{retry_new.rb → retry_flake_detection.rb} +4 -4
  57. data/lib/datadog/ci/test_visibility/component.rb +6 -0
  58. data/lib/datadog/ci/test_visibility/null_component.rb +3 -1
  59. data/lib/datadog/ci/transport/api/agentless.rb +8 -1
  60. data/lib/datadog/ci/transport/api/base.rb +4 -0
  61. data/lib/datadog/ci/transport/api/builder.rb +5 -1
  62. data/lib/datadog/ci/transport/api/evp_proxy.rb +4 -0
  63. data/lib/datadog/ci/version.rb +1 -1
  64. data/lib/datadog/ci.rb +3 -0
  65. metadata +25 -6
  66. data/lib/datadog/ci/test_optimisation/coverage/writer.rb +0 -116
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "open3"
4
4
  require "pathname"
5
+ require "set"
5
6
 
6
7
  require_relative "../ext/telemetry"
7
8
  require_relative "telemetry"
@@ -23,6 +24,8 @@ module Datadog
23
24
  end
24
25
 
25
26
  COMMAND_RETRY_COUNT = 3
27
+ POSSIBLE_BASE_BRANCHES = %w[main master preprod prod dev development trunk].freeze
28
+ DEFAULT_LIKE_BRANCH_FILTER = /^(#{POSSIBLE_BASE_BRANCHES.join("|")}|release\/.*|hotfix\/.*)$/.freeze
26
29
 
27
30
  def self.root
28
31
  return @root if defined?(@root)
@@ -63,7 +66,7 @@ module Datadog
63
66
  res = pathname.relative_path_from(root_path).to_s
64
67
 
65
68
  unless defined?(@prefix_to_root)
66
- @prefix_to_root = res&.gsub(path, "") if res.end_with?(path)
69
+ @prefix_to_root = res.gsub(path, "") if res.end_with?(path)
67
70
  end
68
71
  end
69
72
 
@@ -90,6 +93,7 @@ module Datadog
90
93
 
91
94
  def self.git_repository_url
92
95
  Telemetry.git_command(Ext::Telemetry::Command::GET_REPOSITORY)
96
+ # @type var res: String?
93
97
  res = nil
94
98
 
95
99
  duration_ms = Core::Utils::Time.measure(:float_millisecond) do
@@ -120,6 +124,7 @@ module Datadog
120
124
 
121
125
  def self.git_branch
122
126
  Telemetry.git_command(Ext::Telemetry::Command::GET_BRANCH)
127
+ # @type var res: String?
123
128
  res = nil
124
129
 
125
130
  duration_ms = Core::Utils::Time.measure(:float_millisecond) do
@@ -177,7 +182,9 @@ module Datadog
177
182
  def self.git_commits
178
183
  Telemetry.git_command(Ext::Telemetry::Command::GET_LOCAL_COMMITS)
179
184
 
185
+ # @type var output: String?
180
186
  output = nil
187
+
181
188
  duration_ms = Core::Utils::Time.measure(:float_millisecond) do
182
189
  output = exec_git_command("git log --format=%H -n 1000 --since=\"1 month ago\"")
183
190
  end
@@ -186,7 +193,6 @@ module Datadog
186
193
 
187
194
  return [] if output.nil?
188
195
 
189
- # @type var output: String
190
196
  output.split("\n")
191
197
  rescue => e
192
198
  log_failure(e, "git commits")
@@ -199,6 +205,7 @@ module Datadog
199
205
  included_commits = filter_invalid_commits(included_commits).join(" ")
200
206
  excluded_commits = filter_invalid_commits(excluded_commits).map! { |sha| "^#{sha}" }.join(" ")
201
207
 
208
+ # @type var res: String?
202
209
  res = nil
203
210
 
204
211
  duration_ms = Core::Utils::Time.measure(:float_millisecond) do
@@ -264,6 +271,7 @@ module Datadog
264
271
 
265
272
  def self.git_unshallow
266
273
  Telemetry.git_command(Ext::Telemetry::Command::UNSHALLOW)
274
+ # @type var res: String?
267
275
  res = nil
268
276
 
269
277
  unshallow_command =
@@ -296,7 +304,7 @@ module Datadog
296
304
  nil
297
305
  end
298
306
 
299
- break unless unshallowing_errored
307
+ break [] unless unshallowing_errored
300
308
  end
301
309
  end
302
310
 
@@ -304,6 +312,229 @@ module Datadog
304
312
  res
305
313
  end
306
314
 
315
+ # Returns a Set of normalized file paths changed since the given base_commit.
316
+ # If base_commit is nil, returns nil. On error, returns nil.
317
+ def self.get_changed_files_from_diff(base_commit)
318
+ return nil if base_commit.nil?
319
+
320
+ Datadog.logger.debug { "calculating git diff from base_commit: #{base_commit}" }
321
+
322
+ Telemetry.git_command(Ext::Telemetry::Command::DIFF)
323
+
324
+ begin
325
+ # 1. Run the git diff command
326
+
327
+ # @type var output: String?
328
+ output = nil
329
+ duration_ms = Core::Utils::Time.measure(:float_millisecond) do
330
+ output = exec_git_command("git diff -U0 --word-diff=porcelain #{base_commit} HEAD")
331
+ end
332
+ Telemetry.git_command_ms(Ext::Telemetry::Command::DIFF, duration_ms)
333
+
334
+ Datadog.logger.debug { "git diff output: #{output}" }
335
+
336
+ return nil if output.nil?
337
+
338
+ # 2. Parse the output to extract which files changed
339
+ changed_files = Set.new
340
+ output.each_line do |line|
341
+ # Match lines like: diff --git a/foo/bar.rb b/foo/bar.rb
342
+ # This captures git changes on file level
343
+ match = /^diff --git a\/(?<file>.+) b\/(?<file2>.+)$/.match(line)
344
+ if match && match[:file]
345
+ changed_file = match[:file]
346
+ # Normalize to repo root
347
+ normalized_changed_file = relative_to_root(changed_file)
348
+ changed_files << normalized_changed_file unless normalized_changed_file.nil? || normalized_changed_file.empty?
349
+
350
+ Datadog.logger.debug { "matched changed_file: #{changed_file} from line: #{line}" }
351
+ Datadog.logger.debug { "normalized_changed_file: #{normalized_changed_file}" }
352
+ end
353
+ end
354
+ changed_files
355
+ rescue => e
356
+ telemetry_track_error(e, Ext::Telemetry::Command::DIFF)
357
+ log_failure(e, "get changed files from diff")
358
+ nil
359
+ end
360
+ end
361
+
362
+ # On best effort basis determines the git sha of the most likely
363
+ # base branch for the current PR.
364
+ def self.base_commit_sha(base_branch: nil)
365
+ Telemetry.git_command(Ext::Telemetry::Command::BASE_COMMIT_SHA)
366
+
367
+ remote_name = get_remote_name
368
+ Datadog.logger.debug { "Remote name: '#{remote_name}'" }
369
+
370
+ source_branch = get_source_branch
371
+ return nil if source_branch.nil?
372
+
373
+ Datadog.logger.debug { "Source branch: '#{source_branch}'" }
374
+
375
+ # Early exit if source is a main-like branch
376
+ if main_like_branch?(source_branch, remote_name)
377
+ Datadog.logger.debug { "Branch '#{source_branch}' already matches base branch filter (#{DEFAULT_LIKE_BRANCH_FILTER})" }
378
+ return nil
379
+ end
380
+
381
+ possible_base_branches = base_branch.nil? ? POSSIBLE_BASE_BRANCHES : [base_branch]
382
+ # Check and fetch base branches if they don't exist in local git repository
383
+ check_and_fetch_base_branches(possible_base_branches, remote_name)
384
+
385
+ default_branch = detect_default_branch(remote_name)
386
+ Datadog.logger.debug { "Default branch: '#{default_branch}'" }
387
+
388
+ candidates = build_candidate_list(remote_name, source_branch, base_branch)
389
+ if candidates.nil? || candidates.empty?
390
+ Datadog.logger.debug { "No candidate branches found." }
391
+ return nil
392
+ end
393
+
394
+ metrics = compute_branch_metrics(candidates, source_branch)
395
+ Datadog.logger.debug { "Branch metrics: '#{metrics}'" }
396
+
397
+ best_branch_sha = find_best_branch(metrics, default_branch, remote_name)
398
+ Datadog.logger.debug { "Best branch: '#{best_branch_sha}'" }
399
+
400
+ best_branch_sha
401
+ rescue => e
402
+ telemetry_track_error(e, Ext::Telemetry::Command::BASE_COMMIT_SHA)
403
+ log_failure(e, "git base ref")
404
+ nil
405
+ end
406
+
407
+ def self.check_and_fetch_branch(branch, remote_name)
408
+ # Check if branch exists locally
409
+ exec_git_command("git show-ref --verify --quiet refs/heads/#{branch}")
410
+ Datadog.logger.debug { "Branch '#{branch}' exists locally, skipping" }
411
+ rescue GitCommandExecutionError => e
412
+ Datadog.logger.debug { "Branch '#{branch}' doesn't exist locally, checking remote: #{e}" }
413
+ begin
414
+ remote_heads = exec_git_command("git ls-remote --heads #{remote_name} #{branch}")
415
+ if remote_heads.nil? || remote_heads.empty?
416
+ Datadog.logger.debug { "Branch '#{branch}' doesn't exist in remote" }
417
+ return
418
+ end
419
+
420
+ Datadog.logger.debug { "Branch '#{branch}' exists in remote, fetching" }
421
+ exec_git_command("git fetch --depth 1 #{remote_name} #{branch}:#{branch}")
422
+ rescue GitCommandExecutionError => e
423
+ Datadog.logger.debug { "Branch '#{branch}' couldn't be fetched from remote: #{e}" }
424
+ end
425
+ end
426
+
427
+ def self.check_and_fetch_base_branches(branches, remote_name)
428
+ branches.each do |branch|
429
+ check_and_fetch_branch(branch, remote_name)
430
+ end
431
+ end
432
+
433
+ def self.get_source_branch
434
+ source_branch = exec_git_command("git rev-parse --abbrev-ref HEAD")&.strip
435
+ if source_branch.nil?
436
+ Datadog.logger.debug { "Could not get current branch" }
437
+ return nil
438
+ end
439
+
440
+ exec_git_command("git rev-parse --verify --quiet #{source_branch} > /dev/null")
441
+ source_branch
442
+ end
443
+
444
+ def self.remove_remote_prefix(branch_name, remote_name)
445
+ branch_name&.sub(/^#{Regexp.escape(remote_name)}\//, "")
446
+ end
447
+
448
+ def self.main_like_branch?(branch_name, remote_name)
449
+ short_branch_name = remove_remote_prefix(branch_name, remote_name)
450
+ short_branch_name&.match?(DEFAULT_LIKE_BRANCH_FILTER)
451
+ end
452
+
453
+ def self.detect_default_branch(remote_name)
454
+ # @type var default_branch: String?
455
+ default_branch = nil
456
+ begin
457
+ default_ref = exec_git_command("git symbolic-ref --quiet --short \"refs/remotes/#{remote_name}/HEAD\" 2>/dev/null")
458
+ default_branch = remove_remote_prefix(default_ref, remote_name) unless default_ref.nil?
459
+ rescue
460
+ Datadog.logger.debug { "Could not get symbolic-ref, trying to find a fallback (main, master)..." }
461
+ end
462
+
463
+ default_branch = find_fallback_default_branch(remote_name) if default_branch.nil?
464
+ default_branch
465
+ end
466
+
467
+ def self.find_fallback_default_branch(remote_name)
468
+ ["main", "master"].each do |fallback|
469
+ exec_git_command("git show-ref --verify --quiet \"refs/remotes/#{remote_name}/#{fallback}\"")
470
+ Datadog.logger.debug { "Found fallback default branch '#{fallback}'" }
471
+ return fallback
472
+ rescue
473
+ next
474
+ end
475
+ nil
476
+ end
477
+
478
+ def self.build_candidate_list(remote_name, source_branch, base_branch)
479
+ unless base_branch.nil?
480
+ return [base_branch]
481
+ end
482
+
483
+ candidates = exec_git_command("git for-each-ref --format='%(refname:short)' refs/heads \"refs/remotes/#{remote_name}\"")&.lines&.map(&:strip)
484
+ Datadog.logger.debug { "Available branches: '#{candidates}'" }
485
+ candidates&.select! { |b| b.match?(DEFAULT_LIKE_BRANCH_FILTER) && b != source_branch }
486
+ Datadog.logger.debug { "Candidate branches: '#{candidates}'" }
487
+ candidates
488
+ end
489
+
490
+ def self.compute_branch_metrics(candidates, source_branch)
491
+ metrics = {}
492
+ candidates.each do |cand|
493
+ base_sha = exec_git_command("git merge-base #{cand} #{source_branch} 2>/dev/null")&.strip
494
+ next if base_sha.nil? || base_sha.empty?
495
+
496
+ behind, ahead = exec_git_command("git rev-list --left-right --count #{cand}...#{source_branch}")&.strip&.split&.map(&:to_i)
497
+ metrics[cand] = {behind: behind, ahead: ahead, base_sha: base_sha}
498
+ end
499
+ metrics
500
+ end
501
+
502
+ def self.find_best_branch(metrics, default_branch, remote_name)
503
+ return nil if metrics.empty?
504
+
505
+ _, best_data = metrics.min_by do |cand, data|
506
+ [
507
+ data[:ahead],
508
+ default_branch?(cand, default_branch, remote_name) ? 0 : 1 # prefer default branch on tie
509
+ ]
510
+ end
511
+
512
+ best_data ? best_data[:base_sha] : nil
513
+ end
514
+
515
+ def self.default_branch?(branch, default_branch, remote_name)
516
+ branch == default_branch || branch == "#{remote_name}/#{default_branch}"
517
+ end
518
+
519
+ def self.get_remote_name
520
+ # Try to find remote from upstream tracking
521
+ upstream = nil
522
+ begin
523
+ upstream = exec_git_command("git rev-parse --abbrev-ref --symbolic-full-name @{upstream}")&.strip
524
+ rescue => e
525
+ Datadog.logger.debug { "Error getting upstream: #{e}" }
526
+ end
527
+
528
+ if upstream
529
+ upstream.split("/").first
530
+ else
531
+ # Fallback to first remote if no upstream is set
532
+ first_remote_value = exec_git_command("git remote")&.split("\n")&.first
533
+ Datadog.logger.debug { "First remote value: '#{first_remote_value}'" }
534
+ first_remote_value || "origin"
535
+ end
536
+ end
537
+
307
538
  # makes .exec_git_command private to make sure that this method
308
539
  # is not called from outside of this module with insecure parameters
309
540
  class << self
@@ -316,10 +547,13 @@ module Datadog
316
547
  def exec_git_command(cmd, stdin: nil)
317
548
  # Shell injection is alleviated by making sure that no outside modules call this method.
318
549
  # It is called only internally with static parameters.
319
- # no-dd-sa:ruby-security/shell-injection
550
+
551
+ # @type var out: String
552
+ # @type var status: Process::Status?
320
553
  out, status = Open3.capture2e(cmd, stdin_data: stdin)
321
554
 
322
555
  if status.nil?
556
+ # @type var retry_count: Integer
323
557
  retry_count = COMMAND_RETRY_COUNT
324
558
  Datadog.logger.debug { "Opening pipe failed, starting retries..." }
325
559
  while status.nil? && retry_count.positive?
@@ -15,10 +15,11 @@ module Datadog
15
15
  module CI
16
16
  module Git
17
17
  class TreeUploader
18
- attr_reader :api
18
+ attr_reader :api, :force_unshallow
19
19
 
20
- def initialize(api:)
20
+ def initialize(api:, force_unshallow: false)
21
21
  @api = api
22
+ @force_unshallow = force_unshallow
22
23
  end
23
24
 
24
25
  def call(repository_url)
@@ -45,7 +46,13 @@ module Datadog
45
46
  # ask the backend for the list of commits it already has
46
47
  known_commits, new_commits = fetch_known_commits_and_split(repository_url, latest_commits)
47
48
  # if all commits are present in the backend, we don't need to upload anything
48
- if new_commits.empty?
49
+
50
+ # We optimize unshallowing process by checking the latest available commits with backend:
51
+ # if they are already known to backend, then we don't have to unshallow.
52
+ #
53
+ # Sometimes we need to unshallow anyway: for impacted tests detection feature for example we need
54
+ # to calculate git diffs locally. In this case we skip the optimization and always unshallow.
55
+ if new_commits.empty? && !@force_unshallow
49
56
  Datadog.logger.debug("No new commits to upload")
50
57
  return
51
58
  end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+
5
+ require_relative "../ext/test"
6
+ require_relative "../git/local_repository"
7
+ require_relative "telemetry"
8
+
9
+ module Datadog
10
+ module CI
11
+ module ImpactedTestsDetection
12
+ class Component
13
+ def initialize(enabled:)
14
+ @enabled = enabled
15
+ @changed_files = Set.new
16
+ end
17
+
18
+ def configure(library_settings, test_session)
19
+ @enabled &&= library_settings.impacted_tests_enabled?
20
+
21
+ return unless @enabled
22
+
23
+ # we must unshallow the repository before trying to find base_commit_sha or executing `git diff` command
24
+ git_tree_upload_worker.wait_until_done
25
+
26
+ base_commit_sha = test_session.base_commit_sha || Git::LocalRepository.base_commit_sha
27
+ if base_commit_sha.nil?
28
+ Datadog.logger.debug { "Impacted tests detection disabled: base commit not found" }
29
+ @enabled = false
30
+ return
31
+ end
32
+
33
+ changed_files = Git::LocalRepository.get_changed_files_from_diff(base_commit_sha)
34
+ if changed_files.nil?
35
+ Datadog.logger.debug { "Impacted tests detection disabled: could not get changed files" }
36
+ @enabled = false
37
+ return
38
+ end
39
+
40
+ Datadog.logger.debug do
41
+ "Impacted tests detection: found #{changed_files.size} changed files"
42
+ end
43
+ Datadog.logger.debug do
44
+ "Impacted tests detection: changed files: #{changed_files.inspect}"
45
+ end
46
+
47
+ @changed_files = changed_files
48
+ @enabled = true
49
+ end
50
+
51
+ def enabled?
52
+ @enabled
53
+ end
54
+
55
+ def modified?(test_span)
56
+ return false unless enabled?
57
+
58
+ source_file = test_span.source_file
59
+ return false if source_file.nil?
60
+
61
+ @changed_files.include?(source_file)
62
+ end
63
+
64
+ def tag_modified_test(test_span)
65
+ return unless modified?(test_span)
66
+
67
+ Datadog.logger.debug do
68
+ "Impacted tests detection: test #{test_span.name} with source file #{test_span.source_file} is modified"
69
+ end
70
+
71
+ test_span.set_tag(Ext::Test::TAG_TEST_IS_MODIFIED, "true")
72
+ Telemetry.impacted_test_detected
73
+ end
74
+
75
+ private
76
+
77
+ def git_tree_upload_worker
78
+ Datadog.send(:components).git_tree_upload_worker
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../ext/telemetry"
4
+ require_relative "../utils/telemetry"
5
+
6
+ module Datadog
7
+ module CI
8
+ module ImpactedTestsDetection
9
+ module Telemetry
10
+ def self.impacted_test_detected
11
+ Utils::Telemetry.inc(Ext::Telemetry::METRIC_IMPACTED_TESTS_IS_MODIFIED, 1)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "datadog/core/environment/platform"
4
+
5
+ module Datadog
6
+ module CI
7
+ module Logs
8
+ class Component
9
+ attr_reader :enabled
10
+
11
+ def initialize(enabled:, writer:)
12
+ @enabled = enabled && !writer.nil?
13
+ @writer = writer
14
+ end
15
+
16
+ def write(event)
17
+ return unless enabled
18
+
19
+ add_common_tags!(event)
20
+ @writer&.write(event)
21
+
22
+ nil
23
+ end
24
+
25
+ def shutdown!
26
+ @writer&.stop
27
+ end
28
+
29
+ private
30
+
31
+ def add_common_tags!(event)
32
+ test_session = test_visibility.active_test_session
33
+
34
+ event[:ddsource] ||= "ruby"
35
+ event[:ddtags] ||= "datadog.product:citest"
36
+ event[:service] ||= test_session&.service
37
+ event[:hostname] ||= Datadog::Core::Environment::Platform.hostname
38
+ end
39
+
40
+ def test_visibility
41
+ ::Datadog.send(:components).test_visibility
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ require "datadog/core/chunker"
6
+
7
+ module Datadog
8
+ module CI
9
+ module Logs
10
+ class Transport
11
+ DEFAULT_MAX_PAYLOAD_SIZE = 4.5 * 1024 * 1024
12
+
13
+ attr_reader :api,
14
+ :max_payload_size
15
+
16
+ def initialize(api:, max_payload_size: DEFAULT_MAX_PAYLOAD_SIZE)
17
+ @api = api
18
+ @max_payload_size = max_payload_size
19
+ end
20
+
21
+ def send_events(events)
22
+ return [] if events.nil? || events.empty?
23
+
24
+ Datadog.logger.debug { "[#{self.class.name}] Sending #{events.count} events..." }
25
+
26
+ encoded_events = events.filter_map do |event|
27
+ encoded_event = event.to_json
28
+ if event_too_large?(event, encoded_event)
29
+ next
30
+ end
31
+
32
+ encoded_event
33
+ end
34
+
35
+ responses = []
36
+ Datadog::Core::Chunker.chunk_by_size(encoded_events, max_payload_size).map do |chunk|
37
+ encoded_payload = pack_events(chunk)
38
+ Datadog.logger.debug do
39
+ "[#{self.class.name}] Send chunk of #{chunk.count} events; payload size #{encoded_payload.size}"
40
+ end
41
+
42
+ response = send_payload(encoded_payload)
43
+
44
+ responses << response
45
+ end
46
+
47
+ responses
48
+ end
49
+
50
+ private
51
+
52
+ def pack_events(encoded_events)
53
+ "[#{encoded_events.join(",")}]"
54
+ end
55
+
56
+ def event_too_large?(event, encoded_event)
57
+ return false unless encoded_event.size > max_payload_size
58
+
59
+ # This single event is too large, we can't flush it
60
+ Datadog.logger.debug(
61
+ "[#{self.class.name}] Dropping event for logs intake. Payload too large: '#{event.inspect}'"
62
+ )
63
+
64
+ true
65
+ end
66
+
67
+ def send_payload(encoded_payload)
68
+ @api.logs_intake_request(path: "/v1/input", payload: encoded_payload)
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -46,7 +46,8 @@ module Datadog
46
46
  Worker.new { test_optimisation.configure(@library_configuration, test_session) },
47
47
  Worker.new { test_retries.configure(@library_configuration, test_session) },
48
48
  Worker.new { test_visibility.configure(@library_configuration, test_session) },
49
- Worker.new { test_management.configure(@library_configuration, test_session) }
49
+ Worker.new { test_management.configure(@library_configuration, test_session) },
50
+ Worker.new { impacted_tests_detection.configure(@library_configuration, test_session) }
50
51
  ]
51
52
 
52
53
  # launch configuration workers
@@ -89,6 +90,10 @@ module Datadog
89
90
  Datadog.send(:components).test_retries
90
91
  end
91
92
 
93
+ def impacted_tests_detection
94
+ Datadog.send(:components).impacted_tests_detection
95
+ end
96
+
92
97
  def git_tree_upload_worker
93
98
  Datadog.send(:components).git_tree_upload_worker
94
99
  end
@@ -106,6 +106,14 @@ module Datadog
106
106
  )
107
107
  end
108
108
 
109
+ def impacted_tests_enabled?
110
+ return @impacted_tests_enabled if defined?(@impacted_tests_enabled)
111
+
112
+ @impacted_tests_enabled = Utils::Parsing.convert_to_bool(
113
+ payload.fetch(Ext::Transport::DD_API_SETTINGS_RESPONSE_IMPACTED_TESTS_ENABLED_KEY, false)
114
+ )
115
+ end
116
+
109
117
  def slow_test_retries
110
118
  return @slow_test_retries if defined?(@slow_test_retries)
111
119
 
@@ -6,6 +6,7 @@ require "datadog/core/environment/platform"
6
6
 
7
7
  require_relative "ext/test"
8
8
  require_relative "utils/test_run"
9
+ require_relative "ext/git"
9
10
 
10
11
  module Datadog
11
12
  module CI
@@ -164,6 +165,12 @@ module Datadog
164
165
  tracer_span.get_tag(Ext::Git::TAG_BRANCH)
165
166
  end
166
167
 
168
+ # Returns the base commit SHA for the pull request, if available.
169
+ # @return [String, nil] the base commit SHA or nil if not set.
170
+ def base_commit_sha
171
+ tracer_span.get_tag(Ext::Git::TAG_PULL_REQUEST_BASE_BRANCH_SHA)
172
+ end
173
+
167
174
  # Returns the OS architecture extracted from the environment.
168
175
  # @return [String] OS arch.
169
176
  def os_architecture
@@ -25,6 +25,10 @@ module Datadog
25
25
  # Finishes the current test.
26
26
  # @return [void]
27
27
  def finish
28
+ if is_retry? && retry_reason.nil?
29
+ set_tag(Ext::Test::TAG_RETRY_REASON, Ext::Test::RetryReason::RETRY_EXTERNAL)
30
+ end
31
+
28
32
  test_visibility.deactivate_test
29
33
 
30
34
  super
@@ -68,6 +72,12 @@ module Datadog
68
72
  get_tag(Ext::Test::TAG_IS_RETRY) == "true"
69
73
  end
70
74
 
75
+ # Returns string with a reason why test was retried
76
+ # @return [String] retry reason
77
+ def retry_reason
78
+ get_tag(Ext::Test::TAG_RETRY_REASON)
79
+ end
80
+
71
81
  # Returns "true" if this span represents a test that wasn't known to Datadog before.
72
82
  # @return [Boolean] true if this test is a new one, false otherwise.
73
83
  def is_new?
@@ -92,6 +102,12 @@ module Datadog
92
102
  get_tag(Ext::Test::TAG_IS_ATTEMPT_TO_FIX) == "true"
93
103
  end
94
104
 
105
+ # Returns "true" if this test is marked as modified (e.g., impacted by code changes).
106
+ # @return [Boolean] true if this test is modified, false otherwise.
107
+ def modified?
108
+ get_tag(Ext::Test::TAG_TEST_IS_MODIFIED) == "true"
109
+ end
110
+
95
111
  # Marks this test as unskippable by the Test Impact Analysis.
96
112
  # This must be done before the test execution starts.
97
113
  #
@@ -118,7 +118,8 @@ module Datadog
118
118
  "type" => Ext::Transport::DD_API_TEST_MANAGEMENT_TESTS_TYPE,
119
119
  "attributes" => {
120
120
  "repository_url" => test_session.git_repository_url,
121
- "commit_message" => test_session.git_commit_message
121
+ "commit_message" => test_session.git_commit_message,
122
+ "sha" => test_session.git_commit_sha
122
123
  }
123
124
  }
124
125
  }.to_json