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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +15 -2
- data/ext/datadog_ci_native/ci.c +10 -0
- data/ext/{datadog_cov → datadog_ci_native}/datadog_cov.c +119 -147
- data/ext/datadog_ci_native/datadog_cov.h +3 -0
- data/ext/datadog_ci_native/datadog_source_code.c +28 -0
- data/ext/datadog_ci_native/datadog_source_code.h +3 -0
- data/ext/{datadog_cov → datadog_ci_native}/extconf.rb +1 -1
- data/lib/datadog/ci/contrib/minitest/test.rb +17 -7
- data/lib/datadog/ci/contrib/rspec/example.rb +14 -7
- data/lib/datadog/ci/ext/telemetry.rb +1 -2
- data/lib/datadog/ci/ext/test.rb +1 -0
- data/lib/datadog/ci/git/base_branch_sha_detection/base.rb +66 -0
- data/lib/datadog/ci/git/base_branch_sha_detection/branch_metric.rb +34 -0
- data/lib/datadog/ci/git/base_branch_sha_detection/guesser.rb +137 -0
- data/lib/datadog/ci/git/base_branch_sha_detection/merge_base_extractor.rb +29 -0
- data/lib/datadog/ci/git/base_branch_sha_detector.rb +63 -0
- data/lib/datadog/ci/git/cli.rb +56 -0
- data/lib/datadog/ci/git/local_repository.rb +73 -294
- data/lib/datadog/ci/git/telemetry.rb +14 -0
- data/lib/datadog/ci/impacted_tests_detection/component.rb +0 -2
- data/lib/datadog/ci/test_optimisation/component.rb +10 -6
- data/lib/datadog/ci/test_optimisation/coverage/ddcov.rb +1 -1
- data/lib/datadog/ci/test_visibility/telemetry.rb +3 -0
- data/lib/datadog/ci/utils/command.rb +116 -0
- data/lib/datadog/ci/utils/source_code.rb +31 -0
- data/lib/datadog/ci/version.rb +1 -1
- metadata +16 -5
- 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
|
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("
|
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
|
-
|
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("
|
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("
|
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("
|
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
|
-
|
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("
|
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("
|
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("
|
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("
|
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
|
-
|
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
|
-
|
206
|
-
|
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
|
-
|
213
|
-
"
|
214
|
-
"--objects
|
215
|
-
"--no-object-names
|
216
|
-
"--filter=blob:none
|
217
|
-
"--since=\"1 month ago\"
|
218
|
-
|
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
|
-
|
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
|
-
"
|
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
|
-
|
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("
|
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
|
-
|
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
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
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
|
-
|
298
|
-
|
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
|
-
|
297
|
+
Telemetry.track_error(e, Ext::Telemetry::Command::UNSHALLOW)
|
303
298
|
unshallowing_errored = true
|
304
299
|
nil
|
305
300
|
end
|
306
301
|
|
307
|
-
break
|
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("
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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.
|
408
|
-
|
409
|
-
|
410
|
-
Datadog.logger.debug { "
|
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.
|
479
|
-
|
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.
|
491
|
-
|
492
|
-
|
493
|
-
|
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
|
-
@
|
204
|
-
|
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 "
|
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
|
-
@
|
257
|
-
|
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." }
|
@@ -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
|
|