datadog-ci 1.16.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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +24 -2
  3. data/lib/datadog/ci/codeowners/rule.rb +5 -0
  4. data/lib/datadog/ci/configuration/components.rb +17 -5
  5. data/lib/datadog/ci/configuration/settings.rb +6 -0
  6. data/lib/datadog/ci/contrib/knapsack/patcher.rb +1 -3
  7. data/lib/datadog/ci/contrib/knapsack/runner.rb +2 -0
  8. data/lib/datadog/ci/contrib/minitest/runner.rb +1 -0
  9. data/lib/datadog/ci/contrib/minitest/test.rb +7 -2
  10. data/lib/datadog/ci/contrib/parallel_tests/patcher.rb +1 -3
  11. data/lib/datadog/ci/contrib/patcher.rb +4 -0
  12. data/lib/datadog/ci/contrib/rspec/helpers.rb +1 -3
  13. data/lib/datadog/ci/ext/environment/extractor.rb +4 -6
  14. data/lib/datadog/ci/ext/environment/providers/appveyor.rb +5 -0
  15. data/lib/datadog/ci/ext/environment/providers/base.rb +7 -2
  16. data/lib/datadog/ci/ext/environment/providers/bitbucket.rb +6 -0
  17. data/lib/datadog/ci/ext/environment/providers/bitrise.rb +7 -1
  18. data/lib/datadog/ci/ext/environment/providers/buddy.rb +5 -0
  19. data/lib/datadog/ci/ext/environment/providers/github_actions.rb +37 -18
  20. data/lib/datadog/ci/ext/environment/providers/gitlab.rb +13 -1
  21. data/lib/datadog/ci/ext/environment/providers/user_defined_tags.rb +12 -0
  22. data/lib/datadog/ci/ext/git.rb +3 -0
  23. data/lib/datadog/ci/ext/settings.rb +1 -0
  24. data/lib/datadog/ci/ext/telemetry.rb +4 -0
  25. data/lib/datadog/ci/ext/test.rb +4 -1
  26. data/lib/datadog/ci/ext/transport.rb +1 -0
  27. data/lib/datadog/ci/git/local_repository.rb +238 -4
  28. data/lib/datadog/ci/git/tree_uploader.rb +10 -3
  29. data/lib/datadog/ci/impacted_tests_detection/component.rb +83 -0
  30. data/lib/datadog/ci/impacted_tests_detection/telemetry.rb +16 -0
  31. data/lib/datadog/ci/remote/component.rb +6 -1
  32. data/lib/datadog/ci/remote/library_settings.rb +8 -0
  33. data/lib/datadog/ci/span.rb +7 -0
  34. data/lib/datadog/ci/test.rb +6 -0
  35. data/lib/datadog/ci/test_management/tests_properties.rb +2 -1
  36. data/lib/datadog/ci/test_retries/component.rb +8 -17
  37. data/lib/datadog/ci/test_retries/driver/{retry_new.rb → retry_flake_detection.rb} +1 -1
  38. data/lib/datadog/ci/test_retries/strategy/{retry_new.rb → retry_flake_detection.rb} +4 -4
  39. data/lib/datadog/ci/test_visibility/component.rb +6 -0
  40. data/lib/datadog/ci/version.rb +1 -1
  41. metadata +7 -5
@@ -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
@@ -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
@@ -102,6 +102,12 @@ module Datadog
102
102
  get_tag(Ext::Test::TAG_IS_ATTEMPT_TO_FIX) == "true"
103
103
  end
104
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
+
105
111
  # Marks this test as unskippable by the Test Impact Analysis.
106
112
  # This must be done before the test execution starts.
107
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
@@ -2,11 +2,11 @@
2
2
 
3
3
  require_relative "driver/no_retry"
4
4
  require_relative "driver/retry_failed"
5
- require_relative "driver/retry_new"
5
+ require_relative "driver/retry_flake_detection"
6
6
 
7
7
  require_relative "strategy/no_retry"
8
8
  require_relative "strategy/retry_failed"
9
- require_relative "strategy/retry_new"
9
+ require_relative "strategy/retry_flake_detection"
10
10
  require_relative "strategy/retry_flaky_fixed"
11
11
 
12
12
  require_relative "../ext/telemetry"
@@ -31,13 +31,13 @@ module Datadog
31
31
  )
32
32
  no_retries_strategy = Strategy::NoRetry.new
33
33
 
34
- @retry_failed_strategy = Strategy::RetryFailed.new(
34
+ retry_failed_strategy = Strategy::RetryFailed.new(
35
35
  enabled: retry_failed_tests_enabled,
36
36
  max_attempts: retry_failed_tests_max_attempts,
37
37
  total_limit: retry_failed_tests_total_limit
38
38
  )
39
39
 
40
- @retry_new_strategy = Strategy::RetryNew.new(
40
+ retry_flake_detection_strategy = Strategy::RetryFlakeDetection.new(
41
41
  enabled: retry_new_tests_enabled
42
42
  )
43
43
 
@@ -49,8 +49,8 @@ module Datadog
49
49
  # order is important, we apply the first matching strategy
50
50
  @retry_strategies = [
51
51
  retry_flaky_fixed_strategy,
52
- @retry_new_strategy,
53
- @retry_failed_strategy,
52
+ retry_flake_detection_strategy,
53
+ retry_failed_strategy,
54
54
  no_retries_strategy
55
55
  ]
56
56
  @mutex = Mutex.new
@@ -111,23 +111,14 @@ module Datadog
111
111
  test_span&.set_tag(Ext::Test::TAG_HAS_FAILED_ALL_RETRIES, "true") if test_span&.all_executions_failed?
112
112
 
113
113
  # if we are attempting to fix the test and all retries passed, we indicate that the fix might have worked
114
- if test_span&.attempt_to_fix? && test_span.all_executions_passed?
115
- test_span&.set_tag(Ext::Test::TAG_ATTEMPT_TO_FIX_PASSED, "true")
116
- end
114
+ # otherwise we send "false" to show that it didn't work
115
+ test_span&.set_tag(Ext::Test::TAG_ATTEMPT_TO_FIX_PASSED, test_span&.all_executions_passed?.to_s) if test_span&.attempt_to_fix?
117
116
  end
118
117
 
119
118
  def should_retry?
120
119
  !!current_retry_driver&.should_retry?
121
120
  end
122
121
 
123
- def auto_test_retries_feature_enabled
124
- @retry_failed_strategy.enabled
125
- end
126
-
127
- def early_flake_detection_feature_enabled
128
- @retry_new_strategy.enabled
129
- end
130
-
131
122
  private
132
123
 
133
124
  def current_retry_driver
@@ -9,7 +9,7 @@ module Datadog
9
9
  module TestRetries
10
10
  module Driver
11
11
  # retry every new test up to 10 times (early flake detection)
12
- class RetryNew < Base
12
+ class RetryFlakeDetection < Base
13
13
  def initialize(test_span, max_attempts_thresholds:)
14
14
  @max_attempts_thresholds = max_attempts_thresholds
15
15
  @attempts = 0
@@ -2,13 +2,13 @@
2
2
 
3
3
  require_relative "base"
4
4
 
5
- require_relative "../driver/retry_new"
5
+ require_relative "../driver/retry_flake_detection"
6
6
 
7
7
  module Datadog
8
8
  module CI
9
9
  module TestRetries
10
10
  module Strategy
11
- class RetryNew < Base
11
+ class RetryFlakeDetection < Base
12
12
  DEFAULT_TOTAL_TESTS_COUNT = 100
13
13
 
14
14
  attr_reader :enabled, :max_attempts_thresholds, :total_limit, :retried_count
@@ -31,7 +31,7 @@ module Datadog
31
31
  mark_test_session_faulty(Datadog::CI.active_test_session)
32
32
  end
33
33
 
34
- @enabled && !test_span.skipped? && test_span.is_new?
34
+ @enabled && !test_span.skipped? && (test_span.is_new? || test_span.modified?)
35
35
  end
36
36
 
37
37
  def configure(library_settings, test_session)
@@ -52,7 +52,7 @@ module Datadog
52
52
  end
53
53
  @retried_count += 1
54
54
 
55
- Driver::RetryNew.new(test_span, max_attempts_thresholds: @max_attempts_thresholds)
55
+ Driver::RetryFlakeDetection.new(test_span, max_attempts_thresholds: @max_attempts_thresholds)
56
56
  end
57
57
 
58
58
  private
@@ -261,6 +261,8 @@ module Datadog
261
261
 
262
262
  mark_test_as_new(test) if new_test?(test)
263
263
 
264
+ impacted_tests_detection.tag_modified_test(test)
265
+
264
266
  test_management.tag_test_from_properties(test)
265
267
 
266
268
  test_optimisation.mark_if_skippable(test)
@@ -435,6 +437,10 @@ module Datadog
435
437
  Datadog.send(:components).test_management
436
438
  end
437
439
 
440
+ def impacted_tests_detection
441
+ Datadog.send(:components).impacted_tests_detection
442
+ end
443
+
438
444
  # DISTRIBUTED RUBY CONTEXT
439
445
  def start_drb_service
440
446
  return if @context_service_uri
@@ -4,7 +4,7 @@ module Datadog
4
4
  module CI
5
5
  module VERSION
6
6
  MAJOR = 1
7
- MINOR = 16
7
+ MINOR = 17
8
8
  PATCH = 0
9
9
  PRE = nil
10
10
  BUILD = nil