rspec-mergify 0.1.4 → 0.2.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/lib/mergify/rspec/ci_insights.rb +10 -18
- data/lib/mergify/rspec/flaky_detection.rb +27 -75
- data/lib/mergify/rspec/native.rb +70 -0
- data/lib/mergify/rspec/quarantine.rb +22 -79
- data/lib/mergify/rspec/utils.rb +11 -93
- data/lib/mergify/rspec/version.rb +1 -1
- metadata +2 -7
- data/lib/mergify/rspec/resources/buildkite.rb +0 -54
- data/lib/mergify/rspec/resources/ci.rb +0 -24
- data/lib/mergify/rspec/resources/git.rb +0 -35
- data/lib/mergify/rspec/resources/github_actions.rb +0 -60
- data/lib/mergify/rspec/resources/jenkins.rb +0 -55
- data/lib/mergify/rspec/resources/mergify.rb +0 -24
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2f6831a7105994678c61d541f3ca3840a618f93ed59561cecd23063d91063d7e
|
|
4
|
+
data.tar.gz: e6b1528a39efa7de31ef4ded5d44b2479f78521666cf48acb26edc6135580067
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: acc68c52030c7cf29760e293f99a46faae5c012745f0cd6d9b81c36cbb5c29505b7d16662a1785fd641f3f4ba8e424b592782308ef06356c3694e9bbfe09bddc
|
|
7
|
+
data.tar.gz: c95fc1c14925f61b63d1888e51e47f88af3ced37565a4964e6c30b913f0cbc6261a212562578f41faef937c1a0665fc0a150b302fd83e270bc991bcefb76b663
|
|
@@ -3,13 +3,8 @@
|
|
|
3
3
|
require 'securerandom'
|
|
4
4
|
require 'opentelemetry-sdk'
|
|
5
5
|
require_relative 'utils'
|
|
6
|
+
require_relative 'native'
|
|
6
7
|
require_relative 'synchronous_batch_span_processor'
|
|
7
|
-
require_relative 'resources/ci'
|
|
8
|
-
require_relative 'resources/git'
|
|
9
|
-
require_relative 'resources/github_actions'
|
|
10
|
-
require_relative 'resources/jenkins'
|
|
11
|
-
require_relative 'resources/buildkite'
|
|
12
|
-
require_relative 'resources/mergify'
|
|
13
8
|
require_relative 'resources/rspec'
|
|
14
9
|
|
|
15
10
|
module Mergify
|
|
@@ -26,7 +21,7 @@ module Mergify
|
|
|
26
21
|
# rubocop:disable-next Metrics/MethodLength
|
|
27
22
|
def initialize
|
|
28
23
|
@token = ENV.fetch('MERGIFY_TOKEN', nil)
|
|
29
|
-
@repo_name =
|
|
24
|
+
@repo_name = Native.detect_repository_name
|
|
30
25
|
@api_url = ENV.fetch('MERGIFY_API_URL', 'https://api.mergify.com')
|
|
31
26
|
@test_run_id = SecureRandom.hex(8)
|
|
32
27
|
@tracer_provider = nil
|
|
@@ -95,22 +90,19 @@ module Mergify
|
|
|
95
90
|
[processor, exp]
|
|
96
91
|
end
|
|
97
92
|
|
|
98
|
-
#
|
|
93
|
+
# The cicd.* and vcs.* attributes come from the Rust core, which every
|
|
94
|
+
# Mergify test client shares, so a provider gains them everywhere at once.
|
|
95
|
+
# What stays here is what only Ruby knows: the test framework, and the id
|
|
96
|
+
# this run invented for itself.
|
|
99
97
|
def build_resource
|
|
100
98
|
resources = [
|
|
101
|
-
Resources::
|
|
102
|
-
Resources::
|
|
103
|
-
Resources::
|
|
104
|
-
Resources::Jenkins.detect,
|
|
105
|
-
Resources::Buildkite.detect,
|
|
106
|
-
Resources::Mergify.detect,
|
|
107
|
-
Resources::RSpec.detect
|
|
99
|
+
OpenTelemetry::SDK::Resources::Resource.create(Native.detect_attributes),
|
|
100
|
+
Resources::RSpec.detect,
|
|
101
|
+
OpenTelemetry::SDK::Resources::Resource.create('test.run.id' => @test_run_id)
|
|
108
102
|
]
|
|
109
|
-
|
|
103
|
+
resources.reduce(OpenTelemetry::SDK::Resources::Resource.create({})) do |merged, r|
|
|
110
104
|
merged.merge(r)
|
|
111
105
|
end
|
|
112
|
-
run_id_resource = OpenTelemetry::SDK::Resources::Resource.create('test.run.id' => @test_run_id)
|
|
113
|
-
base.merge(run_id_resource)
|
|
114
106
|
end
|
|
115
107
|
|
|
116
108
|
def extract_branch_name(resource)
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'net/http'
|
|
4
|
-
require 'json'
|
|
5
|
-
require 'uri'
|
|
6
3
|
require 'set'
|
|
7
4
|
require_relative 'utils'
|
|
5
|
+
require_relative 'native'
|
|
6
|
+
require_relative 'version'
|
|
8
7
|
|
|
9
8
|
module Mergify
|
|
10
9
|
module RSpec
|
|
@@ -74,34 +73,25 @@ module Mergify
|
|
|
74
73
|
@tests_to_process = []
|
|
75
74
|
@budget = 0.0
|
|
76
75
|
|
|
77
|
-
fetch_context
|
|
78
|
-
|
|
76
|
+
@context = fetch_context
|
|
77
|
+
raise FlakyDetectionDisabledError unless Native::Budget.should_run(@context, @mode)
|
|
79
78
|
end
|
|
80
79
|
|
|
81
|
-
#
|
|
80
|
+
# Which tests this session reruns, and how long it may spend doing it,
|
|
81
|
+
# both come from the shared budget engine -- so a Ruby suite and a Python
|
|
82
|
+
# one facing the same context spend the same time.
|
|
83
|
+
#
|
|
84
|
+
# The engine sizes the budget from the existing tests *in this session*,
|
|
85
|
+
# where this class counted every existing test the context knew about. A
|
|
86
|
+
# session running part of a suite was handed the whole suite's budget; it
|
|
87
|
+
# now gets its own.
|
|
82
88
|
def prepare_for_session(test_ids)
|
|
83
|
-
|
|
84
|
-
unhealthy = Set.new(@context[:unhealthy_test_names])
|
|
85
|
-
|
|
86
|
-
@tests_to_process =
|
|
87
|
-
if @mode == 'new'
|
|
88
|
-
test_ids.reject { |id| existing.include?(id) }
|
|
89
|
-
else
|
|
90
|
-
test_ids.select { |id| unhealthy.include?(id) }
|
|
91
|
-
end
|
|
89
|
+
plan = Native::Budget.compute(@context, @mode, test_ids, [])
|
|
92
90
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
mean_duration_s = @context[:existing_tests_mean_duration_ms] / 1000.0
|
|
100
|
-
existing_count = @context[:existing_test_names].size
|
|
101
|
-
min_budget_s = @context[:min_budget_duration_ms] / 1000.0
|
|
102
|
-
|
|
103
|
-
ratio_budget = budget_ratio * mean_duration_s * existing_count
|
|
104
|
-
@budget = [ratio_budget, min_budget_s].max
|
|
91
|
+
@tests_to_process = plan['tests_to_process']
|
|
92
|
+
# The engine works in milliseconds; everything downstream compares
|
|
93
|
+
# against RSpec's durations, which are seconds.
|
|
94
|
+
@budget = plan['available_budget_ms'] / 1000.0
|
|
105
95
|
end
|
|
106
96
|
|
|
107
97
|
# rubocop:disable-next Metrics/MethodLength
|
|
@@ -113,7 +103,7 @@ module Mergify
|
|
|
113
103
|
|
|
114
104
|
return unless @tests_to_process.include?(test_id)
|
|
115
105
|
|
|
116
|
-
if test_id.length > @context[
|
|
106
|
+
if test_id.length > @context['max_test_name_length']
|
|
117
107
|
@over_length_tests.add(test_id)
|
|
118
108
|
return
|
|
119
109
|
end
|
|
@@ -153,7 +143,7 @@ module Mergify
|
|
|
153
143
|
return false unless @metrics.key?(test_id)
|
|
154
144
|
|
|
155
145
|
metrics = @metrics[test_id]
|
|
156
|
-
min_exec = @context[
|
|
146
|
+
min_exec = @context['min_test_execution_count']
|
|
157
147
|
(metrics.initial_duration * min_exec) > metrics.remaining_time
|
|
158
148
|
end
|
|
159
149
|
|
|
@@ -161,7 +151,7 @@ module Mergify
|
|
|
161
151
|
return false unless @metrics.key?(test_id)
|
|
162
152
|
|
|
163
153
|
metrics = @metrics[test_id]
|
|
164
|
-
metrics.will_exceed_deadline? || metrics.rerun_count >= @context[
|
|
154
|
+
metrics.will_exceed_deadline? || metrics.rerun_count >= @context['max_test_execution_count']
|
|
165
155
|
end
|
|
166
156
|
|
|
167
157
|
def test_metrics(test_id)
|
|
@@ -196,54 +186,16 @@ module Mergify
|
|
|
196
186
|
|
|
197
187
|
private
|
|
198
188
|
|
|
199
|
-
#
|
|
189
|
+
# A nil context means the repository has not opted into flaky detection,
|
|
190
|
+
# which is the expected default rather than a failure.
|
|
200
191
|
def fetch_context
|
|
201
|
-
|
|
202
|
-
uri = URI("#{@url}/v1/ci/#{owner}/repositories/#{repo}/flaky-detection-context")
|
|
203
|
-
|
|
204
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
|
205
|
-
http.use_ssl = uri.scheme == 'https'
|
|
206
|
-
http.open_timeout = 10
|
|
207
|
-
http.read_timeout = 10
|
|
208
|
-
|
|
209
|
-
request = Net::HTTP::Get.new(uri)
|
|
210
|
-
request['Authorization'] = "Bearer #{@token}"
|
|
211
|
-
|
|
212
|
-
response = http.request(request)
|
|
213
|
-
case response.code.to_i
|
|
214
|
-
when 200
|
|
215
|
-
parse_context(response.body)
|
|
216
|
-
when 404
|
|
217
|
-
# A 404 means the repository has not opted into flaky detection; this
|
|
218
|
-
# is the expected default, not an error.
|
|
219
|
-
raise FlakyDetectionDisabledError
|
|
220
|
-
else
|
|
221
|
-
raise "Mergify API returned HTTP #{response.code}"
|
|
222
|
-
end
|
|
223
|
-
end
|
|
224
|
-
|
|
225
|
-
# rubocop:disable-next Metrics/MethodLength,Metrics/AbcSize
|
|
226
|
-
def parse_context(body)
|
|
227
|
-
data = JSON.parse(body, symbolize_names: true)
|
|
228
|
-
@context = {
|
|
229
|
-
budget_ratio_for_new_tests: data[:budget_ratio_for_new_tests].to_f,
|
|
230
|
-
budget_ratio_for_unhealthy_tests: data[:budget_ratio_for_unhealthy_tests].to_f,
|
|
231
|
-
existing_test_names: Array(data[:existing_test_names]),
|
|
232
|
-
existing_tests_mean_duration_ms: data[:existing_tests_mean_duration_ms].to_f,
|
|
233
|
-
unhealthy_test_names: Array(data[:unhealthy_test_names]),
|
|
234
|
-
max_test_execution_count: data[:max_test_execution_count].to_i,
|
|
235
|
-
max_test_name_length: data[:max_test_name_length].to_i,
|
|
236
|
-
min_budget_duration_ms: data[:min_budget_duration_ms].to_f,
|
|
237
|
-
min_test_execution_count: data[:min_test_execution_count].to_i
|
|
238
|
-
}
|
|
239
|
-
end
|
|
192
|
+
raise FlakyDetectionDisabledError unless Native.available?
|
|
240
193
|
|
|
241
|
-
|
|
242
|
-
|
|
194
|
+
owner, repo = Utils.split_full_repo_name(@full_repository_name)
|
|
195
|
+
context = Native::Client.new(@url, @token, owner, repo, VERSION).fetch_flaky_context
|
|
196
|
+
raise FlakyDetectionDisabledError if context.nil?
|
|
243
197
|
|
|
244
|
-
|
|
245
|
-
# the whole suite. Skip instead of surfacing an error.
|
|
246
|
-
raise FlakyDetectionDisabledError
|
|
198
|
+
context
|
|
247
199
|
end
|
|
248
200
|
|
|
249
201
|
def remaining_budget
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mergify
|
|
4
|
+
module RSpec
|
|
5
|
+
# The Rust extension over mergify-ci-core: CI detection shared with the
|
|
6
|
+
# pytest and TypeScript clients.
|
|
7
|
+
#
|
|
8
|
+
# Loading is best-effort by design. A precompiled gem carries one extension
|
|
9
|
+
# per Ruby under `<ruby>/`, the source gem compiles one alongside this file,
|
|
10
|
+
# and a platform we publish neither for gets neither. Detection is one input
|
|
11
|
+
# to telemetry, not the point of the gem, so an absent extension degrades
|
|
12
|
+
# what we can report rather than breaking the suite under test -- the same
|
|
13
|
+
# fail-open posture the napi binding takes when a platform has no prebuilt
|
|
14
|
+
# binary. `load_error` keeps the reason, for callers that want to say so.
|
|
15
|
+
module Native
|
|
16
|
+
# Raised when a Mergify API call fails outright.
|
|
17
|
+
#
|
|
18
|
+
# Distinct from StandardError on purpose: callers degrade on an API
|
|
19
|
+
# failure, and a bare rescue there would swallow genuine bugs in the
|
|
20
|
+
# binding as though the backend were down.
|
|
21
|
+
class ApiError < StandardError; end
|
|
22
|
+
|
|
23
|
+
class << self
|
|
24
|
+
# The LoadError that prevented the extension loading, or nil.
|
|
25
|
+
attr_accessor :load_error
|
|
26
|
+
|
|
27
|
+
def available?
|
|
28
|
+
load_error.nil?
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
begin
|
|
36
|
+
# Precompiled gems ship lib/mergify/rspec/<ruby>/mergify_ci.<dlext>.
|
|
37
|
+
require_relative "#{RUBY_VERSION.to_f}/mergify_ci"
|
|
38
|
+
rescue LoadError
|
|
39
|
+
begin
|
|
40
|
+
# Source gem, and any local `rake compile`.
|
|
41
|
+
require_relative 'mergify_ci'
|
|
42
|
+
rescue LoadError => e
|
|
43
|
+
Mergify::RSpec::Native.load_error = e
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
unless Mergify::RSpec::Native.available?
|
|
48
|
+
# Stand in for the extension so callers can just ask, and get the same answer
|
|
49
|
+
# they would get from a machine that is not in CI. Branching on availability
|
|
50
|
+
# at every call site would only spread the same nil back through the caller.
|
|
51
|
+
module Mergify
|
|
52
|
+
module RSpec
|
|
53
|
+
module Native
|
|
54
|
+
class << self
|
|
55
|
+
def detect_provider
|
|
56
|
+
nil
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def detect_repository_name
|
|
60
|
+
nil
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def detect_attributes
|
|
64
|
+
{}
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'net/http'
|
|
4
|
-
require 'json'
|
|
5
|
-
require 'uri'
|
|
6
3
|
require 'set'
|
|
7
4
|
require_relative 'utils'
|
|
5
|
+
require_relative 'native'
|
|
6
|
+
require_relative 'version'
|
|
8
7
|
|
|
9
8
|
module Mergify
|
|
10
9
|
module RSpec
|
|
11
10
|
# Fetches quarantined test names from the Mergify API and tracks which are used.
|
|
11
|
+
#
|
|
12
|
+
# The fetch itself -- pagination, the RFC 8288 `next` links, the status
|
|
13
|
+
# codes that mean "not subscribed" rather than "broken" -- belongs to the
|
|
14
|
+
# shared Rust client now, so every Mergify test client reads a quarantine
|
|
15
|
+
# list the same way. What stays here is what RSpec cares about: which of
|
|
16
|
+
# those tests this session actually ran, and the report at the end.
|
|
12
17
|
class Quarantine
|
|
13
18
|
attr_reader :quarantined_tests, :init_error_msg
|
|
14
19
|
|
|
@@ -19,10 +24,7 @@ module Mergify
|
|
|
19
24
|
@used_tests = Set.new
|
|
20
25
|
@init_error_msg = nil
|
|
21
26
|
|
|
22
|
-
|
|
23
|
-
fetch_quarantined_tests(api_url, token, owner, repo, branch_name)
|
|
24
|
-
rescue Utils::InvalidRepositoryFullNameError => e
|
|
25
|
-
@init_error_msg = e.message
|
|
27
|
+
fetch(api_url, token, branch_name)
|
|
26
28
|
end
|
|
27
29
|
|
|
28
30
|
def include?(example_id)
|
|
@@ -53,80 +55,21 @@ module Mergify
|
|
|
53
55
|
|
|
54
56
|
private
|
|
55
57
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
@init_error_msg = "Mergify API returned a malformed quarantine list: #{e.message}"
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
# Follows the RFC 5988 `next` link until exhausted. Returns the full list
|
|
68
|
-
# on success, or `nil` when the run was aborted (subscription missing or
|
|
69
|
-
# an error already recorded in @init_error_msg).
|
|
70
|
-
# rubocop:disable-next Metrics/MethodLength,Metrics/AbcSize
|
|
71
|
-
def walk_paginated_quarantines(uri, token)
|
|
72
|
-
collected = []
|
|
73
|
-
# Guard against a server returning a `next` link that loops back to a
|
|
74
|
-
# URL we have already fetched.
|
|
75
|
-
seen = Set.new
|
|
76
|
-
while uri
|
|
77
|
-
if seen.include?(uri.to_s)
|
|
78
|
-
@init_error_msg = 'Mergify API returned a cyclic `next` link, aborting.'
|
|
79
|
-
return nil
|
|
80
|
-
end
|
|
81
|
-
seen.add(uri.to_s)
|
|
82
|
-
|
|
83
|
-
response = perform_request(uri, token)
|
|
84
|
-
case response.code.to_i
|
|
85
|
-
when 200
|
|
86
|
-
data = JSON.parse(response.body)
|
|
87
|
-
collected.concat(data.fetch('quarantined_tests', []).map { |t| t['test_name'] })
|
|
88
|
-
next_url = parse_next_link(response['Link'])
|
|
89
|
-
uri = next_url ? URI(next_url) : nil
|
|
90
|
-
when 402
|
|
91
|
-
return nil
|
|
92
|
-
else
|
|
93
|
-
@init_error_msg = "Mergify API returned HTTP #{response.code}"
|
|
94
|
-
return nil
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
collected
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
def perform_request(uri, token)
|
|
101
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
|
102
|
-
http.use_ssl = uri.scheme == 'https'
|
|
103
|
-
http.open_timeout = 10
|
|
104
|
-
http.read_timeout = 10
|
|
105
|
-
|
|
106
|
-
request = Net::HTTP::Get.new(uri)
|
|
107
|
-
request['Authorization'] = "Bearer #{token}"
|
|
108
|
-
http.request(request)
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
# Parses RFC 8288 Link headers tolerantly: accepts both quoted
|
|
112
|
-
# (`rel="next"`) and token (`rel=next`) forms, and matches when `next`
|
|
113
|
-
# is one of several space-separated rel-types (`rel="next prev"`).
|
|
114
|
-
def parse_next_link(link_header)
|
|
115
|
-
return nil if link_header.nil? || link_header.empty?
|
|
116
|
-
|
|
117
|
-
link_header.split(',').each do |part|
|
|
118
|
-
match = part.strip.match(/\A<([^>]+)>\s*;\s*(.+)\z/)
|
|
119
|
-
next unless match && next_rel?(match[2])
|
|
120
|
-
|
|
121
|
-
return match[1]
|
|
58
|
+
# A nil list means the repository has no quarantine subscription, which is
|
|
59
|
+
# not an error: the session simply quarantines nothing. Anything that went
|
|
60
|
+
# genuinely wrong is recorded and the suite carries on -- this plugin has
|
|
61
|
+
# never let the backend fail a test run.
|
|
62
|
+
def fetch(api_url, token, branch_name)
|
|
63
|
+
unless Native.available?
|
|
64
|
+
@init_error_msg = "Mergify native extension unavailable: #{Native.load_error}"
|
|
65
|
+
return
|
|
122
66
|
end
|
|
123
|
-
nil
|
|
124
|
-
end
|
|
125
67
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
68
|
+
owner, repo = Utils.split_full_repo_name(@repo_name)
|
|
69
|
+
client = Native::Client.new(api_url, token, owner, repo, VERSION)
|
|
70
|
+
@quarantined_tests = client.fetch_quarantine(branch_name) || []
|
|
71
|
+
rescue Utils::InvalidRepositoryFullNameError, Native::ApiError => e
|
|
72
|
+
@init_error_msg = e.message
|
|
130
73
|
end
|
|
131
74
|
end
|
|
132
75
|
end
|
data/lib/mergify/rspec/utils.rb
CHANGED
|
@@ -1,24 +1,21 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'open3'
|
|
4
|
-
|
|
5
3
|
module Mergify
|
|
6
4
|
module RSpec
|
|
7
5
|
# Utility methods shared across the rspec-mergify gem.
|
|
6
|
+
#
|
|
7
|
+
# CI detection used to live here -- the provider table, the git shelling
|
|
8
|
+
# out, the per-provider environment mappings. It is the Rust core's now, via
|
|
9
|
+
# the extension in Mergify::RSpec::Native, so that every Mergify test client
|
|
10
|
+
# detects identically. What is left is what is genuinely Ruby's: parsing a
|
|
11
|
+
# repository name the API needs split, and deciding whether the plugin
|
|
12
|
+
# should switch itself on at all.
|
|
8
13
|
module Utils
|
|
9
14
|
module_function
|
|
10
15
|
|
|
11
16
|
# Raised when a repository full name (owner/repo) is malformed.
|
|
12
17
|
class InvalidRepositoryFullNameError < StandardError; end
|
|
13
18
|
|
|
14
|
-
SUPPORTED_CIS = {
|
|
15
|
-
'GITHUB_ACTIONS' => :github_actions,
|
|
16
|
-
'CIRCLECI' => :circleci,
|
|
17
|
-
'JENKINS_URL' => :jenkins,
|
|
18
|
-
'BUILDKITE' => :buildkite,
|
|
19
|
-
'_RSPEC_MERGIFY_TEST' => :rspec_mergify_suite
|
|
20
|
-
}.freeze
|
|
21
|
-
|
|
22
19
|
TRUTHY_STRINGS = %w[y yes t true on 1].freeze
|
|
23
20
|
FALSY_STRINGS = %w[n no f false off 0].freeze
|
|
24
21
|
|
|
@@ -40,44 +37,14 @@ module Mergify
|
|
|
40
37
|
|
|
41
38
|
# Returns true when the suite is running inside CI or when
|
|
42
39
|
# RSPEC_MERGIFY_ENABLE is set to a truthy value.
|
|
40
|
+
#
|
|
41
|
+
# Deliberately not the core's provider detection: this asks whether the
|
|
42
|
+
# plugin should run, which an unrecognised CI or a developer setting
|
|
43
|
+
# RSPEC_MERGIFY_ENABLE both answer yes to.
|
|
43
44
|
def in_ci?
|
|
44
45
|
env_truthy?('CI') || env_truthy?('RSPEC_MERGIFY_ENABLE')
|
|
45
46
|
end
|
|
46
47
|
|
|
47
|
-
# Evaluates whether a CI environment variable should be considered enabled.
|
|
48
|
-
# rubocop:disable-next Metrics/MethodLength
|
|
49
|
-
def ci_provider
|
|
50
|
-
SUPPORTED_CIS.each do |envvar, name|
|
|
51
|
-
next unless ENV.key?(envvar)
|
|
52
|
-
|
|
53
|
-
enabled =
|
|
54
|
-
begin
|
|
55
|
-
strtobool(ENV.fetch(envvar, ''))
|
|
56
|
-
rescue ArgumentError
|
|
57
|
-
!ENV.fetch(envvar, '').strip.empty?
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
return name if enabled
|
|
61
|
-
end
|
|
62
|
-
nil
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
# Parse a git remote URL (SSH or HTTPS) into "owner/repo" form.
|
|
66
|
-
# Returns nil when the URL cannot be recognised.
|
|
67
|
-
def repository_name_from_url(url)
|
|
68
|
-
# SSH: git@github.com:owner/repo.git
|
|
69
|
-
if (m = url.match(%r{\Agit@[\w.-]+:(?<full_name>[\w.-]+/[\w.-]+?)(?:\.git)?/?$}))
|
|
70
|
-
return m[:full_name]
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
# HTTPS/HTTP with optional host (and optional port)
|
|
74
|
-
if (m = url.match(%r{\A(?:https?://[\w.-]+(?::\d+)?/)?(?<full_name>[\w.-]+/[\w.-]+)/?\z}))
|
|
75
|
-
return m[:full_name]
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
nil
|
|
79
|
-
end
|
|
80
|
-
|
|
81
48
|
# Split "owner/repo" into [owner, repo].
|
|
82
49
|
# Raises InvalidRepositoryFullNameError when the format is wrong.
|
|
83
50
|
def split_full_repo_name(full_repo_name)
|
|
@@ -86,55 +53,6 @@ module Mergify
|
|
|
86
53
|
|
|
87
54
|
raise InvalidRepositoryFullNameError, "Invalid repository name: #{full_repo_name}"
|
|
88
55
|
end
|
|
89
|
-
|
|
90
|
-
# Run a git subcommand via Open3.
|
|
91
|
-
# Returns stripped stdout on success, nil on failure.
|
|
92
|
-
def git(*args)
|
|
93
|
-
stdout, status = Open3.capture2('git', *args, err: File::NULL)
|
|
94
|
-
status.success? ? stdout.strip : nil
|
|
95
|
-
rescue StandardError
|
|
96
|
-
nil
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
# Build an attribute hash from a mapping of
|
|
100
|
-
# { attr_name => [cast_method_symbol, env_var_name_or_callable] }.
|
|
101
|
-
# Attributes whose env var is unset or whose callable returns nil are omitted.
|
|
102
|
-
def get_attributes(mapping)
|
|
103
|
-
mapping.each_with_object({}) do |(attr, (cast, env_or_callable)), result|
|
|
104
|
-
value = env_or_callable.respond_to?(:call) ? env_or_callable.call : ENV.fetch(env_or_callable, nil)
|
|
105
|
-
|
|
106
|
-
next if value.nil?
|
|
107
|
-
next if value.respond_to?(:empty?) && value.empty?
|
|
108
|
-
|
|
109
|
-
result[attr] = value.public_send(cast)
|
|
110
|
-
end
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
# Detect the repository name using CI environment variables or a git
|
|
114
|
-
# remote fallback.
|
|
115
|
-
# rubocop:disable-next Metrics/MethodLength,Metrics/CyclomaticComplexity
|
|
116
|
-
def repository_name
|
|
117
|
-
provider = ci_provider
|
|
118
|
-
|
|
119
|
-
case provider
|
|
120
|
-
when :jenkins
|
|
121
|
-
url = ENV.fetch('GIT_URL', nil)
|
|
122
|
-
return repository_name_from_url(url) if url
|
|
123
|
-
when :github_actions
|
|
124
|
-
return ENV.fetch('GITHUB_REPOSITORY', nil)
|
|
125
|
-
when :circleci
|
|
126
|
-
url = ENV.fetch('CIRCLE_REPOSITORY_URL', nil)
|
|
127
|
-
return repository_name_from_url(url) if url
|
|
128
|
-
when :buildkite
|
|
129
|
-
url = ENV.fetch('BUILDKITE_REPO', nil)
|
|
130
|
-
return repository_name_from_url(url) if url
|
|
131
|
-
when :rspec_mergify_suite
|
|
132
|
-
return 'Mergifyio/rspec-mergify'
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
url = git('config', '--get', 'remote.origin.url')
|
|
136
|
-
repository_name_from_url(url) if url
|
|
137
|
-
end
|
|
138
56
|
end
|
|
139
57
|
end
|
|
140
58
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rspec-mergify
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mergify
|
|
@@ -66,13 +66,8 @@ files:
|
|
|
66
66
|
- lib/mergify/rspec/configuration.rb
|
|
67
67
|
- lib/mergify/rspec/flaky_detection.rb
|
|
68
68
|
- lib/mergify/rspec/formatter.rb
|
|
69
|
+
- lib/mergify/rspec/native.rb
|
|
69
70
|
- lib/mergify/rspec/quarantine.rb
|
|
70
|
-
- lib/mergify/rspec/resources/buildkite.rb
|
|
71
|
-
- lib/mergify/rspec/resources/ci.rb
|
|
72
|
-
- lib/mergify/rspec/resources/git.rb
|
|
73
|
-
- lib/mergify/rspec/resources/github_actions.rb
|
|
74
|
-
- lib/mergify/rspec/resources/jenkins.rb
|
|
75
|
-
- lib/mergify/rspec/resources/mergify.rb
|
|
76
71
|
- lib/mergify/rspec/resources/rspec.rb
|
|
77
72
|
- lib/mergify/rspec/synchronous_batch_span_processor.rb
|
|
78
73
|
- lib/mergify/rspec/utils.rb
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'opentelemetry-sdk'
|
|
4
|
-
require_relative '../utils'
|
|
5
|
-
require_relative 'git'
|
|
6
|
-
|
|
7
|
-
module Mergify
|
|
8
|
-
module RSpec
|
|
9
|
-
module Resources
|
|
10
|
-
# Detects OpenTelemetry Resource attributes for Buildkite.
|
|
11
|
-
module Buildkite
|
|
12
|
-
module_function
|
|
13
|
-
|
|
14
|
-
BUILDKITE_MAPPING = {
|
|
15
|
-
'cicd.pipeline.name' => [:to_s, 'BUILDKITE_PIPELINE_SLUG'],
|
|
16
|
-
'cicd.pipeline.task.name' => [
|
|
17
|
-
:to_s,
|
|
18
|
-
lambda {
|
|
19
|
-
label = ENV.fetch('BUILDKITE_LABEL', nil)
|
|
20
|
-
label && !label.empty? ? label : ENV.fetch('BUILDKITE_STEP_KEY', nil)
|
|
21
|
-
}
|
|
22
|
-
],
|
|
23
|
-
'cicd.pipeline.run.id' => [:to_s, 'BUILDKITE_BUILD_ID'],
|
|
24
|
-
'cicd.pipeline.run.url' => [:to_s, 'BUILDKITE_BUILD_URL'],
|
|
25
|
-
'cicd.pipeline.run.attempt' => [
|
|
26
|
-
:to_i,
|
|
27
|
-
-> { ENV.fetch('BUILDKITE_RETRY_COUNT', '0').to_i + 1 }
|
|
28
|
-
],
|
|
29
|
-
'cicd.pipeline.runner.name' => [:to_s, 'BUILDKITE_AGENT_NAME'],
|
|
30
|
-
'vcs.ref.head.name' => [:to_s, 'BUILDKITE_BRANCH'],
|
|
31
|
-
'vcs.ref.base.name' => [:to_s, 'BUILDKITE_PULL_REQUEST_BASE_BRANCH'],
|
|
32
|
-
'vcs.ref.head.revision' => [:to_s, 'BUILDKITE_COMMIT'],
|
|
33
|
-
'vcs.repository.url.full' => [:to_s, 'BUILDKITE_REPO'],
|
|
34
|
-
'vcs.repository.name' => [
|
|
35
|
-
:to_s,
|
|
36
|
-
lambda {
|
|
37
|
-
url = ENV.fetch('BUILDKITE_REPO', nil)
|
|
38
|
-
Utils.repository_name_from_url(url) if url
|
|
39
|
-
}
|
|
40
|
-
]
|
|
41
|
-
}.freeze
|
|
42
|
-
|
|
43
|
-
def detect
|
|
44
|
-
return OpenTelemetry::SDK::Resources::Resource.create({}) if Utils.ci_provider != :buildkite
|
|
45
|
-
|
|
46
|
-
git_attrs = Utils.get_attributes(Git::GIT_MAPPING)
|
|
47
|
-
buildkite_attrs = Utils.get_attributes(BUILDKITE_MAPPING)
|
|
48
|
-
merged = git_attrs.merge(buildkite_attrs)
|
|
49
|
-
OpenTelemetry::SDK::Resources::Resource.create(merged)
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
end
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'opentelemetry-sdk'
|
|
4
|
-
require_relative '../utils'
|
|
5
|
-
|
|
6
|
-
module Mergify
|
|
7
|
-
module RSpec
|
|
8
|
-
module Resources
|
|
9
|
-
# Detects OpenTelemetry Resource attributes for the CI provider.
|
|
10
|
-
module CI
|
|
11
|
-
module_function
|
|
12
|
-
|
|
13
|
-
def detect
|
|
14
|
-
provider = Utils.ci_provider
|
|
15
|
-
return OpenTelemetry::SDK::Resources::Resource.create({}) if provider.nil?
|
|
16
|
-
|
|
17
|
-
OpenTelemetry::SDK::Resources::Resource.create(
|
|
18
|
-
'cicd.provider.name' => provider.to_s
|
|
19
|
-
)
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'opentelemetry-sdk'
|
|
4
|
-
require_relative '../utils'
|
|
5
|
-
|
|
6
|
-
module Mergify
|
|
7
|
-
module RSpec
|
|
8
|
-
module Resources
|
|
9
|
-
# Detects OpenTelemetry Resource attributes from git.
|
|
10
|
-
module Git
|
|
11
|
-
module_function
|
|
12
|
-
|
|
13
|
-
GIT_MAPPING = {
|
|
14
|
-
'vcs.ref.head.name' => [:to_s, -> { Utils.git('rev-parse', '--abbrev-ref', 'HEAD') }],
|
|
15
|
-
'vcs.ref.head.revision' => [:to_s, -> { Utils.git('rev-parse', 'HEAD') }],
|
|
16
|
-
'vcs.repository.url.full' => [:to_s, -> { Utils.git('config', '--get', 'remote.origin.url') }],
|
|
17
|
-
'vcs.repository.name' => [
|
|
18
|
-
:to_s,
|
|
19
|
-
lambda {
|
|
20
|
-
url = Utils.git('config', '--get', 'remote.origin.url')
|
|
21
|
-
Utils.repository_name_from_url(url) if url
|
|
22
|
-
}
|
|
23
|
-
]
|
|
24
|
-
}.freeze
|
|
25
|
-
|
|
26
|
-
def detect
|
|
27
|
-
return OpenTelemetry::SDK::Resources::Resource.create({}) if Utils.ci_provider.nil?
|
|
28
|
-
|
|
29
|
-
attributes = Utils.get_attributes(GIT_MAPPING)
|
|
30
|
-
OpenTelemetry::SDK::Resources::Resource.create(attributes)
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
end
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'json'
|
|
4
|
-
require 'opentelemetry-sdk'
|
|
5
|
-
require_relative '../utils'
|
|
6
|
-
|
|
7
|
-
module Mergify
|
|
8
|
-
module RSpec
|
|
9
|
-
module Resources
|
|
10
|
-
# Detects OpenTelemetry Resource attributes for GitHub Actions.
|
|
11
|
-
module GitHubActions
|
|
12
|
-
module_function
|
|
13
|
-
|
|
14
|
-
def detect
|
|
15
|
-
return OpenTelemetry::SDK::Resources::Resource.create({}) if Utils.ci_provider != :github_actions
|
|
16
|
-
|
|
17
|
-
attributes = Utils.get_attributes(GHA_MAPPING)
|
|
18
|
-
OpenTelemetry::SDK::Resources::Resource.create(attributes)
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
GHA_MAPPING = {
|
|
22
|
-
'cicd.pipeline.name' => [:to_s, 'GITHUB_WORKFLOW'],
|
|
23
|
-
'cicd.pipeline.task.name' => [:to_s, 'GITHUB_JOB'],
|
|
24
|
-
'cicd.pipeline.run.id' => [:to_i, 'GITHUB_RUN_ID'],
|
|
25
|
-
'cicd.pipeline.run.attempt' => [:to_i, 'GITHUB_RUN_ATTEMPT'],
|
|
26
|
-
'cicd.pipeline.runner.name' => [:to_s, 'RUNNER_NAME'],
|
|
27
|
-
'vcs.ref.head.name' => [:to_s, -> { head_ref_name }],
|
|
28
|
-
'vcs.ref.head.type' => [:to_s, 'GITHUB_REF_TYPE'],
|
|
29
|
-
'vcs.ref.base.name' => [:to_s, 'GITHUB_BASE_REF'],
|
|
30
|
-
'vcs.repository.name' => [:to_s, 'GITHUB_REPOSITORY'],
|
|
31
|
-
'vcs.repository.id' => [:to_i, 'GITHUB_REPOSITORY_ID'],
|
|
32
|
-
'vcs.repository.url.full' => [:to_s, -> { repository_url }],
|
|
33
|
-
'vcs.ref.head.revision' => [:to_s, -> { head_sha }]
|
|
34
|
-
}.freeze
|
|
35
|
-
|
|
36
|
-
def head_ref_name
|
|
37
|
-
ref = ENV.fetch('GITHUB_HEAD_REF', '')
|
|
38
|
-
ref.empty? ? ENV.fetch('GITHUB_REF_NAME', nil) : ref
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def repository_url
|
|
42
|
-
server = ENV.fetch('GITHUB_SERVER_URL', nil)
|
|
43
|
-
repo = ENV.fetch('GITHUB_REPOSITORY', nil)
|
|
44
|
-
"#{server}/#{repo}" if server && repo
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def head_sha
|
|
48
|
-
if ENV.fetch('GITHUB_EVENT_NAME', nil) == 'pull_request'
|
|
49
|
-
event_path = ENV.fetch('GITHUB_EVENT_PATH', nil)
|
|
50
|
-
if event_path && File.file?(event_path)
|
|
51
|
-
event = JSON.parse(File.read(event_path))
|
|
52
|
-
return event.dig('pull_request', 'head', 'sha').to_s
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
ENV.fetch('GITHUB_SHA', nil)
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
end
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'opentelemetry-sdk'
|
|
4
|
-
require_relative '../utils'
|
|
5
|
-
require_relative 'git'
|
|
6
|
-
|
|
7
|
-
module Mergify
|
|
8
|
-
module RSpec
|
|
9
|
-
module Resources
|
|
10
|
-
# Detects OpenTelemetry Resource attributes for Jenkins.
|
|
11
|
-
module Jenkins
|
|
12
|
-
module_function
|
|
13
|
-
|
|
14
|
-
GIT_BRANCH_PREFIXES = %w[origin/ refs/heads/].freeze
|
|
15
|
-
|
|
16
|
-
JENKINS_MAPPING = {
|
|
17
|
-
'cicd.pipeline.name' => [:to_s, 'JOB_NAME'],
|
|
18
|
-
'cicd.pipeline.task.name' => [:to_s, 'JOB_NAME'],
|
|
19
|
-
'cicd.pipeline.run.id' => [:to_s, 'BUILD_ID'],
|
|
20
|
-
'cicd.pipeline.run.url' => [:to_s, 'BUILD_URL'],
|
|
21
|
-
'cicd.pipeline.runner.name' => [:to_s, 'NODE_NAME'],
|
|
22
|
-
'vcs.ref.head.name' => [:to_s, -> { branch }],
|
|
23
|
-
'vcs.ref.head.revision' => [:to_s, 'GIT_COMMIT'],
|
|
24
|
-
'vcs.repository.url.full' => [:to_s, 'GIT_URL'],
|
|
25
|
-
'vcs.repository.name' => [
|
|
26
|
-
:to_s,
|
|
27
|
-
lambda {
|
|
28
|
-
url = ENV.fetch('GIT_URL', nil)
|
|
29
|
-
Utils.repository_name_from_url(url) if url
|
|
30
|
-
}
|
|
31
|
-
]
|
|
32
|
-
}.freeze
|
|
33
|
-
|
|
34
|
-
def detect
|
|
35
|
-
return OpenTelemetry::SDK::Resources::Resource.create({}) if Utils.ci_provider != :jenkins
|
|
36
|
-
|
|
37
|
-
git_attrs = Utils.get_attributes(Git::GIT_MAPPING)
|
|
38
|
-
jenkins_attrs = Utils.get_attributes(JENKINS_MAPPING)
|
|
39
|
-
merged = git_attrs.merge(jenkins_attrs)
|
|
40
|
-
OpenTelemetry::SDK::Resources::Resource.create(merged)
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def branch
|
|
44
|
-
raw = ENV.fetch('GIT_BRANCH', nil)
|
|
45
|
-
return nil unless raw
|
|
46
|
-
|
|
47
|
-
GIT_BRANCH_PREFIXES.each do |prefix|
|
|
48
|
-
return raw[prefix.length..] if raw.start_with?(prefix)
|
|
49
|
-
end
|
|
50
|
-
raw
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
end
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'opentelemetry-sdk'
|
|
4
|
-
require_relative '../utils'
|
|
5
|
-
|
|
6
|
-
module Mergify
|
|
7
|
-
module RSpec
|
|
8
|
-
module Resources
|
|
9
|
-
# Detects OpenTelemetry Resource attributes for Mergify-specific fields.
|
|
10
|
-
module Mergify
|
|
11
|
-
module_function
|
|
12
|
-
|
|
13
|
-
MERGIFY_MAPPING = {
|
|
14
|
-
'mergify.test.job.name' => [:to_s, 'MERGIFY_TEST_JOB_NAME']
|
|
15
|
-
}.freeze
|
|
16
|
-
|
|
17
|
-
def detect
|
|
18
|
-
attributes = Utils.get_attributes(MERGIFY_MAPPING)
|
|
19
|
-
OpenTelemetry::SDK::Resources::Resource.create(attributes)
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|