rspec-mergify 0.2.0 → 0.4.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/README.md +66 -0
- data/lib/mergify/rspec/ci_insights.rb +246 -76
- data/lib/mergify/rspec/configuration.rb +160 -65
- data/lib/mergify/rspec/flaky_detection.rb +16 -10
- data/lib/mergify/rspec/formatter.rb +90 -47
- data/lib/mergify/rspec/native.rb +31 -10
- data/lib/mergify/rspec/quarantine.rb +7 -1
- data/lib/mergify/rspec/resources/rspec.rb +9 -5
- data/lib/mergify/rspec/session_verdict.rb +86 -0
- data/lib/mergify/rspec/test_selection.rb +299 -0
- data/lib/mergify/rspec/trace.rb +145 -0
- data/lib/mergify/rspec/utils.rb +7 -0
- data/lib/mergify/rspec/version.rb +1 -1
- metadata +5 -31
- data/lib/mergify/rspec/synchronous_batch_span_processor.rb +0 -34
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2d222991a085c33d95515483a7a41823b3db8c978d71e9e34a5c2ba8d4ca1777
|
|
4
|
+
data.tar.gz: a555d7a13c32f2bc1205c2060b5bd353b06f2b72c3460119d2800549f68a1c89
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3908a8fb75af4875c54edfd828e1d638ca85730597d8283c8a558a06e50023b62345881a6bfbbcd37f1c69aef4e0d6043feb8bb027f00e83b492752295b2a8e2
|
|
7
|
+
data.tar.gz: ea86ab73dbab04394f1cd4c5718a4347592adf940aa1b82c2c54ccc4a56fa540a120127c54eb244531cfd90a94c38907bf22ec4f7c49129defeb57d5594fe2ac
|
data/README.md
CHANGED
|
@@ -7,6 +7,7 @@ RSpec plugin for [Mergify Test Insights](https://docs.mergify.com/ci-insights/).
|
|
|
7
7
|
- **Test tracing** — Sends OpenTelemetry traces for every test to Mergify's API
|
|
8
8
|
- **Flaky test detection** — Intelligently reruns tests to detect flakiness with budget constraints
|
|
9
9
|
- **Test quarantine** — Quarantines failing tests so they don't block CI
|
|
10
|
+
- **Test selection** — Runs only the previously-failing examples when Mergify's merge queue reruns a job
|
|
10
11
|
|
|
11
12
|
## Installation
|
|
12
13
|
|
|
@@ -18,6 +19,23 @@ gem 'rspec-mergify'
|
|
|
18
19
|
|
|
19
20
|
Then run `bundle install`.
|
|
20
21
|
|
|
22
|
+
### Supported platforms
|
|
23
|
+
|
|
24
|
+
The gem ships precompiled, so nothing is built on your machine. Each platform gem carries an extension for Ruby 3.1 through 4.0:
|
|
25
|
+
|
|
26
|
+
| Platform | Requirement |
|
|
27
|
+
|---|---|
|
|
28
|
+
| Linux x86_64 / aarch64 (glibc) | **glibc 2.30 or newer** — Debian 11+, Ubuntu 20.04+, RHEL 9+, Amazon Linux 2023 |
|
|
29
|
+
| Linux x86_64 / aarch64 (musl) | Alpine; on Ruby 3.1 also `apk add libgcc` |
|
|
30
|
+
| macOS arm64 / x86_64 | — |
|
|
31
|
+
| Windows x64 (UCRT) | — |
|
|
32
|
+
|
|
33
|
+
Anywhere else, and on glibc older than 2.30 — RHEL, Rocky, AlmaLinux and Oracle Linux 8, Amazon Linux 2, CentOS 7 — the extension cannot load. The gem still installs and your suite still runs, but nothing is reported to Mergify. Pin the last pure-Ruby release on those systems:
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
gem 'rspec-mergify', '0.1.4'
|
|
37
|
+
```
|
|
38
|
+
|
|
21
39
|
## Configuration
|
|
22
40
|
|
|
23
41
|
Set the `MERGIFY_TOKEN` environment variable with your Mergify API token.
|
|
@@ -34,6 +52,54 @@ The plugin activates automatically when running in CI (detected via the `CI` env
|
|
|
34
52
|
| `RSPEC_MERGIFY_DEBUG` | Print spans to console | `false` |
|
|
35
53
|
| `MERGIFY_TRACEPARENT` | W3C distributed trace context | — |
|
|
36
54
|
| `MERGIFY_TEST_JOB_NAME` | Mergify test job name | — |
|
|
55
|
+
| `MERGIFY_TEST_SELECTION_ENABLE` | Opt this job into test selection (see below) | `false` |
|
|
56
|
+
|
|
57
|
+
### Test selection
|
|
58
|
+
|
|
59
|
+
When Mergify's merge queue reruns a job — a retry, or a step of a batch
|
|
60
|
+
bisection — only the examples that failed on the previous attempt are
|
|
61
|
+
informative. The gem asks Mergify whether the current run is such a rerun and,
|
|
62
|
+
if so, runs only those examples.
|
|
63
|
+
|
|
64
|
+
**This is off until you turn it on, per job.** Set
|
|
65
|
+
`MERGIFY_TEST_SELECTION_ENABLE=true` on the job you want reduced. Installing the
|
|
66
|
+
gem is not enough — a feature that decides not to run tests starts only where
|
|
67
|
+
you wrote that it should. Anything that is not a recognised yes (unset, empty,
|
|
68
|
+
`false`, unparsable) means no, and a job that has not opted in never asks.
|
|
69
|
+
|
|
70
|
+
Once opted in, it only ever removes work:
|
|
71
|
+
|
|
72
|
+
- the answer is applied **after** RSpec's own filters — file and line
|
|
73
|
+
arguments, `--tag`, `-e`, `--only-failures` all still narrow the run, never
|
|
74
|
+
widen it;
|
|
75
|
+
- examples are matched by their id (`./spec/models/user_spec.rb[1:2]`), the
|
|
76
|
+
identity RSpec itself reruns failures by;
|
|
77
|
+
- if Mergify says the previous attempt already ran every one of these examples
|
|
78
|
+
and they passed, the run executes none and exits green;
|
|
79
|
+
- if any example Mergify asks for is not collected here, the full suite runs;
|
|
80
|
+
- any error, timeout, or unrecognised answer runs the full suite;
|
|
81
|
+
- if Mergify refuses to pick a previous attempt (several runs of this job
|
|
82
|
+
report under one name), the run **fails** with Mergify's explanation: give
|
|
83
|
+
each run its own `MERGIFY_TEST_JOB_NAME`;
|
|
84
|
+
- at the end of the run the gem sends Mergify what it concluded (the counts
|
|
85
|
+
and the failing examples) — unless the run failed outside any example (a
|
|
86
|
+
failing `after(:context)` or suite hook), in which case a retry runs the
|
|
87
|
+
full suite.
|
|
88
|
+
|
|
89
|
+
Each parallel_tests worker, and each CI job handed a slice of the spec files,
|
|
90
|
+
is matched to the same slice of the previous attempt, by the examples it
|
|
91
|
+
collects. This needs the slices to be the same on both attempts, which is the
|
|
92
|
+
case for a split by file size or by a committed runtime log; Knapsack Pro's
|
|
93
|
+
Queue Mode hands files out dynamically and is not supported.
|
|
94
|
+
|
|
95
|
+
### Parallel runs
|
|
96
|
+
|
|
97
|
+
[parallel_tests](https://github.com/grosser/parallel_tests) and [turbo_tests](https://github.com/serpapi/turbo_tests) need no setup. Each worker is a separate RSpec process, and each reports to Mergify as its own session:
|
|
98
|
+
|
|
99
|
+
- flaky detection sizes each worker's rerun budget from the tests that worker runs;
|
|
100
|
+
- each worker prints its own Mergify report, so a report covers only that worker's tests.
|
|
101
|
+
|
|
102
|
+
Suites split across CI jobs (knapsack, `circleci tests split`, Buildkite `parallelism`) behave the same way, one session per job.
|
|
37
103
|
|
|
38
104
|
For detailed documentation, see the [official guide](https://docs.mergify.com/ci-insights/test-frameworks/rspec/).
|
|
39
105
|
|
|
@@ -1,40 +1,172 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'securerandom'
|
|
4
|
-
|
|
4
|
+
require_relative 'trace'
|
|
5
5
|
require_relative 'utils'
|
|
6
6
|
require_relative 'native'
|
|
7
|
-
require_relative 'synchronous_batch_span_processor'
|
|
8
7
|
require_relative 'resources/rspec'
|
|
8
|
+
require_relative 'test_selection'
|
|
9
|
+
require_relative 'session_verdict'
|
|
9
10
|
|
|
10
11
|
module Mergify
|
|
11
12
|
module RSpec
|
|
12
|
-
# Central orchestrator for Mergify Test Insights:
|
|
13
|
-
#
|
|
13
|
+
# Central orchestrator for Mergify Test Insights: records the run's spans,
|
|
14
|
+
# uploads them, and coordinates flaky detection, quarantine and test
|
|
15
|
+
# selection.
|
|
14
16
|
# rubocop:disable-next Metrics/ClassLength
|
|
15
17
|
class CIInsights
|
|
18
|
+
# The resource attributes pytest-mergify reports, under the same keys.
|
|
19
|
+
# The fingerprint and count describe what this run collected, on every
|
|
20
|
+
# run that took one; the selection keys echo what Mergify answered, as
|
|
21
|
+
# sent, and are absent on a run it never answered.
|
|
22
|
+
TEST_COLLECTION_FINGERPRINT = 'test.collection.fingerprint'
|
|
23
|
+
TEST_COLLECTION_COUNT = 'test.collection.count'
|
|
24
|
+
TEST_SELECTION_ANSWER = 'test.selection.answer'
|
|
25
|
+
TEST_SELECTION_REASON = 'test.selection.reason'
|
|
26
|
+
TEST_SELECTION_KEPT_COUNT = 'test.selection.kept_count'
|
|
27
|
+
TEST_SELECTION_NOT_APPLIED_REASON = 'test.selection.not_applied_reason'
|
|
28
|
+
|
|
29
|
+
# The largest value the engine's counters take (a signed 32-bit int).
|
|
30
|
+
MAX_COUNT = (2**31) - 1
|
|
31
|
+
|
|
16
32
|
attr_reader :token, :repo_name, :api_url, :test_run_id,
|
|
17
|
-
:
|
|
33
|
+
:recorder,
|
|
18
34
|
:branch_name,
|
|
19
|
-
:flaky_detector, :flaky_detector_error_message, :quarantined_tests
|
|
35
|
+
:flaky_detector, :flaky_detector_error_message, :quarantined_tests,
|
|
36
|
+
:test_selection, :session_verdict, :session_verdict_result,
|
|
37
|
+
:captured_session_verdict, :session_verdict_withheld
|
|
20
38
|
|
|
21
|
-
# rubocop:disable-next Metrics/MethodLength
|
|
39
|
+
# rubocop:disable-next Metrics/MethodLength,Metrics/AbcSize
|
|
22
40
|
def initialize
|
|
23
41
|
@token = ENV.fetch('MERGIFY_TOKEN', nil)
|
|
24
42
|
@repo_name = Native.detect_repository_name
|
|
25
43
|
@api_url = ENV.fetch('MERGIFY_API_URL', 'https://api.mergify.com')
|
|
26
44
|
@test_run_id = SecureRandom.hex(8)
|
|
27
|
-
@
|
|
28
|
-
@
|
|
29
|
-
@exporter = nil
|
|
45
|
+
@recorder = nil
|
|
46
|
+
@uploads = false
|
|
30
47
|
@branch_name = nil
|
|
31
48
|
@flaky_detector = nil
|
|
32
49
|
@flaky_detector_error_message = nil
|
|
33
50
|
@quarantined_tests = nil
|
|
51
|
+
@test_selection = nil
|
|
52
|
+
@selection_echo = nil
|
|
53
|
+
@session_verdict = SessionVerdict.new
|
|
54
|
+
@session_verdict_result = SessionVerdict::Result.new(sent: false, truncated: false)
|
|
55
|
+
@captured_session_verdict = nil
|
|
56
|
+
@session_verdict_withheld = false
|
|
34
57
|
|
|
35
58
|
setup_tracing if Utils.in_ci?
|
|
36
59
|
end
|
|
37
60
|
|
|
61
|
+
# Take the identity of what this run is about to execute, and ask
|
|
62
|
+
# Mergify whether part of it is enough.
|
|
63
|
+
#
|
|
64
|
+
# Called once RSpec has loaded the spec files and applied every filter
|
|
65
|
+
# of its own -- file and line arguments, `--tag`, `-e`, `--only-failures`
|
|
66
|
+
# -- so the fingerprint describes this process's examples and nothing
|
|
67
|
+
# else. A parallel_tests worker, or a CI job handed a slice of the spec
|
|
68
|
+
# files, collects its own slice and so reports its own identity: the
|
|
69
|
+
# engine matches each against the previous attempt of the same slice.
|
|
70
|
+
#
|
|
71
|
+
# The fingerprint is reported on every run that can take one, whether
|
|
72
|
+
# or not this job opted into the selection, as pytest-mergify does.
|
|
73
|
+
def on_examples_collected(example_ids)
|
|
74
|
+
return unless @recorder && Native.available?
|
|
75
|
+
|
|
76
|
+
fingerprint = Native.test_collection_fingerprint(example_ids)
|
|
77
|
+
@recorder.resource_attributes[TEST_COLLECTION_FINGERPRINT] = fingerprint
|
|
78
|
+
@recorder.resource_attributes[TEST_COLLECTION_COUNT] = example_ids.size
|
|
79
|
+
# A process with nothing to run has nothing to reduce, and asking would
|
|
80
|
+
# cost the job its next retry: every parallel worker left empty by a
|
|
81
|
+
# filter reports the same empty-set fingerprint under the same job, and
|
|
82
|
+
# Mergify refuses to choose between sessions it cannot tell apart.
|
|
83
|
+
load_test_selection(fingerprint) unless example_ids.empty?
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Report what Mergify answered and what this run made of it, once the
|
|
87
|
+
# answer has met the collection. Nothing is echoed unless Mergify
|
|
88
|
+
# actually answered: a run that never asked, or whose question went
|
|
89
|
+
# unanswered, was offered nothing.
|
|
90
|
+
# rubocop:disable-next Metrics/MethodLength
|
|
91
|
+
def on_selection_resolved(kept_count)
|
|
92
|
+
return unless @recorder && @test_selection&.served?
|
|
93
|
+
|
|
94
|
+
@selection_echo = {
|
|
95
|
+
'answer' => @test_selection.selection,
|
|
96
|
+
'reason' => @test_selection.reason,
|
|
97
|
+
'kept_count' => kept_count
|
|
98
|
+
}
|
|
99
|
+
not_applied = @test_selection.not_applied_reason
|
|
100
|
+
@selection_echo['not_applied_reason'] = not_applied if not_applied
|
|
101
|
+
|
|
102
|
+
resource = @recorder.resource_attributes
|
|
103
|
+
resource[TEST_SELECTION_ANSWER] = @selection_echo['answer']
|
|
104
|
+
resource[TEST_SELECTION_REASON] = @selection_echo['reason']
|
|
105
|
+
resource[TEST_SELECTION_KEPT_COUNT] = kept_count
|
|
106
|
+
resource[TEST_SELECTION_NOT_APPLIED_REASON] = not_applied if not_applied
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Write what this session concluded to Mergify, before the spans go.
|
|
110
|
+
#
|
|
111
|
+
# Sent exactly when the selection was asked for -- including when that
|
|
112
|
+
# request failed: the verdict is what the NEXT rerun of this job needs.
|
|
113
|
+
# Withheld when the run failed outside any example (a failing
|
|
114
|
+
# `after(:context)` or suite hook): its examples then read as passed,
|
|
115
|
+
# and a verdict naming no failure would have the retry run nothing and
|
|
116
|
+
# turn green on the same breakage. Without a verdict, the retry runs the
|
|
117
|
+
# whole suite. Never raises.
|
|
118
|
+
# rubocop:disable-next Metrics/MethodLength,Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
|
|
119
|
+
def send_session_verdict(failed_outside_examples:)
|
|
120
|
+
return if @test_selection.nil?
|
|
121
|
+
|
|
122
|
+
if failed_outside_examples
|
|
123
|
+
@session_verdict_withheld = true
|
|
124
|
+
return
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
body = session_verdict_body
|
|
128
|
+
return if body.nil?
|
|
129
|
+
|
|
130
|
+
if test_mode?
|
|
131
|
+
@captured_session_verdict = body
|
|
132
|
+
@session_verdict_result = SessionVerdict::Result.new(sent: true, truncated: false)
|
|
133
|
+
return
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
if debug_mode?
|
|
137
|
+
puts "MERGIFY SESSION VERDICT: #{body}"
|
|
138
|
+
@session_verdict_result = SessionVerdict::Result.new(sent: true, truncated: false)
|
|
139
|
+
return
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
receipt = api_client.send_session_verdict(body)
|
|
143
|
+
# Dormant: the feature is not enabled for this repository, which the
|
|
144
|
+
# selection block already says.
|
|
145
|
+
return if receipt.nil?
|
|
146
|
+
|
|
147
|
+
@session_verdict_result = SessionVerdict::Result.new(sent: true, truncated: receipt['truncated'])
|
|
148
|
+
rescue StandardError => e
|
|
149
|
+
# Broader than ApiError on purpose: a value the binding cannot marshal
|
|
150
|
+
# raises something else, and a verdict must never cost the run.
|
|
151
|
+
message = e.is_a?(Native::ApiError) ? e.message : "#{e.class}: #{e.message}"
|
|
152
|
+
@session_verdict_result = SessionVerdict::Result.new(sent: false, truncated: false, error: message)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Send the run, once, at the end. Failing to report a run is worth
|
|
156
|
+
# saying out loud but never worth failing a suite that just passed,
|
|
157
|
+
# so this answers with a message instead of raising.
|
|
158
|
+
def flush
|
|
159
|
+
return nil unless @recorder && @uploads
|
|
160
|
+
return nil if @recorder.finished_spans.empty?
|
|
161
|
+
|
|
162
|
+
owner, repo = Utils.split_full_repo_name(@repo_name)
|
|
163
|
+
client = Native::Client.new(@api_url, @token, owner, repo, Mergify::RSpec::VERSION)
|
|
164
|
+
client.upload_trace(@recorder.resource_attributes, @recorder.finished_spans.map(&:to_h))
|
|
165
|
+
nil
|
|
166
|
+
rescue Native::ApiError, Utils::InvalidRepositoryFullNameError => e
|
|
167
|
+
e.message
|
|
168
|
+
end
|
|
169
|
+
|
|
38
170
|
def mark_test_as_quarantined_if_needed(example_id) # rubocop:disable Naming/PredicateMethod
|
|
39
171
|
return false unless @quarantined_tests&.include?(example_id)
|
|
40
172
|
|
|
@@ -44,30 +176,104 @@ module Mergify
|
|
|
44
176
|
|
|
45
177
|
private
|
|
46
178
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
179
|
+
# Opt-in, per job, and asked for only where the run's identity is
|
|
180
|
+
# complete: the head branch and revision (a merge-queue draft branch on a
|
|
181
|
+
# rerun) and the job coordinates, the very values each uploaded example
|
|
182
|
+
# carries, so the server can match its records.
|
|
183
|
+
# rubocop:disable-next Metrics/MethodLength,Metrics/AbcSize,Metrics/CyclomaticComplexity
|
|
184
|
+
def load_test_selection(fingerprint)
|
|
185
|
+
return unless TestSelection.enabled?
|
|
186
|
+
# An empty token is what a fork or a bot pull request gets for a secret.
|
|
187
|
+
return if @token.to_s.empty? || @repo_name.to_s.empty?
|
|
50
188
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
189
|
+
resource = @recorder.resource_attributes
|
|
190
|
+
branch = resource['vcs.ref.head.name']
|
|
191
|
+
head_sha = resource['vcs.ref.head.revision']
|
|
192
|
+
pipeline_name = resource['cicd.pipeline.name']
|
|
193
|
+
job_name = job_name_from(resource)
|
|
194
|
+
return if [branch, head_sha, pipeline_name, job_name].any? { |value| value.to_s.empty? }
|
|
195
|
+
|
|
196
|
+
answer = api_client.fetch_test_selection(branch.to_s, head_sha.to_s, pipeline_name.to_s,
|
|
197
|
+
job_name.to_s, fingerprint)
|
|
198
|
+
@test_selection = answer.nil? ? TestSelection.unanswered : TestSelection.served(answer)
|
|
199
|
+
rescue Native::ApiError, Utils::InvalidRepositoryFullNameError => e
|
|
200
|
+
@test_selection = TestSelection.unanswered(init_error_msg: e.message)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# `mergify.test.job.name` is the operator-set override; the provider's
|
|
204
|
+
# own task name is the fallback. Same precedence as the other clients.
|
|
205
|
+
def job_name_from(resource)
|
|
206
|
+
override = resource['mergify.test.job.name']
|
|
207
|
+
override.to_s.empty? ? resource['cicd.pipeline.task.name'] : override
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# The verdict body, keyed on exactly what the selection call was keyed
|
|
211
|
+
# on, so a verdict is found by what the asking run knows.
|
|
212
|
+
# rubocop:disable-next Metrics/MethodLength,Metrics/AbcSize
|
|
213
|
+
def session_verdict_body
|
|
214
|
+
resource = @recorder.resource_attributes
|
|
215
|
+
fingerprint = resource[TEST_COLLECTION_FINGERPRINT]
|
|
216
|
+
head_sha = resource['vcs.ref.head.revision']
|
|
217
|
+
pipeline_name = resource['cicd.pipeline.name']
|
|
218
|
+
job_name = job_name_from(resource)
|
|
219
|
+
return nil if [fingerprint, head_sha, pipeline_name, job_name].any? { |value| value.to_s.empty? }
|
|
220
|
+
|
|
221
|
+
body = {
|
|
222
|
+
'test_run_id' => @test_run_id,
|
|
223
|
+
'head_sha' => head_sha.to_s,
|
|
224
|
+
'pipeline_name' => pipeline_name.to_s,
|
|
225
|
+
'job_name' => job_name.to_s,
|
|
226
|
+
'collection_fingerprint' => fingerprint,
|
|
227
|
+
'collection_count' => resource[TEST_COLLECTION_COUNT],
|
|
228
|
+
'total_test_runtime_ms' => @session_verdict.total_test_runtime_ms,
|
|
229
|
+
'failing_tests' => @session_verdict.failing_tests,
|
|
230
|
+
'quarantined_failing_tests' => @session_verdict.quarantined_failing_tests
|
|
231
|
+
}.merge(@session_verdict.counts)
|
|
232
|
+
head_branch = resource['vcs.ref.head.name']
|
|
233
|
+
body['head_branch'] = head_branch.to_s unless head_branch.to_s.empty?
|
|
234
|
+
add_run_identity(body, resource)
|
|
235
|
+
body['selection'] = @selection_echo.dup if @selection_echo
|
|
236
|
+
body
|
|
59
237
|
end
|
|
60
238
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
239
|
+
# `run_attempt` only next to a `run_id`: the engine refuses an attempt of
|
|
240
|
+
# nothing. An attempt the engine's counter cannot hold is left out
|
|
241
|
+
# rather than refused.
|
|
242
|
+
def add_run_identity(body, resource)
|
|
243
|
+
run_id = resource['cicd.pipeline.run.id']
|
|
244
|
+
return if run_id.to_s.empty?
|
|
245
|
+
|
|
246
|
+
body['run_id'] = run_id.to_s
|
|
247
|
+
attempt = resource['cicd.pipeline.run.attempt']
|
|
248
|
+
body['run_attempt'] = attempt if attempt.is_a?(Integer) && attempt.between?(0, MAX_COUNT)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def api_client
|
|
252
|
+
@api_client ||= begin
|
|
253
|
+
owner, repo = Utils.split_full_repo_name(@repo_name)
|
|
254
|
+
Native::Client.new(@api_url, @token, owner, repo, Mergify::RSpec::VERSION)
|
|
68
255
|
end
|
|
69
256
|
end
|
|
70
257
|
|
|
258
|
+
# Recording is unconditional; whether the run is *uploaded* is what the
|
|
259
|
+
# token and repository decide. Debug and test runs keep their spans and
|
|
260
|
+
# send nothing, which is what they always did -- the difference is that
|
|
261
|
+
# the collector is the same object either way instead of two processors.
|
|
262
|
+
def setup_tracing
|
|
263
|
+
resource = build_resource
|
|
264
|
+
@recorder = Trace::Recorder.new(resource_attributes: resource,
|
|
265
|
+
traceparent: ENV.fetch('MERGIFY_TRACEPARENT', nil))
|
|
266
|
+
@uploads = uploadable?
|
|
267
|
+
# Only a pull request has a base branch, and that is what puts flaky
|
|
268
|
+
# detection in 'new' mode. GitHub Actions still sets GITHUB_BASE_REF on
|
|
269
|
+
# every other event, to an empty string, so empty counts as absent.
|
|
270
|
+
base_branch_name = resource['vcs.ref.base.name']
|
|
271
|
+
@base_branch_name = base_branch_name unless base_branch_name.to_s.empty?
|
|
272
|
+
@branch_name = @base_branch_name || resource['vcs.ref.head.name']
|
|
273
|
+
load_flaky_detector
|
|
274
|
+
load_quarantine
|
|
275
|
+
end
|
|
276
|
+
|
|
71
277
|
def debug_mode?
|
|
72
278
|
ENV.key?('RSPEC_MERGIFY_DEBUG')
|
|
73
279
|
end
|
|
@@ -76,59 +282,23 @@ module Mergify
|
|
|
76
282
|
ENV['_RSPEC_MERGIFY_TEST'] == 'true'
|
|
77
283
|
end
|
|
78
284
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
285
|
+
# A run is uploaded when there is somewhere to upload it to and nothing
|
|
286
|
+
# asking us not to: debug and test runs record and keep.
|
|
287
|
+
def uploadable?
|
|
288
|
+
return false if debug_mode? || test_mode?
|
|
289
|
+
return false unless @token && @repo_name && Native.available?
|
|
84
290
|
|
|
85
|
-
|
|
86
|
-
owner, repo = Utils.split_full_repo_name(@repo_name)
|
|
87
|
-
endpoint = "#{@api_url}/v1/ci/#{owner}/repositories/#{repo}/traces"
|
|
88
|
-
exp = create_otlp_exporter(endpoint)
|
|
89
|
-
processor = SynchronousBatchSpanProcessor.new(exp)
|
|
90
|
-
[processor, exp]
|
|
291
|
+
true
|
|
91
292
|
end
|
|
92
293
|
|
|
93
294
|
# The cicd.* and vcs.* attributes come from the Rust core, which every
|
|
94
|
-
# Mergify test client shares, so a provider
|
|
95
|
-
# What stays here is what only Ruby knows: the test
|
|
96
|
-
# this run invented for itself.
|
|
295
|
+
# Mergify test client shares, so a CI provider added there reaches every
|
|
296
|
+
# client at once. What stays here is what only Ruby knows: the test
|
|
297
|
+
# framework and its language, and the id this run invented for itself.
|
|
97
298
|
def build_resource
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
OpenTelemetry::SDK::Resources::Resource.create('test.run.id' => @test_run_id)
|
|
102
|
-
]
|
|
103
|
-
resources.reduce(OpenTelemetry::SDK::Resources::Resource.create({})) do |merged, r|
|
|
104
|
-
merged.merge(r)
|
|
105
|
-
end
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
def extract_branch_name(resource)
|
|
109
|
-
attrs = resource.attribute_enumerator.to_h
|
|
110
|
-
@base_branch_name = attrs['vcs.ref.base.name']
|
|
111
|
-
@base_branch_name || attrs['vcs.ref.head.name']
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
# rubocop:disable-next Metrics/MethodLength
|
|
115
|
-
def create_otlp_exporter(endpoint)
|
|
116
|
-
require 'opentelemetry-exporter-otlp'
|
|
117
|
-
original_env = ENV.fetch('OTEL_EXPORTER_OTLP_TRACES_ENDPOINT', nil)
|
|
118
|
-
ENV['OTEL_EXPORTER_OTLP_TRACES_ENDPOINT'] = endpoint
|
|
119
|
-
begin
|
|
120
|
-
OpenTelemetry::Exporter::OTLP::Exporter.new(
|
|
121
|
-
endpoint: endpoint,
|
|
122
|
-
headers: { 'Authorization' => "Bearer #{@token}" },
|
|
123
|
-
compression: 'gzip'
|
|
124
|
-
)
|
|
125
|
-
ensure
|
|
126
|
-
if original_env
|
|
127
|
-
ENV['OTEL_EXPORTER_OTLP_TRACES_ENDPOINT'] = original_env
|
|
128
|
-
else
|
|
129
|
-
ENV.delete('OTEL_EXPORTER_OTLP_TRACES_ENDPOINT')
|
|
130
|
-
end
|
|
131
|
-
end
|
|
299
|
+
Native.detect_attributes
|
|
300
|
+
.merge(Resources::RSpec.detect)
|
|
301
|
+
.merge('test.run.id' => @test_run_id)
|
|
132
302
|
end
|
|
133
303
|
|
|
134
304
|
# rubocop:disable-next Metrics/MethodLength
|