datadog-ci 1.17.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 (29) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +15 -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/contrib/minitest/test.rb +17 -7
  10. data/lib/datadog/ci/contrib/rspec/example.rb +14 -7
  11. data/lib/datadog/ci/ext/telemetry.rb +1 -2
  12. data/lib/datadog/ci/ext/test.rb +1 -0
  13. data/lib/datadog/ci/git/base_branch_sha_detection/base.rb +66 -0
  14. data/lib/datadog/ci/git/base_branch_sha_detection/branch_metric.rb +34 -0
  15. data/lib/datadog/ci/git/base_branch_sha_detection/guesser.rb +137 -0
  16. data/lib/datadog/ci/git/base_branch_sha_detection/merge_base_extractor.rb +29 -0
  17. data/lib/datadog/ci/git/base_branch_sha_detector.rb +63 -0
  18. data/lib/datadog/ci/git/cli.rb +56 -0
  19. data/lib/datadog/ci/git/local_repository.rb +73 -294
  20. data/lib/datadog/ci/git/telemetry.rb +14 -0
  21. data/lib/datadog/ci/impacted_tests_detection/component.rb +0 -2
  22. data/lib/datadog/ci/test_optimisation/component.rb +10 -6
  23. data/lib/datadog/ci/test_optimisation/coverage/ddcov.rb +1 -1
  24. data/lib/datadog/ci/test_visibility/telemetry.rb +3 -0
  25. data/lib/datadog/ci/utils/command.rb +116 -0
  26. data/lib/datadog/ci/utils/source_code.rb +31 -0
  27. data/lib/datadog/ci/version.rb +1 -1
  28. metadata +16 -5
  29. data/lib/datadog/ci/impacted_tests_detection/telemetry.rb +0 -16
@@ -5,6 +5,9 @@ require "pathname"
5
5
  require "set"
6
6
 
7
7
  require_relative "../ext/telemetry"
8
+ require_relative "../utils/command"
9
+ require_relative "base_branch_sha_detector"
10
+ require_relative "cli"
8
11
  require_relative "telemetry"
9
12
  require_relative "user"
10
13
 
@@ -12,21 +15,6 @@ module Datadog
12
15
  module CI
13
16
  module Git
14
17
  module LocalRepository
15
- class GitCommandExecutionError < StandardError
16
- attr_reader :output, :command, :status
17
- def initialize(message, output:, command:, status:)
18
- super(message)
19
-
20
- @output = output
21
- @command = command
22
- @status = status
23
- end
24
- end
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
29
-
30
18
  def self.root
31
19
  return @root if defined?(@root)
32
20
 
@@ -34,7 +22,7 @@ module Datadog
34
22
  end
35
23
 
36
24
  # ATTENTION: this function is running in a hot path
37
- # and should be optimized for performance
25
+ # and must be optimized for performance
38
26
  def self.relative_to_root(path)
39
27
  return "" if path.nil?
40
28
 
@@ -97,26 +85,26 @@ module Datadog
97
85
  res = nil
98
86
 
99
87
  duration_ms = Core::Utils::Time.measure(:float_millisecond) do
100
- res = exec_git_command("git ls-remote --get-url")
88
+ res = CLI.exec_git_command(["ls-remote", "--get-url"])
101
89
  end
102
90
 
103
91
  Telemetry.git_command_ms(Ext::Telemetry::Command::GET_REPOSITORY, duration_ms)
104
92
  res
105
93
  rescue => e
106
94
  log_failure(e, "git repository url")
107
- telemetry_track_error(e, Ext::Telemetry::Command::GET_REPOSITORY)
95
+ Telemetry.track_error(e, Ext::Telemetry::Command::GET_REPOSITORY)
108
96
  nil
109
97
  end
110
98
 
111
99
  def self.git_root
112
- exec_git_command("git rev-parse --show-toplevel")
100
+ CLI.exec_git_command(["rev-parse", "--show-toplevel"])
113
101
  rescue => e
114
102
  log_failure(e, "git root path")
115
103
  nil
116
104
  end
117
105
 
118
106
  def self.git_commit_sha
119
- exec_git_command("git rev-parse HEAD")
107
+ CLI.exec_git_command(["rev-parse", "HEAD"])
120
108
  rescue => e
121
109
  log_failure(e, "git commit sha")
122
110
  nil
@@ -128,26 +116,26 @@ module Datadog
128
116
  res = nil
129
117
 
130
118
  duration_ms = Core::Utils::Time.measure(:float_millisecond) do
131
- res = exec_git_command("git rev-parse --abbrev-ref HEAD")
119
+ res = CLI.exec_git_command(["rev-parse", "--abbrev-ref", "HEAD"])
132
120
  end
133
121
 
134
122
  Telemetry.git_command_ms(Ext::Telemetry::Command::GET_BRANCH, duration_ms)
135
123
  res
136
124
  rescue => e
137
125
  log_failure(e, "git branch")
138
- telemetry_track_error(e, Ext::Telemetry::Command::GET_BRANCH)
126
+ Telemetry.track_error(e, Ext::Telemetry::Command::GET_BRANCH)
139
127
  nil
140
128
  end
141
129
 
142
130
  def self.git_tag
143
- exec_git_command("git tag --points-at HEAD")
131
+ CLI.exec_git_command(["tag", "--points-at", "HEAD"])
144
132
  rescue => e
145
133
  log_failure(e, "git tag")
146
134
  nil
147
135
  end
148
136
 
149
137
  def self.git_commit_message
150
- exec_git_command("git log -n 1 --format=%B")
138
+ CLI.exec_git_command(["log", "-n", "1", "--format=%B"])
151
139
  rescue => e
152
140
  log_failure(e, "git commit message")
153
141
  nil
@@ -155,7 +143,7 @@ module Datadog
155
143
 
156
144
  def self.git_commit_users
157
145
  # Get committer and author information in one command.
158
- output = exec_git_command("git show -s --format='%an\t%ae\t%at\t%cn\t%ce\t%ct'")
146
+ output = CLI.exec_git_command(["show", "-s", "--format=%an\t%ae\t%at\t%cn\t%ce\t%ct"])
159
147
  unless output
160
148
  Datadog.logger.debug(
161
149
  "Unable to read git commit users: git command output is nil"
@@ -186,7 +174,7 @@ module Datadog
186
174
  output = nil
187
175
 
188
176
  duration_ms = Core::Utils::Time.measure(:float_millisecond) do
189
- output = exec_git_command("git log --format=%H -n 1000 --since=\"1 month ago\"")
177
+ output = CLI.exec_git_command(["log", "--format=%H", "-n", "1000", "--since=\"1 month ago\""], timeout: CLI::LONG_TIMEOUT)
190
178
  end
191
179
 
192
180
  Telemetry.git_command_ms(Ext::Telemetry::Command::GET_LOCAL_COMMITS, duration_ms)
@@ -196,27 +184,28 @@ module Datadog
196
184
  output.split("\n")
197
185
  rescue => e
198
186
  log_failure(e, "git commits")
199
- telemetry_track_error(e, Ext::Telemetry::Command::GET_LOCAL_COMMITS)
187
+ Telemetry.track_error(e, Ext::Telemetry::Command::GET_LOCAL_COMMITS)
200
188
  []
201
189
  end
202
190
 
203
191
  def self.git_commits_rev_list(included_commits:, excluded_commits:)
204
192
  Telemetry.git_command(Ext::Telemetry::Command::GET_OBJECTS)
205
- included_commits = filter_invalid_commits(included_commits).join(" ")
206
- excluded_commits = filter_invalid_commits(excluded_commits).map! { |sha| "^#{sha}" }.join(" ")
193
+ included_commits_list = filter_invalid_commits(included_commits)
194
+ excluded_commits_list = filter_invalid_commits(excluded_commits).map { |sha| "^#{sha}" }
207
195
 
208
196
  # @type var res: String?
209
197
  res = nil
210
198
 
211
199
  duration_ms = Core::Utils::Time.measure(:float_millisecond) do
212
- res = exec_git_command(
213
- "git rev-list " \
214
- "--objects " \
215
- "--no-object-names " \
216
- "--filter=blob:none " \
217
- "--since=\"1 month ago\" " \
218
- "#{excluded_commits} #{included_commits}"
219
- )
200
+ cmd = [
201
+ "rev-list",
202
+ "--objects",
203
+ "--no-object-names",
204
+ "--filter=blob:none",
205
+ "--since=\"1 month ago\""
206
+ ] + excluded_commits_list + included_commits_list
207
+
208
+ res = CLI.exec_git_command(cmd, timeout: CLI::LONG_TIMEOUT)
220
209
  end
221
210
 
222
211
  Telemetry.git_command_ms(Ext::Telemetry::Command::GET_OBJECTS, duration_ms)
@@ -224,7 +213,7 @@ module Datadog
224
213
  res
225
214
  rescue => e
226
215
  log_failure(e, "git commits rev list")
227
- telemetry_track_error(e, Ext::Telemetry::Command::GET_OBJECTS)
216
+ Telemetry.track_error(e, Ext::Telemetry::Command::GET_OBJECTS)
228
217
  nil
229
218
  end
230
219
 
@@ -239,9 +228,10 @@ module Datadog
239
228
  Telemetry.git_command(Ext::Telemetry::Command::PACK_OBJECTS)
240
229
 
241
230
  duration_ms = Core::Utils::Time.measure(:float_millisecond) do
242
- exec_git_command(
243
- "git pack-objects --compression=9 --max-pack-size=3m #{path}/#{basename}",
244
- stdin: commit_tree
231
+ CLI.exec_git_command(
232
+ ["pack-objects", "--compression=9", "--max-pack-size=3m", "#{path}/#{basename}"],
233
+ stdin: commit_tree,
234
+ timeout: CLI::LONG_TIMEOUT
245
235
  )
246
236
  end
247
237
  Telemetry.git_command_ms(Ext::Telemetry::Command::PACK_OBJECTS, duration_ms)
@@ -249,7 +239,7 @@ module Datadog
249
239
  basename
250
240
  rescue => e
251
241
  log_failure(e, "git generate packfiles")
252
- telemetry_track_error(e, Ext::Telemetry::Command::PACK_OBJECTS)
242
+ Telemetry.track_error(e, Ext::Telemetry::Command::PACK_OBJECTS)
253
243
  nil
254
244
  end
255
245
 
@@ -258,14 +248,14 @@ module Datadog
258
248
  res = false
259
249
 
260
250
  duration_ms = Core::Utils::Time.measure(:float_millisecond) do
261
- res = exec_git_command("git rev-parse --is-shallow-repository") == "true"
251
+ res = CLI.exec_git_command(["rev-parse", "--is-shallow-repository"]) == "true"
262
252
  end
263
253
  Telemetry.git_command_ms(Ext::Telemetry::Command::CHECK_SHALLOW, duration_ms)
264
254
 
265
255
  res
266
256
  rescue => e
267
257
  log_failure(e, "git shallow clone")
268
- telemetry_track_error(e, Ext::Telemetry::Command::CHECK_SHALLOW)
258
+ Telemetry.track_error(e, Ext::Telemetry::Command::CHECK_SHALLOW)
269
259
  false
270
260
  end
271
261
 
@@ -274,19 +264,15 @@ module Datadog
274
264
  # @type var res: String?
275
265
  res = nil
276
266
 
277
- unshallow_command =
278
- "git fetch " \
279
- "--shallow-since=\"1 month ago\" " \
280
- "--update-shallow " \
281
- "--filter=\"blob:none\" " \
282
- "--recurse-submodules=no " \
283
- "$(git config --default origin --get clone.defaultRemoteName)"
284
-
285
- unshallow_remotes = [
286
- "$(git rev-parse HEAD)",
287
- "$(git rev-parse --abbrev-ref --symbolic-full-name @{upstream})",
288
- nil
289
- ]
267
+ default_remote = CLI.exec_git_command(["config", "--default", "origin", "--get", "clone.defaultRemoteName"])&.strip
268
+ head_commit = git_commit_sha
269
+ upstream_branch = get_upstream_branch
270
+
271
+ # Build array of remotes to try, filtering out nil values
272
+ unshallow_remotes = []
273
+ unshallow_remotes << head_commit if head_commit
274
+ unshallow_remotes << upstream_branch if upstream_branch
275
+ unshallow_remotes << nil # Ensure the loop runs at least once, even if no valid remotes are available. This acts as a fallback mechanism.
290
276
 
291
277
  duration_ms = Core::Utils::Time.measure(:float_millisecond) do
292
278
  unshallow_remotes.each do |remote|
@@ -294,17 +280,27 @@ module Datadog
294
280
 
295
281
  res =
296
282
  begin
297
- exec_git_command(
298
- "#{unshallow_command} #{remote}"
299
- )
283
+ # @type var cmd: Array[String]
284
+ cmd = [
285
+ "fetch",
286
+ "--shallow-since=\"1 month ago\"",
287
+ "--update-shallow",
288
+ "--filter=blob:none",
289
+ "--recurse-submodules=no",
290
+ default_remote
291
+ ]
292
+ cmd << remote if remote
293
+
294
+ CLI.exec_git_command(cmd, timeout: CLI::UNSHALLOW_TIMEOUT)
300
295
  rescue => e
301
296
  log_failure(e, "git unshallow")
302
- telemetry_track_error(e, Ext::Telemetry::Command::UNSHALLOW)
297
+ Telemetry.track_error(e, Ext::Telemetry::Command::UNSHALLOW)
303
298
  unshallowing_errored = true
304
299
  nil
305
300
  end
306
301
 
307
- break [] unless unshallowing_errored
302
+ # If the command succeeded, break and return the result
303
+ break [] if res && !unshallowing_errored
308
304
  end
309
305
  end
310
306
 
@@ -327,7 +323,7 @@ module Datadog
327
323
  # @type var output: String?
328
324
  output = nil
329
325
  duration_ms = Core::Utils::Time.measure(:float_millisecond) do
330
- output = exec_git_command("git diff -U0 --word-diff=porcelain #{base_commit} HEAD")
326
+ output = CLI.exec_git_command(["diff", "-U0", "--word-diff=porcelain", base_commit, "HEAD"], timeout: CLI::LONG_TIMEOUT)
331
327
  end
332
328
  Telemetry.git_command_ms(Ext::Telemetry::Command::DIFF, duration_ms)
333
329
 
@@ -340,7 +336,7 @@ module Datadog
340
336
  output.each_line do |line|
341
337
  # Match lines like: diff --git a/foo/bar.rb b/foo/bar.rb
342
338
  # This captures git changes on file level
343
- match = /^diff --git a\/(?<file>.+) b\/(?<file2>.+)$/.match(line)
339
+ match = /^diff --git a\/(?<file>.+?) b\//.match(line)
344
340
  if match && match[:file]
345
341
  changed_file = match[:file]
346
342
  # Normalize to repo root
@@ -353,7 +349,7 @@ module Datadog
353
349
  end
354
350
  changed_files
355
351
  rescue => e
356
- telemetry_track_error(e, Ext::Telemetry::Command::DIFF)
352
+ Telemetry.track_error(e, Ext::Telemetry::Command::DIFF)
357
353
  log_failure(e, "get changed files from diff")
358
354
  nil
359
355
  end
@@ -364,245 +360,28 @@ module Datadog
364
360
  def self.base_commit_sha(base_branch: nil)
365
361
  Telemetry.git_command(Ext::Telemetry::Command::BASE_COMMIT_SHA)
366
362
 
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
363
+ BaseBranchShaDetector.base_branch_sha(base_branch)
401
364
  rescue => e
402
- telemetry_track_error(e, Ext::Telemetry::Command::BASE_COMMIT_SHA)
365
+ Telemetry.track_error(e, Ext::Telemetry::Command::BASE_COMMIT_SHA)
403
366
  log_failure(e, "git base ref")
404
367
  nil
405
368
  end
406
369
 
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
370
+ def self.get_upstream_branch
371
+ CLI.exec_git_command(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"])
372
+ rescue => e
373
+ Datadog.logger.debug { "Error getting upstream: #{e}" }
475
374
  nil
476
375
  end
477
376
 
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
377
+ def self.filter_invalid_commits(commits)
378
+ commits.filter { |commit| Utils::Git.valid_commit_sha?(commit) }
488
379
  end
489
380
 
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
-
538
- # makes .exec_git_command private to make sure that this method
539
- # is not called from outside of this module with insecure parameters
540
- class << self
541
- private
542
-
543
- def filter_invalid_commits(commits)
544
- commits.filter { |commit| Utils::Git.valid_commit_sha?(commit) }
545
- end
546
-
547
- def exec_git_command(cmd, stdin: nil)
548
- # Shell injection is alleviated by making sure that no outside modules call this method.
549
- # It is called only internally with static parameters.
550
-
551
- # @type var out: String
552
- # @type var status: Process::Status?
553
- out, status = Open3.capture2e(cmd, stdin_data: stdin)
554
-
555
- if status.nil?
556
- # @type var retry_count: Integer
557
- retry_count = COMMAND_RETRY_COUNT
558
- Datadog.logger.debug { "Opening pipe failed, starting retries..." }
559
- while status.nil? && retry_count.positive?
560
- # no-dd-sa:ruby-security/shell-injection
561
- out, status = Open3.capture2e(cmd, stdin_data: stdin)
562
- Datadog.logger.debug { "After retry status is [#{status}]" }
563
- retry_count -= 1
564
- end
565
- end
566
-
567
- if status.nil? || !status.success?
568
- raise GitCommandExecutionError.new(
569
- "Failed to run git command [#{cmd}] with input [#{stdin}] and output [#{out}]",
570
- output: out,
571
- command: cmd,
572
- status: status
573
- )
574
- end
575
-
576
- # Sometimes Encoding.default_external is somehow set to US-ASCII which breaks
577
- # commit messages with UTF-8 characters like emojis
578
- # We force output's encoding to be UTF-8 in this case
579
- # This is safe to do as UTF-8 is compatible with US-ASCII
580
- if Encoding.default_external == Encoding::US_ASCII
581
- out = out.force_encoding(Encoding::UTF_8)
582
- end
583
- out.strip! # There's always a "\n" at the end of the command output
584
-
585
- return nil if out.empty?
586
-
587
- out
588
- end
589
-
590
- def log_failure(e, action)
591
- Datadog.logger.debug(
592
- "Unable to perform #{action}: #{e.class.name} #{e.message} at #{Array(e.backtrace).first}"
593
- )
594
- end
595
-
596
- def telemetry_track_error(e, command)
597
- case e
598
- when Errno::ENOENT
599
- Telemetry.git_command_errors(command, executable_missing: true)
600
- when GitCommandExecutionError
601
- Telemetry.git_command_errors(command, exit_code: e.status&.to_i)
602
- else
603
- Telemetry.git_command_errors(command, exit_code: -9000)
604
- end
605
- end
381
+ def self.log_failure(e, action)
382
+ Datadog.logger.debug(
383
+ "Unable to perform #{action}: #{e.class.name} #{e.message} at #{Array(e.backtrace).first}"
384
+ )
606
385
  end
607
386
  end
608
387
  end
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../utils/telemetry"
4
+ require_relative "cli"
5
+
3
6
  module Datadog
4
7
  module CI
5
8
  module Git
@@ -21,6 +24,17 @@ module Datadog
21
24
  Utils::Telemetry.distribution(Ext::Telemetry::METRIC_GIT_COMMAND_MS, duration_ms, tags_for_command(command))
22
25
  end
23
26
 
27
+ def self.track_error(e, command)
28
+ case e
29
+ when Errno::ENOENT
30
+ git_command_errors(command, executable_missing: true)
31
+ when CLI::GitCommandExecutionError
32
+ git_command_errors(command, exit_code: e.status&.to_i)
33
+ else
34
+ git_command_errors(command, exit_code: -9000)
35
+ end
36
+ end
37
+
24
38
  def self.tags_for_command(command)
25
39
  {Ext::Telemetry::TAG_COMMAND => command}
26
40
  end
@@ -4,7 +4,6 @@ require "set"
4
4
 
5
5
  require_relative "../ext/test"
6
6
  require_relative "../git/local_repository"
7
- require_relative "telemetry"
8
7
 
9
8
  module Datadog
10
9
  module CI
@@ -69,7 +68,6 @@ module Datadog
69
68
  end
70
69
 
71
70
  test_span.set_tag(Ext::Test::TAG_TEST_IS_MODIFIED, "true")
72
- Telemetry.impacted_test_detected
73
71
  end
74
72
 
75
73
  private
@@ -150,7 +150,7 @@ module Datadog
150
150
  def skippable?(datadog_test_id)
151
151
  return false if !enabled? || !skipping_tests?
152
152
 
153
- @skippable_tests.include?(datadog_test_id)
153
+ @mutex.synchronize { @skippable_tests.include?(datadog_test_id) }
154
154
  end
155
155
 
156
156
  def mark_if_skippable(test)
@@ -200,8 +200,10 @@ module Datadog
200
200
  end
201
201
 
202
202
  def restore_state(state)
203
- @correlation_id = state[:correlation_id]
204
- @skippable_tests = state[:skippable_tests]
203
+ @mutex.synchronize do
204
+ @correlation_id = state[:correlation_id]
205
+ @skippable_tests = state[:skippable_tests]
206
+ end
205
207
  end
206
208
 
207
209
  def storage_key
@@ -225,7 +227,7 @@ module Datadog
225
227
  end
226
228
 
227
229
  def load_datadog_cov!
228
- require "datadog_cov.#{RUBY_VERSION}_#{RUBY_PLATFORM}"
230
+ require "datadog_ci_native.#{RUBY_VERSION}_#{RUBY_PLATFORM}"
229
231
 
230
232
  Datadog.logger.debug("Loaded Datadog code coverage collector, using coverage mode: #{code_coverage_mode}")
231
233
  rescue LoadError => e
@@ -253,8 +255,10 @@ module Datadog
253
255
  .fetch_skippable_tests(test_session)
254
256
  @skippable_tests_fetch_error = skippable_response.error_message unless skippable_response.ok?
255
257
 
256
- @correlation_id = skippable_response.correlation_id
257
- @skippable_tests = skippable_response.tests
258
+ @mutex.synchronize do
259
+ @correlation_id = skippable_response.correlation_id
260
+ @skippable_tests = skippable_response.tests
261
+ end
258
262
 
259
263
  Datadog.logger.debug { "Fetched skippable tests: \n #{@skippable_tests}" }
260
264
  Datadog.logger.debug { "Found #{@skippable_tests.count} skippable tests." }
@@ -5,7 +5,7 @@ module Datadog
5
5
  module TestOptimisation
6
6
  module Coverage
7
7
  # Placeholder for code coverage collection
8
- # Implementation in ext/datadog_cov
8
+ # Implementation in ext/datadog_ci_native/datadog_cov.c
9
9
  class DDCov
10
10
  end
11
11
  end
@@ -75,6 +75,9 @@ module Datadog
75
75
  tags[Ext::Telemetry::TAG_IS_TEST_DISABLED] = "true" if span.get_tag(Ext::Test::TAG_IS_TEST_DISABLED)
76
76
  tags[Ext::Telemetry::TAG_HAS_FAILED_ALL_RETRIES] = "true" if span.get_tag(Ext::Test::TAG_HAS_FAILED_ALL_RETRIES)
77
77
 
78
+ # impacted tests tags
79
+ tags[Ext::Telemetry::TAG_IS_MODIFIED] = "true" if span.get_tag(Ext::Test::TAG_TEST_IS_MODIFIED)
80
+
78
81
  tags
79
82
  end
80
83