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
@@ -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
@@ -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
@@ -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
 
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+ require "datadog/core/utils/time"
5
+
6
+ module Datadog
7
+ module CI
8
+ module Utils
9
+ # Provides a way to call external commands with timeout
10
+ module Command
11
+ DEFAULT_TIMEOUT = 10 # seconds
12
+ BUFFER_SIZE = 1024
13
+
14
+ OPEN_STDIN_RETRY_COUNT = 3
15
+
16
+ # Executes a command with optional timeout and stdin data
17
+ #
18
+ # @param command [Array<String>] Command to execute.
19
+ # @param stdin_data [String, nil] Data to write to stdin
20
+ # @param timeout [Integer] Maximum execution time in seconds
21
+ # @return [Array<String, Process::Status?>] Output and exit status
22
+ #
23
+ # @example Safe usage with array (recommended)
24
+ # Command.exec_command(["git", "log", "-n", "1"])
25
+ #
26
+ #
27
+ def self.exec_command(command, stdin_data: nil, timeout: DEFAULT_TIMEOUT)
28
+ output = +""
29
+ exit_value = nil
30
+ timeout_reached = false
31
+
32
+ begin
33
+ start = Core::Utils::Time.get_time
34
+
35
+ _, stderrout, thread = popen_with_stdin(command, stdin_data: stdin_data)
36
+ pid = thread[:pid]
37
+
38
+ # wait for output and read from stdout/stderr
39
+ while (Core::Utils::Time.get_time - start) < timeout
40
+ # wait for data to appear in stderrout channel
41
+ # maximum wait time 100ms
42
+ Kernel.select([stderrout], [], [], 0.1)
43
+
44
+ begin
45
+ output << stderrout.read_nonblock(1024)
46
+ rescue IO::WaitReadable
47
+ rescue EOFError
48
+ # we're done here, we return from this cycle when we processed the whole output of the command
49
+ break
50
+ end
51
+ end
52
+
53
+ if (Core::Utils::Time.get_time - start) > timeout
54
+ timeout_reached = true
55
+ end
56
+
57
+ if thread.alive?
58
+ begin
59
+ Process.kill("TERM", pid)
60
+ rescue
61
+ # Process already terminated
62
+ end
63
+ end
64
+
65
+ thread.join(1)
66
+ exit_value = thread.value
67
+ rescue Errno::EPIPE
68
+ return ["Error writing to stdin", nil]
69
+ ensure
70
+ stderrout&.close
71
+ end
72
+
73
+ # we read command's output as binary so now we need to set an appropriate encoding for the result
74
+ encoding = Encoding.default_external
75
+
76
+ # Sometimes Encoding.default_external is somehow set to US-ASCII which breaks
77
+ # commit messages with UTF-8 characters like emojis
78
+ # We force output's encoding to be UTF-8 in this case
79
+ # This is safe to do as UTF-8 is compatible with US-ASCII
80
+ if Encoding.default_external == Encoding::US_ASCII
81
+ encoding = Encoding::UTF_8
82
+ end
83
+
84
+ output.force_encoding(encoding)
85
+ output.strip! # There's always a "\n" at the end of the command output
86
+
87
+ if timeout_reached && output.empty?
88
+ output = "Command timed out after #{timeout} seconds"
89
+ end
90
+
91
+ [output, exit_value]
92
+ end
93
+
94
+ def self.popen_with_stdin(command, stdin_data: nil, retries_left: OPEN_STDIN_RETRY_COUNT)
95
+ result = Open3.popen2e(*command)
96
+ stdin = result.first
97
+
98
+ # write input to stdin
99
+ begin
100
+ stdin.write(stdin_data) if stdin_data
101
+ rescue Errno::EPIPE => e
102
+ if retries_left > 0
103
+ return popen_with_stdin(command, stdin_data: stdin_data, retries_left: retries_left - 1)
104
+ else
105
+ raise e
106
+ end
107
+ end
108
+
109
+ result
110
+ ensure
111
+ stdin.close
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module CI
5
+ module Utils
6
+ module SourceCode
7
+ begin
8
+ require "datadog_ci_native.#{RUBY_VERSION}_#{RUBY_PLATFORM}"
9
+
10
+ LAST_LINE_AVAILABLE = true
11
+ rescue LoadError
12
+ LAST_LINE_AVAILABLE = false
13
+ end
14
+
15
+ def self.last_line(target)
16
+ return nil if target.nil?
17
+ return nil unless LAST_LINE_AVAILABLE
18
+
19
+ # Ruby has outdated RBS for RubyVM::InstructionSequence where method `of` is not defined
20
+ # steep:ignore:start
21
+ iseq = RubyVM::InstructionSequence.of(target)
22
+ return nil unless iseq.is_a?(RubyVM::InstructionSequence)
23
+ # steep:ignore:end
24
+
25
+ # this function is implemented in ext/datadog_ci_native/datadog_source_code.c
26
+ _native_last_line_from_iseq(iseq)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -4,7 +4,7 @@ module Datadog
4
4
  module CI
5
5
  module VERSION
6
6
  MAJOR = 1
7
- MINOR = 16
7
+ MINOR = 18
8
8
  PATCH = 0
9
9
  PRE = nil
10
10
  BUILD = nil
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: datadog-ci
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.16.0
4
+ version: 1.18.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Datadog, Inc.
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-04-15 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: datadog
@@ -60,7 +60,7 @@ email:
60
60
  executables:
61
61
  - ddcirb
62
62
  extensions:
63
- - ext/datadog_cov/extconf.rb
63
+ - ext/datadog_ci_native/extconf.rb
64
64
  extra_rdoc_files: []
65
65
  files:
66
66
  - CHANGELOG.md
@@ -71,8 +71,12 @@ files:
71
71
  - NOTICE
72
72
  - README.md
73
73
  - exe/ddcirb
74
- - ext/datadog_cov/datadog_cov.c
75
- - ext/datadog_cov/extconf.rb
74
+ - ext/datadog_ci_native/ci.c
75
+ - ext/datadog_ci_native/datadog_cov.c
76
+ - ext/datadog_ci_native/datadog_cov.h
77
+ - ext/datadog_ci_native/datadog_source_code.c
78
+ - ext/datadog_ci_native/datadog_source_code.h
79
+ - ext/datadog_ci_native/extconf.rb
76
80
  - lib/datadog/ci.rb
77
81
  - lib/datadog/ci/async_writer.rb
78
82
  - lib/datadog/ci/auto_instrument.rb
@@ -187,6 +191,12 @@ files:
187
191
  - lib/datadog/ci/ext/telemetry.rb
188
192
  - lib/datadog/ci/ext/test.rb
189
193
  - lib/datadog/ci/ext/transport.rb
194
+ - lib/datadog/ci/git/base_branch_sha_detection/base.rb
195
+ - lib/datadog/ci/git/base_branch_sha_detection/branch_metric.rb
196
+ - lib/datadog/ci/git/base_branch_sha_detection/guesser.rb
197
+ - lib/datadog/ci/git/base_branch_sha_detection/merge_base_extractor.rb
198
+ - lib/datadog/ci/git/base_branch_sha_detector.rb
199
+ - lib/datadog/ci/git/cli.rb
190
200
  - lib/datadog/ci/git/local_repository.rb
191
201
  - lib/datadog/ci/git/packfiles.rb
192
202
  - lib/datadog/ci/git/search_commits.rb
@@ -194,6 +204,7 @@ files:
194
204
  - lib/datadog/ci/git/tree_uploader.rb
195
205
  - lib/datadog/ci/git/upload_packfile.rb
196
206
  - lib/datadog/ci/git/user.rb
207
+ - lib/datadog/ci/impacted_tests_detection/component.rb
197
208
  - lib/datadog/ci/logs/component.rb
198
209
  - lib/datadog/ci/logs/transport.rb
199
210
  - lib/datadog/ci/readonly_test_module.rb
@@ -221,14 +232,14 @@ files:
221
232
  - lib/datadog/ci/test_retries/driver/base.rb
222
233
  - lib/datadog/ci/test_retries/driver/no_retry.rb
223
234
  - lib/datadog/ci/test_retries/driver/retry_failed.rb
235
+ - lib/datadog/ci/test_retries/driver/retry_flake_detection.rb
224
236
  - lib/datadog/ci/test_retries/driver/retry_flaky_fixed.rb
225
- - lib/datadog/ci/test_retries/driver/retry_new.rb
226
237
  - lib/datadog/ci/test_retries/null_component.rb
227
238
  - lib/datadog/ci/test_retries/strategy/base.rb
228
239
  - lib/datadog/ci/test_retries/strategy/no_retry.rb
229
240
  - lib/datadog/ci/test_retries/strategy/retry_failed.rb
241
+ - lib/datadog/ci/test_retries/strategy/retry_flake_detection.rb
230
242
  - lib/datadog/ci/test_retries/strategy/retry_flaky_fixed.rb
231
- - lib/datadog/ci/test_retries/strategy/retry_new.rb
232
243
  - lib/datadog/ci/test_session.rb
233
244
  - lib/datadog/ci/test_suite.rb
234
245
  - lib/datadog/ci/test_visibility/component.rb
@@ -263,11 +274,13 @@ files:
263
274
  - lib/datadog/ci/transport/http.rb
264
275
  - lib/datadog/ci/transport/telemetry.rb
265
276
  - lib/datadog/ci/utils/bundle.rb
277
+ - lib/datadog/ci/utils/command.rb
266
278
  - lib/datadog/ci/utils/configuration.rb
267
279
  - lib/datadog/ci/utils/file_storage.rb
268
280
  - lib/datadog/ci/utils/git.rb
269
281
  - lib/datadog/ci/utils/parsing.rb
270
282
  - lib/datadog/ci/utils/rum.rb
283
+ - lib/datadog/ci/utils/source_code.rb
271
284
  - lib/datadog/ci/utils/stateful.rb
272
285
  - lib/datadog/ci/utils/telemetry.rb
273
286
  - lib/datadog/ci/utils/test_run.rb
@@ -298,7 +311,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
298
311
  - !ruby/object:Gem::Version
299
312
  version: 2.0.0
300
313
  requirements: []
301
- rubygems_version: 3.6.2
314
+ rubygems_version: 3.6.7
302
315
  specification_version: 4
303
316
  summary: Datadog Test Optimization for your ruby application
304
317
  test_files: []