datadog-ci 1.18.0 → 1.19.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/lib/datadog/ci/ext/environment/providers/appveyor.rb +1 -1
- data/lib/datadog/ci/ext/environment/providers/buildkite.rb +4 -0
- data/lib/datadog/ci/ext/environment/providers/gitlab.rb +0 -4
- data/lib/datadog/ci/git/changed_lines.rb +109 -0
- data/lib/datadog/ci/git/diff.rb +90 -0
- data/lib/datadog/ci/git/local_repository.rb +23 -29
- data/lib/datadog/ci/impacted_tests_detection/component.rb +15 -9
- data/lib/datadog/ci/test.rb +16 -0
- data/lib/datadog/ci/utils/command.rb +2 -1
- data/lib/datadog/ci/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6c730f78f9fefc725ba4499a38729eff14f5fc48a4a8e6a055ade2b4cedc030a
|
4
|
+
data.tar.gz: 86a9682f7623bc85a8e8a05fd8b1f91ecb12f459e7f2425fd2fb79d4ce217227
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ebb3fca5a5cf1db85795b017ffefef4bb09560c4c8c65f54b4d4cc88b93016776a861ec5c48bf700bdff4ee18c4416f6aee632b5abd392ae930a1a1b00042f0c
|
7
|
+
data.tar.gz: 9b5b171277dc7bb8510dec4bd332a937431ac4bb3f277c9ad3221dd38f3d5eec27b1c92415aa47c0289259543d2ee21fe8ae1dc532e0f9d9376706a38e59e712
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,15 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [1.19.0] - 2025-06-16
|
4
|
+
|
5
|
+
### Changed
|
6
|
+
|
7
|
+
* Impacted tests detection works with line level granularity ([#335][])
|
8
|
+
|
9
|
+
### Fixed
|
10
|
+
|
11
|
+
* Fix stdin close in Command.popen_with_stdin ([#339][])
|
12
|
+
|
3
13
|
## [1.18.0] - 2025-06-06
|
4
14
|
|
5
15
|
### Added
|
@@ -453,7 +463,8 @@ Currently test suite level visibility is not used by our instrumentation: it wil
|
|
453
463
|
|
454
464
|
- Ruby versions < 2.7 no longer supported ([#8][])
|
455
465
|
|
456
|
-
[Unreleased]: https://github.com/DataDog/datadog-ci-rb/compare/v1.
|
466
|
+
[Unreleased]: https://github.com/DataDog/datadog-ci-rb/compare/v1.19.0...main
|
467
|
+
[1.19.0]: https://github.com/DataDog/datadog-ci-rb/compare/v1.18.0...v1.19.0
|
457
468
|
[1.18.0]: https://github.com/DataDog/datadog-ci-rb/compare/v1.17.0...v1.18.0
|
458
469
|
[1.17.0]: https://github.com/DataDog/datadog-ci-rb/compare/v1.16.0...v1.17.0
|
459
470
|
[1.16.0]: https://github.com/DataDog/datadog-ci-rb/compare/v1.15.0...v1.16.0
|
@@ -647,4 +658,6 @@ Currently test suite level visibility is not used by our instrumentation: it wil
|
|
647
658
|
[#321]: https://github.com/DataDog/datadog-ci-rb/issues/321
|
648
659
|
[#323]: https://github.com/DataDog/datadog-ci-rb/issues/323
|
649
660
|
[#327]: https://github.com/DataDog/datadog-ci-rb/issues/327
|
650
|
-
[#329]: https://github.com/DataDog/datadog-ci-rb/issues/329
|
661
|
+
[#329]: https://github.com/DataDog/datadog-ci-rb/issues/329
|
662
|
+
[#335]: https://github.com/DataDog/datadog-ci-rb/issues/335
|
663
|
+
[#339]: https://github.com/DataDog/datadog-ci-rb/issues/339
|
@@ -85,7 +85,7 @@ module Datadog
|
|
85
85
|
|
86
86
|
def git_pull_request_base_branch
|
87
87
|
# from docs: build branch. For Pull Request commits it is base branch PR is merging into
|
88
|
-
env["APPVEYOR_REPO_BRANCH"]
|
88
|
+
env["APPVEYOR_REPO_BRANCH"] if env["APPVEYOR_PULL_REQUEST_HEAD_REPO_BRANCH"]
|
89
89
|
end
|
90
90
|
|
91
91
|
private
|
@@ -104,10 +104,6 @@ module Datadog
|
|
104
104
|
env["CI_MERGE_REQUEST_TARGET_BRANCH_NAME"]
|
105
105
|
end
|
106
106
|
|
107
|
-
def git_pull_request_base_branch_sha
|
108
|
-
env["CI_MERGE_REQUEST_TARGET_BRANCH_SHA"]
|
109
|
-
end
|
110
|
-
|
111
107
|
def git_commit_head_sha
|
112
108
|
env["CI_MERGE_REQUEST_SOURCE_BRANCH_SHA"]
|
113
109
|
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Datadog
|
4
|
+
module CI
|
5
|
+
module Git
|
6
|
+
# Helper class to efficiently store and query changed line intervals for a single file
|
7
|
+
# Uses merged sorted intervals with binary search for O(log n) query performance
|
8
|
+
class ChangedLines
|
9
|
+
def initialize
|
10
|
+
@intervals = [] # Array of [start, end] pairs
|
11
|
+
@built = false
|
12
|
+
@mutex = Mutex.new
|
13
|
+
end
|
14
|
+
|
15
|
+
# Add an interval (defers merging until build! is called)
|
16
|
+
def add_interval(start_line, end_line)
|
17
|
+
return if start_line > end_line
|
18
|
+
|
19
|
+
@mutex.synchronize do
|
20
|
+
@intervals << [start_line, end_line]
|
21
|
+
@built = false
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Sort and merge all intervals
|
26
|
+
# Call this after all intervals have been added
|
27
|
+
def build!
|
28
|
+
@mutex.synchronize do
|
29
|
+
return false if @built
|
30
|
+
|
31
|
+
@built = true
|
32
|
+
return false if @intervals.empty?
|
33
|
+
|
34
|
+
# Sort intervals by start line
|
35
|
+
@intervals.sort_by!(&:first)
|
36
|
+
|
37
|
+
# Merge overlapping intervals
|
38
|
+
merged = []
|
39
|
+
|
40
|
+
# @type var current_start: Integer
|
41
|
+
# @type var current_end: Integer
|
42
|
+
current_start, current_end = @intervals.first
|
43
|
+
|
44
|
+
@intervals.each_with_index do |interval, index|
|
45
|
+
next if index == 0
|
46
|
+
# @type var start_line: Integer
|
47
|
+
# @type var end_line: Integer
|
48
|
+
start_line, end_line = interval
|
49
|
+
|
50
|
+
if start_line <= current_end + 1
|
51
|
+
# Overlapping or adjacent intervals, merge them
|
52
|
+
current_end = [current_end, end_line].max
|
53
|
+
else
|
54
|
+
# Non-overlapping interval, save current and start new
|
55
|
+
merged << [current_start, current_end]
|
56
|
+
current_start = start_line
|
57
|
+
current_end = end_line
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
merged << [current_start, current_end]
|
62
|
+
|
63
|
+
@intervals = merged
|
64
|
+
true
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Check if any line in the query interval overlaps with changed lines
|
69
|
+
# Uses binary search for O(log n) performance
|
70
|
+
def overlaps?(query_start, query_end)
|
71
|
+
build! unless @built
|
72
|
+
|
73
|
+
return false if @intervals.empty? || query_start > query_end
|
74
|
+
|
75
|
+
# Binary search for the first interval that might overlap
|
76
|
+
left = 0
|
77
|
+
right = @intervals.length - 1
|
78
|
+
|
79
|
+
while left <= right
|
80
|
+
mid = (left + right) / 2
|
81
|
+
# @type var interval_start: Integer
|
82
|
+
# @type var interval_end: Integer
|
83
|
+
interval_start, interval_end = @intervals[mid]
|
84
|
+
|
85
|
+
if interval_end < query_start
|
86
|
+
left = mid + 1
|
87
|
+
elsif interval_start > query_end
|
88
|
+
right = mid - 1
|
89
|
+
else
|
90
|
+
# Found overlap
|
91
|
+
return true
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
false
|
96
|
+
end
|
97
|
+
|
98
|
+
def empty?
|
99
|
+
@intervals.empty?
|
100
|
+
end
|
101
|
+
|
102
|
+
def intervals
|
103
|
+
build! unless @built
|
104
|
+
@intervals.dup
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "set"
|
4
|
+
require_relative "changed_lines"
|
5
|
+
|
6
|
+
module Datadog
|
7
|
+
module CI
|
8
|
+
module Git
|
9
|
+
class Diff
|
10
|
+
FILE_CHANGE_REGEX = /^diff --git a\/(?<file>.+?) b\//.freeze
|
11
|
+
LINES_CHANGE_REGEX = /^@@ -\d+(?:,\d+)? \+(?<start>\d+)(?:,(?<count>\d+))? @@/.freeze
|
12
|
+
|
13
|
+
def initialize(changed_files: {})
|
14
|
+
@changed_files = changed_files # Hash of file_path => ChangedLines
|
15
|
+
end
|
16
|
+
|
17
|
+
# Check if any lines in the given range are changed for the specified file
|
18
|
+
def lines_changed?(file_path, start_line: nil, end_line: nil)
|
19
|
+
changed_lines = @changed_files[file_path]
|
20
|
+
unless changed_lines
|
21
|
+
Datadog.logger.debug { "No changes found for file: #{file_path}" }
|
22
|
+
return false
|
23
|
+
end
|
24
|
+
|
25
|
+
# If either start_line or end_line is nil, return true if file is present
|
26
|
+
return true if start_line.nil? || end_line.nil?
|
27
|
+
|
28
|
+
changed_lines.overlaps?(start_line, end_line)
|
29
|
+
end
|
30
|
+
|
31
|
+
def empty?
|
32
|
+
@changed_files.empty?
|
33
|
+
end
|
34
|
+
|
35
|
+
# for debug purposes
|
36
|
+
def size
|
37
|
+
@changed_files.size
|
38
|
+
end
|
39
|
+
|
40
|
+
# for debug purposes
|
41
|
+
def inspect
|
42
|
+
@changed_files.inspect
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.parse_diff_output(output)
|
46
|
+
return new if output.nil? || output.empty?
|
47
|
+
|
48
|
+
changed_files = {}
|
49
|
+
current_file = nil
|
50
|
+
|
51
|
+
output.each_line do |line|
|
52
|
+
# Match lines like: diff --git a/foo/bar.rb b/foo/bar.rb
|
53
|
+
# This captures git changes on file level
|
54
|
+
match = FILE_CHANGE_REGEX.match(line)
|
55
|
+
if match && match[:file]
|
56
|
+
# this path here is already relative from the git root
|
57
|
+
changed_file = match[:file]
|
58
|
+
|
59
|
+
unless changed_file.nil? || changed_file.empty?
|
60
|
+
current_file = changed_file
|
61
|
+
changed_files[current_file] ||= ChangedLines.new
|
62
|
+
end
|
63
|
+
|
64
|
+
Datadog.logger.debug { "matched changed_file: #{changed_file} from git diff line: #{line}" }
|
65
|
+
|
66
|
+
next
|
67
|
+
end
|
68
|
+
|
69
|
+
# Match lines like: @@ -1,2 +3,4 @@
|
70
|
+
match = LINES_CHANGE_REGEX.match(line)
|
71
|
+
if match && match[:start] && current_file
|
72
|
+
start_line = match[:start].to_i
|
73
|
+
|
74
|
+
line_count = 1 # Default to 1 line if count not specified
|
75
|
+
line_count = match[:count].to_i if match[:count]
|
76
|
+
|
77
|
+
end_line = start_line + line_count - 1
|
78
|
+
|
79
|
+
changed_files[current_file].add_interval(start_line, end_line)
|
80
|
+
|
81
|
+
Datadog.logger.debug { "Added interval [#{start_line}, #{end_line}] for file: #{current_file}" }
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
new(changed_files: changed_files)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -8,6 +8,7 @@ require_relative "../ext/telemetry"
|
|
8
8
|
require_relative "../utils/command"
|
9
9
|
require_relative "base_branch_sha_detector"
|
10
10
|
require_relative "cli"
|
11
|
+
require_relative "diff"
|
11
12
|
require_relative "telemetry"
|
12
13
|
require_relative "user"
|
13
14
|
|
@@ -37,14 +38,22 @@ module Datadog
|
|
37
38
|
# that root is a prefix of the path
|
38
39
|
return "" if path.size < prefix_index
|
39
40
|
|
40
|
-
|
41
|
-
|
41
|
+
# this means that the root is not a prefix of this path somehow
|
42
|
+
return "" if path[prefix_index] != File::SEPARATOR
|
43
|
+
|
44
|
+
res = path[prefix_index + 1..]
|
42
45
|
else
|
43
46
|
# prefix_to_root is a difference between the root path and the given path
|
44
|
-
if @prefix_to_root
|
45
|
-
|
46
|
-
|
47
|
-
|
47
|
+
if defined?(@prefix_to_root)
|
48
|
+
# if path starts with ./ remove the dot before applying the optimization
|
49
|
+
# @type var path: String
|
50
|
+
path = path[1..] if path.start_with?("./")
|
51
|
+
|
52
|
+
if @prefix_to_root == ""
|
53
|
+
return path
|
54
|
+
elsif @prefix_to_root
|
55
|
+
return File.join(@prefix_to_root, path)
|
56
|
+
end
|
48
57
|
end
|
49
58
|
|
50
59
|
pathname = Pathname.new(File.expand_path(path))
|
@@ -308,10 +317,10 @@ module Datadog
|
|
308
317
|
res
|
309
318
|
end
|
310
319
|
|
311
|
-
# Returns a
|
320
|
+
# Returns a Diff object with relative file paths for files that were changed since the given base_commit.
|
312
321
|
# If base_commit is nil, returns nil. On error, returns nil.
|
313
|
-
def self.
|
314
|
-
return
|
322
|
+
def self.get_changes_since(base_commit)
|
323
|
+
return Diff.new if base_commit.nil?
|
315
324
|
|
316
325
|
Datadog.logger.debug { "calculating git diff from base_commit: #{base_commit}" }
|
317
326
|
|
@@ -329,29 +338,14 @@ module Datadog
|
|
329
338
|
|
330
339
|
Datadog.logger.debug { "git diff output: #{output}" }
|
331
340
|
|
332
|
-
return
|
333
|
-
|
334
|
-
# 2. Parse the output
|
335
|
-
|
336
|
-
output.each_line do |line|
|
337
|
-
# Match lines like: diff --git a/foo/bar.rb b/foo/bar.rb
|
338
|
-
# This captures git changes on file level
|
339
|
-
match = /^diff --git a\/(?<file>.+?) b\//.match(line)
|
340
|
-
if match && match[:file]
|
341
|
-
changed_file = match[:file]
|
342
|
-
# Normalize to repo root
|
343
|
-
normalized_changed_file = relative_to_root(changed_file)
|
344
|
-
changed_files << normalized_changed_file unless normalized_changed_file.nil? || normalized_changed_file.empty?
|
345
|
-
|
346
|
-
Datadog.logger.debug { "matched changed_file: #{changed_file} from line: #{line}" }
|
347
|
-
Datadog.logger.debug { "normalized_changed_file: #{normalized_changed_file}" }
|
348
|
-
end
|
349
|
-
end
|
350
|
-
changed_files
|
341
|
+
return Diff.new if output.nil?
|
342
|
+
|
343
|
+
# 2. Parse the output using Git::Diff
|
344
|
+
Diff.parse_diff_output(output)
|
351
345
|
rescue => e
|
352
346
|
Telemetry.track_error(e, Ext::Telemetry::Command::DIFF)
|
353
347
|
log_failure(e, "get changed files from diff")
|
354
|
-
|
348
|
+
Diff.new
|
355
349
|
end
|
356
350
|
end
|
357
351
|
|
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "set"
|
4
|
-
|
5
3
|
require_relative "../ext/test"
|
6
4
|
require_relative "../git/local_repository"
|
7
5
|
|
@@ -11,7 +9,7 @@ module Datadog
|
|
11
9
|
class Component
|
12
10
|
def initialize(enabled:)
|
13
11
|
@enabled = enabled
|
14
|
-
@
|
12
|
+
@git_diff = Git::Diff.new
|
15
13
|
end
|
16
14
|
|
17
15
|
def configure(library_settings, test_session)
|
@@ -29,21 +27,21 @@ module Datadog
|
|
29
27
|
return
|
30
28
|
end
|
31
29
|
|
32
|
-
|
33
|
-
if
|
30
|
+
git_diff = Git::LocalRepository.get_changes_since(base_commit_sha)
|
31
|
+
if git_diff.empty?
|
34
32
|
Datadog.logger.debug { "Impacted tests detection disabled: could not get changed files" }
|
35
33
|
@enabled = false
|
36
34
|
return
|
37
35
|
end
|
38
36
|
|
39
37
|
Datadog.logger.debug do
|
40
|
-
"Impacted tests detection: found #{
|
38
|
+
"Impacted tests detection: found #{git_diff.size} changed files"
|
41
39
|
end
|
42
40
|
Datadog.logger.debug do
|
43
|
-
"Impacted tests detection: changed files: #{
|
41
|
+
"Impacted tests detection: changed files: #{git_diff.inspect}"
|
44
42
|
end
|
45
43
|
|
46
|
-
@
|
44
|
+
@git_diff = git_diff
|
47
45
|
@enabled = true
|
48
46
|
end
|
49
47
|
|
@@ -57,7 +55,15 @@ module Datadog
|
|
57
55
|
source_file = test_span.source_file
|
58
56
|
return false if source_file.nil?
|
59
57
|
|
60
|
-
|
58
|
+
# convert to relative path without leading slash
|
59
|
+
# @type var source_file: String
|
60
|
+
source_file = source_file[1..] if source_file.start_with?("/")
|
61
|
+
|
62
|
+
result = @git_diff.lines_changed?(source_file, start_line: test_span.start_line, end_line: test_span.end_line)
|
63
|
+
Datadog.logger.debug do
|
64
|
+
"Impacted tests detection: test #{test_span.name} with source file #{source_file} is modified: #{result}"
|
65
|
+
end
|
66
|
+
result
|
61
67
|
end
|
62
68
|
|
63
69
|
def tag_modified_test(test_span)
|
data/lib/datadog/ci/test.rb
CHANGED
@@ -66,6 +66,22 @@ module Datadog
|
|
66
66
|
get_tag(Ext::Test::TAG_TEST_SESSION_ID)
|
67
67
|
end
|
68
68
|
|
69
|
+
# Returns the starting line number of the test in the source file.
|
70
|
+
# @return [Integer] the starting line number
|
71
|
+
# @return [nil] if the starting line is not available
|
72
|
+
def start_line
|
73
|
+
line = get_tag(Ext::Test::TAG_SOURCE_START)
|
74
|
+
line&.to_i
|
75
|
+
end
|
76
|
+
|
77
|
+
# Returns the ending line number of the test in the source file.
|
78
|
+
# @return [Integer] the ending line number
|
79
|
+
# @return [nil] if the ending line is not available
|
80
|
+
def end_line
|
81
|
+
line = get_tag(Ext::Test::TAG_SOURCE_END)
|
82
|
+
line&.to_i
|
83
|
+
end
|
84
|
+
|
69
85
|
# Returns "true" if test span represents a retry.
|
70
86
|
# @return [Boolean] true if this test is a retry, false otherwise.
|
71
87
|
def is_retry?
|
@@ -92,6 +92,7 @@ module Datadog
|
|
92
92
|
end
|
93
93
|
|
94
94
|
def self.popen_with_stdin(command, stdin_data: nil, retries_left: OPEN_STDIN_RETRY_COUNT)
|
95
|
+
stdin = nil
|
95
96
|
result = Open3.popen2e(*command)
|
96
97
|
stdin = result.first
|
97
98
|
|
@@ -108,7 +109,7 @@ module Datadog
|
|
108
109
|
|
109
110
|
result
|
110
111
|
ensure
|
111
|
-
stdin
|
112
|
+
stdin&.close
|
112
113
|
end
|
113
114
|
end
|
114
115
|
end
|
data/lib/datadog/ci/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: datadog-ci
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.19.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Datadog, Inc.
|
@@ -196,7 +196,9 @@ files:
|
|
196
196
|
- lib/datadog/ci/git/base_branch_sha_detection/guesser.rb
|
197
197
|
- lib/datadog/ci/git/base_branch_sha_detection/merge_base_extractor.rb
|
198
198
|
- lib/datadog/ci/git/base_branch_sha_detector.rb
|
199
|
+
- lib/datadog/ci/git/changed_lines.rb
|
199
200
|
- lib/datadog/ci/git/cli.rb
|
201
|
+
- lib/datadog/ci/git/diff.rb
|
200
202
|
- lib/datadog/ci/git/local_repository.rb
|
201
203
|
- lib/datadog/ci/git/packfiles.rb
|
202
204
|
- lib/datadog/ci/git/search_commits.rb
|