raider_reporter 0.8.0 → 0.9.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 300255531548132ae2fedcd7f10e8e9832925a204f5790ed8accb1d272ab64c6
4
- data.tar.gz: ae60aacce4ecd140c8d771b92e8d83b704ec945a3339917d79fb7aeb50d49da9
3
+ metadata.gz: dda0a98d0886eefbfd851be91d57c881e74abee367956b0272fc9b7a464045cf
4
+ data.tar.gz: 8012de5d41e890b7dae144ab5f208c20ed2ffa94736645a4308901f6abe24174
5
5
  SHA512:
6
- metadata.gz: 0213a3b50dbeeedb77ac4e335afc632bf6a25ec815e875cabd4f54bed79fb4164b23770968f38911503055bdb6bc941460f379f08c7e34238f4c379740352f8e
7
- data.tar.gz: 6c2c8759eee7dccfb81f93095d4a5c522b6029680ba999ab56073a91a76f5b7e7524da659c9350ea22f8185e1d9c1f294a75ee53aea4ceced898c36b699e0a8d
6
+ metadata.gz: fa44978dfb6d93f9bbb316fe36d837b3ecb1ae668c7292b7f7e6ac345646b4c9099dd30375d8881e466f8f21eeb4ae6ae01d53e76652f975ffeaaa83b263939c
7
+ data.tar.gz: 6a43fa45d7a90997e64e95b9f5e40e978c398343695ce74950809f9cadbafd7420edce162c4560d01dd67892861b61180c5c7938afc2cf28a6191f1d1e18a54b
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.0 — 2026-10-24
4
+
5
+ - `raider-reporter gate` waits for the Raider quality gate verdict of the CI build (`GET /api/v1/builds/gate`, every 10 seconds, up to `RAIDER_GATE_TIMEOUT` seconds, default 600, 1–3600) and exits 0 when the gate passed or the project has no gate rules, 1 when it failed (naming the failed rules), and 2 when it gets no verdict (timeout, Raider unreachable, token rejected, build not found, any other error). 429, 5xx and network errors are retried within the time limit
6
+ - It posts a `raider/gate` status: a GitHub commit status on the PR head commit (`pull_request` events) or `GITHUB_SHA`, or an Azure Repos PR status; pending while it waits, then success, failure or error. Best effort: a post that fails prints a warning and never changes the exit code
7
+ - `exec`, uploads and `comment` are unchanged and stay fail-open
8
+ - Needs a Raider server with the gate endpoint
9
+
3
10
  ## 0.8.0 — 2026-10-24
4
11
 
5
12
  - Quarantine: with `RAIDER_QUARANTINE=1` (or `config.quarantine = true`) and a token and endpoint, the formatters fetch the project's quarantine list once per run (`GET /api/v1/quarantine`, 3-second timeout) and report matching failures as `quarantined`, keeping their message, backtrace and attachments. Fail-open: any fetch problem prints `[raider_reporter] quarantine list unavailable (<reason>); no tests quarantined` and nothing is quarantined
data/README.md CHANGED
@@ -228,6 +228,42 @@ On pull requests, `raider-reporter comment` posts one comment with the build's r
228
228
  - The command never fails your CI: it always exits 0 and prints notices to stderr, prefixed `[raider_reporter]`. It needs `GITHUB_TOKEN`, `GITHUB_REPOSITORY`, `GITHUB_EVENT_PATH`, `RAIDER_TOKEN` and `RAIDER_ENDPOINT`; if one is missing, or the event is not a pull request, it does nothing.
229
229
  - It requires a Raider server that supports the summary endpoint and the `git.pr` field. Upgrade the server before using it.
230
230
 
231
+ ## Quality gate
232
+
233
+ A Raider project can turn on gate rules (minimum pass rate, no new failures against the default branch, no new flaky tests) under its Gate page. Raider computes one verdict per build once every shard is in, and never changes it. `raider-reporter gate` waits for that verdict in CI and fails the job when the gate fails. Run it once, after every test job:
234
+
235
+ ```yaml
236
+ raider-gate:
237
+ needs: [test] # every test job id
238
+ if: always() && vars.RAIDER_GATE == '1'
239
+ runs-on: ubuntu-latest
240
+ permissions:
241
+ contents: read
242
+ statuses: write
243
+ steps:
244
+ - uses: actions/checkout@v4
245
+ - uses: ruby/setup-ruby@v1
246
+ with: { ruby-version: "3.4", bundler-cache: true }
247
+ - name: Wait for the Raider quality gate
248
+ run: bundle exec raider-reporter gate
249
+ env:
250
+ RAIDER_TOKEN: ${{ secrets.RAIDER_TOKEN }}
251
+ RAIDER_ENDPOINT: ${{ vars.RAIDER_ENDPOINT }}
252
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
253
+ ```
254
+
255
+ | Outcome | Exit | Output |
256
+ |---|---|---|
257
+ | passed | 0 | stdout: `raider-reporter gate: passed` and one line per rule |
258
+ | no gate rules | 0 | stdout: `raider-reporter gate: no gate rules for this project` |
259
+ | failed | 1 | stderr: `raider-reporter gate: failed`, one line per failed rule (` ✗ pass rate: 96% < 98%`) and the build URL |
260
+ | no verdict | 2 | stderr: timeout, Raider unreachable, token rejected, build not found, or another error |
261
+
262
+ - It polls every 10 seconds (10-second timeouts) for up to `RAIDER_GATE_TIMEOUT` seconds (default 600, 1–3600) and retries 429, 5xx and network errors. While waiting it prints, at most once a minute, `waiting for the verdict (build incomplete: 2 of 4 shards)` or `(analysing results)`.
263
+ - It needs `RAIDER_TOKEN`, `RAIDER_ENDPOINT` and the CI build key that uploads and `comment` use (GitHub Actions or Azure Pipelines); without them it exits 2.
264
+ - It posts a `raider/gate` status: pending while it waits, then success, failure (the failed rules) or error. On GitHub it needs `GITHUB_TOKEN` and `statuses: write`, and uses the PR head commit on `pull_request` events, else `GITHUB_SHA`. On Azure Repos pull requests it needs `SYSTEM_ACCESSTOKEN: $(System.AccessToken)`; other Azure builds get no status. A status that cannot be posted prints a warning and never changes the exit code. Require the `raider/gate` check in branch protection to block merges.
265
+ - Unlike `comment`, `gate` fails closed. Turn it on per repository (the `RAIDER_GATE` variable above) so pull requests from forks and projects without a token are not blocked. The token is never printed.
266
+
231
267
  ## Azure Pipelines
232
268
 
233
269
  The gem detects Azure Pipelines when `TF_BUILD` is `true`. It reports the build (`provider: azure_pipelines`, build URL, attempt, job), the branch and commit, the pull request and a build key, so shards of one build group together. On a pull request from Azure Repos, `raider-reporter comment` posts one comment thread with the results and sets a `raider/tests` status on the PR.
@@ -26,6 +26,16 @@ module RaiderReporter
26
26
  value(env, "BUILD_REASON") == "PullRequest" && value(env, "BUILD_REPOSITORY_PROVIDER") == "TfsGit"
27
27
  end
28
28
 
29
+ # The REST base of a pull request (threads, statuses), or nil when a part is unknown.
30
+ def pull_request_api_url(env, number)
31
+ base = collection_base(env)
32
+ project = value(env, "SYSTEM_TEAMPROJECTID")
33
+ repo = value(env, "BUILD_REPOSITORY_ID")
34
+ return nil unless base && project && repo && number
35
+
36
+ "#{base}#{project}/_apis/git/repositories/#{repo}/pullRequests/#{number}"
37
+ end
38
+
29
39
  def strip_heads(ref)
30
40
  return nil if ref.nil?
31
41
  return ref.delete_prefix("refs/heads/") if ref.start_with?("refs/heads/")
@@ -2,9 +2,11 @@
2
2
 
3
3
  require "raider_reporter"
4
4
  require "raider_reporter/exec"
5
+ require "raider_reporter/gate"
5
6
 
6
7
  module RaiderReporter
7
8
  # `raider-reporter exec -- <command…>` runs the test suite and decides the build's exit code (see Exec).
9
+ # `raider-reporter gate` waits for the Raider quality gate verdict and exits 0, 1 or 2 (see Gate).
8
10
  # `raider-reporter comment` posts the build summary as a PR comment and sets a commit status
9
11
  # (fail-open: always exits 0, notices go to stderr). `raider-reporter report [dir]` rebuilds the
10
12
  # local HTML report (exits 1 when there are no run files or the report cannot be written).
@@ -24,10 +26,11 @@ module RaiderReporter
24
26
  "failed" => "Raider: tests failed",
25
27
  "incomplete" => "Raider: build incomplete"
26
28
  }.freeze
27
- DEFAULT_API_URL = "https://api.github.com"
28
29
  USAGE = <<~TEXT
29
30
  #{Exec::USAGE.chomp}
30
31
 
32
+ #{Gate::USAGE.chomp}
33
+
31
34
  Usage: raider-reporter report [dir]
32
35
 
33
36
  Rebuilds <dir>/index.html (default raider-results) from every run*.json in it. Prints its path.
@@ -53,6 +56,7 @@ module RaiderReporter
53
56
 
54
57
  def run
55
58
  return run_exec if @argv.first == "exec"
59
+ return run_gate if @argv.first == "gate"
56
60
 
57
61
  run_reporting
58
62
  end
@@ -65,6 +69,16 @@ module RaiderReporter
65
69
  Exec.new(command, err: @err).run
66
70
  end
67
71
 
72
+ # gate decides the CI job's exit code; Gate owns its own failure handling.
73
+ def run_gate
74
+ if @argv.include?("--help") || @argv.include?("-h")
75
+ @err.puts(Gate::USAGE)
76
+ return 0
77
+ end
78
+
79
+ Gate.new(env: @env, out: @out, err: @err).run
80
+ end
81
+
68
82
  def run_reporting
69
83
  return report if @argv.first == "report" && !@argv.include?("--help") && !@argv.include?("-h")
70
84
 
@@ -161,8 +175,7 @@ module RaiderReporter
161
175
  end
162
176
 
163
177
  def azure_pr_url(pr)
164
- "#{AzureEnv.collection_base(@env)}#{AzureEnv.value(@env, 'SYSTEM_TEAMPROJECTID')}/_apis/git/repositories/" \
165
- "#{AzureEnv.value(@env, 'BUILD_REPOSITORY_ID')}/pullRequests/#{pr['number']}"
178
+ AzureEnv.pull_request_api_url(@env, pr["number"])
166
179
  end
167
180
 
168
181
  def azure_forbidden_message
@@ -193,8 +206,7 @@ module RaiderReporter
193
206
  end
194
207
 
195
208
  def api_url
196
- url = @env["GITHUB_API_URL"].to_s.strip
197
- url.empty? ? DEFAULT_API_URL : url
209
+ Comment::GithubClient.api_url(@env)
198
210
  end
199
211
 
200
212
  # "**Raider: failed** - 2 failed, 1 quarantined, 148 passed (1m 12s)" -> "2 failed, 1 quarantined, 148 passed".
@@ -38,9 +38,9 @@ module RaiderReporter
38
38
  end
39
39
  end
40
40
 
41
- def set_status(state:, description:, target_url:)
41
+ def set_status(state:, description:, target_url:, name: "tests")
42
42
  request(:post, "/statuses", { state: state, description: description.to_s[0, DESCRIPTION_LIMIT],
43
- targetUrl: target_url, context: { genre: "raider", name: "tests" } },
43
+ targetUrl: target_url, context: { genre: "raider", name: name } },
44
44
  version: STATUS_API_VERSION)
45
45
  nil
46
46
  end
@@ -15,8 +15,15 @@ module RaiderReporter
15
15
  MAX_PAGES = 10
16
16
  PER_PAGE = 100
17
17
  DESCRIPTION_LIMIT = 140
18
+ DEFAULT_API_URL = "https://api.github.com"
18
19
 
19
- def initialize(token:, repository:, api_url: "https://api.github.com", timeout: 30)
20
+ # GITHUB_API_URL from a CI env, falling back to github.com when unset or blank.
21
+ def self.api_url(env)
22
+ url = env["GITHUB_API_URL"].to_s.strip
23
+ url.empty? ? DEFAULT_API_URL : url
24
+ end
25
+
26
+ def initialize(token:, repository:, api_url: DEFAULT_API_URL, timeout: 30)
20
27
  @token = token
21
28
  @repository = repository
22
29
  @api_url = api_url.to_s.strip.sub(%r{/+\z}, "")
@@ -0,0 +1,193 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "raider_reporter/azure_env"
4
+ require "raider_reporter/ci_detector"
5
+ require "raider_reporter/gate_client"
6
+ require "raider_reporter/gate_status"
7
+
8
+ module RaiderReporter
9
+ # `raider-reporter gate` (spec §5): waits for the Raider server's frozen quality gate
10
+ # verdict of this CI build. Exits 0 when it passed or the project has no gate rules,
11
+ # 1 when it failed, and 2 for every way it gets no verdict. Unlike `exec` and
12
+ # `comment`, it fails closed. The tokens are never printed.
13
+ class Gate
14
+ PREFIX = "raider-reporter gate:"
15
+ PASSED = 0
16
+ FAILED = 1
17
+ NO_VERDICT = 2
18
+ DEFAULT_TIMEOUT = 600
19
+ TIMEOUT_RANGE = (1..3600)
20
+ INTERVAL = 10
21
+ PROGRESS_EVERY = 60
22
+ LABELS = { "pass_rate" => "pass rate", "new_failures" => "new failures", "new_flaky" => "new flaky" }.freeze
23
+ MARKS = { "passed" => "✓", "failed" => "✗", "skipped" => "–" }.freeze
24
+ USAGE = <<~TEXT
25
+ Usage: raider-reporter gate
26
+
27
+ Waits for the Raider quality gate verdict of this CI build and posts a "raider/gate" status.
28
+ Exits 0 when the gate passed or the project has no gate rules, 1 when it failed, and 2 when no
29
+ verdict arrived (timeout, Raider unreachable, token rejected, build not found).
30
+ Environment: RAIDER_TOKEN, RAIDER_ENDPOINT, RAIDER_GATE_TIMEOUT (seconds, 1-3600, default 600).
31
+ Status: GITHUB_TOKEN (GitHub Actions) or SYSTEM_ACCESSTOKEN (Azure Repos pull requests).
32
+ TEXT
33
+
34
+ def initialize(env: ENV, out: $stdout, err: $stderr, clock: nil, sleeper: nil)
35
+ @env = env
36
+ @out = out
37
+ @err = err
38
+ @clock = clock || -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) }
39
+ @sleeper = sleeper || ->(seconds) { sleep(seconds) }
40
+ @status = nil
41
+ @url = nil
42
+ @detail = nil
43
+ @last_progress = nil
44
+ end
45
+
46
+ def run
47
+ build_key = CiDetector.new(env: @env, git: ->(*) {}).ci&.dig("build_key")
48
+ return refuse("no CI build key (run this in GitHub Actions or Azure Pipelines)") unless build_key
49
+
50
+ token = AzureEnv.value(@env, "RAIDER_TOKEN")
51
+ endpoint = AzureEnv.value(@env, "RAIDER_ENDPOINT")
52
+ return refuse("RAIDER_TOKEN and RAIDER_ENDPOINT are required") unless token && endpoint
53
+
54
+ timeout = parse_timeout
55
+ return refuse("RAIDER_GATE_TIMEOUT must be a whole number of seconds from 1 to 3600") unless timeout
56
+
57
+ @status = GateStatus.new(env: @env, warn: ->(message) { say(@err, "#{PREFIX} #{message}") })
58
+ @status.post(:pending, "Waiting for the Raider verdict")
59
+ finish(poll(GateClient.new(endpoint: endpoint, token: token), build_key, timeout), timeout)
60
+ rescue StandardError => e
61
+ no_verdict("#{e.class}: #{e.message}")
62
+ end
63
+
64
+ private
65
+
66
+ def parse_timeout
67
+ raw = AzureEnv.value(@env, "RAIDER_GATE_TIMEOUT")
68
+ return DEFAULT_TIMEOUT if raw.nil?
69
+
70
+ value = Integer(raw, 10, exception: false)
71
+ value if value && TIMEOUT_RANGE.cover?(value)
72
+ end
73
+
74
+ # The last response that ends the wait, or nil when the time limit ran out.
75
+ def poll(client, build_key, timeout)
76
+ deadline = @clock.call + timeout
77
+ loop do
78
+ response = client.fetch(build_key)
79
+ return response unless response.kind == :retry || pending?(response)
80
+
81
+ note(response)
82
+ remaining = deadline - @clock.call
83
+ return nil if remaining <= 0
84
+
85
+ @sleeper.call([INTERVAL, remaining].min)
86
+ end
87
+ end
88
+
89
+ def pending?(response)
90
+ response.kind == :verdict && response.data["state"] == "pending"
91
+ end
92
+
93
+ def note(response)
94
+ if response.kind == :retry
95
+ @detail = response.reason
96
+ return
97
+ end
98
+
99
+ remember_url(response.data)
100
+ @detail = pending_detail(response.data)
101
+ now = @clock.call
102
+ return if @last_progress && now - @last_progress < PROGRESS_EVERY
103
+
104
+ @last_progress = now
105
+ say(@out, "#{PREFIX} waiting for the verdict (#{@detail})")
106
+ end
107
+
108
+ def pending_detail(data)
109
+ shards = data["shards"].is_a?(Hash) ? data["shards"] : {}
110
+ received = shards["received"]
111
+ expected = shards["expected"]
112
+ if received.is_a?(Integer) && expected.is_a?(Integer) && received < expected
113
+ "build incomplete: #{received} of #{expected} shards"
114
+ else
115
+ "analysing results"
116
+ end
117
+ end
118
+
119
+ def finish(response, timeout)
120
+ return no_verdict("no verdict after #{timeout}s (#{@detail || 'no response'})") unless response
121
+
122
+ case response.kind
123
+ when :rejected then no_verdict("token rejected (HTTP #{response.code})")
124
+ when :not_found then no_verdict("build not found (did the tests upload?)")
125
+ when :error then no_verdict(response.reason)
126
+ else report(response.data)
127
+ end
128
+ end
129
+
130
+ def report(data)
131
+ remember_url(data)
132
+ rules = data["rules"].select { |rule| rule.is_a?(Hash) }
133
+ case data["state"]
134
+ when "none"
135
+ say(@out, "#{PREFIX} no gate rules for this project")
136
+ @status.post(:success, "No gate rules", target_url: @url)
137
+ PASSED
138
+ when "passed"
139
+ say(@out, "#{PREFIX} passed")
140
+ rules.each { |rule| say(@out, rule_line(rule)) }
141
+ @status.post(:success, "Gate passed", target_url: @url)
142
+ PASSED
143
+ else
144
+ report_failed(rules.select { |rule| rule["outcome"] == "failed" })
145
+ end
146
+ end
147
+
148
+ def report_failed(failed)
149
+ say(@err, "#{PREFIX} failed")
150
+ failed.each { |rule| say(@err, rule_line(rule)) }
151
+ say(@err, " #{@url}") if @url
152
+ description = failed.map { |rule| rule_text(rule) }.join("; ")
153
+ @status.post(:failure, description.empty? ? "Gate failed" : description, target_url: @url)
154
+ FAILED
155
+ end
156
+
157
+ def rule_line(rule)
158
+ " #{MARKS.fetch(rule['outcome'].to_s, '?')} #{rule_text(rule)}"
159
+ end
160
+
161
+ def rule_text(rule)
162
+ name = rule["rule"].to_s
163
+ "#{LABELS.fetch(name, name)}: #{rule['detail']}"
164
+ end
165
+
166
+ def remember_url(data)
167
+ @url = data["url"] if data["url"].is_a?(String) && !data["url"].empty?
168
+ end
169
+
170
+ # Exit 2 before polling: nothing was posted, so there is no status to clear.
171
+ def refuse(message)
172
+ say(@err, "#{PREFIX} #{message}")
173
+ NO_VERDICT
174
+ end
175
+
176
+ # Exit 2 after the pending status: replace it, so a required check is never left pending.
177
+ def no_verdict(message)
178
+ say(@err, "#{PREFIX} #{message}")
179
+ @status&.post(:error, "No verdict", target_url: @url)
180
+ NO_VERDICT
181
+ end
182
+
183
+ def say(io, text)
184
+ io.puts(redact(text))
185
+ end
186
+
187
+ def redact(text)
188
+ secrets = [AzureEnv.value(@env, "RAIDER_TOKEN"), AzureEnv.value(@env, "GITHUB_TOKEN"),
189
+ AzureEnv.value(@env, "SYSTEM_ACCESSTOKEN")].compact
190
+ secrets.reduce(text.to_s) { |memo, secret| memo.gsub(secret, "[REDACTED]") }
191
+ end
192
+ end
193
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cgi/escape"
4
+ require "json"
5
+ require "net/http"
6
+ require "openssl"
7
+ require "timeout"
8
+ require "uri"
9
+ require "raider_reporter/version"
10
+
11
+ module RaiderReporter
12
+ # One GET /api/v1/builds/gate call for `raider-reporter gate` (spec §5), classified
13
+ # so the caller can decide: a verdict, retry (429, 5xx, network), or stop.
14
+ class GateClient
15
+ class InvalidResponse < StandardError; end
16
+
17
+ PATH = "/api/v1/builds/gate"
18
+ TIMEOUT = 10
19
+ STATES = %w[pending none passed failed].freeze
20
+ NETWORK_ERRORS = [Timeout::Error, SystemCallError, SocketError, IOError, OpenSSL::SSL::SSLError,
21
+ Net::ProtocolError, Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError].freeze
22
+
23
+ # kind: :verdict, :retry, :rejected, :not_found or :error.
24
+ Response = Data.define(:kind, :data, :code, :reason)
25
+
26
+ def initialize(endpoint:, token:)
27
+ @endpoint = endpoint
28
+ @token = token
29
+ end
30
+
31
+ def fetch(build_key)
32
+ classify(request(build_key))
33
+ rescue *NETWORK_ERRORS => e
34
+ Response.new(kind: :retry, data: nil, code: nil, reason: redact("#{e.class}: #{e.message}"))
35
+ end
36
+
37
+ private
38
+
39
+ def request(build_key)
40
+ uri = URI.parse("#{@endpoint.to_s.strip.chomp('/')}#{PATH}?build_key=#{CGI.escape(build_key.to_s)}")
41
+ raise ArgumentError, "RAIDER_ENDPOINT must start with http:// or https://" unless uri.is_a?(URI::HTTP)
42
+
43
+ req = Net::HTTP::Get.new(uri)
44
+ req["Authorization"] = "Bearer #{@token}"
45
+ req["Accept"] = "application/json"
46
+ req["User-Agent"] = "raider_reporter/#{VERSION}"
47
+ Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: TIMEOUT,
48
+ read_timeout: TIMEOUT, max_retries: 0) { |http| http.request(req) }
49
+ end
50
+
51
+ def classify(response)
52
+ code = response.code.to_i
53
+ case code
54
+ when 200 then parse(response.body)
55
+ when 401, 403 then Response.new(kind: :rejected, data: nil, code: code, reason: nil)
56
+ when 404 then Response.new(kind: :not_found, data: nil, code: code, reason: nil)
57
+ when 429, 500..599 then Response.new(kind: :retry, data: nil, code: code, reason: "HTTP #{code}")
58
+ else error_response(code, "unexpected HTTP #{code}")
59
+ end
60
+ end
61
+
62
+ def parse(body)
63
+ data = JSON.parse(body.to_s)
64
+ return error_response(200, "invalid gate response") unless valid?(data)
65
+
66
+ Response.new(kind: :verdict, data: data, code: 200, reason: nil)
67
+ rescue JSON::ParserError => e
68
+ Response.new(kind: :error, data: nil, code: 200, reason: redact("#{e.class}: #{e.message}"))
69
+ end
70
+
71
+ def valid?(data)
72
+ data.is_a?(Hash) && STATES.include?(data["state"]) && data["rules"].is_a?(Array)
73
+ end
74
+
75
+ # Spec §5 "other error" form: <class>: <message>.
76
+ def error_response(code, message)
77
+ Response.new(kind: :error, data: nil, code: code, reason: "#{InvalidResponse}: #{message}")
78
+ end
79
+
80
+ def redact(text)
81
+ @token.to_s.empty? ? text : text.gsub(@token.to_s, "[REDACTED]")
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "raider_reporter/version"
4
+ require "raider_reporter/azure_env"
5
+ require "raider_reporter/ci_detector"
6
+ require "raider_reporter/comment/github_client"
7
+ require "raider_reporter/comment/azure_client"
8
+
9
+ module RaiderReporter
10
+ # Posts the "raider/gate" status for `raider-reporter gate` (spec §5): a GitHub commit
11
+ # status (PR head SHA on pull_request events, else GITHUB_SHA) or an Azure Repos PR
12
+ # status (genre raider, name gate). Best effort: a failed post warns and never raises.
13
+ class GateStatus
14
+ CONTEXT = "raider/gate"
15
+ AZURE_NAME = "gate"
16
+ TIMEOUT = 10
17
+ GITHUB_STATES = { pending: "pending", success: "success", failure: "failure", error: "error" }.freeze
18
+ AZURE_STATES = { pending: "pending", success: "succeeded", failure: "failed", error: "error" }.freeze
19
+
20
+ def initialize(env:, warn:)
21
+ @env = env
22
+ @warn = warn
23
+ @provider = begin
24
+ detect
25
+ rescue StandardError
26
+ nil
27
+ end
28
+ end
29
+
30
+ def active?
31
+ !@provider.nil?
32
+ end
33
+
34
+ # state: :pending, :success, :failure or :error.
35
+ def post(state, description, target_url: nil)
36
+ case @provider
37
+ when :github
38
+ github_client.set_status(@sha, state: GITHUB_STATES.fetch(state), target_url: target_url,
39
+ description: description, context: CONTEXT)
40
+ when :azure
41
+ azure_client.set_status(state: AZURE_STATES.fetch(state), description: description, target_url: target_url,
42
+ name: AZURE_NAME)
43
+ end
44
+ nil
45
+ rescue StandardError => e
46
+ @warn.call("could not post the #{CONTEXT} status (#{e.class}: #{e.message})")
47
+ nil
48
+ end
49
+
50
+ private
51
+
52
+ def detect
53
+ detector = CiDetector.new(env: @env, git: ->(*) {})
54
+ if @env["GITHUB_ACTIONS"] == "true"
55
+ @sha = detector.git&.dig("commit_sha")
56
+ :github if [@env["GITHUB_TOKEN"], @env["GITHUB_REPOSITORY"], @sha].none? { |value| value.to_s.strip.empty? }
57
+ elsif AzureEnv.azure?(@env)
58
+ pr = detector.git&.dig("pr")
59
+ @azure_url = pr && AzureEnv.pull_request_api_url(@env, pr["number"])
60
+ :azure if @azure_url && AzureEnv.value(@env, "SYSTEM_ACCESSTOKEN")
61
+ end
62
+ end
63
+
64
+ def github_client
65
+ Comment::GithubClient.new(token: @env["GITHUB_TOKEN"], repository: @env["GITHUB_REPOSITORY"],
66
+ api_url: Comment::GithubClient.api_url(@env), timeout: TIMEOUT)
67
+ end
68
+
69
+ def azure_client
70
+ Comment::AzureClient.new(token: AzureEnv.value(@env, "SYSTEM_ACCESSTOKEN"), base_url: @azure_url, timeout: TIMEOUT)
71
+ end
72
+ end
73
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RaiderReporter
4
- VERSION = "0.8.0"
4
+ VERSION = "0.9.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: raider_reporter
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Augustin Gottlieb
@@ -38,6 +38,9 @@ files:
38
38
  - lib/raider_reporter/cucumber/formatter.rb
39
39
  - lib/raider_reporter/exec.rb
40
40
  - lib/raider_reporter/exec_summary.rb
41
+ - lib/raider_reporter/gate.rb
42
+ - lib/raider_reporter/gate_client.rb
43
+ - lib/raider_reporter/gate_status.rb
41
44
  - lib/raider_reporter/github_event.rb
42
45
  - lib/raider_reporter/models.rb
43
46
  - lib/raider_reporter/payload.rb
@@ -71,7 +74,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
71
74
  - !ruby/object:Gem::Version
72
75
  version: '0'
73
76
  requirements: []
74
- rubygems_version: 4.0.21
77
+ rubygems_version: 4.0.6
75
78
  specification_version: 4
76
79
  summary: RSpec and Cucumber formatters that write Raider reports locally and upload
77
80
  them to Raider