datadog-ci 1.16.0 → 1.18.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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +37 -2
  3. data/ext/datadog_ci_native/ci.c +10 -0
  4. data/ext/{datadog_cov → datadog_ci_native}/datadog_cov.c +119 -147
  5. data/ext/datadog_ci_native/datadog_cov.h +3 -0
  6. data/ext/datadog_ci_native/datadog_source_code.c +28 -0
  7. data/ext/datadog_ci_native/datadog_source_code.h +3 -0
  8. data/ext/{datadog_cov → datadog_ci_native}/extconf.rb +1 -1
  9. data/lib/datadog/ci/codeowners/rule.rb +5 -0
  10. data/lib/datadog/ci/configuration/components.rb +17 -5
  11. data/lib/datadog/ci/configuration/settings.rb +6 -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/minitest/runner.rb +1 -0
  15. data/lib/datadog/ci/contrib/minitest/test.rb +24 -9
  16. data/lib/datadog/ci/contrib/parallel_tests/patcher.rb +1 -3
  17. data/lib/datadog/ci/contrib/patcher.rb +4 -0
  18. data/lib/datadog/ci/contrib/rspec/example.rb +14 -7
  19. data/lib/datadog/ci/contrib/rspec/helpers.rb +1 -3
  20. data/lib/datadog/ci/ext/environment/extractor.rb +4 -6
  21. data/lib/datadog/ci/ext/environment/providers/appveyor.rb +5 -0
  22. data/lib/datadog/ci/ext/environment/providers/base.rb +7 -2
  23. data/lib/datadog/ci/ext/environment/providers/bitbucket.rb +6 -0
  24. data/lib/datadog/ci/ext/environment/providers/bitrise.rb +7 -1
  25. data/lib/datadog/ci/ext/environment/providers/buddy.rb +5 -0
  26. data/lib/datadog/ci/ext/environment/providers/github_actions.rb +37 -18
  27. data/lib/datadog/ci/ext/environment/providers/gitlab.rb +13 -1
  28. data/lib/datadog/ci/ext/environment/providers/user_defined_tags.rb +12 -0
  29. data/lib/datadog/ci/ext/git.rb +3 -0
  30. data/lib/datadog/ci/ext/settings.rb +1 -0
  31. data/lib/datadog/ci/ext/telemetry.rb +3 -0
  32. data/lib/datadog/ci/ext/test.rb +5 -1
  33. data/lib/datadog/ci/ext/transport.rb +1 -0
  34. data/lib/datadog/ci/git/base_branch_sha_detection/base.rb +66 -0
  35. data/lib/datadog/ci/git/base_branch_sha_detection/branch_metric.rb +34 -0
  36. data/lib/datadog/ci/git/base_branch_sha_detection/guesser.rb +137 -0
  37. data/lib/datadog/ci/git/base_branch_sha_detection/merge_base_extractor.rb +29 -0
  38. data/lib/datadog/ci/git/base_branch_sha_detector.rb +63 -0
  39. data/lib/datadog/ci/git/cli.rb +56 -0
  40. data/lib/datadog/ci/git/local_repository.rb +131 -118
  41. data/lib/datadog/ci/git/telemetry.rb +14 -0
  42. data/lib/datadog/ci/git/tree_uploader.rb +10 -3
  43. data/lib/datadog/ci/impacted_tests_detection/component.rb +81 -0
  44. data/lib/datadog/ci/remote/component.rb +6 -1
  45. data/lib/datadog/ci/remote/library_settings.rb +8 -0
  46. data/lib/datadog/ci/span.rb +7 -0
  47. data/lib/datadog/ci/test.rb +6 -0
  48. data/lib/datadog/ci/test_management/tests_properties.rb +2 -1
  49. data/lib/datadog/ci/test_optimisation/component.rb +10 -6
  50. data/lib/datadog/ci/test_optimisation/coverage/ddcov.rb +1 -1
  51. data/lib/datadog/ci/test_retries/component.rb +8 -17
  52. data/lib/datadog/ci/test_retries/driver/{retry_new.rb → retry_flake_detection.rb} +1 -1
  53. data/lib/datadog/ci/test_retries/strategy/{retry_new.rb → retry_flake_detection.rb} +4 -4
  54. data/lib/datadog/ci/test_visibility/component.rb +6 -0
  55. data/lib/datadog/ci/test_visibility/telemetry.rb +3 -0
  56. data/lib/datadog/ci/utils/command.rb +116 -0
  57. data/lib/datadog/ci/utils/source_code.rb +31 -0
  58. data/lib/datadog/ci/version.rb +1 -1
  59. metadata +21 -8
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+ require_relative "branch_metric"
5
+
6
+ require_relative "../local_repository"
7
+
8
+ module Datadog
9
+ module CI
10
+ module Git
11
+ module BaseBranchShaDetection
12
+ class Guesser < Base
13
+ POSSIBLE_BASE_BRANCHES = %w[main master preprod prod dev development trunk].freeze
14
+ DEFAULT_LIKE_BRANCH_FILTER = /^(#{POSSIBLE_BASE_BRANCHES.join("|")}|release\/.*|hotfix\/.*)$/.freeze
15
+
16
+ def call
17
+ # Check and fetch base branches if they don't exist in local git repository
18
+ check_and_fetch_base_branches(POSSIBLE_BASE_BRANCHES, remote_name)
19
+
20
+ candidates = build_candidate_list(remote_name)
21
+
22
+ if candidates.nil? || candidates.empty?
23
+ Datadog.logger.debug { "No candidate branches found." }
24
+ return nil
25
+ end
26
+
27
+ metrics = compute_branch_metrics(candidates, source_branch)
28
+ Datadog.logger.debug { "Branch metrics: '#{metrics}'" }
29
+
30
+ best_branch_sha = find_best_branch(metrics, remote_name)
31
+ Datadog.logger.debug { "Best branch SHA: '#{best_branch_sha}'" }
32
+
33
+ best_branch_sha
34
+ end
35
+
36
+ private
37
+
38
+ def check_and_fetch_base_branches(branches, remote_name)
39
+ branches.each do |branch|
40
+ check_and_fetch_branch(branch, remote_name)
41
+ end
42
+ end
43
+
44
+ def main_like_branch?(branch_name, remote_name)
45
+ short_branch_name = remove_remote_prefix(branch_name, remote_name)
46
+ short_branch_name&.match?(DEFAULT_LIKE_BRANCH_FILTER)
47
+ end
48
+
49
+ def detect_default_branch(remote_name)
50
+ # @type var default_branch: String?
51
+ default_branch = nil
52
+ begin
53
+ default_ref = CLI.exec_git_command(["symbolic-ref", "--quiet", "--short", "refs/remotes/#{remote_name}/HEAD"])
54
+ default_branch = remove_remote_prefix(default_ref, remote_name) unless default_ref.nil?
55
+ rescue
56
+ Datadog.logger.debug { "Could not get symbolic-ref, trying to find a fallback (main, master)..." }
57
+ end
58
+
59
+ default_branch = find_fallback_default_branch(remote_name) if default_branch.nil?
60
+ default_branch
61
+ end
62
+
63
+ def find_fallback_default_branch(remote_name)
64
+ ["main", "master"].each do |fallback|
65
+ CLI.exec_git_command(["show-ref", "--verify", "--quiet", "refs/remotes/#{remote_name}/#{fallback}"])
66
+ Datadog.logger.debug { "Found fallback default branch '#{fallback}'" }
67
+ return fallback
68
+ rescue
69
+ next
70
+ end
71
+ nil
72
+ end
73
+
74
+ def build_candidate_list(remote_name)
75
+ # we cannot assume that local branches are the same as remote branches
76
+ # so we need to go over remote branches only
77
+ candidates = CLI.exec_git_command(["for-each-ref", "--format=%(refname:short)", "refs/remotes/#{remote_name}"])&.lines&.map(&:strip)
78
+ Datadog.logger.debug { "Available branches: '#{candidates}'" }
79
+ candidates&.select! do |candidate_branch|
80
+ main_like_branch?(candidate_branch, remote_name)
81
+ end
82
+ Datadog.logger.debug { "Candidate branches: '#{candidates}'" }
83
+ candidates
84
+ end
85
+
86
+ def compute_branch_metrics(candidates, source_branch)
87
+ metrics = []
88
+ candidates.each do |cand|
89
+ base_sha = merge_base_sha(cand, source_branch)
90
+ next if base_sha.nil? || base_sha.empty?
91
+
92
+ rev_list_output = CLI.exec_git_command(["rev-list", "--left-right", "--count", "#{cand}...#{source_branch}"], timeout: CLI::LONG_TIMEOUT)&.strip
93
+ next if rev_list_output.nil?
94
+
95
+ behind, ahead = rev_list_output.split.map(&:to_i)
96
+ next if behind.nil? || ahead.nil?
97
+
98
+ metric = BranchMetric.new(
99
+ branch_name: cand,
100
+ behind: behind,
101
+ ahead: ahead,
102
+ base_sha: base_sha
103
+ )
104
+
105
+ if metric.up_to_date?
106
+ Datadog.logger.debug { "Branch '#{cand}' is up to date with '#{source_branch}'" }
107
+ next
108
+ end
109
+
110
+ metrics << metric
111
+ end
112
+ metrics
113
+ end
114
+
115
+ def find_best_branch(metrics, remote_name)
116
+ return nil if metrics.empty?
117
+
118
+ # If there's only one metric, return its base SHA
119
+ return metrics.first.base_sha if metrics.size == 1
120
+
121
+ default_branch = detect_default_branch(remote_name)
122
+ Datadog.logger.debug { "Default branch: '#{default_branch}'" }
123
+
124
+ best_metric = metrics.min_by do |metric|
125
+ [
126
+ metric.divergence_score,
127
+ branches_equal?(metric.branch_name, default_branch, remote_name) ? 0 : 1 # prefer default branch on tie
128
+ ]
129
+ end
130
+
131
+ best_metric&.base_sha
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+ require_relative "../local_repository"
5
+
6
+ module Datadog
7
+ module CI
8
+ module Git
9
+ module BaseBranchShaDetection
10
+ class MergeBaseExtractor < Base
11
+ attr_reader :base_branch
12
+
13
+ def initialize(remote_name, source_branch, base_branch)
14
+ super(remote_name, source_branch)
15
+
16
+ @base_branch = base_branch
17
+ end
18
+
19
+ def call
20
+ check_and_fetch_branch(base_branch, remote_name)
21
+
22
+ full_base_branch_name = "#{remote_name}/#{remove_remote_prefix(base_branch, remote_name)}"
23
+ merge_base_sha(full_base_branch_name, source_branch)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_branch_sha_detection/guesser"
4
+ require_relative "base_branch_sha_detection/merge_base_extractor"
5
+
6
+ module Datadog
7
+ module CI
8
+ module Git
9
+ module BaseBranchShaDetector
10
+ def self.base_branch_sha(base_branch)
11
+ Datadog.logger.debug { "Base branch: '#{base_branch}'" }
12
+
13
+ remote_name = get_remote_name
14
+ Datadog.logger.debug { "Remote name: '#{remote_name}'" }
15
+
16
+ source_branch = get_source_branch
17
+ return nil if source_branch.nil?
18
+
19
+ Datadog.logger.debug { "Source branch: '#{source_branch}'" }
20
+
21
+ strategy = if base_branch.nil?
22
+ BaseBranchShaDetection::Guesser.new(remote_name, source_branch)
23
+ else
24
+ BaseBranchShaDetection::MergeBaseExtractor.new(remote_name, source_branch, base_branch)
25
+ end
26
+
27
+ strategy.call
28
+ end
29
+
30
+ def self.get_remote_name
31
+ # Try to find remote from upstream tracking
32
+ upstream = LocalRepository.get_upstream_branch
33
+
34
+ if upstream
35
+ upstream.split("/").first
36
+ else
37
+ # Fallback to first remote if no upstream is set
38
+ first_remote_value = CLI.exec_git_command(["remote"])&.split("\n")&.first
39
+ Datadog.logger.debug { "First remote value: '#{first_remote_value}'" }
40
+ first_remote_value || "origin"
41
+ end
42
+ end
43
+
44
+ def self.get_source_branch
45
+ source_branch = CLI.exec_git_command(["rev-parse", "--abbrev-ref", "HEAD"])
46
+ if source_branch.nil?
47
+ Datadog.logger.debug { "Could not get current branch" }
48
+ return nil
49
+ end
50
+
51
+ # Verify the branch exists
52
+ begin
53
+ CLI.exec_git_command(["rev-parse", "--verify", "--quiet", source_branch])
54
+ rescue CLI::GitCommandExecutionError
55
+ # Branch verification failed
56
+ return nil
57
+ end
58
+ source_branch
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../utils/command"
4
+
5
+ module Datadog
6
+ module CI
7
+ module Git
8
+ module CLI
9
+ class GitCommandExecutionError < StandardError
10
+ attr_reader :output, :command, :status
11
+ def initialize(message, output:, command:, status:)
12
+ super(message)
13
+
14
+ @output = output
15
+ @command = command
16
+ @status = status
17
+ end
18
+ end
19
+
20
+ # Timeout constants for git commands (in seconds)
21
+ # These values were set based on internal telemetry
22
+ UNSHALLOW_TIMEOUT = 500
23
+ LONG_TIMEOUT = 30
24
+ SHORT_TIMEOUT = 3
25
+
26
+ # Execute a git command with optional stdin input and timeout
27
+ #
28
+ # @param cmd [Array<String>] The git command as an array of strings
29
+ # @param stdin [String, nil] Optional stdin data to pass to the command
30
+ # @param timeout [Integer] Timeout in seconds for the command execution
31
+ # @return [String, nil] The command output, or nil if the output is empty
32
+ # @raise [GitCommandExecutionError] If the command fails or times out
33
+ def self.exec_git_command(cmd, stdin: nil, timeout: SHORT_TIMEOUT)
34
+ # @type var out: String
35
+ # @type var status: Process::Status?
36
+ out, status = Utils::Command.exec_command(["git"] + cmd, stdin_data: stdin, timeout: timeout)
37
+
38
+ if status.nil? || !status.success?
39
+ # Convert command to string representation for error message
40
+ cmd_str = cmd.join(" ")
41
+ raise GitCommandExecutionError.new(
42
+ "Failed to run git command [#{cmd_str}] with input [#{stdin}] and output [#{out}]. Status: #{status}",
43
+ output: out,
44
+ command: cmd_str,
45
+ status: status
46
+ )
47
+ end
48
+
49
+ return nil if out.empty?
50
+
51
+ out
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end